An Introduction To GLSL in LWJGL

This is a small overview of using the OpenGL high level shader language (GLSL) in LWJGL. This is by no means an exaustive coverage on shaders or GLSL - the OpenGL Orange Book ( http://3dshaders.com/ ) gives a complete overview of the language and is definatly recommended for anyone doing GLSL work.

For the uninitiated, shaders allow a graphics programmer to replace the conventional fixed-function pipeline and replace it with their own custom shading code. This is split into two parts - vertex shading and fragment shading. Vertex shaders accept a single incomming vertex (usually in world space) and output a transformed vertex in screen space. Fragment shaders accept an incomming fragment and output a single colour value to be written to the destination pixel. Note that some parts of the pipeline (such as stencil buffer testing) are not replaced by shader code (and may be used in conjunction with shaders).

Loading And Creating Our Shaders

First we must load our shader code into a (direct) ByteBuffer. This is typically stored in a text file. The following is borrowed from JavaCoolDude's framework code:

private static ByteBuffer getProgramCode(String filename)
	{
		ClassLoader fileLoader = GLShaders.class.getClassLoader();
		InputStream fileInputStream = fileLoader.getResourceAsStream(filename);
		byte[] shaderCode = null;
 
		try
		{
			if (fileInputStream == null)
					fileInputStream = new FileInputStream(filename);
			DataInputStream dataStream = new DataInputStream(fileInputStream);
			dataStream.readFully(shaderCode = new byte[ fileInputStream.available() ]);
			fileInputStream.close();
			dataStream.close();
		}
		catch (Exception e)
		{
			System.out.println(e.getMessage());
		}
		ByteBuffer shaderPro = BufferUtils.createByteBuffer(shaderCode.length);
 
		shaderPro.put(shaderCode);
		shaderPro.flip();
 
		return shaderPro;
	}

Both vertex and fragment shaders are represented by shader object. Much like OpenGL's texture objects you manipulate these though an object id (int). First we must generate an empty shader object:

int vertexShader = ARBShaderObjects.glCreateShaderObjectARB(ARBVertexShader.GL_VERTEX_SHADER_ARB);
// Or use the constant ARBFragmentShader.GL_FRAGMENT_SHADER_ARB to generate an empty fragment shader

Now we can attach our shader's source code to our new shader object and compile the source.

ARBShaderObjects.glShaderSourceARB(vertexShader, vertexShaderSourceByteBuffer);
ARBShaderObjects.glCompileShaderARB(vertexShader);

Before you can use your shader objects you need to create a shader 'program'. These typically include a pair of shaders - one vertex, one fragment - which work together. But you can also just include one or the other, in which case regular OpenGL fixed function pipeline takes over for the half that isn't specified.

Again we see that the shader programs are managed though an int handle, so first we must generate an empty program to use and attach our shader(s) to it.

int programObject  = ARBShaderObjects.glCreateProgramObjectARB();
ARBShaderObjects.glAttachObjectARB(programObject, vertexShader);

You then need to 'link' the program, which resolves uniform variables, attributes etc. (more on those later).

ARBShaderObjects.glLinkProgramARB(programObject);

You probably want to validate the resulting program as well (definatly during development at least, you can take it out if you know it'll validate ok):

ARBShaderObjects.glValidateProgramARB(programObject);

At various points (particularly after compiling, linking and validating) various log messages can be generated by the OpenGL implementation. Check this info log in between these stages so you can see any errors in your shader source. The following utility function will query OpenGL for any log information and print it to the console.

private void printLogInfo(int obj)
	{
		IntBuffer iVal = BufferUtils.createIntBuffer(1);
		ARBShaderObjects.glGetObjectParameterARB(obj, ARBShaderObjects.GL_OBJECT_INFO_LOG_LENGTH_ARB, iVal);
 
		int length = iVal.get();
		System.out.println("Info log length:"+length);
		if (length > 0)
		{
			// We have some info we need to output.
			ByteBuffer infoLog = BufferUtils.createByteBuffer(length);
			iVal.flip();
			ARBShaderObjects.glGetInfoLogARB(obj,  iVal, infoLog);
			byte[] infoBytes = new byte[length];
			infoLog.get(infoBytes);
			String out = new String(infoBytes);
 
			System.out.println("Info log:\n"+out);
		}
 
		Util.checkGLError();
	}

Using Our Shaders

Now we have a usable shader program which has compiled and validated without errors we can use it for rendering. We need to bind it before using it much like a texture object. Once bound all geometry rendered will be processed by this program.

ARBShaderObjects.glUseProgramObjectARB(programObject);

Once you've finished rendering with your program you can either bind another instead to use another one, or bind null (0) as a program to disable it and revert back to the fixed function pipeline.

Shader Input Variables

Unless you're doing something really simple you're going to need to pass data from your rendering code to your shaders. The types for input into shaders are 'uniform', 'attribute' and 'varying'.

Uniform Variables

Uniform variables are global to an entire shader, and can be used in both vertex and fragment shaders. Eg. an ambient colour, or scale factor applied to every vertex/fragment. As well as defining your own uniform variables there are also a whole bunch of built-in uniforms which you can use to access the fixed-function state (like the modelview matrix, enabled lights, etc.).

Vertex Attributes

Vertex attributes are unique to each vertex and so are only applicable to vertex shaders. Again, built-in attributes allow you to access fixed-function variables (like per-vertex colour or texture coords). You can define your own uniform and attribute values in order to pass custom data to your vertex shader. For example, you could use a uniform 'scale' variable to scale the size of each vertex a fixed amount along an axis. Or you could use a per-vertex 'scale' amount to displace each vertex individually.

Fragment Varying Variables

Varying variables are the bridge between vertex and fragment shaders. Vertex shaders can output their result into varyings and the value will automatically be interpolated across the triangle for each fragment. For example the built-in varying for vertex colour will be interpolated and the fragment shader will receive this value as an input varying.

Unlike attributes or uniforms, varying variables are never touched by the Java program itself, as the values are always generated in the vertex shader. For a single program object, the output varyings from the vertex shader must match the input varyings in the fragment shader. If these do not match then an error will be generated when you link the program (and the program will most likely not be usable).

Using uniforms and attributes

You need to retrieve the locations for uniform and attribute variables before you can use them. This must be done after compiling and linking your program.

ByteBuffer name = toByteString(uniformName, true);
	int location = ARBShaderObjects.glGetUniformLocationARB(programId, name);

If this is sucessful, you should get a valid location. Invalid names should result in a location of -1 (and generally an error message in the log info). For attribute locations you use glGetAttribLocationARB instead, but it works the same.

Using uniform vars is much like setting regular OpenGL state:

ARBShaderObjects.glUniform1fARB(location, newValue);

As with most OpenGL data methods, theres various versions for different data types and lengths. Attribute vars are more tricky, because they are per-vertex. If you're rendering with vertex arrays you need to bind them much like you'd bind a colour or texture coord buffer:

ARBVertexProgram.glEnableVertexAttribArrayARB(location);
ARBVertexProgram.glVertexAttribPointerARB(location, size, isNormalised, 0, vertexData);

(note, the 'normalised' param indicates whether the data should be limited to a 0..1 range (like colours) or left as-is (like texture coords).

The following utility function will convert a Java String into a ByteBuffer version usable for retriving GLSL variable locations:

private ByteBuffer toByteString(String str, boolean isNullTerminated)
	{
		int length = str.length();
		if (isNullTerminated)
			length++;
		ByteBuffer buff = BufferUtils.createByteBuffer(length);
		buff.put( str.getBytes() );
 
		if (isNullTerminated)
			buff.put( (byte)0 );
 
		buff.flip();
		return buff;
	}

From testing, attrib locations need null-terminated strings, but binding uniform locations requires non-null-terminated strings to match variable names correctly. But that might just be a quirk of the current set of nVidia drivers.

Todo:

  • Explain fragment shader variables
  • Explain varying vars for vertex→fragment shader interaction.
  • Example vertex and fragment shaders
  • Explain Samplers for texture access.
 
lwjgl/tutorials/opengl/basicshaders.txt · Last modified: 2007/07/07 00:38 (external edit)
 
Recent changes RSS feed Creative Commons License Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki