Chapter 3-That last chapter was pretty shady. Let's learn about Shaders!

Shaders are a necessary part of LWJGL and Computer Graphics as a whole. Here, you'll learn about what they are, what they're used for, and how to use them for your LWJGL projects

I know last chapter ended abruptly, and we didn't get to see a proper end to it. The reason for that is because on its own, OpenGL can't actually do anything with the Textures we've loaded. In order to apply the texture to our Godly Pointy Point, we need to use Shaders.

Now, Shaders are a means of further manipulating the content on-screen. Initially, their use was to handle lighting and shadow calculations, hence the name. What they are is code that alters the way the content of the screen is rendered. In OpenGL/LWJGL, Shaders are written in the OpenGL Shading Language (GLSL), and are broken down into three main types:

  • Vertex Shaders, which handle the processing of the individual vertices.

  • Geometry Shaders, which handle the processing of primitive shapes.

  • Fragment Shaders, which handles the processing of the fragments, and is responsible for textures, shading, glossiness, and various other cool neatness.

So, going into our project, we'll need to make a new package called tutorials.render.shader, and create a new class called Shader.

This class is going to require the following to be imported:

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.nio.FloatBuffer;

import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL20;
import org.lwjgl.util.vector.Matrix4f;
import org.lwjgl.util.vector.Vector3f;

First thing you'll need to know is that this class will be an abstract class. Now, an abstract class is a class that itsef can't be instanced, however it can be subclassed. This means the class is a template that allows other classes to share code and be swapped out.

Right now, we'll need the following declaration and methods in our Shader class:

public abstract class Shader{
	private int programID;
	private int vertexID;
	private int fragmentID;
	
	private FloatBuffer matrix = BufferUtils.createFloatBuffer(16);
	
	public Shader(String Vert, String Frag){
	
	}

	public void start(){
		GL20.glUseProgram(programID);
	}
	
	public void stop(){
		GL20.glUseProgram(0);
	}
	
	protected abstract void bindAttributes();
	
	protected abstract void getAllUniformLocations();
}

Now, this is a very basic scaffolding of how a Shader class looks. In the Constructor for the Shader class, we have two arguments, Vert and Frag. These are going to be the locations that the Vertex and Fragment GLSL shader files are located.

By now you should start noticing recurring themes with how OpenGL operates. However, when I first started LWJGL Programming back in 2010 I would get fucking lost. But you're smarter than me. You know that the programID, vertexID, and fragmentID are the IDs for the Shader Program itself, as well as the Vertex and Fragment shader code.

Continuing on with common themes, we see the start() and stop() methods, and we know that by binding the programID in glUseProgram(...), we're telling OpenGL that it's going to be rendering files with that shader, and when we set it to 0, we're going back to default render mode.

Now, the abstract classes aren't used here, and the reason for that is when we write the class for our actual Shader, we'll need to have common methods for binding Attributes and getting the Uniform locations. We'll go into what those two mean soon.

Now, these aren't the only methods we're gonna be using. We'll also need a method called loadShader(String file, int type) And we'll find that RIGHT HERE!

private static int loadShader(String file, int type) {
	StringBuilder shaderSource = new StringBuilder();
	try {
		BufferedReader reader = new BufferedReader(new FileReader("res/shaders/"+file));
		String line;
		while((line = reader.readLine()) !=null) {
			shaderSource.append(line).append("\n");
		}
		reader.close();
	}catch(IOException e){
		System.err.println("Can't read file");
		e.printStackTrace();
		System.exit(-1);
	}
	int ID = GL20.glCreateShader(type);
	GL20.glShaderSource(ID, shaderSource);
	GL20.glCompileShader(ID);
	if(GL20.glGetShaderi(ID, GL20.GL_COMPILE_STATUS)==GL11.GL_FALSE) {
		System.out.println(GL20.glGetShaderInfoLog(ID, 512));
		System.err.println("Couldn't compile the shader");
		System.exit(-1);
	}
	return ID;
}

Gosh that's a lot of new stuff! But don't worry, let's explain everything.

A StringBuilder basically takes the data that BufferedReader is taking from our Shader file, and loading everything into the String shaderSource as one string of code to send to GLSL.

Once it's done, our buffered reader will close. Now in the event that your program can't read the file, it'll throw an IOException and shut down. If the shader can't load or compile, it's only going to bugger up the system.

We create the ID for the shader with glCompileShader, and the type tells OpenGL whether it's compiling a Vertex, Geometry, or Fragment shader. With the ID set, we use glShaderSource(...) to send the shaderSource string and then glCompileShader(...) compiles it and sends it off to the GPU.

Finally, we check the status of the shader with GL_COMPILE_STATUS to make sure the shader doesn't have errors. If it does, it prints out the error log. If not, it returns the ID of the shader.

Now we need to put together our shaders into a full Shader Program. Going back into our constructor, we'll be writing the following code:

vertexID = loadShader(Vert,GL20.GL_VERTEX_SHADER);
fragmentID = loadShader(Frag, GL20.GL_FRAGMENT_SHADER);
programID = GL20.glCreateProgram();
GL20.glAttachShader(programID, vertexID);
GL20.glAttachShader(programID, fragmentID);
bindAttributes();
GL20.glLinkProgram(programID);
GL20.glValidateProgram(programID);
getAllUniformLocations();

We pass the Vert and Frag file locations to loadShader(...), and let it know what type of shader it's compiling. Next, we create the programID through glCreateProgram(). In order to link the individual shaders to the program, we use glAttachShader(...) and have it tie both shader IDs to the programID.

In the shader class files that build off of this, they'll then use the bindAttributes() method to link all components of the shader together. With everythind done, we use glLinkProgram(...) and glValidateProgram(...) to finalize everything, and then we use getAllUniformLocations() to get all the Uniform variables within the shader.

So, we've got a decent chunk of the shader stuff done and ready. However, we've still got work to do. Next, we'll make two protected methods called bindAttribute(String name) and getUniformLocation(int attribute, String name). A protected method is a method that can only be called by subclasses, meaning anything that isn't based off of the Shader class can't access these methods. In these methods, you'll write the following code:

protected int getUniformLocation(String uniformName) {
	return GL20.glGetUniformLocation(programID, uniformName);
}
	
protected void bindAttribute(int attribute, String variableName) {
	GL20.glBindAttribLocation(programID, attribute, variableName);
}

What these methods do is they get the Uniform Variable Location and Attribute Variable Location of variables in the GLSL shader. A Uniform Variable is a per-primitive parameter that is used during the whole drawing call, whereas a Attribute Variable is per-vertex, and refers to the UV, Color, Positions, and what have you. This is how you pass data to and from the GLSL shader. Now, what data is passed normally?

Floats, Vectors, Matrices, and Booleans. Now we'll need to make methods for sending these data types to our GLSL shader. Add the following methods to your Shader class:

protected void loadFloat(int location, float value) {
	GL20.glUniform1f(location, value);
}
	
protected void loadVector(int location, Vector3f vector) {
	GL20.glUniform3f(location, vector.x, vector.y, vector.z);
}
	
protected void loadBoolean(int location, boolean value) {
	float tovec = 0;
	if(value) {
		tovec = 1;
	}
	GL20.glUniform1f(location, tovec);
}
	
protected void loadMatrix(int location, Matrix4f value) {
	value.store(matrix);
	matrix.flip();
	GL20.glUniformMatrix4(location, false, matrix);
}

These methods are pretty straightforward. When writing to a Uniform variable, you'll use the relevant glUniformXYZ(...) methods. Since GLSL doesn't natively support Boolean variables, however, we have to use a float with either a 1 or 0 instead, and interpret it as such in GLSL.

Now we're almost done. However, we do need to make a two new files and one class for our Shader. Let's start by creating a class in the tutorials.render.shader package called ShaderTextured.

package tutorials.render.shader;

public class ShaderTextured extends Shader{
	
	public ShaderTextured() {
		super("Textured.vs", "Textured.fs");
	}

	@Override
	protected void bindAttributes() {
		super.bindAttribute(0, "position");
		super.bindAttribute(1, "textureCoords");
	}

	@Override
	protected void getAllUniformLocations() {}
}

Now, as mentioned before, we're loading the Vertex and Fragment Shaders to OpenGL, and binding two Attributes, the position and uvs. For now, that's all we need to do, so let's get started on writing some shaders!

For the shader files, the extension names don't matter so long as they're not .java or .class. For the sake of this tutorial, we'll be using .vs and .fs for the extensions. To create these, first create a new folder in the root directory by clicking the project name in the sidebar and going to New->Folder and make a folder named res for resources. Next, make another folder inside of that named shaders. Now, click on the shaders folder and add two new files by going to New->File, and create one file named Textured.vs, and another named Textured.fs.

First, we'll do the Vertex Shader class, Textured.vs

Textured.vs
#version 450 core

in vec3 position;
in vec2 uvs;

out vec2 pass_uvs;

void main(void){
	gl_Position = vec4(position, 1.0);
	pass_uvs = uvs;
}

Now, the #version determines what version of GLSL we're using. In this case, we're using version 450 core. Note that this line doesn't need a semicolon at the end.

in vecX references the attributes we mentioned in the bindAttributes() method.

Since GLSL is based on C, it follows the same conventions of needing a main(void) method for the code to execute. In this class, we're only telling OpenGL what the gl_Position variable is, and sending uvs to the Fragment shader through pass_uvs. Now, let's look at Textured.fs:

#version 450 core

in vec2 pass_texCoords;

out vec4 out_Color;

uniform sampler2D textureSampler;

void main(){
	out_Color = texture(textureSampler,pass_texCoords);
}

The new thing we're seeing is uniform sampler2D, which is how OpenGL gets texture data. In order to tell it what to paint the specific fragment, we set out_Color to texture(textureSampler, pass_uvs).

Congrats, you've made your first shader! Now let's go back to the main game loop in our Boot class and set things up!

Boot.java
int texture = Texture.loadTexture("coffee.png");

while(!GLFW.glfwWindowShouldClose(window)) {
			GL11.glClear(GL11.GL_COLOR_BUFFER_BIT|GL11.GL_DEPTH_BUFFER_BIT);
			
			GL30.glBindVertexArray(meshmeyek.getVaoID());
			GL20.glEnableVertexAttribArray(0);
			GL20.glEnableVertexAttribArray(1)
			GL13.glActiveTexture(GL13.GL_TEXTURE0);
			GL11.glBindTexture(GL11.GL_TEXTURE_2D, texture);
			GL11.glDrawElements(GL11.GL_TRIANGLES, meshmeyek.getVertexCount(), GL11.GL_UNSIGNED_INT,0);
			GL20.glDisableVertexAttribArray(0);
			GL20.glDisableVertexAttribArray(1);
			GL30.glBindVertexArray(0);
			
			GLFW.glfwSwapBuffers(window);
			GLFW.glfwPollEvents();
}

As we saw in the Mesh class, the texture Coordinates are located in Attribute Location 1. By activating GL_TEXTURE0 and using glBindTexture(...) with the texture we loaded in texture, we now have everything ready!

Once we compile, we'll be greeted with a beautiful image:

Congratulations! You've textured your first Triangle! In the next tutorial, we'll start to build a proper render method, and begin to build a true game engine.

Last updated