ShaderProgram - Fish-In-A-Suit/Conquest GitHub Wiki

Prerequisite knowledge

Class layout

Imports

  • import static org.lwjgl.opengl.GL20.*;

  • import static org.lwjgl.opengl.GL11.*;

  • import static org.lwjgl.opengl.GL15.*;

  • import static org.lwjgl.opengl.GL30.*;

  • import static org.lwjgl.opengl.GL40.*;

  • import java.nio.FloatBuffer;

  • import java.util.HashMap;

  • import java.util.Map;

  • import math.Matrix4f;

  • import utils.BufferUtilities;

Private instance fields

  • final int programID
  • vertexShaderID
  • fragmentShaderID
  • final Map<String, Integer> uniforms

Public instance fields

/

Constructors

  • ShaderProgram() throws Exception

Private methods

Protected methods

  • int createShader(String shaderPath, int shaderType) throws Exception

Public methods

  • void createVertexShader(String shaderPath) throws Exception
  • void createFragmentShader(String shaderPath) throws Exception
  • void link() throws Exception
  • void bind()
  • void unbind()
  • void cleanup()
  • void getProgramID()

Explanation

At the point of creation of any mesh instance (when it's implemented constructor is called), the value of programID is determined by glCreateProgram(). If this method returns 0, no program was created, which is handled as an exception:

public ShaderProgram() throws Exception {
	programID = glCreateProgram();
		
	if (programID == 0) {
		throw new Exception("Couldn't create shader!");
	}
} 

glCreateProgram creates an empty program object and returns a non-zero value by which it can be referenced. A program object is an object to which shader objects can be attached. This provides a mechanism to specify the shader objects that will be linked to create a program. It also provides means for checking the compatibility of the shaders that will be used to create a program (for instance, checking the compatibility between a vertex shader and a fragment shader). When no longer needed as part of a program object, shader objects can be detached.

One or more executables are created in a program object by successfully attaching shader objects to it with glAttachShader, successfully compiling the shader objects with glCompileShader, and successfully linking the program object with glLinkProgram. These executables are made part of current state when glUseProgram is called. Program objects can be deleted by calling glDeleteProgram. The memory associated with the program object will be deleted when it is no longer part of current rendering state for any context.

createShader(String shaderPath, int shaderType)

  • shaderType can be one of GL_VERTEX_SHADER, GL_TESS_CONTROL_SHADER, GL_TESS_EVALUATION_SHADER, GL_GEOMETRY_SHADER, GL_FRAGMENT_SHADER or GL_COMPUTE_SHADER
  • shaderPath is the file path of the specified shader file

This method creates, populates with source code, compiles the source code and attaches a shader object of particular type to the currently active program.

First, it declares a new variable called shaderID and initializes it with the value provided by glCreateShader(shaderType). The method ps int glCreateShader(int type) creates an empty shader object and returns a non-zero value by which it can be referenced. A shader object is used to maintain the source code strings that define a file of that shader. The type parameter indicates the type of shader to be created.

Then, the success of creating a new shader object is eavluated by checking the value of shaderID. If it's 0, the shader object couldn't be created --> exception:

if (shaderID == 0) {
	throw new Exception("Error creating shader. Type: " + shaderType);
}

After that, the empty shader object has to be populated with source code. This is achieved with the method void glShaderSource(int shader, java.lang.CharSequence string):

  • shader: the shader object to which the source code should be written from the array of strings specified by string
  • string: sets the source code of a shader object specified by shader to the source code in the array of strings specified by string. It copies the contents of the specific shader file to the actual shader object - say you had a vertexShader.vs file and a vertexShaderID shader object reference, you would copy the contents of vertexShader.vs into the vertex shader referenced by vertexShaderID.

Now, we already have the source code specified in an existent shader object. That source code, however, cannot be run in its raw form. It is written in a high-language called GLSL, devised to enable us easier programming. The machine can't understand this high-level language. So the raw source code of the shader has to be compiled so as the "machine" can read and use it. This is achieved by glCompileShader(int shader) method. It compiles the source code strings which have been stored in the shader object of the specified shader object (reference it as an int!):

glCompileShader(shaderID);

If the compilation of the specified shader was unsuccessful, the value of its GL_COMPILE_STATUS parameter will be 0 --> if that is true, then throw an exception:

if (glGetShaderi(shaderID, GL_COMPILE_STATUS) == 0) {
	throw new Exception("Error compiling shader code: " + glGetShaderInfoLog(shaderID, 1024));
}

int glGetShaderi(int shader, int pname) method returns a parameter from a shader object.

  • shader: the shader object to be queried
  • pname: the object parameter, one of: SHADER_TYPE, DELETE_STATUS, COMPILE_STATUS, SHADER_SOURCE_LENGTH, INFO_LOG_LENGTH

Attaching shaders to programs is achieved with the method: void glAttachShader(int program, int shader)

  • program: the program object to which a shader object will be attached
  • shader: the shader object that is to be attached

The glGetShaderInfoLog(int shader, int maxLength) returns the information log for a specified shader object, with the specified number of bytes (characters) limit which will be displayed (maxLength).

In order to create a complete shader program, there must be a way to specify the list of things that will be linked together. Program objects provide this mechanism. Shaders that are to be linked together in a program object must first be attached to that program object. glAttachShader(int program, int shader) attaches the shader object specified by shader to the program object specified by program. This indicates that shader will be included in link operations that will be performed on program:

glAttachShader(programID, shaderID);

All operations that can be performed on a shader object are valid whether or not the shader object is attached to a program object. It is permissible to attach a shader object to a program object before source code has been loaded into the shader object or before the shader object has been compiled. It is permissible to attach multiple shader objects of the same type because each may contain a portion of the complete shader. It is also permissible to attach a shader object to more than one program object. If a shader object is deleted while it is attached to a program object, it will be flagged for deletion, and deletion will not occur until glDetachShader is called to detach it from all program objects to which it is attached.

After the shader is attached to the program, the reference value for that shader is returned:

return shaderID;

The full code of the method createShader(String shaderPath, int shaderType)

protected int createShader(String shaderPath, int shaderType) throws Exception {
	int shaderID = glCreateShader(shaderType);
	
	if (shaderID == 0) {
		throw new Exception("Error creating shader. Type: " + shaderType);
	}
		
	glShaderSource(shaderID, shaderPath);
	glCompileShader(shaderID);
	
	if (glGetShaderi(shaderID, GL_COMPILE_STATUS) == 0) {
		throw new Exception("Error compiling shader code: " + glGetShaderInfoLog(shaderID, 1024));
	}
	
	glAttachShader(programID, shaderID);
	return shaderID;
}

createVertexShader(String shaderPath)

This method assigns an int value to the vertexShaderID field, which references an existing vertex shader object. To achieve this, the createShader method is called (note that shaderPath can be specified by the user as a parameter to the method, whereas the GL_VERTEX_SHADER parameter is fixed):

public void createVertexShader(String shaderPath) throws Exception {
	vertexShaderID = createShader(shaderPath, GL_VERTEX_SHADER);
}

createFragmentShader(String shaderPath)

This method assigns an int value to the fragmentShaderID field, which references an existing fragment shader object. To achieve this, the createShader method is called (note that shaderPath can be specified by the user as a parameter to the method, whereas the GL_FRAGMENT_SHADER parameter is fixed):

public void createFragmentShader(String shaderPath) throws Exception {
	fragmentShaderID = createShader(shaderPath, GL_FRAGMENT_SHADER);
}

link()

glLinkProgram(programID) links the program object specified by programID. It is called from the init() method of the **Renderer **class, after the shader objects are created.

If any shader objects of type GL_VERTEX_SHADER are attached to program, they will be used to create an executable that will run on the programmable vertex processor. If any shader objects of type GL_GEOMETRY_SHADER are attached to program, they will be used to create an executable that will run on the programmable geometry processor. If any shader objects of type GL_FRAGMENT_SHADER are attached to program, they will be used to create an executable that will run on the programmable fragment processor.

The status of the link operation will be stored as part of the program object's state. This status can be queried by performing the following:

if (glGetProgrami(programID, GL_LINK_STATUS) == 0) {
	throw new Exception("Error linking shader code: " + glGetProgramInfoLog(programID, 1024));
}

int glGetProgrami(int program, int pname) returns a parameter for the program object. Look in the Retrieving object parameters section of OpenGL object to see the full method description.

  • glGetProgrami(programID, GL_LINK_STATUS) returns 1 if the last link operation performed on programID was successful, or 0 if unsuccessful.

  • glGetProgramInfoLog(programID, 1024) returns the information log for the program object specified by programID, of max length 1024 lines.

If any of the shader objects don't exist at this point (their ID is 0), they should be detached from the program to which they are currently attached to. This is done with the method glDetachShader(int program, int shader), which detaches a shader object from a program object to which it's attached:

if (vertexShaderID != 0) {
	glDetachShader(programID, vertexShaderID);
}
		
if (fragmentShaderID != 0) {
	glDetachShader(programID, fragmentShaderID);
}

void glValidateProgram(int program) checks to see whether the executable contained in program can execute given the current OpenGL state: glValidateProgram(programID); The information generated by the validation process will be stored in program's information log.

The status of the validation operation will be stored as part of the program object's state. This value will be set to 1 if the validation succeeded, and 0 otherwise. It can be queried by calling glGetProgram with arguments program and GL_VALIDATE_STATUS. If validation is successful, program is guaranteed to execute given the current state. Otherwise, program is guaranteed to not execute.

if (glGetProgrami(programID, GL_VALIDATE_STATUS) == 0) {
	System.out.println("Warning validating shader code: " + glGetShaderInfoLog(programID, 1024));
}

bind()

This method contains only one method: glUseProgram(programID). It is called at the very start of the render() method of Renderer, prior to issuing the draw call.

It installs the program object programID as part of the current rendering state. One or more executables are created in a program object by successfully attaching shader objects to it with glAttachShader, successfully compiling the shader objects with glCompileShader, and successfully linking the program object with glLinkProgram.

A program object will contain an executable that will run on the vertex processor if it contains one or more shader objects of type GL_VERTEX_SHADER that have been successfully compiled and linked. A program object will contain an executable that will run on the geometry processor if it contains one or more shader objects of type GL_GEOMETRY_SHADER that have been successfully compiled and linked. Similarly, a program object will contain an executable that will run on the fragment processor if it contains one or more shader objects of type GL_FRAGMENT_SHADER that have been successfully compiled and linked.

While a program object is in use, applications are free to modify attached shader objects, compile attached shader objects, attach additional shader objects, and detach or delete shader objects. None of these operations will affect the executables that are part of the current state. However, relinking the program object that is currently in use will install the program object as part of the current rendering state if the link operation was successful (see glLinkProgram ). If the program object currently in use is relinked unsuccessfully, its link status will be set to 0, but the executables and associated state will remain part of the current state until a subsequent call to glUseProgram removes it from use. After it is removed from use, it cannot be made part of current state until it has been successfully relinked.

unbind()

This method contains only one line of code: glUseProgram(0);. It's called at the end of the render() method of Renderer, after the draw call had been issued. It removes the current program object from the current rendering state.

cleanup()

This method first unbinds the current program from the rendering state (unbind()) and then, if the programID isn't already 0, deletes it:

public void cleanup() {
	unbind();
	if (programID != 0) {
		glDeleteProgram(programID);
	}
}	

createUnifrom(String unfiromName)

This method creates a new uniform location for a desired uniform variable the path (name) of which is specified as a parameter to this method. Note that the uniformName must exactly match with the name of the corresponding uniform variable specified in the shader.

public void createUniform(String uniformName) throws Exception {
	int uniformLocation = glGetUniformLocation(programID, uniformName);
		
	if (uniformLocation < 0) {
		throw new Exception("[ShaderProgram.createUniform]: Couldn't find uniform: " + uniformName);
	}
	uniforms.put(uniformName, uniformLocation);
}

uniformLocation holds an integer which represents the location of a specific uniform variable withing the program object. It is assigned the value using void glGetUniformLocation(int program, String name) method, where

  • program represents an active program object
  • name represents a null-terminated String with no spaces in between

Then the success of the above operation is checked. If OpenGL can't find a match (uniform variable) to the String specified as name, it returns -1.

Then, if the operation was successful, the name of the uniform variable along with the corresponding integer (which represents its location) are stored inside the uniforms Map.

void setUniformMatrix(String uniformName, Matrix4f inMatrix) {

    FloatBuffer matrixBuffer = BufferUtilities.storeDataInFloatBuffer(inMatrix.mat4fToArray());
glUniformMatrix4fv(uniforms.get(uniformName), false, matrixBuffer);

} The matrix is first stored inside a FloatBuffer and then "pushed" to the CONTINUE HERE