2D Chapter 0-Projection Matrices

Let's resize our window! Wait, something's not right...

Alright, gents! If you've tried changing the dimensions of your window, you've probably noticed by now that there's some issues.

So, the first thing we gotta do is go into our Window class and add the following:

public int width;
public int height;
public int aspectRatio;

private GLFWWindowSizeCallback windowSize;

The first two integers are pretty self-explanatory, as they correspond to the width and height of our GLFW window. The aspectRatio, however, refers to the aspect ratio of the screen, whether it's 4:3 like an old school monitor or TV, or 16:9 like most TVs and monitors today. The means of getting this ratio will be dividing the width of your window by its height.

The GLFWWindowSizeCallback, however, is going to be our way for GLFW and LWJGL to know the new dimensions of the screen, and update them accordingly.

Going into our init() method, we'll set the first three variables at the beginning:

Window.java
this.width = width;
this.height = height;
this.aspectRatio = width/height;

Now, we'll go down to the end of the init() method and initialize our windowSize callback. This gets a little bit weird, but bear with me:

Window.java
//This goes after the closing bracket of the "try(MemoryStack stack = stackPush()){" block

GLFW.glfwSetWindowSizeCallback(window, windowSize = new GLFWWindowSizeCallback(){
    @Override
    public void invoke(long window, int width, int height){
        Boot.window.height = height;
        Boot.window.width = width;
        Boot.window.aspectRatio = width/height;
        GL11.glViewPort(0,0,Boot.window.width, Boot.window.height);
    }
}

//After this will be the GL.createCapabilities(); call

Awesome! Now we have a resizable window, right?

Now, you're probably thinking "Coffee you dingdong, that's even worse!" And to that I say yes, yes it is. To go into a bit of detail as to why the window is squashing things the way it is, you need to understand that right now, when we draw to the OpenGL viewPort, we're telling it to draw relative to the screen size. Right now, the screen will always cover the content being drawn between X=-1,Y=-1 and X=1,Y=1. If you were to draw a square that's went from -1 to +1 on the screen, it will always cover 100% of the window.

So how do we fix this? By using a thing known as a Projection Matrix. Now, for lack of better words, a Projection Matrix provides a means of "correcting" the way OpenGL natively displays graphics. The mathematics are fairly complicated, and unfortunately to go into details about how Projection Matrices and Matrix logic in general works would require a tutorial in and of itself.

Now, back in the days of LWJGL 2, LWJGL natively included the lwjgl_util library, which provided classes for Matrices, Vectors, and a myriad of useful resources for mathematics. With the release of LWJGL 3 they have since stopped developing this library. In place of it we will be using the Java Open Math Library (JOML), pre-compiled jar file found here.

Once you add JOML to your build path, same as how you installed LWJGL, go into the Shader class and add the following method:

Shader.java
public void loadMatrix(int location, Matrix4f value){
	this.matrix = BufferUtils.createFloatBuffer(16);
	value.get(matrix);
	GL20.glUniformMatrix4fv(location, false, matrix);
}

Remember that FloatBuffer we made in Chapter 3 that had absolutely no explanation or reasoning behind it? Yeah, short story long I was originally going to include Projection Matrices in that chapter, however I had forgotten that LWJGL 3 had removed the lwjgl_util library. So I spent a while writing a Matrix class until I came across JOML, which works excellently for our project here.

Basically, we take a Matrix4f as the value we're loading to OpenGL. Since OpenGL requires Matrices to be sent to GPU in FloatBuffers, we use get(...) to set our matrix FloatBuffer to the contents of our input Matrix. Once that's done, we ship it off to the GPU through glUniformMatrix4fv. Now, let's go into the ShaderTextured class and set up a method for loading the Projection Matrix.

ShaderTextured.java
//Put this near the beginning of the class, before the constructor
private int locationProjection

/**
*After the bindAttributes method, we'll be writing the following
*/

public void setProjection(Matrix4f mat) {
	this.loadMatrix(locationProjection, mat);
}
	
@Override
protected void getAllUniformLocations() {
	this.locationProjection = this.getUniformLocation("projection");
}

In order to send the Projection Matrix data, we have to create a method that can access the protected loadMatrix method, which we'll be using the new setProjection(...) method.

If you recall in Chapter 3, we created an unused method called getAllUniformLocations. This method allows us to get all of the locations for Uniform Variables within the shader program in a way we can pass data to and from. The name can be anything, but for simplicity sake we'll use "projection".

Let's open up the Textured.vs shader file and see how we implement this!

Textured.vs
#version 450 core

in vec3 position;
in vec2 uvs;

out vec2 pass_uvs;

uniform mat4 projection;

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

Not that hard, right? When recieving uniform variables in GLSL, the variable typing has to start with uniform instead of in or out. Once we have the variable in our shader, we merely have to set gl_Position to projection * vec4(position,1.0);

KEEP IN MIND THAT THE ORDER MATTERS FOR THIS TYPE OF MULTIPLICATION! I don't know why, but it does, and ordering things differently will lead to the math messing up.

Thankfully, we don't need to alter the Fragment Shader for this, as it only impacts the vertices of the geometry.

Now, let's mosy on back to the Window class and add a method for creating a Projection Matrix.

Window.java
public Matrix4f getProjectionMatrix() {
	Matrix4f matrix =new Matrix4f().ortho2D(-this.width/2, this.width/2, -this.height/2,this.height/2);
	return matrix;
}

What this method does is create an Orthographic 2D Matrix. An orthographic matrix is a flat projection matrix that transforms geometry to flat, uniform shapes unaffected by location or distance. For this reason, this type of Matrix will not work in most types of 3D game engines, and likewise the primary matrix type for 3D games (View Matrix) will not work for 2D game engines.

When we use ortho2D(...), we're setting the dimensions of the new area OpenGL is rendering to. So, instead of rendering from -1,-1 to 1,1, it'll render from -(width/2),-(height/2) to (width/2),(height/2). So on the default size we open the game at, we now have a 640x480 space to draw meshes.

We're done here, so let's go to the Render class! This one's really easy, right after shader.start(), you put the following line:

Render.java
shader.setProjection(Boot.window.getProjectionMatrix());

This allows our Shader to get the Projection Matrix before rendering the meshes. Now, before we compile, we'll need to change our sample mesh to accomodate the new screen size.

Back in the Boot class, we'll change the Vertices, Indices, and UVs of the mesh to the following:

Boot.java
float[] vertices = {-50f,-50f,0f,
				50f, -50f, 0,
				-50f,50f,0f,
				50,50,0};
int[] indices = {0,1,2,
				1,2,3};
		
		float[] UVs = {0f,1f,
						1f, 1f,
						0f,0f,
						1f,0f};

Now that we're done, let's compile everything and see what we get!

Congratulations! We now have a window that scales without warping the contents of the screen! There's other things we can do, such as scaling the image to prevent getting a wider view of the game world, but we'll focus on that on a later date. In our next tutorial, I'll show you how to make a simple and easy to use tile grid, and we'll begin to take the first steps towards making a 2D game!

Last updated