Android GL ES 2.0 02 Aug 2013

Continue cat

This is a continuating of my previous post, where I went over how to make a very basic graphics/game-ey engine with android GL ES 1.1, and integrate with JBox2D. I received a few requests to show how to do the same thing with ES 2.0, so I’ll go ahead and show exactly that.

GL ES 2.0 vs 1.1

OpenGL ES 2.0 has been avaliable on Android for a very long time, and according to the google opengl version dashboard, 99.8% of Android devices support both GL ES 2.0 and 1.1

I opted to stick with 1.1 in my previous tutorial, because of its simplicity and the lack of a need for 2.0 features. 2.0 does away with the fixed function pipeline, requiring you to manage your matrices and supply fragment and vertex shaders for rendering. It isn’t that difficult to understand, if you compare the pipeline offered by 1.1 with what you need to do in 2.0

We’ll start with a brief bit of theory, then dive in and modify the code from the previous tutorial. Our render routine currently consists of the following GL ES 1.1 related code:

1     gl.glMatrixMode(GL10.GL_MODELVIEW);
2     gl.glLoadIdentity();
3     gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);

Then we looped through each BaseObject, and performed the following:

 1     // Save the current state of things
 2     gl.glPushMatrix();
 3 
 4     // Enabling this allows us to give GL a pointer to an array containing our vertices
 5     // This is instead of manually drawing every triangle ourselves using glVertex3f() statements
 6     // which are missing from GL ES anyways. As far as I know (not that far), this is the only way
 7     // to actually draw in 1.1
 8     gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
 9 
10     // Move to where our object is positioned.
11     gl.glTranslatef(position.x, position.y, 1.0f);
12 
13     // Set the angle on each axis, 0 on x and y, our angle on z
14     gl.glRotatef(0.0f, 1.0f, 0.0f, 0.0f);
15     gl.glRotatef(0.0f, 0.0f, 1.0f, 0.0f);
16     gl.glRotatef(rotation, 0.0f, 0.0f, 1.0f);
17 
18     // Grab our color, convert it to the 0.0 - 1.0 range
19     Vec3 renderCol = color.toFloat();
20     gl.glColor4f(renderCol.x, renderCol.y, renderCol.z, 1.0f);
21 
22     gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertBuffer);
23     gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, vertices.length / 3);
24 
25     gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
26     gl.glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
27     gl.glPopMatrix();

What’s happening here is GL is managing matrices internally for us. When we call gl.glMatrixMode(GL10.GL_MODELVIEW) we state that we will be working with the ModelView matrix. The next glLoadIdentity() call loads the identity matrix into it. So on and so forth. Another example of this can be seen in our onSurfaceChanged() function:

1   gl.glViewport(0, 0, width, height);
2   gl.glMatrixMode(GL10.GL_PROJECTION);
3   gl.glOrthof(0, width, 0, height, -10, 10);

Here we are stating that we want to work with the Projection matrix, and then we load an orthographic projection into it. Internally, all of these matrices work together to render our objects. If you want to know more detail about how the matrices directly effect rendering (in great detail), check out this awesome article. I will go into it a bit here, but I don’t understand enough of it myself to come close to the level of detail that article presents. Pictures and diagrams abound.

So, to GL, the screen is a 1x1 square. Device coordinates are between 0.0f and 1.0f, and matrices serve to transform everything to fit into that space. Our object vertices are all centered around the origin, and so need to be transformed to their proper positions in space. Initially, vertices are rotated, transformed, and then projected by an ortho projection matrix. GL 1.1 handles the projection matrix, but you need to rotate and transform your objects manually. The glTranslatef() and glRotatef() calls in 1.1 mess with the ModelView matrix before rendering with glDrawArrays()

We’ll have to do all of that manually, but once you understand what is going on, it’s really quite simple. So, without further ado:

Shaders!

Shaders are programs that execute on the GPU, and serve to render our shapes. Since we are porting the old code to ES 2.0, we won’t go into too many details. All we need is a really simple set of shaders, to apply our matrices and color our shapes. We will create the ModelView and Projection matrices outside of the shader, and then pass them in. Two shaders are needed, a vertex and a fragment shader. The vertex shader needs to set the value of a special variable named gl_Position, and the frag shader one named gl_FragColor. The vertex shader positions the vertex, and the frag shader colors the primitive(simplification).

