Schabby's Blog
OpenGL, Java, Cassandra and other stuff that totally makes the world go round

This post provides a clean, up-to-date and concise example on how to set up a simple custom shader in OpenGL that runs out of the box. My target language is Java with LWJGL, but the code can easily be ported to different languages on this level. To my surprise, I is quite difficult to find a state of the art example of a simple yet complete setup for shaders that can serve as a get-go for more complex programs. Most OpenGL shader tutorials are written for the pre-2.0 era (ie. using shaders through ARB extensions) that is long over. This post is therefore trying to provide a modern "Hello World of Shaders" set up example.

Let's get started with a brief overview. We will have two classes. The first one (ShaderExample.java) will serve as the mere boiler plate code to initialize the OpenGL context and to hold the (small) render loop. If you are familiar with OpenGL applications, especially with vertex array objects (VAO) and vertex buffer objects (VBOs) than this class will not reveal anything new to you. The second class (RenderProgram) holds the code to load and compile the vertex and fragment shaders and to link them into a program within the OpenGL context. The last two files will contain the actual vertex and fragment shaders.

Please find the soure code below for ShaderExample.java:

import static org.lwjgl.opengl.GL11.GL_NO_ERROR;
import static org.lwjgl.opengl.GL11.glGetError;
 
import java.nio.FloatBuffer;
 
import org.lwjgl.BufferUtils;
import org.lwjgl.LWJGLException;
import org.lwjgl.opengl.*;
import org.lwjgl.util.glu.GLU;
 
public class ShaderExample 
{
	/**
	 * General initialization stuff for OpenGL
	 */
	public void initGl() throws LWJGLException
	{
		// width and height of window and view port
		int width = 640;
		int height = 480;
 
		// set up window and display
		Display.setDisplayMode(new DisplayMode(width, height));
		Display.setVSyncEnabled(true);
		Display.setTitle("Shader Example");
 
		// set up OpenGL to run in forward-compatible mode
		// so that using deprecated functionality will
		// throw an error.
		PixelFormat pixelFormat = new PixelFormat();
		ContextAttribs contextAtrributes = new ContextAttribs(3, 2);
		contextAtrributes.withForwardCompatible(true);
		contextAtrributes.withProfileCore(true);
		Display.create(pixelFormat, contextAtrributes);
 
		// initialize basic OpenGL stuff
		GL11.glViewport(0, 0, width, height);
		GL11.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
	}
 
	public void run()
	{
		// compile and link vertex and fragment shaders into
		// a "program" that resides in the OpenGL driver
		ShaderProgram shader = new ShaderProgram();
 
		// do the heavy lifting of loading, compiling and linking
		// the two shaders into a usable shader program
		shader.init("src/com/whiletrue/example/simpleshader/simple.vertex", "src/com/whiletrue/example/simpleshader/simple.fragment");		
 
		int vaoHandle = constructVertexArrayObject();
 
		while( Display.isCloseRequested() == false )
		{
			GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT);
 
			// tell OpenGL to use the shader
			GL20.glUseProgram( shader.getProgramId() );
 
			// bind vertex and color data
			GL30.glBindVertexArray(vaoHandle);
			GL20.glEnableVertexAttribArray(0); // VertexPosition
			GL20.glEnableVertexAttribArray(1); // VertexColor
 
			// draw VAO
			GL11.glDrawArrays(GL11.GL_TRIANGLES, 0, 3);
 
			// check for errors
			if( glGetError() != GL_NO_ERROR )
			{
				throw new RuntimeException("OpenGL error: "+GLU.gluErrorString(glGetError()));
			}
 
			// swap buffers and sync frame rate to 60 fps
			Display.update();
			Display.sync(60);
		}
 
