Renderer class - Fish-In-A-Suit/Conquest GitHub Wiki

Prerequisite knowledge

Class layout

Imports

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

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

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

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

  • shaders.ShaderProgram;

  • utils.FileUtilities;

Private instance fields

  • ShaderProgram shaderProgram;
  • Transformations transformation
  • double angleOfView
  • float FOVY
  • float zNear
  • float zFar

Public instance fields

/

Constructors

  • Renderer()

Private methods

/

Public methods

  • void init()
  • void clear()
  • void render(Window window, GameEntity[] entities)
  • void cleanup()
  • void runAssertions()

Explanation

The Renderer class provides the functionality to draw onto the window.

public Renderer()

This constructor initializes the transformation field by creating a new instance of Transformations.

void init()

This method is responsible for setting up the shaders. As the rest of the rendering process is determined upon successfully created shaders, it should be called prior to the main game loop (more specifically, it is called from the init() method of the window.

shaderProgram = new ShaderProgram(); creates a new instance (object) of the ShaderProgram class. It sets the programID of that instance to a value specified by glCreateProgram(). Then, it prints out that programID to console for debugging purposes. The glCreateProgram() method returns 0 if a shader program couldn't be created. Therefore, if programID is 0, an exception "Couldn't create shader! (class ShaderProgram)" is thrown:

	public ShaderProgram() throws Exception {
		programID = glCreateProgram();
		System.out.println("programID: " + programID);
		
		if (programID == 0) {
			throw new Exception("Couldn't create shader! (class ShaderProgram)");
		}
	} 

Now, explanation for this piece of code: shaderProgram.createVertexShader(FileUtilities.loadResource("/shaders/vertexShader.vs"));. Obviously, the public void createVertexShader(String shaderPath) throws Exception is called by the shaderProgram instance. The FileUtilities.loadResource("/shaders/vertexShader.vs") part specifies the full String file path to the specified file (vertexShader.vs). So, long story short, the full file path of the vertexShader.vs file is passed into the createVertexShader(String shaderPath) method of class ShaderProgram:

	public void init() throws Exception {
		shaderProgram = new ShaderProgram();
		shaderProgram.createVertexShader(FileUtilities.loadResource("/shaders/vertexShader.vs"));
		shaderProgram.createFragmentShader(FileUtilities.loadResource("/shaders/fragmentShader.fs"));
		shaderProgram.link();
	}

Inside the createVertexShader method, the following code is executed: vertexShaderID = createShader(shaderPath, GL_VERTEX_SHADER);. It creates a new shader of the specified type (GL_VERTEX_SHADER), specifies the source file for that shader, compiles the shader and attaches the shader to the currently running program (programID). Further explanation can be found in the ShaderProgram class.

//in ShaderProgram class
	public void createVertexShader(String shaderPath) throws Exception {
		vertexShaderID = createShader(shaderPath, GL_VERTEX_SHADER);
	}
//in ShaderProgram class
	public void createFragmentShader(String shaderPath) throws Exception {
		fragmentShaderID = createShader(shaderPath, GL_FRAGMENT_SHADER);
	}
//in ShaderProgram class
	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;
	}

The same is repeated to create a fragment shader, using the fragmentShader.fs file: shaderProgram.createFragmentShader(FileUtilities.loadResource("/shaders/fragmentShader.fs"));

So, in the end we end up with a shader program (programID) with attached vertex and fragment shader.

Then, the link() method of the shaderProgram instance is called: shaderProgram.link(). The attached vertex shader (GL_VERTEX_SHADER) and fragment shader (GL_FRAGMENT_SHADER) are used to create an executable that will run on the programmable vertex/fragment processor (this is performed by glLinkProgram(programID) method inside the ShaderProgram class). Then, the executables contained in programID, which had been created by glLinkProgram(programID) are checked whether they can execute given the current OpenGL state by glValidateProgram(programID).

After that, uniform locarions are defined ("pathways from Java code to the shader uniform variables arecreated"). This is achieved by the method void defineUniformLocations(). It calls the createUniform(String uniform) method of type ShaderProgram for each uniform variable to be used:

private void defineUniformLocations() {
    shaderProgram.createUniform("translationMatrix");
    shaderProgram.createUniform("rotationMatrix");
    shaderProgram.createUniform("projectionMatrix");
}

void clear()

This method clears the background colour of the window. It is called from the start of the render

glClearColor(1.0f, 1.0f, 1.0f, 1.0f); specifies the bit depths of blue, red, green and alpha channels of GL_COLOR_BUFFER_BIT. The parameters are ordered as follows: red, green, blue, alpha. The specified values can range from 0.0f to 1.0f. 0.0f indicates the lowest colour intensity (black), while 1.0f indicates the highest colour intensity (white). The alpha value indicates transparency (0.0f == fully transparent, 1.0f == opaque/non-transparent).

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) sets the background color of the window to the values specified in the GL_COLOR_BUFFER_BIT. Ignore the GL_DEPTH_BUFFER_BIT for now.

