Draw the cube with OpenGL ES 2.0

The previous part of this tutorial discussed how to draw in 3-D using OpenGL ES 1.1. You can also do the same thing using OpenGL ES 2.0, and this section describes this alternate approach.

OpenGL ES 2.0 requires more upfront work in comparison to OpenGL ES 1.1. But OpenGL ES 2.0 is more flexible and powerful, although our sample app doesn’t really take advantage of these advanced features.

Define constants and functions

First, in main.c, we need to include a library, string.h, to access some functions such as memset().

#include <string.h>

Next, we declare some variables for tasks that are specific to OpenGL ES 2.0. We need a program object, a vertex buffer ID, and a color buffer ID. We also need three placeholder variables to connect to the shading language:

  • The mvpLoc variable is a handle for the modelview-projection matrix.
  • The positionLoc variable is a handle for the vertex position.
  • The colorLoc variable is a handle for the pixel color of the vertex.

We declare all these variables on the global scope and as static, so that each function can use the variables and so that they exist for the life of the program. If some of these variables already exist in the template, just make sure they are defined as in the code below:

static GLuint program;
static GLuint vertexID;
static GLuint colorID;

static GLuint mvpLoc;
static GLuint positionLoc;
static GLuint colorLoc;

Since we're using OpenGL ES 2.0, some of the functions for matrix transformations that are available in OpenGL ES 1.1 aren't available here, so we need to create them ourselves. To help us out, we define a glMatrix structure to represent the matrices we want to use. Then, we define three matrices to use: the projection matrix, modelview matrix, and a combination of both. Last, we declare variables to store the current EGL surface width and height.

typedef struct{
GLfloat mat[4][4];
}glMatrix;

static glMatrix *projectionMat;
static glMatrix *modelviewMat;
static glMatrix *mvpMat;

static EGLint surface_width, surface_height;

Now, we can use the glMatrix structure and write some functions to perform the matrix transformations we need. We define a function for multiplying matrices that returns the product of two matrices passed to it. This function re-creates glMultMatrix() from the OpenGL ES 1.1 API. It takes the srcA and srcB matrices as input parameters, multiplies them, and returns the multiplied matrix in result.

void multMatrix(glMatrix *result, glMatrix *srcA, glMatrix *srcB)
{
    glMatrix    tmp;
    int         i;

    for (i=0; i<4; i++)
    {
        tmp.mat[i][0] = (srcA->mat[i][0] * srcB->mat[0][0]) +
                        (srcA->mat[i][1] * srcB->mat[1][0]) +
                        (srcA->mat[i][2] * srcB->mat[2][0]) +
                        (srcA->mat[i][3] * srcB->mat[3][0]) ;

        tmp.mat[i][1] = (srcA->mat[i][0] * srcB->mat[0][1]) +
                        (srcA->mat[i][1] * srcB->mat[1][1]) +
                        (srcA->mat[i][2] * srcB->mat[2][1]) +
                        (srcA->mat[i][3] * srcB->mat[3][1]) ;

        tmp.mat[i][2] = (srcA->mat[i][0] * srcB->mat[0][2]) +
                        (srcA->mat[i][1] * srcB->mat[1][2]) +
                        (srcA->mat[i][2] * srcB->mat[2][2]) +
                        (srcA->mat[i][3] * srcB->mat[3][2]) ;

        tmp.mat[i][3] = (srcA->mat[i][0] * srcB->mat[0][3]) +
                        (srcA->mat[i][1] * srcB->mat[1][3]) +
                        (srcA->mat[i][2] * srcB->mat[2][3]) +
                        (srcA->mat[i][3] * srcB->mat[3][3]) ;
    }
    memcpy(result, &tmp, sizeof(glMatrix));
}

Next, we create a function that's equivalent to glLoadIdentity(), and our new function loads the result matrix with an identity matrix. For more information about glLoadIdentity(), see the API reference on the Khronos Group website. Also, we create a function that's equivalent to glScale(), and our function takes the result matrix and performs the same matrix multiplication as glScale() does. For more information on scaling matrices, see the glScale() API reference.

void loadIdentity(glMatrix *result)
{
    memset(result, 0x0, sizeof(glMatrix));
    result->mat[0][0] = 1.0f;
    result->mat[1][1] = 1.0f;
    result->mat[2][2] = 1.0f;
    result->mat[3][3] = 1.0f;
}

void scaleMatrix(glMatrix *result, GLfloat sx, GLfloat sy, GLfloat sz)
{
    result->mat[0][0] *= sx;
    result->mat[0][1] *= sx;
    result->mat[0][2] *= sx;
    result->mat[0][3] *= sx;

    result->mat[1][0] *= sy;
    result->mat[1][1] *= sy;
    result->mat[1][2] *= sy;
    result->mat[1][3] *= sy;

    result->mat[2][0] *= sz;
    result->mat[2][1] *= sz;
    result->mat[2][2] *= sz;
    result->mat[2][3] *= sz;
}

We also define a function that's equivalent to glRotate(), and our function takes the result matrix and the vector to rotate on and performs the rotation. We multiply the rotation matrix by the existing result matrix, and then store the multiplied matrices into result. For more information on the rotation matrix, see the glRotate() API reference.

void rotationMatrix(glMatrix *result, GLfloat angle, GLfloat x, GLfloat y, GLfloat z)
{
    GLfloat sinAngle, cosAngle;
    GLfloat mag = sqrtf(x * x + y * y + z * z);

    sinAngle = sin ( angle * M_PI / 180.0f );
    cosAngle = cos ( angle * M_PI / 180.0f );
    if ( mag > 0.0f )
    {
        GLfloat xx, yy, zz, xy, yz, zx, xs, ys, zs;
        GLfloat oneMinusCos;
        glMatrix rotMat;

        x /= mag;
        y /= mag;
        z /= mag;

        xx = x * x;
        yy = y * y;
        zz = z * z;
        xy = x * y;
        yz = y * z;
        zx = z * x;
        xs = x * sinAngle;
        ys = y * sinAngle;
        zs = z * sinAngle;
        oneMinusCos = 1.0f - cosAngle;

        rotMat.mat[0][0] = (oneMinusCos * xx) + cosAngle;
        rotMat.mat[0][1] = (oneMinusCos * xy) - zs;
        rotMat.mat[0][2] = (oneMinusCos * zx) + ys;
        rotMat.mat[0][3] = 0.0F;

        rotMat.mat[1][0] = (oneMinusCos * xy) + zs;
        rotMat.mat[1][1] = (oneMinusCos * yy) + cosAngle;
        rotMat.mat[1][2] = (oneMinusCos * yz) - xs;
        rotMat.mat[1][3] = 0.0F;

        rotMat.mat[2][0] = (oneMinusCos * zx) - ys;
        rotMat.mat[2][1] = (oneMinusCos * yz) + xs;
        rotMat.mat[2][2] = (oneMinusCos * zz) + cosAngle;
        rotMat.mat[2][3] = 0.0F;

        rotMat.mat[3][0] = 0.0F;
        rotMat.mat[3][1] = 0.0F;
        rotMat.mat[3][2] = 0.0F;
        rotMat.mat[3][3] = 1.0F;

        multMatrix( result, &rotMat, result );
    }
}

Finally, we define a function that's equivalent to glFrustum(), and our function sets up a perspective projection. The function we define takes the result matrix, sets up the perspective matrix, multiplies the two matrices, and returns the multplied matrix in result. For more information on the perspective matrix, see the glFrustum() API reference.

