The Cube
component needs a Material
to render it on the display. Our Cube
has separate colors for each face, defined as separate vertex colors. We'll define a VertexColorMaterial
instance and the corresponding shaders.
At a minimum, the OpenGL pipeline requires that we define a vertex shader, which transforms vertices from 3D space to 2D, and a fragment shader, which calculates the pixel color values for a raster segment. Similar to the simple shaders that we created in Chapter 3, Cardboard Box, we'll create two files, vertex_color_vertex.shader
and vertex_color_fragment.shader
. Unless you have done so already, create a new Android resource directory with the raw
type and name it raw
. Then, for each file, right-click on the directory, and go to New | File. Use the following code for each of the two files. The code for the vertex shader is as follows:
// File:res/raw/vertex_color_vertex.shader uniform mat4 u_Model; uniform mat4 u_MVP; attribute vec4 a_Position; attribute vec4 a_Color; varying vec4 v_Color; void main() { v_Color = a_Color; gl_Position = u_MVP * a_Position; }
The code for the fragment shader is as follows:
//File: res/raw/vertex_color_fragment.shader precision mediump float; varying vec4 v_Color; void main() { gl_FragColor = v_Color; }
The vertex
shader transforms each vertex by the u_MVP
matrix, which will be supplied by the Material
class's draw function. The fragment shader simply passes through the color specified by the vertex shader.
Now, we're ready to implement our first material, the VertexColorMaterial
class. Create a new Java class named VertexColorMaterial
in the renderbox/materials/
directory. Define the class as extends Material
:
public class VertexColorMaterial extends Material {
The methods we're going to implement are as follows:
Here's the complete code:
public class VertexColorMaterial extends Material { static int program = -1; static int positionParam; static int colorParam; static int modelParam; static int MVPParam; FloatBuffer vertexBuffer; FloatBuffer colorBuffer; int numIndices; public VertexColorMaterial(){ super(); setupProgram(); } public static void setupProgram(){ //Already setup? if (program != -1) return; //Create shader program program = createProgram(R.raw.vertex_color_vertex, R.raw.vertex_color_fragment); //Get vertex attribute parameters positionParam = GLES20.glGetAttribLocation(program, "a_Position"); colorParam = GLES20.glGetAttribLocation(program, "a_Color"); //Enable vertex attribute parameters GLES20.glEnableVertexAttribArray(positionParam); GLES20.glEnableVertexAttribArray(colorParam); //Shader-specific parameters modelParam = GLES20.glGetUniformLocation(program, "u_Model"); MVPParam = GLES20.glGetUniformLocation(program, "u_MVP"); RenderBox.checkGLError("Solid Color Lighting params"); } public void setBuffers(FloatBuffer vertexBuffer, FloatBuffer colorBuffer, int numIndices){ this.vertexBuffer = vertexBuffer; this.colorBuffer = colorBuffer; this.numIndices = numIndices; } @Override public void draw(float[] view, float[] perspective) { Matrix.multiplyMM(modelView, 0, view, 0, RenderObject.model, 0); Matrix.multiplyMM(modelViewProjection, 0, perspective, 0, modelView, 0); GLES20.glUseProgram(program); // Set the Model in the shader, used to calculate lighting GLES20.glUniformMatrix4fv(modelParam, 1, false, RenderObject.model, 0); // Set the position of the cube GLES20.glVertexAttribPointer(positionParam, 3, GLES20.GL_FLOAT, false, 0, vertexBuffer); // Set the ModelViewProjection matrix in the shader. GLES20.glUniformMatrix4fv(MVPParam, 1, false, modelViewProjection, 0); // Set the normal positions of the cube, again for shading GLES20.glVertexAttribPointer(colorParam, 4, GLES20.GL_FLOAT, false, 0, colorBuffer); // Set the ModelViewProjection matrix in the shader. GLES20.glUniformMatrix4fv(MVPParam, 1, false, modelViewProjection, 0); GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, numIndices); } public static void destroy(){ program = -1; } }
The setupProgram
method creates an OpenGL ES program for the two shaders that we created in res/raw/
directory—vertex_color_vertex
and vertex_color_fragment
. It then gets references to the positionParam
, colorParam
, and MVPParm
shader variables using the GetAttribLocation
and GetUniformLocation
calls that provide memory locations within the shader program, which are used later for drawing.
The setBuffers
method sets the memory buffers for vertices that define an object that will be drawn using this material. The method assumes that an object model consists of a set of 3D vertices (X, Y, and Z coordinates).
The draw()
method renders the object specified in the buffers with a given set of
model-view-perspective (MVP) transformation matrices. (Refer to the 3D camera, perspective, and head rotation section of Chapter 3, Cardboard Box, for detailed explanations.)
You may have noticed that we aren't using that ShortBuffer
function mentioned earlier. Later on, materials will use the glDrawElements
call along with an index buffer. glDrawArrays
is essentially a degenerate form of glDrawElements
, which assumes a sequential index buffer (that is, 0, 1, 2, 3, and so on). It is more efficient with complex models to reuse vertices between triangles, which necessitates an index buffer.
For completeness, we will also provide a destroy()
method for each of the Material
classes. We will come to know exactly why the material must be destroyed a little later.
As you can see, Material
encapsulates much of the lower level OpenGL ES 2.0 calls to compile the shader script, create a render program, set the model-view-perspective matrices in the shader, and draw the 3D graphic elements.
We can now implement the Camera
component.