Using Shaders to Calculate Model's Position

From LWJGL
Jump to: navigation, search

This tutorial explains how to use shaders to handle positioning objects in the game world. Given the player is always updating their position it may seem calculating player position would be more of a burden than positioning stationary objects such as tables and crates, etc. If you look at your code, however, you will find that any transformations required to place objects in the game world are called as many times as transformations required to locate the player; that is, once every game loop. The calculations, furthermore, are not that trivial; translations along the three axis require constructing a 4x4 floating point matrix which gets multiplied with the modelview matrix; rotations require floating point matrices for every axis rotated around, and each matrix involves two sin calculations and two cos calculations. It makes sense therefore to move as much of these calculations as reasonable off to the GPU.

1) Calls such glTranslatef() cannot be used in shaders so we will be dealing directly with the matrices themselves. To grasp what is happening we will start by writing code that positions objects by manipulating matrices on the CPU side, then migrate the code to a vertex shader.

The full code for the first part can be accessed here: [1]. It is fully commented, so rather than repeating it here, I will examine and explain only relevant aspects of it. Specifically the class named Box.java.

2) First, we need to understand how matrices used in openGL can be represented in Java.

private float[] mat={       1,0,0,0,
                      0,1,0,0,
                      0,0,1,0,
                      0,0,0,1};

mat represents the identity matrix, the same matrix used when glLoadIdentity() is called. Although it is displayed here as a float array, the indexes into the array are non-standard because matrices in openGL are column major. The following is the same 4x4 matrix filled with column major index positions:

index positions of mat   {      0,4, 8,12,
					 1,5, 9,13,
					 2,6,10,14,
					 3,7,11,15};

3) It is worth noting that understanding matrix math is not a requirement of understanding which index needs to be altered to effect different transformations. Knowing which indices need to be altered and how they need to be altered, is.

The relevant information to alter the x, y, z location, heading, pitch and roll can be found in the setter methods. Before looking at them a brief explanation of heading, pitch and roll might be in order. A ship’s steering wheel or rudder is there to effect the ship’s heading… basically, the direction it is heading. Pitch is the up down tilt of the ship as it plunges down, and climbs up the waves. Roll is the nauseating side to side leaning which when added with the pitch finds you chundering all over the deck.


4) The index positions of the FloatBuffer put() method can be mapped to the index positions of the identity matrix above to reveal which indices of the matrix affect the various transformations.

public void setPos(float x, float y, float z){
        posMat.put(12, x);
        posMat.put(13, y);
        posMat.put(14, z);
}

The top three values of the right most column, for example, affect the x, y, and z positions of the object.


5) It is generally noted that the glTranslate/Rotate methods are more efficient than using the glMultMatrix method. All being equal, sure; but things are not equal. Specifically, the trig functions will be called for every loop of the game if the glRotate functions are used to position the object; the code above calculates the trig values at initialization, stores them in the respective matrices, and reduces calculations to glMultMatrix calls. Further efficiency could be gained by multiplying all the matrices into a single position/rotation matrix at initialization, leaving just a single glMultMatrix call needed in the draw method. Probably, glGenLists would be even more efficient. Academic really, we want to migrate all of this onto the graphics card, and that trumps them all.


6) To understand the migration we need to examine the vertex shader first. If you need to figure out how to set up LWJGL to run shaders look here: [2].

7) The primary purpose of vertex shaders is to calculate the position of every vertex of every object you are rendering relative to the camera, or more simplistically, in the screen. The piece of code responsible for this is:

gl_Position = gl_ModelViewProjectionMatrix*gl_Vertex;

The Projection matrix is responsible for setting perspective (wide angle lens, zoom lens, sort of). When multiplied with the ModelView matrix you have the ModelViewProjectionMatrix. The ModelView matrix is the matrix that gets set to the identity matrix by glLoadIdentity, and is the matrix that contains the object’s position when the object is positioned using glTranslate/Rotate. Inasmuch we are performing all the positioning transformations in the shader, the ModelViewProjectionMatrix will be the identity matrix multiplied by the Projection matrix.

gl_Vertex is an attribute variable that holds the position of the vertex currently being processed. This value is set by the GL11.glVertex3f(x, y, z); calls in the draw method.


8) We handle positioning objects by creating the same 4x4 matrices we created in Box.java, but we create them in the shader. pos contains the x, y, z values that define the objects location, rot holds the degrees to rotate the object around the x, y, and z axis. mat4x4 position=mat4x4(1.0); creates an identity matrix named position. To which we replace the x, y, z values as we did in Box.java. Once the matrices are setup we multiply the ModelViewProjectionMatrix with them, and with gl_vertex to give the final position of the vertex being processed