void frustumMatrix(glMatrix *result, float left, float right, float bottom, float top, float nearZ, float farZ)
{
    float       deltaX = right - left;
    float       deltaY = top - bottom;
    float       deltaZ = farZ - nearZ;
    glMatrix    frust;

    if ( (nearZ <= 0.0f) || (farZ <= 0.0f) ||
            (deltaX <= 0.0f) || (deltaY <= 0.0f) || (deltaZ <= 0.0f) )
            return;

    frust.mat[0][0] = 2.0f * nearZ / deltaX;
    frust.mat[0][1] = frust.mat[0][2] = frust.mat[0][3] = 0.0f;

    frust.mat[1][1] = 2.0f * nearZ / deltaY;
    frust.mat[1][0] = frust.mat[1][2] = frust.mat[1][3] = 0.0f;

    frust.mat[2][0] = (right + left) / deltaX;
    frust.mat[2][1] = (top + bottom) / deltaY;
    frust.mat[2][2] = -(nearZ + farZ) / deltaZ;
    frust.mat[2][3] = -1.0f;

    frust.mat[3][2] = -2.0f * nearZ * farZ / deltaZ;
    frust.mat[3][0] = frust.mat[3][1] = frust.mat[3][3] = 0.0f;

    multMatrix(result, &frust, result);
}

Initialize the model

Now that all the necessary matrix transformation functions are defined, we can write a function to initialize the model. We use the existing initialize() function from the template and modify it to match the code below. We need the EGL surface weight and height, so we call eglQuerySurface() for each dimension. After we obtain those values, we can get the clear depth and clear color of the model. We also allow polygons to be culled based on their window coordinates.