		Display.destroy();
	}
 
	/**
	 * Create Vertex Array Object necessary to pass data to the shader
	 */
	private int constructVertexArrayObject()
	{
		// create vertex data 
		float[] positionData = new float[] {
		    	0f,		0f,		0f,
		    	-1f,	0f, 	0f,
		    	0f,		1f,		0f
		};
 
		// create color data
		float[] colorData = new float[]{
				0f,			0f,			1f,
				1f,			0f,			0f,
				0f,			1f,			0f
		};
 
		// convert vertex array to buffer
		FloatBuffer positionBuffer = BufferUtils.createFloatBuffer(positionData.length);
		positionBuffer.put(positionData);
		positionBuffer.flip();
 
		// convert color array to buffer
		FloatBuffer colorBuffer = BufferUtils.createFloatBuffer(colorData.length);
		colorBuffer.put(colorData);
		colorBuffer.flip();		
 
		// create vertex byffer object (VBO) for vertices
		int positionBufferHandle = GL15.glGenBuffers();
		GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, positionBufferHandle);
		GL15.glBufferData(GL15.GL_ARRAY_BUFFER, positionBuffer, GL15.GL_STATIC_DRAW);
 
		// create VBO for color values
		int colorBufferHandle = GL15.glGenBuffers();
		GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, colorBufferHandle);
		GL15.glBufferData(GL15.GL_ARRAY_BUFFER, colorBuffer, GL15.GL_STATIC_DRAW);
 
		// unbind VBO
		GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
 
		// create vertex array object (VAO)
		int vaoHandle = GL30.glGenVertexArrays();
		GL30.glBindVertexArray(vaoHandle);
		GL20.glEnableVertexAttribArray(0);
		GL20.glEnableVertexAttribArray(1);
 
		// assign vertex VBO to slot 0 of VAO
		GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, positionBufferHandle);
		GL20.glVertexAttribPointer(0, 3, GL11.GL_FLOAT, false, 0, 0);
 
		// assign vertex VBO to slot 1 of VAO
		GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, colorBufferHandle);
		GL20.glVertexAttribPointer(1, 3, GL11.GL_FLOAT, false, 0, 0);
 
		// unbind VBO
		GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
 
		return vaoHandle;
	}
 
	/**
	 * main method to run the example
	 */
	public static void main(String[] args) throws LWJGLException
	{
		ShaderExample example = new ShaderExample();
		example.initGl();
		example.run();
	}
}

Note that the instance of RenderProgram also holds the handle for the program which is retrievable in the render loop through getProgramId(). So you need to keep the instance around for rendering. This is actually good practice and many scene graphs allow the developer to attach different shaders to be attached to different nodes in the graph. This enables to apply different shaders for different objects (eg. particle system shader versus terrain shader).

The source code for RenderProgram which loads, compiles and links the shaders to an executable OpenGL shader follows below. Please check out the comments in the code for explanations.

import java.io.BufferedReader;
import java.io.FileReader;
 
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL20;
import static org.lwjgl.opengl.GL20.*;
 
public class ShaderProgram 
{
	// OpenGL handle that will point to the executable shader program
	// that can later be used for rendering
	private int programId;
 
	public void init(String vertexShaderFilename, String fragmentShaderFilename)
	{
		// create the shader program. If OK, create vertex and fragment shaders
		programId = glCreateProgram();
 
		// load and compile the two shaders
		int vertShader = loadAndCompileShader(vertexShaderFilename, GL_VERTEX_SHADER);
		int fragShader = loadAndCompileShader(fragmentShaderFilename, GL_FRAGMENT_SHADER);
 
		// attach the compiled shaders to the program
		glAttachShader(programId, vertShader);
		glAttachShader(programId, fragShader);
 
		// now link the program
		glLinkProgram(programId);
 
		// validate linking
		if (glGetProgram(programId, GL_LINK_STATUS) == GL11.GL_FALSE) 
		{
			throw new RuntimeException("could not link shader. Reason: " + glGetProgramInfoLog(programId, 1000));
		}
 
		// perform general validation that the program is usable
		glValidateProgram(programId);
 
		if (glGetProgram(programId, GL_VALIDATE_STATUS) == GL11.GL_FALSE)
		{
			throw new RuntimeException("could not validate shader. Reason: " + glGetProgramInfoLog(programId, 1000));            
		}
	}
 
   /*
    * With the exception of syntax, setting up vertex and fragment shaders
    * is the same.
    * @param the name and path to the vertex shader
    */
	private int loadAndCompileShader(String filename, int shaderType)
	{
		//vertShader will be non zero if succefully created
		int handle = glCreateShader(shaderType);
 
		if( handle == 0 )
		{
			throw new RuntimeException("could not created shader of type "+shaderType+" for file "+filename+". "+ glGetProgramInfoLog(programId, 1000));
		}
 
		// load code from file into String
		String code = loadFile(filename);
 
		// upload code to OpenGL and associate code with shader
		glShaderSource(handle, code);
 
		// compile source code into binary
		glCompileShader(handle);
 
		// acquire compilation status
		int shaderStatus = glGetShader(handle, GL20.GL_COMPILE_STATUS);
 
		// check whether compilation was successful
		if( shaderStatus == GL11.GL_FALSE)
		{
			throw new IllegalStateException("compilation error for shader ["+filename+"]. Reason: " + glGetShaderInfoLog(handle, 1000));
		}
 
		return handle;
	}
 
	/**
	 * Load a text file and return it as a String.
	 */
	private String loadFile(String filename)
	{
		StringBuilder vertexCode = new StringBuilder();
		String line = null ;
		try
		{
		    BufferedReader reader = new BufferedReader(new FileReader(filename));
		    while( (line = reader.readLine()) !=null )
		    {
		    	vertexCode.append(line);
		    	vertexCode.append('\n');
		    }
		}
		catch(Exception e)
		{
			throw new IllegalArgumentException("unable to load shader from file ["+filename+"]", e);
		}
 
		return vertexCode.toString();
	}
 
 
	public int getProgramId() {
		return programId;
	}    
}

