previous page: Creating the Cube

Drawing the Cube with GLView

Mathematical Functions for Matrices

After the model is initialized, create functionality to manipulate the scene. OpenGL ES 2.0 provided by GLView requires more preliminary work that the previous version of the library, but gives more power and flexibility, although our example does not take much benefit.

First, declare additional global variables for specific OpenGL ES 2.0 tasks: a program object, an identifier for the vertices buffer and another for the colors. Add also three variables to ensure the connection with the shader language:

  • mvpLoc is an identifier for model-view-projection matrix.
  • positionLoc is an identifier for the vertex position.
  • colorLoc is an identifier for the vertex color.

Declare all these variables in the GLData object:

struct _GLData
{
    GLuint       program;
    GLuint       vtx_shader;
    GLuint       fgmt_shader;
    GLuint       vbo;
    GLuint       vertexID;
    GLuint       colorID;
    GLuint       mvpLoc;
    GLuint       positionLoc;
    GLuint       colorLoc;
}

Since OpenGL ES 2.0, some functions for matrix transformations have been removed. Define three matrix functions to use: projection matrix, model-view matrix, and a combination of these allows you to perform any transformations on the initial vertices matrix.

Matrix Multiplication Function (glMultMatrix)

First, define a function that is able to return the inner product of two matrices. This function reproduces the behavior of glMultMatrix() available in OpenGL ES 1.1. This function is very useful since almost every matrix transformation can be translated as multiplications of matrices.

The function takes three arguments, one is for the result and the other 2 matrices are operands:

static void
customMutlMatrix(float matrix[16], const float matrix0[16], const float matrix1[16])
{
   int i, row, column;
   float temp[16];
   for (column = 0; column < 4; column++)
     {
        for (row = 0; row < 4; row++)
          {
             temp[column * 4 + row] = 0.0f;
             for (i = 0; i < 4; i++)
                 temp[column * 4 + row] += matrix0[i * 4 + row] * matrix1[column * 4 + i];
          }
     }
   for (i = 0; i < 16; i++)
        matrix[i) = temp[i];
}
Matrix Identity Function (glLoadIdentity)

Implement a function equivalent to glLoadIdentity() that replaces the current matrix with the identity matrix:

const float unit_matrix[] =
{
   1.0f, 0.0f, 0.0f, 0.0f,
   0.0f, 1.0f, 0.0f, 0.0f,
   0.0f, 0.0f, 1.0f, 0.0f,
   0.0f, 0.0f, 0.0f, 1.0f
};
 
static void
customLoadIdentity(float matrix[16])
{
   for (int i = 0; i < 16; i++)
        matrix[i] = unit_matrix[i];
}
Matrix Projection Function (glFrustum)

Since glFrustum has been depreciated, implement a function that produces perspective projection matrices that are used to transform from eye coordinate space to clip coordinate space. This matrix projects a portion of the space (the “fustum”) to your screen. Many caveats apply (normalized device coordinates, perspective divide, etc), but that is the idea:

static int
customFrustum(float result[16], const float left, const float right, const float bottom, const float top, const float near, const float far)
{
   if ((right - left) == 0.0f || (top - bottom) == 0.0f || (far - near) == 0.0f) return 0;
 
   result[0] = 2.0f / (right - left);
   result[1] = 0.0f;
   result[2] = 0.0f;
   result[3] = 0.0f;
 
   result[4] = 0.0f;
   result[5] = 2.0f / (top - bottom);
   result[6] = 0.0f;
   result[7] = 0.0f;
 
   result[8] = 0.0f;
   result[9] = 0.0f;
   result[10] = -2.0f / (far - near);
   result[11] = 0.0f;
 
   result[12] = -(right + left) / (right - left);
   result[13] = -(top + bottom) / (top - bottom);
   result[14] = -(far + near) / (far - near);
   result[15] = 1.0f;
 
   return 1;
}
Matrix Scaling Function (glScale)

Depreciated glScale() function represents a non-uniform scaling along the x, y, and z axes. The three parameters indicate the desired scale factor along each of the three axes:

const float scale_matrix[] =
{
   x,    0.0f, 0.0f, 0.0f,
   0.0f, y,    0.0f, 0.0f,
   0.0f, 0.0f, z,    0.0f,
   0.0f, 0.0f, 0.0f, 1.0f
}

Here is the implementation of the matrix scaling function:

static void
customScale(float matrix[16], const float sx, const float sy, const float sz)
{
    matrix[0]  *= sx;
    matrix[1]  *= sx;
    matrix[2]  *= sx;
    matrix[3]  *= sx;
 
    matrix[4]  *= sy;
    matrix[5]  *= sy;
    matrix[6]  *= sy;
    matrix[7]  *= sy;
 
    matrix[8]  *= sz;
    matrix[9]  *= sz;
    matrix[10] *= sz;
    matrix[11] *= sz;
}
Matrix Rotation Function (glRotate)

Define a function to represent a rotation by the vector (x y z). The current matrix is multiplied by a rotation matrix:

static void
customRotate(float matrix[16], const float anglex, const float angley, const float anglez)
{
    const float pi = 3.141592f;
    float temp[16];
    float rz = 2.0f * pi * anglez / 360.0f;
    float rx = 2.0f * pi * anglex / 360.0f;
    float ry = 2.0f * pi * angley / 360.0f;
    float sy = sinf(ry);
    float cy = cosf(ry);
    float sx = sinf(rx);
    float cx = cosf(rx);
    float sz = sinf(rz);
    float cz = cosf(rz);
 
    customLoadIdentity(temp);
 
    temp[0] = cy * cz - sx * sy * sz;
    temp[1] = cz * sx * sy + cy * sz;
    temp[2] = -cx * sy;
 
    temp[4] = -cx * sz;
    temp[5] = cx * cz;
    temp[6] = sx;
 
    temp[8] = cz * sy + cy * sx * sz;
    temp[9] = -cy * cz * sx + sy * sz;
    temp[10] = cx * cy;
 
    customMutlMatrix(matrix, matrix, temp);
}

Create the Shader

Define the source for the shader using a GLbyte array. First comes out vertex shader, which is used to a medium precision for float values. Then build a uniform matrix with dimensions 4×4 intended to hold the model-view-projection matrix. Also create two vector attributes which have 4 components for the vertex position and the color. This varying variable v_color can be accessed from the fragment shader. In the main function of the shader, initialize the position of the current vertex, gl_Position, with the product of the vertex position and the model-view-projection matrix, in order to normalize the position for the target screen. The pixel color is calculated by the varying variable from the vertex shader.

In the fragment shader, declare a varying variable, then set the color of the pixel with this interpolated color.

GLbyte vertex_shader[] =
    "#ifdef GL_ES\n"
    "precision mediump float;\n"
    "#endif\n"
    "attribute vec4 a_position;\n"
    "attribute vec4 a_color;\n"
    "uniform mat4 u_mvp_mat;\n"
    "varying vec4 v_color;\n"
    "void main()\n"
    "{\n"
    "   gl_Position = u_mvp_mat*a_position;\n"
    "   v_color = a_color;\n"
    "}";
GLbyte fragment_shader[] =
    "#ifdef GL_ES\n"
    "precision mediump float;\n"
    "#endif\n"
    "varying vec4 v_color;\n"
    "void main()\n"
    "{\n"
    "   gl_FragColor = v_color;\n"
    "}";

Create the shaders, attach the source code that is just defined and compile the program object:

static int
init_shaders(GLData *gld)
{
   Evas_GL_API *gl = gld->glapi;
 
   GLbyte vertex_shader[] =
       "#ifdef GL_ES\n"
       "precision mediump float;\n"
       "#endif\n"
       "attribute vec4 a_position;\n"
       "attribute vec4 a_color;\n"
       "uniform mat4 u_mvp_mat;\n"
       "varying vec4 v_color;\n"
       "void main()\n"
       "{\n"
       "   gl_Position = u_mvp_mat*a_position;\n"
       "   v_color = a_color;\n"
       "}";
   GLbyte fragment_shader[] =
       "#ifdef GL_ES\n"
       "precision mediump float;\n"
       "#endif\n"
       "varying vec4 v_color;\n"
       "void main()\n"
       "{\n"
       "   gl_FragColor = v_color;\n"
       "}";
 
   GLint compiled;
   const char *p;
 
   /*vertex_shader*/
   p = vertex_shader;
   gld->vtx_shader = gl->glCreateShader(GL_VERTEX_SHADER);
   gl->glShaderSource(gld->vtx_shader, 1, &p, NULL); //get vertex_shader source
   gl->glCompileShader(gld->vtx_shader); //compile
   //get compile status
   gl->glGetShaderiv(gld->vtx_shader, GL_COMPILE_STATUS, &compiled);
   //if NULL: error
   if (!compiled)
     {
        GLint info_len = 0;
        gl->glGetShaderiv(gld->vtx_shader, GL_INFO_LOG_LENGTH, &info_len);
        if (info_len > 1)
          {
             char* info_log = malloc(sizeof(char) * info_len);
             gl->glGetShaderInfoLog(gld->vtx_shader, info_len, NULL, info_log);
             printf("Error compiling shader:\n%s\n======\n%s\n======\n", info_log, p);
             free(info_log);
          }
        gl->glDeleteShader(gld->vtx_shader);
     }
 
   /*fragment_shader*/
   p = fragment_shader;
   gld->fgmt_shader = gl->glCreateShader(GL_FRAGMENT_SHADER);
   gl->glShaderSource(gld->fgmt_shader, 1, &p, NULL);
   gl->glCompileShader(gld->fgmt_shader);
   //get compile status
   gl->glGetShaderiv(gld->fgmt_shader, GL_COMPILE_STATUS, &compiled);
   //if NULL: error
   if (!compiled)
     {
        GLint info_len = 0;
        gl->glGetShaderiv(gld->fgmt_shader, GL_INFO_LOG_LENGTH, &info_len);
        if (info_len > 1)
          {
             char* info_log = malloc(sizeof(char) * info_len);
             gl->glGetShaderInfoLog(gld->fgmt_shader, info_len, NULL, info_log);
             printf("Error compiling shader:\n%s\n======\n%s\n======\n", info_log, p);
             free(info_log);
          }
        gl->glDeleteShader(gld->fgmt_shader);
     }

Once the shaders are ready, instantiate the program object and link the shaders. If the linking succeeds, you can destroy the shaders afterwards (using glDeleteShader). Since they are inside the program object, so it is pointless to keep them in memory.

    gld->program = gl->glCreateProgram();
 
    GLint linked;
    // Load the vertex/fragment shaders
    gl->glAttachShader(gld->program, gld->vtx_shader);
    gl->glAttachShader(gld->program, gld->fgmt_shader);
 
    gl->glDeleteShader(gld->vtx_shader);
    gl->glDeleteShader(gld->fgmt_shader);
 
    gl->glBindAttribLocation(gld->program, 0, "a_position");
    gl->glLinkProgram(gld->program);
    gl->glGetProgramiv(gld->program, GL_LINK_STATUS, &linked);
    if (!linked)
      {
         GLint info_len = 0;
         gl->glGetProgramiv(gld->program, GL_INFO_LOG_LENGTH, &info_len);
         if (info_len > 1)
           {
              char* info_log = malloc(sizeof(char) * info_len);
              gl->glGetProgramInfoLog(gld->program, info_len, NULL, info_log);
              printf("Error linking program:\n%s\n", info_log);
              free(info_log);
           }
         gl->glDeleteProgram(gld->program);
         return 0;
      }

For shader process, create identifiers for the attribute variables used in the shader program. First create an identifier for the model-view-projection matrix, another one for the current vertex position, and a last one for the vertex color.

    gld->mvpLoc     = gl->glGetUniformLocation(gld->program, "u_mvp_mat");
    gld->positionLoc = gl->glGetAttribLocation(gld->program, "a_position");
    gld->colorLoc   = gl->glGetAttribLocation(gld->program, "a_color");

Finally, generate the buffers for the vertex positions and colors.

   gl->glGenBuffers(1, &gld->vertexID);
   gl->glBindBuffer(GL_ARRAY_BUFFER, gld->vertexID);
   gl->glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
 
   gl->glGenBuffers(1, &gld->colorID);
   gl->glBindBuffer(GL_ARRAY_BUFFER, gld->colorID);
   gl->glBufferData(GL_ARRAY_BUFFER, sizeof(colors), colors, GL_STATIC_DRAW);
 
   gld->initialized = EINA_TRUE;

Allocate memory for the matrix and load a unit matrix into it. Then define the value that is used in order to build the perspective projection matrix. The customFrustum() function is used for it. Multiply this resulting matrix with a resizing matrix, so the model is correctly adjusted to the screen.

float aspect;
 
elm_glview_size_get(obj, &w, &h);
customLoadIdentity(gld->view);
 
if (w > h)
  {
     aspect = (float) w / h;
     customFrustum(gld->view, -1.0 * aspect, 1.0 * aspect, -1.0, 1.0, -1.0, 1.0);
  }
else
  {
     aspect = (float) h / w;
     customFrustum(gld->view, -1.0, 1.0, -1.0 * aspect, 1.0 * aspect, -1.0, 1.0);
  }


next page: Rendering the Cube