int initialize()
{
    eglQuerySurface(egl_disp, egl_surf, EGL_WIDTH, &surface_width);
    EGLint err = eglGetError();
    if (err != 0x3000) {
        return EXIT_FAILURE;
    }

    eglQuerySurface(egl_disp, egl_surf, EGL_HEIGHT, &surface_height);
    err = eglGetError();
    if (err != 0x3000) {
        return EXIT_FAILURE;
    }

    glClearDepthf(1.0f);
    glClearColor(0.0f,0.0f,0.0f,1.0f);

    glEnable(GL_CULL_FACE);

Next, we define our shader source as an array of strings. We start with our vertex shader, for which we use medium precision floating-point values. Then, we define a uniform 4x4 matrix for the modelview-projection matrix. We also define two four-value vector variables for the vertex position and color. We also define a v_color variable that we interpolate across the model, and we can access this variable from our fragment shader.

In the main() function of the shader, we set the vertex position, gl_Position, as the vertex position multiplied with the modelview-projection matrix, so that the position is normalized for the screen. We also set the interpolated varying color to the vertex color. In the fragment shader, we declare v_color, and then set the pixel color to that interpolated color.

    const char* vSource =
            "precision mediump float;"
    		"uniform mat4 u_mvpMat;"
            "attribute vec4 a_position;"
    		"attribute vec4 a_color;"
            "varying vec4 v_color;"
            "void main()"
            "{"
    		"gl_Position = u_mvpMat * a_position;"
            "v_color = a_color;"
            "}";

    const char* fSource =
            "varying lowp vec4 v_color;"
            "void main()"
            "{"
            "    gl_FragColor = v_color;"
            "}";

Now, we need to create the shaders, attach the source we defined, compile the program object, and check for any errors. We'll create the vertex shaders first, and then create the fragment shaders.

    GLint status;

    // Compile the vertex shader
    GLuint vs = glCreateShader(GL_VERTEX_SHADER);
    if (!vs) {
        fprintf(stderr, "Failed to create vertex shader: %d\n",
                glGetError());
        return EXIT_FAILURE;
    } else {
        glShaderSource(vs, 1, &vSource, 0);
        glCompileShader(vs);
        glGetShaderiv(vs, GL_COMPILE_STATUS, &status);
        if (GL_FALSE == status) {
            GLchar log[256];
            glGetShaderInfoLog(vs, 256, NULL, log);

            fprintf(stderr, "Failed to compile vertex shader: %s\n",
                    log);

            glDeleteShader(vs);
        }
    }

    // Compile the fragment shader
    GLuint fs = glCreateShader(GL_FRAGMENT_SHADER);
    if (!fs) {
        fprintf(stderr, "Failed to create fragment shader: %d\n",
                glGetError());
        return EXIT_FAILURE;
    } else {
        glShaderSource(fs, 1, &fSource, 0);
        glCompileShader(fs);
        glGetShaderiv(fs, GL_COMPILE_STATUS, &status);
        if (GL_FALSE == status) {
            GLchar log[256];
            glGetShaderInfoLog(fs, 256, NULL, log);

            fprintf(stderr, "Failed to compile fragment shader: %s\n",
                    log);

            glDeleteShader(vs);
            glDeleteShader(fs);

            return EXIT_FAILURE;
        }
    }

Now that the shaders are ready, we can create a program object, link the shaders, and check for any errors that might occur. If the linking is successful, we can delete the shaders, since they are already in the program object and it's not necessary to reserve the extra memory.

    // Create and link the program
    program = glCreateProgram();
    if (program)
    {
        glAttachShader(program, vs);
        glAttachShader(program, fs);
        glLinkProgram(program);

        glGetProgramiv(program, GL_LINK_STATUS, &status);
        if (status == GL_FALSE)    {
            GLchar log[256];
            glGetProgramInfoLog(fs, 256, NULL, log);

            fprintf(stderr, "Failed to link shader program: %s\n", log);

            glDeleteProgram(program);
            program = 0;

            return EXIT_FAILURE;
        }
    } else {
        fprintf(stderr, "Failed to create a shader program\n");

        glDeleteShader(vs);
        glDeleteShader(fs);
        return EXIT_FAILURE;
    }
    
    // We don't need the shaders anymore - the program is enough
    glDeleteShader(fs);
    glDeleteShader(vs);

With our shaders in the program object, we need handles to some of the variables we defined in the shader source, so that we can perform calculations and set the values of the variables in the shader source. We need one handle for the modelview-projection matrix, one for the vertex position, and one for the vertex color.

    mvpLoc = glGetUniformLocation(program, "u_mvpMat");
    positionLoc = glGetAttribLocation(program, "a_position");
    colorLoc = glGetAttribLocation(program, "a_color");

We need to generate buffer objects for the vertex and color data arrays. We bind the buffers to the IDs that we declared near the beginning of this part of the tutorial, and then load the data into the buffers with glBufferData().

    // Generate vertex and color buffers and fill with data
    glGenBuffers(1, &vertexID);
    glBindBuffer(GL_ARRAY_BUFFER, vertexID);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices,
                 GL_STATIC_DRAW);

    glGenBuffers(1, &colorID);
    glBindBuffer(GL_ARRAY_BUFFER, colorID);
    glBufferData(GL_ARRAY_BUFFER, sizeof(colors), colors,
                 GL_STATIC_DRAW);

To mimic how this sample application is done in OpenGL ES 1.1, we define a projection matrix and a modelview matrix, and then combine the two. We pass the combination of the matrices to the u_mvpMat variable from the shader source.

First, we allocate memory for the matrix and load the identity matrix. Then, we set the value we will use to define a perspective projection, and we call frustumMatrix() to produce the perspective projection. We also multiply that matrix by a scaling matrix using scaleMatrix(), so that the model is scaled correctly. We make sure to select the correct x and y scaling values based on the screen size.

    projectionMat = malloc( sizeof(glMatrix) );
    loadIdentity( projectionMat );

    GLfloat aspect = (float)surface_width/(float)surface_height;
    GLfloat near = -2.0f;
    GLfloat far  = 2.0f;
    GLfloat yFOV  = 75.0f;
    GLfloat height = tanf( yFOV / 360.0f * M_PI ) * near;
    GLfloat width = height * aspect;


    frustumMatrix( projectionMat, -width, width, -height, height, near,
                   far );

    if ( surface_width > surface_height ){
        scaleMatrix( projectionMat, (float)surface_height/(float)surface_width,
                     1.0f, 1.0f );
    } else {
        scaleMatrix( projectionMat, 1.0f,
                     (float)surface_width/(float)surface_height, 1.0f );
    }