uniform vec3 pos;
uniform vec3 rot;
varying vec4 vertColor;

void main(){
    mat4x4 position=mat4x4(1.0);
    position[3].x=pos.x;
    position[3].y=pos.y;
    position[3].z=pos.z;
    mat4x4 heading=mat4x4(1.0);
    heading[0][0]=cos(rot.y);
    heading[0][2]=-(sin(rot.y));
    heading[2][0]=sin(rot.y);
    heading[2][2]=cos(rot.y);
    mat4x4 pitch=mat4x4(1.0);
    pitch[1][1]=cos(rot.x);
    pitch[1][2]=sin(rot.x);
    pitch[2][1]=-(sin(rot.x));
    pitch[2][2]=cos(rot.x);
    mat4x4 roll=mat4x4(1.0);
    roll[0][0]=cos(rot.z);
    roll[0][1]=sin(rot.z);
    roll[1][0]=-(sin(rot.z));
    roll[1][1]=cos(rot.z);

    gl_Position= gl_ModelViewProjectionMatrix*position*heading*pitch*roll*gl_Vertex;
    vertColor = vec4(0.6,0.5,0.3,1.0f);
}

9) Note the uniform qualifier in front of pos and rot. It means that the values for pos and rot will be set in the Java program, not the shader. Which begs the question, what is the interface between shaders running GLSL on the graphics card, and Java programs running on the CPU?

10) The reason the vertex shader was examined first was, the uniform qualified variables begin their life in vertex and fragment shader programs. The code for these shaders is read and linked into an over arching shader program. This program is declared to be in use, then, and only then (as far as I can see), is the uniform variable accessed from within the Java program and initialized. Also, each uniform variable (as far as I can see), is accessed and initialized in turn.


11) See the above link for an explanation of setting up shaders in LWJGL. Here I will only examine the accessing and initializing uniform variables which was not covered there.


12) Accessing and setting the shader’s uniform variables occurs in Box’s draw method. The complete code for this shader side of the tutorial can be accessed here: [3].

13) We are particularly interested in the code at the head of the draw method:

public void draw(){
  GL11.glLoadIdentity();
  if(shaderAccess.useShader()) {
    ARBShaderObjects.glUseProgramObjectARB(
                    shaderAccess.getShader());
}

  int pos=ARBShaderObjects.glGetUniformLocationARB(
                      shaderAccess.getShader(), "pos");
  if(pos!=-1){
    ARBShaderObjects.glUniform3fARB(pos,posx, posy, posz);
  }else ARBShaderObjects.glUseProgramObjectARB(0);

int rot=ARBShaderObjects.glGetUniformLocationARB( 
                   shaderAccess.getShader(), "rot");
  if(rot!=-1){
    ARBShaderObjects.glUniform3fARB(rot,pitch, heading, roll);
  }else ARBShaderObjects.glUseProgramObjectARB(1);

a) First we declare the shader in use.

b) When the vertex and fragment shaders containing uniform variables are successfully linked and compiled into the overarching shader program, a table is set up containing indexes to those uniform variables. This index must be got using the name of the uniform variable as it is written in the vertex or fragment shader.

c) If the uniform variable is not found in the table a value of -1 will be returned so we check for it. Please note the code linked above uses the incorrect >0, which works but is incorrect.

d) If the variable is in the table we can set its value using one of the predefined openGL setter methods.

e) Note, I had problems getting the code to work until I accessed and initialized the variables one at a time.

13) Well that’s about it really, just a few thoughts. Specifically, using the graphics card to calculate the position of objects in the game world lightens the load upon the CPU reasonably significantly. But it bears rather more heavily upon the GPU, for whereas the CPU only has to calculate the position of the object once per game loop, the vertex shader has to perform the same computation once per loop for every single vertex contained in the object. There are several mitigating factors that lead me to think (provisionally) that it is still a good idea: a) GPU’s are thoroughly parallel and are designed to handle these calculations in a way the CPU is not

b) bottle necks in graphics game performance seem to be related to fragment shaders not keeping pace with all the texture related computations asked of them, and since vertex shaders run prior to fragment shaders they might be underutilized.

c) the real bottlenecks probably still lie with the CPU and dare I say Java, so anything to lighten its load, even if it costs more to the GPU is a worthwhile trade so long as the GPU can handle the increased workload.