That was the tough part. What follows are the two shader code snippets. First the vertex shader which needs to be in a text file called simple.vertex so that it can be loaded from ShaderProgram as deducible from the code provided above:

/* Very simple vertex shader that applies the model view
 * and projection matrix to the given vertex and overrides
 * the color with a constant for all vertices. 
 */
#version 400
 
layout (location = 0) in vec3 VertexPosition;
layout (location = 1) in vec3 VertexColor;
 
out vec3 Color;
 
void main()
{
	Color = VertexColor;
    gl_Position = vec4(VertexPosition, 1.0);
}

Second the fragment shader, which needs to be in a text file simple.fragment again so that it can be loaded by ShaderProgram.

/* Very simple fragment shader. It basically passes the
 * (interpolated) vertex color on to the individual pixels.
 */ 
#version 400
 
// corresponds with output from vertex shader
in vec3 Color;
 
out vec4 FragColor;
 
void main()
{
	// assign vertex color to pixel color
    FragColor = vec4(Color, 1.0);
}

Alright and that's it. All you need to do is to get this code to compile in your favorite IDE and run it. You should get the following output:

Please leave a comment if you liked the post of if you found an error. Also, if you have questions, just let me know, as usual. I try to answer them as quickly as possible.


7 Antworten

  1. Cool snippet! Thanks!

  2. One more thing. In the class ShaderExample, where is 'shader' declared? I see it's only calling the init() method, but wouldn't this yield a compiler error?

    Thanks!

  3. Hurf says:

    Thank you very much. I had the same problem of being unable to find examples--except that I was specifically looking for OpenGL 4 + LWJGL examples; in fact I already have written my own simple OpenGL 4 + JOGL programs. So I thought I could just port one of those from JOGL to LWJGL to get myself started, but no, my ported LWJGL program just doesn't work at all! And the LWJGL site provides little help. It's strange how people who make libraries like that act as if nobody should ever really consider using the CURRENT version of OpenGL. Even the shader tutorials for LWJGL use the old fixed-function model-view matrices and such... and some of the most basic tutorials are still using glBegin()!

    Your example program (with a little hacking to get the shader files to load via getClass().getResourceAsStream() for my convenience) is working for me, and I think I will be able to use it as a reference point to figure out why in the world my own program is showing only a black display.

  4. Thank you!!!!
    giuseppe zanotti design online store http://www.mantleplace.me/

  5. herdtie says:

    Thanks a lot for this code, works for me as opposed to lwjgl example ( http://lwjgl.org/wiki/index.php?title=GLSL_Shaders_with_LWJGL ). Maybe due to old hardware or drivers (Debian 7, integrated Intel GMA4500MHD graphics chip).

    However, I had to use their shaders (screen.frag, screen.vert) because with your shaders I got the error:
    error: unrecognized layout identifier `location'
    in loadAndCompileShader.

  6. akihiro says:

    Thanks a lot, your sample code helps very much!
    But I had to change a little. I tell you about it just FYI.

    Original code:
    ContextAttribs contextAtrributes = new ContextAttribs(3, 2);
    contextAtrributes.withForwardCompatible(true);
    contextAtrributes.withProfileCore(true);

    is need to be changed to:
    ContextAttribs contextAtrributes = new ContextAttribs(3, 2)
    .withForwardCompatible(true)
    .withProfileCore(true);

    since ContextAttribs class is immutable.

    And:
    shader.init(".../simple.vertex", ".../simple.fragment");
    int vaoHandle = constructVertexArrayObject();

    is need to be changed to:
    int vaoHandle = constructVertexArrayObject();
    shader.init(".../simple.vertex", ".../simple.fragment");

    because in my environment (mac os X) the original one cause following error:
    could not validate shader. Reason: Validation Failed: No vertex array object bound.

    I think this info. may suggest you to write more platform compatible code.

  7. Jay says:

    This seriously was so fucking awesome.
    I think a comment is typo'd though.

    // assign vertex VBO to slot 1 of VAO
    GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, colorBufferHandle);
    GL20.glVertexAttribPointer(1, 3, GL11.GL_FLOAT, false, 0, 0);

    I think it is meant to say color buffer to slot 1, slot 0 is vertex buffer. Right? I am just learning this but that makes sense hence the parameters in the bind.

    This is so great though, thanks for the simplicity with comments, spaced out and few methods for easier following. Anytime I follow tutorials on VBA/VBO's they have like 100 methods which works for their program but to follow their made up methods gets kinda hectic sometimes/

Post Comment

Please notice: Comments are moderated by an Admin.