Now we set up the modelview matrix. At this point, we allocate memory for this matrix and load it with the identity matrix, and we'll apply the matrix transformations for the modelview matrix later. Since we will combine the projection and modelview matrices, we need to allocate memory for that matrix too.

    modelviewMat = malloc( sizeof(glMatrix) );
    loadIdentity( modelviewMat );
    mvpMat = malloc( sizeof(glMatrix) );

    return EXIT_SUCCESS;
}

Rotate the model

Now, let's implement the render() function. First, we specify the viewport using 0,0 as the lower-left corner and the EGL surface width and height as the top-right corner. We clear the color and depth buffers to the values we set in the initialize() function. Then we call glUseProgram() to use the program object that has our shaders attached to it.

void render(){
    glViewport(0, 0, surface_width, surface_height);

    //Typical render pass
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    glUseProgram(program);

We also bind the vertex position and vertex color handles to the buffers we defined earlier:

    // Enable and bind the vertex information
    glEnableVertexAttribArray(positionLoc);
    glBindBuffer(GL_ARRAY_BUFFER, vertexID);
    glVertexAttribPointer(positionLoc, 3, GL_FLOAT, GL_FALSE,
                          3 * sizeof(GLfloat), 0);

    // Enable and bind the color information
    glEnableVertexAttribArray(colorLoc);
    glBindBuffer(GL_ARRAY_BUFFER, colorID);
    glVertexAttribPointer(colorLoc, 4, GL_FLOAT, GL_FALSE,
                          4 * sizeof(GLfloat), 0);

Now, we perform the matrix transformation on the modelview matrix, by calling rotationMatrix(). This function rotates our view of the cube so that it appears to rotate on the screen. We need to invert the x and y values of the rotation so that the cube rotates in the correct direction. When the modelview matrix is ready, we multiply the projection matrix and the modelview matrix. Then, we load our calculated modelview-projection matrix into the shader and call glDrawArrays() to draw the model.

    rotationMatrix( modelviewMat, 1.0f, -1.0f, -1.0f, 0.0f);
    multMatrix( mvpMat, modelviewMat, projectionMat);
    glUniformMatrix4fv(mvpLoc, 1, false, &mvpMat->mat[0][0]);

    // Same draw call as in GLES1.
    glDrawArrays(GL_TRIANGLES, 0 , 36);

Last, we disable the attribute arrays for vertex position and vertex color, and we swap the EGL surface buffers to display our changes to the model.

    // Disable attribute arrays
    glDisableVertexAttribArray(positionLoc);
    glDisableVertexAttribArray(colorLoc);

    bbutil_swap();
}

In the main application loop, we call render() on each iteration to display the changes to the model. When our program exits the main application loop (for example, when the user closes the app), remember to free the memory for the matrices we allocated.

    while (!exit_application) {
        //Request and process BPS next available event
        bps_event_t *event = NULL;
        for(;;) {
            if (BPS_SUCCESS != bps_get_event(&event, 0)) {
                fprintf(stderr, "bps_get_event failed\n");
                break;
            }
        
            if (event) {
                int domain = bps_event_get_domain(event);

                if (domain == screen_get_domain()) {
                    handleScreenEvent(event);
                } else if ((domain == navigator_get_domain())
                             && (NAVIGATOR_EXIT ==
                                 bps_event_get_code(event))) {
                    exit_application = 1;
                }
            } else {
                break;
            }
        }
        render();
    }
    
    free(projectionMat);
    free(modelviewMat);
    free(mvpMat);

We're finished! Build and run the app to see the result: your own multicolored rotating cube, created using OpenGL ES 2.0.


Device image showing the finished 3-D app.

Last modified: 2014-09-29



Got questions about leaving a comment? Get answers from our Disqus FAQ.

comments powered by Disqus