Our vertex shader:

1 attribute vec3 Position;
2 uniform mat4 Projection
3 uniform mat4 ModelView;
4 
5 void main() {
6   mat4 mvp = Projection * ModelView;
7   gl_Position = mvp * vec3(Position.xyz, 1);
8 }

And our fragment shader:

1 precision mediump float;
2 
3 uniform vec4 Color
4 
5 void main() {
6   gl_FragColor = Color;
7 }

These are written in a C-style language named GLSL. We can access attributes and uniforms from outside these programs, and will modify them before executing. Since they are programs, we will need to compile and link them together to make a shader program. Yes, this happens at runtime :)

Before we do anything else, we need to add an entry to our manifest stating that we use GL ES 2.0, so add this little snippet:

<uses-feature android:glEsVersion="0x00020000" android:required="true" />

We also need to state that we want to use ES 2.0 with our GL surface, so add this to the Main activity, right after creating the view:

view.setEGLContextClientVersion(2);

We also require a minimum API version of 8, so change that if necessary. Go ahead and add the programs as strings in our Renderer class:

 1   private static String vertShaderCode =
 2       "attribute vec3 Position;" +
 3       "uniform mat4 Projection;" +
 4       "uniform mat4 ModelView;" +
 5       "void main() {" +
 6       "  mat4 mvp = Projection * ModelView;" +
 7       "  gl_Position = mvp * vec4(Position.xyz, 1);" +
 8       "}\n";
 9 
10   private static String fragShaderCode =
11       "precision mediump float;" +
12       "uniform vec4 Color;" +
13       "void main() {" +
14       "  gl_FragColor = Color;" +
15       "}\n";

After building our shader program, we will need to keep a handle on it, and provide a method of fetching it from outside our renderer. We’ll also go ahead and make an projection matrix container, so add this to the Renderer class:

1   private static int shaderProg;
2   private static float[] projection = new float[16];
3 
4   public static int getShaderProg() { return shaderProg; }

We will build the program inside the onSurfaceCreated() method. Go ahead and change the gl.glClearColor to GLES20.glClearColor. All calls will be made to the GLES20 object, so we won’t be using GL10 anymore. We’ll first create a program and get a handle to it. Then we’ll compile each shader in turn, attach them to our program, link it, and request to use it.

You should make shaders for every individual use case, and switch between them when rendering, but beware doing so is slightly expensive. Generally you should batch render your objects based on which shader they use, but since we can stick with this one for the entire program, we’ll just use it from the beginning. The onSurfaceCreated() method now looks like this:

 1   @Override
 2   public void onSurfaceCreated(GL10 unused, EGLConfig eglConfig) {
 3 
 4     // Create program
 5     shaderProg = GLES20.glCreateProgram();
 6 
 7     // Compile shaders
 8     int vertShader = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER);
 9     GLES20.glShaderSource(vertShader, vertShaderCode);
10     GLES20.glCompileShader(vertShader);
11 
12     int fragShader = GLES20.glCreateShader(GLES20.GL_FRAGMENT_SHADER);
13     GLES20.glShaderSource(fragShader, fragShaderCode);
14     GLES20.glCompileShader(fragShader);
15 
16     // Attach shaders
17     GLES20.glAttachShader(shaderProg, vertShader);
18     GLES20.glAttachShader(shaderProg, fragShader);
19 
20     // Link and use the program
21     GLES20.glLinkProgram(shaderProg);
22     GLES20.glUseProgram(shaderProg);
23 
24     // Normal stuff
25     GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
26 
27     bob = new BoxObject(800, 50);
28     bob.color = new Color3(255, 0, 0);
29 
30     Physics.setGravity(new Vec2(0, -10));
31   }

The function names are self explanatory, so I won’t comment on the specifics. Note that we don’t do any error checking; shaders have no debugging facilities, and simply fail to link or compile with no specific errors. You can check for failure like so:

1   final int[] status = new int[1];
2 
3   // Pass vertShader or fragShader depending on which to check on
4   // Use GL_LINK_STATUS or GL_COMPILE_STATUS
5   GLES20.getShaderiv(vertShader, GLES20.GL_COMPILE_STATUS, status, 0);
6   if(status[0] == 0) {
7     // Failed
8   }

Now, we have our shaders, so we need to prepare our matrices. Inside our onSurfaceChanged() function, we were setting the internal Projection matrix using glOrthof() method. That is no longer avaliable to us in ES 2.0, but Android provides a helpful Matrix class that can do the same thing. We will get a handle on the Projection variable from our vert shader, fill our projection matrix using Matrix.orthoM, and then send the value to our shader. We also need to use the GLES20.glViewport function now, and take out the glMatrixMode() call. Our onSurfaceChanged() function now looks like:

 1   @Override
 2   public void onSurfaceChanged(GL10 gl, int width, int height) {
 3 
 4     GLES20.glViewport(0, 0, width, height);
 5 
 6     // Set ortho projection
 7     int projectionHandle = GLES20.glGetUniformLocation(shaderProg, "Projection");
 8     Matrix.orthoM(projection, 0, 0, width, 0, height, -10, 10);
 9     GLES20.glUniformMatrix4fv(projectionHandle, 1, false, projection, 0);
10 
11     screenW = width;
12     screenH = height;
13 
14     bob.setPosition(new Vec2(screenW / 2, 100));
15     bob.createPhysicsBody(0, 0.5f, 0.8f);
16   }

You’ll recognize the orthoM parameters, so no need to explain it again. Now our vertex shader holds a proper ortho Projection matrix for us to use. One more small change before we get into the meat of things, we need to use the GLES20 glClear, and remove the glMatrixMode() and glLoadIdentity() calls in our onDrawFrame() function. Go ahead and do that based on what you saw above, keeping the rest of the method intact.

A teensy bit of actual matrix manipulation

Since our vertex shader is handling the application of our projection matrix, all we need to do is rotate and translate our vertices before passing them in. Head over to our BaseObject’s draw() function, and clear out all gl related function calls. We’ll add three new ints to the BaseObject class, to get handles on the Position, Color, and ModelView variables in our shaders:

1   private int positionHandle = GLES20.glGetAttribLocation(Renderer.getShaderProg(), "Position");
2   private int colorHandle = GLES20.glGetUniformLocation(Renderer.getShaderProg(), "Color");
3   private int modelHandle = GLES20.glGetUniformLocation(Renderer.getShaderProg(), "ModelView");

After the if(body != null) check, go ahead and add in the following:

 1     // Construct modelView to be applied to every vertex
 2     float[] modelView = new float[16];
 3 
 4     // Equivalent of gl.glLoadIdentity()
 5     Matrix.setIdentityM(modelView, 0);
 6 
 7     // gl.glTranslatef()
 8     Matrix.translateM(modelView, 0, position.x, position.y, 1.0f);
 9 
10     // gl.glRotatef()
11     Matrix.rotateM(modelView, 0, rotation, 0, 0, 1.0f);

The comments should clear things up. This is the equivalent of our old gl.glTranslate/glRotate block. modelView is the matrix we are passing to our vert shader. Finish it off with:

1     // Load our matrix and color into our shader
2     GLES20.glUniformMatrix4fv(modelHandle, 1, false, modelView, 0);
3     GLES20.glUniform4fv(colorHandle, 1, color.toFloatArray(), 0);
4 
5     // Set up pointers, and draw using our vertBuffer as before
6     GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 0, vertBuffer);
7     GLES20.glEnableVertexAttribArray(positionHandle);
8     GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, vertices.length / 3);
9     GLES20.glDisableVertexAttribArray(positionHandle);

There you have it! Well, almost. See that color.toFloatArray() call there? That just returns the color as a series of floats, as before, but in a flat float array. Add that to our Color3 class, and we are done:

1   public float[] toFloatArray() {
2 
3     return new float[]{ (float)r / 255.0f, (float)g / 255.0f, (float)b / 255.0f, 1.0f };
4   }

Right. That’s all there is to it really. The shaders in GL 2.0 add a ton of flexibilty and power, putting you in control of the rendering pipeline. Of course, that is after you get over the initial learning curve. I hope this saves you some time :)

Final source files.