void render(Window window, GameEntity[] entities)

This method handles window resize events and draws the specified GameEntity instance onto the window (using it's mesh and position/rotation/scale).

First, the background colour is cleared: clear();.

Then, the window resize events are handled:

   	if (window.isResized()) {
   		glViewport(0, 0, window.getWidth(), window.getHeight());
   		window.setResized(false);
   	}

If the value of resize field if window instance is true (checked by: window.isResized(),), then the contents of the if-clause will be executed. If the window is resized, you need to take care of adjusting the viewport (the area captured by the borders of the windows onto which the image is rendered). This is done by the glViewport(int x, int y, int width, int height) method, where:

  • x, y: specify the lower left corner of the viewport rectangle
  • width, height: specify the width and height of the viewport - to capture the whole area of the window, set them to the width and height of the window.

After resizing the viewport, the resize field of the window is set to false by calling window.setResized(false).

Then, the methods which collectively produce the seen image on the window are called. First, the bind() method is called on shaderProgram (shaderProgram.bind()), which in turn calls the method glUseProgram(programID). It installs the program object specified by programID as part of the current rendering state. Take a further look in the ShaderProgram class explanation.

//bind() method of ShaderProgram class
public void bind() {
    glUseProgram(programID);
}

Then, the projection matrix is created out of FOVY, width of the window, height of the window, zFar and zNear values:

Matrix4f projectionMatrix = transformation.getProjectionMatrix(FOVY, window.getWidth(), window.getHeight(), zNear, zFar);

Then, projectionMatrix is sent to the vertex uniform projection matrix using the setUniform method for type ShaderProgram: shaderProgram.setUniformMatrix("projectionMatrix", projectionMatrix);

Why is the perspective projection matrix updated each rendering call you say? a) because window dimensions change if it is resized and b) because one might want to switch between different perspectives ie. third person and first person.

Then, every GameEntity instance in the entities array is rendered. This is achieved through the for loop. First, the transation and rotation matrices are set based on the entities position and rotation and are sent to the vertex shader. Then, the entity is rendered:

for(GameEntity entity : entities) {
	Matrix4f translationMat = transformation.getTranslationMatrix(entity.getPosition());
	shaderProgram.setUniformMatrix("translationMatrix", translationMat);
	Matrix4f rotationMat = transformation.getRotationMatrix(entity.getRotation().x, entity.getRotation().y, entity.getRotation().z);
	shaderProgram.setUniformMatrix("rotationMatrix", rotationMat);
	entity.getMesh().render();
}

When the render method is called (entity.getMesh().render()), the vao of the instance of type Mesh is bound to the context (enabled) by glBindVertexArray: glBindVertexArray(getVaoID());. Then, the glEnableVertexAttribArray(0); enables the use of the vertex attribute list of the active vao (which had been defined by mesh.getVaoID()). Then, the indices buffer referenced as indicesVboID is bound to the ELEMENT_ARRAY_BUFFER target in the context: glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mesh.getIndicesVboID());.

glBindVertexArray(getVaoID());
glEnableVertexAttribArray(0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mesh.getIndicesVboID());

With all of the components for rendering in place, a call is made to glDrawElements method to draw the image to the back-screen buffer: glDrawElements(GL_TRIANGLES, getVertexCount(), GL_UNSIGNED_INT, 0);. When glDrawElements is called, it uses "getVertexCount()" (which equals the length of the indices array) sequential elements from an enabled array object, starting at 0 (the last parameter - 0 means the first element in the array) to construct a sequence of geometric primitives specified by the first parameter. If more than one array is enabled, each of them is used.

`glDrawElements(GL_TRIANGLES, mesh.getVertexCount(), GL_UNSIGNED_INT, 0);`

After the image is drawn, everything is disabled in the reverse order in which they were enabled:

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
glDisableVertexAttribArray(0);
glBindVertexArray(0);
shaderProgram.unbind();

void cleanup()

This method cleans the shader program from the memory by calling its cleanup() method: shaderProgram.cleanup(). First, the program is specified as not being used anymore: glUseProgram(0); and then deleted: glDeleteProgram(programID);. This method should be called in the shutdown process of the program.

public void cleanup() {
	if (shaderProgram != null) {
		shaderProgram.cleanup();
	}
}
//cleanup() and unbind() of ShaderProgram class

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

public void unbind() {
	glUseProgram(0);
}	

Debugging:

  • System.out.println("Name of the active program object: " + glGetInteger(GL_CURRENT_PROGRAM)); returns the name of the program object that is currently active or 0 if no program object is active.