Upto: Table of Contents of full book "Programming the Raspberry Pi GPU"

OpenGL ES on the Raspberry Pi

OpenGL ES is one of the means of drawing graphics on the RPi. It is an extremely sophisticated system that we intruduce but do not attempt to cover in detail. The major contribution of this chapter is to establish how you use Dispmanx to create an EGL surface so you can do OpenGL ES rendering. It describes a version of esUtils.c from the Munshi et al book adapted to the RPi and gives some examples of its use. This gives the basics of what is required to run OpenGL ES programs on the RPi, but is not intended to be more than an introduction to general OpenGL ES programming.

Resources

Files

Files used are here

OpenGL ES

In this chapter we can actually get to draw things, using the OpenGL ES API supported by the RPi. OpenGL was first specified in 1992, arising out of earlier systems by Silicon Graphics. Obviously, being so old it has accreted a certain amount of fluff. OpenGL ES is a simplified form, more suitable to low profile computers such as the RPi. OpenGL ES is now upto version 3, but the RPi only supports version 2. (Actually, it also supports version 1 but we will ignore that.)

OopenGL ES is a significantly complex system: generating 2D and 3D images is an exceedingly non-trivial task! The canonical reference for this API is the OpenGLĀ® ES 2.0 Programming Guide by Aaftab Munshi, Dan Ginsburg and Dave Shreiner. It is nearly 500 pages and is packed with information about how to program OpenGL ES.

Obviously, we cannot hope to replicate all the material in that book. Nevertheless we will address the issues of getting OpenGL ES running on the RPi and give a number of simple examples.

The esUtil functions

The Munshi et al book tries to present a platform-independent view of OpenGL ES programming, so that the example programs will run essentially unchanged across multiple platforms, including Linux, Windows, the iPhone and Android. in order to do this, it has to abstract above the O/S dependent layers. It does so with a set of functions wrapped in files such as esUtil.c, one version for each O/S.

The original book did not contain any version of this file for the RPi (it hadn't been invented then), but authors such as Benosteen have created versions. His version contains code which will run both under the X Window System and standalone; we will just consider a standalone version.

The esUtil.c file essentially defines four functions

Their library uses a struct

	
typedef struct _escontext
{
   void*       userData;
   GLint       width;
   GLint       height;
   EGLNativeWindowType  hWnd;
   EGLDisplay  eglDisplay;
   EGLContext  eglContext;
   EGLSurface  eglSurface;
   void (ESCALLBACK *drawFunc) ( struct _escontext * );
   void (ESCALLBACK *keyFunc) ( struct _escontext *, unsigned char, int, int );
   void (ESCALLBACK *updateFunc) ( struct _escontext *, float deltaTime );
} ESContext;
	
      

which gives O/S independent information needed for drawing OpenGL ES. The four fields of EGLNativeWindowType , EGLDisplay , EGLContext and EGLSurface were all covered for the RPi in previous chapters and are defined for the RPi as

	
///
// CreateEGLContext()
//
//    Creates an EGL rendering context and all associated elements
//
EGLBoolean CreateEGLContext ( EGLNativeWindowType hWnd, EGLDisplay* eglDisplay,
                              EGLContext* eglContext, EGLSurface* eglSurface,
                              EGLint attribList[])
{
   EGLint numConfigs;
   EGLint majorVersion;
   EGLint minorVersion;
   EGLDisplay display;
   EGLContext context;
   EGLSurface surface;
   EGLConfig config;
   EGLint contextAttribs[] = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE, EGL_NONE };

   // Get Display
   display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
   if ( display == EGL_NO_DISPLAY )
   {
      return EGL_FALSE;
   }

   // Initialize EGL
   if ( !eglInitialize(display, &majorVersion, &minorVersion) )
   {
      return EGL_FALSE;
   }

   // Get configs
   if ( !eglGetConfigs(display, NULL, 0, &numConfigs) )
   {
      return EGL_FALSE;
   }

   // Choose config
   if ( !eglChooseConfig(display, attribList, &config, 1, &numConfigs) )
   {
      return EGL_FALSE;
   }

   // Create a surface
   surface = eglCreateWindowSurface(display, config, (EGLNativeWindowType)hWnd, NULL);
   if ( surface == EGL_NO_SURFACE )
   {
      return EGL_FALSE;
   }

   // Create a GL context
   context = eglCreateContext(display, config, EGL_NO_CONTEXT, contextAttribs );
   if ( context == EGL_NO_CONTEXT )
   {
      return EGL_FALSE;
   }   
   
   // Make the context current
   if ( !eglMakeCurrent(display, surface, surface, context) )
   {
      return EGL_FALSE;
   }
   
   *eglDisplay = display;
   *eglSurface = surface;
   *eglContext = context;
   return EGL_TRUE;
} 


///
//  WinCreate()
//
//      This function initialized the native X11 display and window for EGL
//
EGLBoolean WinCreate(ESContext *esContext, const char *title)
{
    int32_t success = 0;   
    uint32_t screen_width;
    uint32_t screen_height;

    EGL_DISPMANX_WINDOW_T *nativewindow = malloc(sizeof(EGL_DISPMANX_WINDOW_T));
    DISPMANX_ELEMENT_HANDLE_T dispman_element;
    DISPMANX_DISPLAY_HANDLE_T dispman_display;
    DISPMANX_UPDATE_HANDLE_T dispman_update;
    VC_RECT_T dst_rect;
    VC_RECT_T src_rect;

    bcm_host_init();

    // create an EGL window surface
    success = graphics_get_display_size(0 /* LCD */, 
					&screen_width, 
					&screen_height);
    assert( success >= 0 );

    dst_rect.x = 0;
    dst_rect.y = 0;
    dst_rect.width = screen_width;
    dst_rect.height = screen_height;

    src_rect.x = 0;
    src_rect.y = 0;
    src_rect.width = screen_width << 16;
    src_rect.height = screen_height << 16;        

    dispman_display = vc_dispmanx_display_open( 0 /* LCD */);
    dispman_update = vc_dispmanx_update_start( 0 );

    dispman_element = 
	vc_dispmanx_element_add(dispman_update, dispman_display,
				0/*layer*/, &dst_rect, 0/*src*/,
				&src_rect, DISPMANX_PROTECTION_NONE, 
				0 /*alpha*/, 0/*clamp*/, 0/*transform*/);

    // Build an EGL_DISPMANX_WINDOW_T from the Dispmanx window
    nativewindow->element = dispman_element;
    nativewindow->width = screen_width;
    nativewindow->height = screen_height;
    vc_dispmanx_update_submit_sync(dispman_update);

    printf("Got a Dispmanx window\n");

    esContext->hWnd = (EGLNativeWindowType) nativewindow;

    return EGL_TRUE;
}
	
      

These are used in a typical application by e.g.

       
   ESContext esContext;
   UserData  userData;

   esInitContext ( &esContext );
   esContext.userData = &userData;

   esCreateWindow ( &esContext, "My application", 320, 240, ES_WINDOW_RGB );
       
     

where the UserData is some application-dependent structure.

Vertices

A key data structure for drawing is the vertex - a point in two- or three-dimensional space. Each vertex may have attributes associated with it such as a colour. A line has two vertices, a triangle has three, a square four, and a circle ... as many points around the circumference as are needed to look like a circle.

Using 3-dimensional (x, y, z) co-ordinates, we could specify the vertices of a triangle by

	
GLfloat vVertices[] = {  0.0f,  0.5f, 0.0f, 
                        -0.5f, -0.5f, 0.0f,
                         0.5f, -0.5f, 0.0f };
	
      

We note that the co-ordinate system for OpenGL ES is

so that this triangle is

(but note, we haven't given the vertices any colour yet, so it isn't going to appear as red until we do.)

Shaders

Shaders are programs that run on the GPU rather than on the CPU. There is a communications pipeline from the CPU to the GPU: the CPU sends programs and data to the GPU which then executes them. There are two types of programs which run on the GPU: vertex shaders and fragment shaders. Simplifying horribly dramatically the relationship between these is

The vertex shader is handed vertices one at a time and performs some operation such as moving the vertex to a new position. The OpenGL ES pipeline will then work out which pixels should be drawn and hand them one at a time to the fragment shader which works out the colour of the pixel.

How does OpenGL ES know which pixels are to be drawn? Because the application will have said: "draw triangles incorporating these triplets of vertices, and fill them in" (more details soon).

So what do shaders look like? They are in a C-like language, custom designed for OpenGL ES. The language supports all the operations required for graphics and is quite extensive. We will need to cover some of it in the sequel. The specification is OpenGL ES Shading Language

Minimal vertex shader

A really simple vertex shader is

	
attribute vec4 vPosition;
void main()
{
   gl_Position = vPosition;
}       
	
      

This takes a parameter vPosition and assigns it to the variable gl_Position. Every vertex shader must assign a value to this variable. This shader is taking an input value for the location of the vertex and assigning the final location of that vertex to the same value.

The data type of gl_Position is a 4-dimensional vector (x, y, z, w). The first three components are the (x, y, z) values of the vertex, while the fourth is typically set to one.

The shader is not C code; it is not compiled by the application. It is passed as a string to the GPU and compiled at runtime by the GPU.

Minimal fragment shader

A fragment shader is responsible for setting a colour on a pixel. A colour is also a 4-dimensional vector, of the form (red, green, blue, alpha), with all values between zero and one. The alpha value is the amount of "transparency", where one means opaque and zero means transparent.

A minimal fragment shader just sets the pixel to opaque red:

	
void main()
{
   gl_FragColor = vec4 ( 1.0, 0.0, 0.0, 1.0 );
}
	
      

The variable gl_FragColor must be assigned a value.

This also is not compiled by the application but is passed as a string to the GPU which compiles it at runtime.

Loading the shaders

The shaders are given as source code strings to OpenGL ES. We have to create a shader object, tell it the source code, compile the shader and check for errors. The code for this is

	
GLuint LoadShader ( GLenum type, const char *shaderSrc )
{
   GLuint shader;
   GLint compiled;
   
   // Create the shader object
   shader = glCreateShader ( type );

   if ( shader == 0 )
   	return 0;

   // Load the shader source
   glShaderSource ( shader, 1, &shaderSrc, NULL );
   
   // Compile the shader
   glCompileShader ( shader );

   // Check the compile status
   glGetShaderiv ( shader, GL_COMPILE_STATUS, &compiled );

   if ( !compiled ) 
   {
      GLint infoLen = 0;

      glGetShaderiv ( shader, GL_INFO_LOG_LENGTH, &infoLen );
      
      if ( infoLen > 1 )
      {
         char* infoLog = malloc (sizeof(char) * infoLen );
         char* infoLog = malloc (sizeof(char) * infoLen );

         glGetShaderInfoLog ( shader, infoLen, NULL, infoLog );
         esLogMessage ( "Error compiling shader:\n%s\n", infoLog );            
         
         free ( infoLog );
      }

      glDeleteShader ( shader );
      return 0;
   }

   return shader;

}
	
      

Creating the program object

The OpenGL ES program to run in the GPU must be created and the shaders attached. This is a bit messy as we have to define the shaders, load the shaders, create the program which will run the shaders, attach the shaders to the program, add in information about the attributes (here vPosition), link the program and take error correction action if it fails.

Typical code to do this is

	
int Init ( ESContext *esContext )
{
   esContext->userData = malloc(sizeof(UserData));

   UserData *userData = esContext->userData;
   GLbyte vShaderStr[] =  
      "attribute vec4 vPosition;    \n"
      "void main()                  \n"
      "{                            \n"
      "   gl_Position = vPosition;  \n"
      "}                            \n";
   
   GLbyte fShaderStr[] =  
       "void main()                                  \n"
      "{                                            \n"
      "  gl_FragColor = vec4 ( 1.0, 0.0, 0.0, 1.0 );\n"
      "}                                            \n";

   GLuint vertexShader;
   GLuint fragmentShader;
   GLuint programObject;
   GLint linked;

   // Load the vertex/fragment shaders
   vertexShader = LoadShader ( GL_VERTEX_SHADER, vShaderStr );
   fragmentShader = LoadShader ( GL_FRAGMENT_SHADER, fShaderStr );

   // Create the program object
   programObject = glCreateProgram ( );
   
   if ( programObject == 0 )
      return 0;

   glAttachShader ( programObject, vertexShader );
   glAttachShader ( programObject, fragmentShader );

   // Bind vPosition to attribute 0   
   glBindAttribLocation ( programObject, 0, "vPosition" );

   // Link the program
   glLinkProgram ( programObject );

   // Check the link status
   glGetProgramiv ( programObject, GL_LINK_STATUS, &linked );

   if ( !linked ) 
   {
      GLint infoLen = 0;

      glGetProgramiv ( programObject, GL_INFO_LOG_LENGTH, &infoLen );
      
      if ( infoLen > 1 )
      {
         char* infoLog = malloc (sizeof(char) * infoLen );

         glGetProgramInfoLog ( programObject, infoLen, NULL, infoLog );
         esLogMessage ( "Error linking program:\n%s\n", infoLog );            
         
         free ( infoLog );
      }

      glDeleteProgram ( programObject );
      return GL_FALSE;
   }

   // Store the program object
   userData->programObject = programObject;

   glClearColor ( 0.0f, 0.0f, 0.0f, 0.0f );
   return GL_TRUE;
}
	
      

Drawing something

That's a lot of code so far, and we still haven't drawn anything! That's the purpose of the next function, which will set up the array of vertices, set a viewport in which to see the drawing, establish the vertices to be drawn - and finally draw them.

Establishing the vertices involves firstly setting the active program by glUseProgram - multiple programs can exist at any one time but only one is active for drawing (we have only defined one so far).

The array of vertices given as C code must then be set so that OpenGL ES can hand each vertex to the attribute vPosition. The array is e.g. the set of triangle vertices that we have seen early on

	
GLfloat vVertices[] = {  0.0f,  0.5f, 0.0f, 
                        -0.5f, -0.5f, 0.0f,
                         0.5f, -0.5f, 0.0f };
	
      

The attribute that will be set to each vertex in turn is vPosition in the vertex shader. In the C code, we don't use the string "vPosition" to identify it, but instead the integer index zero that we set once when creating and linking the program by

	
// Bind vPosition to attribute 0   
glBindAttribLocation ( programObject, 0, "vPosition" );
	
      

The link is made by the function

	
VertexAttribPointer( uint index, int size, enum type,
                     boolean normalized, sizei stride, const
                     void *pointer );
	
      

where we specify zero for the index attribute, three for the number of elements in each vertex (three for x, y and z), GL_FLOAT for the data type of ach array element, GL_FALSE for normalised data. and a stride of zero meaning that we only have vertex data and nothing else in the vertex array:

	
glVertexAttribPointer ( 0, 3, GL_FLOAT, GL_FALSE, 0, vVertices );
	
      

One last little wrinkle and then we can draw: attributes by default are disabled, meaning that the C data will not be given to the attribute. The attribute (with index zero) must be enabled by

	
glEnableVertexAttribArray ( 0 );
	
      

Now we draw using the function glDrawArrays which takes three parameters: the drawing mode (we will use triangle mode for our single triangle), the initial vertex in the vertex array and the number of vertices to be drawn

	
glDrawArrays ( GL_TRIANGLES, 0, 3 );
	
      

The draw function is

	
void Draw ( ESContext *esContext )
{
   UserData *userData = esContext->userData;
   GLfloat vVertices[] = {  0.0f,  0.5f, 0.0f, 
                           -0.5f, -0.5f, 0.0f,
                            0.5f, -0.5f, 0.0f };
      
   // Set the viewport
   glViewport ( 0, 0, esContext->width, esContext->height );
   
   // Clear the color buffer
   glClear ( GL_COLOR_BUFFER_BIT );

   // Use the program object
   glUseProgram ( userData->programObject );

   // Load the vertex data
   glVertexAttribPointer ( 0, 3, GL_FLOAT, GL_FALSE, 0, vVertices );
   glEnableVertexAttribArray ( 0 );

   glDrawArrays ( GL_TRIANGLES, 0, 3 );
}
	
      

Putting it together

The main function brings it all together

	
int main ( int argc, char *argv[] )
{
   ESContext esContext;
   UserData  userData;

   esInitContext ( &esContext );
   esContext.userData = &userData;

   esCreateWindow ( &esContext, "Hello Triangle", 320, 240, ES_WINDOW_RGB );

   if ( !Init ( &esContext ) )
      return 0;

   esRegisterDrawFunc ( &esContext, Draw );

   esMainLoop ( &esContext );
}
	
      

Drawing an opaque red triangle

if we now take the set of functions from esUtil.c, the convenience functions defined above with the special values for arrays and shaders, then we can finally draw a red opaque triangle. The program is Hello_Triangle.c:

       //
// Book:      OpenGL(R) ES 2.0 Programming Guide
// Authors:   Aaftab Munshi, Dan Ginsburg, Dave Shreiner
// ISBN-10:   0321502795
// ISBN-13:   9780321502797
// Publisher: Addison-Wesley Professional
// URLs:      http://safari.informit.com/9780321563835
//            http://www.opengles-book.com
//

// Hello_Triangle.c
//
//    This is a simple example that draws a single triangle with
//    a minimal vertex/fragment shader.  The purpose of this 
//    example is to demonstrate the basic concepts of 
//    OpenGL ES 2.0 rendering.
#include <stdlib.h>
#include "esUtil.h"

typedef struct
{
   // Handle to a program object
   GLuint programObject;

} UserData;

///
// Create a shader object, load the shader source, and
// compile the shader.
//
GLuint LoadShader ( GLenum type, const char *shaderSrc )
{
   GLuint shader;
   GLint compiled;
   
   // Create the shader object
   shader = glCreateShader ( type );

   if ( shader == 0 )
   	return 0;

   // Load the shader source
   glShaderSource ( shader, 1, &shaderSrc, NULL );
   
   // Compile the shader
   glCompileShader ( shader );

   // Check the compile status
   glGetShaderiv ( shader, GL_COMPILE_STATUS, &compiled );

   if ( !compiled ) 
   {
      GLint infoLen = 0;

      glGetShaderiv ( shader, GL_INFO_LOG_LENGTH, &infoLen );
      
      if ( infoLen > 1 )
      {
         char* infoLog = malloc (sizeof(char) * infoLen );

         glGetShaderInfoLog ( shader, infoLen, NULL, infoLog );
         esLogMessage ( "Error compiling shader:\n%s\n", infoLog );            
         
         free ( infoLog );
      }

      glDeleteShader ( shader );
      return 0;
   }

   return shader;

}

///
// Initialize the shader and program object
//
int Init ( ESContext *esContext )
{
   esContext->userData = malloc(sizeof(UserData));

   UserData *userData = esContext->userData;
   GLbyte vShaderStr[] =  
      "attribute vec4 vPosition;    \n"
      "void main()                  \n"
      "{                            \n"
      "   gl_Position = vPosition;  \n"
      "}                            \n";
   
   GLbyte fShaderStr[] =  
      "precision mediump float;\n"\
      "void main()                                  \n"
      "{                                            \n"
      "  gl_FragColor = vec4 ( 1.0, 0.0, 0.0, 1.0 );\n"
      "}                                            \n";

   GLuint vertexShader;
   GLuint fragmentShader;
   GLuint programObject;
   GLint linked;

   // Load the vertex/fragment shaders
   vertexShader = LoadShader ( GL_VERTEX_SHADER, vShaderStr );
   fragmentShader = LoadShader ( GL_FRAGMENT_SHADER, fShaderStr );

   // Create the program object
   programObject = glCreateProgram ( );
   
   if ( programObject == 0 )
      return 0;

   glAttachShader ( programObject, vertexShader );
   glAttachShader ( programObject, fragmentShader );

   // Bind vPosition to attribute 0   
   glBindAttribLocation ( programObject, 0, "vPosition" );

   // Link the program
   glLinkProgram ( programObject );

   // Check the link status
   glGetProgramiv ( programObject, GL_LINK_STATUS, &linked );

   if ( !linked ) 
   {
      GLint infoLen = 0;

      glGetProgramiv ( programObject, GL_INFO_LOG_LENGTH, &infoLen );
      
      if ( infoLen > 1 )
      {
         char* infoLog = malloc (sizeof(char) * infoLen );

         glGetProgramInfoLog ( programObject, infoLen, NULL, infoLog );
         esLogMessage ( "Error linking program:\n%s\n", infoLog );            
         
         free ( infoLog );
      }

      glDeleteProgram ( programObject );
      return GL_FALSE;
   }

   // Store the program object
   userData->programObject = programObject;

   glClearColor ( 0.0f, 0.0f, 0.0f, 0.0f );
   return GL_TRUE;
}

///
// Draw a triangle using the shader pair created in Init()
//
void Draw ( ESContext *esContext )
{
   UserData *userData = esContext->userData;
   GLfloat vVertices[] = {  0.0f,  0.5f, 0.0f, 
                           -0.5f, -0.5f, 0.0f,
                            0.5f, -0.5f, 0.0f };
      
   // Set the viewport
   glViewport ( 0, 0, esContext->width, esContext->height );
   
   // Clear the color buffer
   glClear ( GL_COLOR_BUFFER_BIT );

   // Use the program object
   glUseProgram ( userData->programObject );

   // Load the vertex data
   glVertexAttribPointer ( 0, 3, GL_FLOAT, GL_FALSE, 0, vVertices );
   glEnableVertexAttribArray ( 0 );

   glDrawArrays ( GL_TRIANGLES, 0, 3 );
}

int main ( int argc, char *argv[] )
{
   ESContext esContext;
   UserData  userData;

   esInitContext ( &esContext );
   esContext.userData = &userData;

   esCreateWindow ( &esContext, "Hello Triangle", 320, 240, ES_WINDOW_RGB );

   if ( !Init ( &esContext ) )
      return 0;

   esRegisterDrawFunc ( &esContext, Draw );

   esMainLoop ( &esContext );
}

     

A coloured triangle

In the last example, we drew a triangle with a solid red colour. This was done by setting the fragment shader variable fragmentColor to opaque red. But what if we wanted to get a "colour triangle" such as this?

Do we have to calculate all of the colour values ourselves?

Varyings

We could do it by ourselves, but fortunately OpenGL ES can do the work for us. It has the concept of varyings which are variables with values interpolated from the vertex shader data. For the coloured triangle, the interpolated values are calculated using the vertex colours, averaged out based on the distance from each vertex.

To use them, we define a variable to be varying in both the vertex and the fragment shader, with the same name. We set a value in each vertex using a new attribute, and then can access the interpolated value in each fragment shader. The vertex shader is

	
attribute vec4 vPosition;
attribute vec4 vColour;

varying vFragmentColour;

void main()
{
   gl_Position = vPosition;
   vFragmentColour = vColour;
}      
	
      

and the fragment shader is

	
varying vec4 vFragmentColour;

void main()
{
   gl_FragColor = vFragmentColour;
}
	
      

Passing multiple attributes to the vertex shader

In the simple triangle program we passed a set of vertex values to the vertex shader by first setting the index of the attribute to zero and then calling the vertex attribute pointer function. To add more attributes the simplest way (but not the most efficient) is to set extra attributes to higher index values (one, two, etc) and call the vertex attribute pointer function on these as well.

The changes required to set colours for the colour attribute

	
   GLfloat vColours[] = {1.0f, 0.0f, 0.0f,
			 0.0f, 1.0f, 0.0f,
			 0.0f, 0.0f, 1.0f,};
      
   ...

   glVertexAttribPointer ( 1, 3, GL_FLOAT, GL_FALSE, 0, vColours );
   glEnableVertexAttribArray ( 1 );
	
      

and

	
   // Bind vColour to attribute 1
   glBindAttribLocation ( programObject, 1, "vColour" );
	
      

The program is Hello_TriangleColour.c. It isn't worth including the full code here as only the above lines differ from the Hello_Triangle.c program.

Drawing squares and other shapes

If you want to draw complex shapes such as squares, cubes or even circles and spheres, you need to break them down into triangles. Squares and other regular shapes are easy to divide into triangles, but even then there can be many choices (divide on this diagonal or on that one?).

Far more complex - and more important - is how to label the vertices of these triangles, because that affects how you ask OpenGL ES to draw them. The OpenGL ES specification illustrates this with

Making separate triangles is easiest for the programmer: you just have to cut the polygons into any triangles with any labelling and call glDrawArrays where every three vertices define a separate triangle. But it may cost more in processing time.

A square, for example, can be divided into two separate triangles with a total of six vertices. Doing so would look like this:

	
   GLfloat vVertices[] = { // first triangle
			  1.0f, -1.0f, 0.0f,
                          1.0f, 1.0f, 0.0f,
			  -1.0f, -1.0f, 0.0f,

                          // second triangle
			  -1.0f, 1.0f, 0.0f,
			  -1.0f, -1.0f, 0.0f,
			  1.0f, 1.0f, 0.0f
                         };
	
      

with drawing by

	
 glVertexAttribPointer ( 0, 3, GL_FLOAT, GL_FALSE, 0, vVertices );
 glEnableVertexAttribArray ( 0 );

 glDrawArrays ( GL_TRIANGLES, 0, 6 );
	
      

But a square can also be divided into a triangle strip or even a triangle fan with only four vertices. But you have to be more careful: if you want a strip, the very first vertex must not be shared with any other triangle and neither can the last. Here is one possibility for a square at (±1, ±1):

	
   GLfloat vVertices[] = {
			  1.0f, -1.0f, 0.0f,
                          1.0f, 1.0f, 0.0f,
			  -1.0f, -1.0f, 0.0f,
			  -1.0f, 1.0f, 0.0f
                         };
	
      

The square can then be drawn by

	
 glVertexAttribPointer ( 0, 3, GL_FLOAT, GL_FALSE, 0, vVertices );
 glEnableVertexAttribArray ( 0 );

 glDrawArrays ( GL_TRIANGLE_STRIP, 0, 4 );
	
      

An alternative mechanism uses the vertex arrays as above, but draws elements by explicit indexes into this array rather than the implicit indexes caused by the element ordering. This makes use of another array, this time of the indices. The vertex array must be used too, of course.

Even if an index array is used, there are still the choices of drawing individual triangles, strips or fans. Using triangles the code is

	
   GLfloat vVertices[] = {
			  1.0f, -1.0f, 0.0f,
                          1.0f, 1.0f, 0.0f,
			  -1.0f, -1.0f, 0.0f,
			  -1.0f, 1.0f, 0.0f,
			  -1.0f, -1.0f, 0.0f,
			  1.0f, 1.0f, 0.0f
                         };
   GLubyte vIndices[] = {
                         0, 1, 2,
                         3, 4, 5
                        };
	
      

(This is usually optimised by re-using some of the vertices as {0, 1, 2, 3, 2, 1} and omitting the last two entries of the vertices array) The square can then be drawn by

	
 glVertexAttribPointer ( 0, 3, GL_FLOAT, GL_FALSE, 0, vVertices );
 glEnableVertexAttribArray ( 0 );

 glDrawElements ( GL_TRIANGLES, 6, GL_UNSIGNED_BYTE, vIndices );
	
      

Using strips the code is

	
   GLfloat vVertices[] = {
			  1.0f, -1.0f, 0.0f,
                          1.0f, 1.0f, 0.0f,
			  -1.0f, -1.0f, 0.0f,
			  -1.0f, 1.0f, 0.0f
                         };
   GLubyte vIndices[] = {
                         0, 1, 2, 3
                        };
	
      

The square can then be drawn by

	
 glVertexAttribPointer ( 0, 3, GL_FLOAT, GL_FALSE, 0, vVertices );
 glEnableVertexAttribArray ( 0 );

 glDrawElements ( GL_TRIANGLE_STRIP, 4, GL_UNSIGNED_BYTE, vIndices );
	
      

These possibilities can all be seen in a revised Draw function, where the different choices described above can be made by setting TRIANGLES and ELEMENTS to zero or one:

	
void Draw ( ESContext *esContext )
{
#define TRIANGLES 1
#define ELEMENTS 1

   UserData *userData = esContext->userData;
   GLfloat vVertices[] = {
			  1.0f, -1.0f, 0.0f,
                          1.0f, 1.0f, 0.0f,
			  -1.0f, -1.0f, 0.0f,
			  -1.0f, 1.0f, 0.0f,
#if TRIANGLES
			  -1.0f, -1.0f, 0.0f,
			  1.0f, 1.0f, 0.0f
#endif
                         };

#if ELEMENTS
   GLubyte vIndices[] = {
                         0, 1, 2,
                         3, 
#  if TRIANGLES
                         4, 5
#  endif
                        };
#endif

   // Set the viewport
   glViewport ( 0, 0, esContext->width, esContext->height );
   
   // Clear the color buffer
   glClear ( GL_COLOR_BUFFER_BIT );

   // Use the program object
   glUseProgram ( userData->programObject );

   // Load the vertex data
   glVertexAttribPointer ( 0, 3, GL_FLOAT, GL_FALSE, 0, vVertices );
   glEnableVertexAttribArray ( 0 );

#if TRIANGLES
#  if ELEMENTS
   glDrawElements ( GL_TRIANGLES, 6, GL_UNSIGNED_BYTE, vIndices );
#  else
   glDrawArrays ( GL_TRIANGLES, 0, 6 );
#  endif
#else
#  if ELEMENTS
   glDrawElements ( GL_TRIANGLE_STRIP, 4, GL_UNSIGNED_BYTE, vIndices );
#  else
   glDrawArrays ( GL_TRIANGLE_STRIP, 0, 4 );
#  endif
#endif
}
	
      

The full program is Hello_Square.c

Which to choose?

The choice of triangles, strip or fan will depend on the geometry of the shape, and any semantic context it might have e.g. drawing a wall of rectangles would use a strip while drawing circles would use a fan.

The choice of drawing arrays or elements usually comes down in favour of elements for efficiency.

Textures

Textures are another means of filling triangles. A texture is image data that can be manipulated by the fragment shader. Textures can be 2-dimensional rectangular images or the six sides of a cube. In this section we only look at 2-dimensional images. Textures will typically come from image data stored in files or hard-coded into the program. The Munshi et al book uses a simple hard-coded 2x2 image. Here we shall take a file image.

TGA files

There are many, many different file formats, some lossy, some lossless, some compressed, some not, some with metadata, some without. In this section we just use TGA - an uncompressed format with enough useful metadata, simple to load.

TGA files can be created from e.g. JPEG files by using the convert utility from the Gimp drawing system. It is simple to convert a file from JPEG to TGA format: just give the appropriate file extensions:

        
convert image.jpg image.tga
        
      

A TGA file has a header section which gives the width and height of the image from which its size can be calculated. The default format will be RGB, as 24-bit pixels. Reading in such a file is just a matter of locating the dimensions, malloc'ing the right size buffer, skipping to the start of the image data and reading it all in.

The esUtil.c file includes a function esLoadTGA which will read in an image, returning the image and its dimensions.

The default is for the origin of the image to be the top lefthand corner, with the y-axis growing down. OpenGL ES on the other hand has the origin in the bottom lefthand corner with the y-axis growing up. So the image will be upside down relative to the OpenGL coordinates. This can be fixed by reading the data in differently, or by using an OpenGL ES reflection. We will map the texture upside-down to give the correct orientation.

Mipmaps

An application may need to render an image in fine detail, or, say if it is far away, in only coarse detail. To avoid GPU load, multiple images can be given for one texture at varying levels of detail. We won't explore that, and just set one image for all levels of detail.

Creating a texture object

Textures are stored in texture objects which have to be created using an image with known format, dimensions and pixel values. Assuming that we have stored the width, height and image in the userData field of an esContext, where the format is known to be RGB as unsigned bytes, the code is

	
GLuint CreateSimpleTexture2D(ESContext *esContext)
{
   // Texture object handle
   GLuint textureId;
   UserData *userData = esContext->userData;
   char *pixels = userData->image;
   
   // Use tightly packed data
   glPixelStorei ( GL_UNPACK_ALIGNMENT, 1 );

   // Generate a texture object
   glGenTextures ( 1, &textureId );

   // Bind the texture object
   glBindTexture ( GL_TEXTURE_2D, textureId );

   // Load the texture
   glTexImage2D ( GL_TEXTURE_2D, 0, GL_RGB, 
		  userData->width, userData->height, 
		  0, GL_RGB, GL_UNSIGNED_BYTE, pixels );

   // Set the filtering mode
   glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST );
   glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST );

   return textureId;

}
	
      

Texture attributes and the shaders

2-dimensional textures will only need (x, y) values as co-ordinates, unlike vertex co-ordinates which are described by four co-ordinates (x, y, z, w). The vertices of the 2D texture will need to be fed into the vertex shaders in order that the texture can be properly located within the final rendering. This will need a parameter within the shader of type vec2. The texture co-ordinates are limited to the square with vertices (0, 0), (1, 0), (0, 1) and (1, 1).

The fragment shader will need a varying texture co-ordinate, which would be initialised by the vertex shader but then interpolated. This will need to be integrated with the image pixel values, and this is done by a fragment shader function texture2D.

The vertex shader program will typically look like

	
      attribute vec4 a_position;
      attribute vec2 a_texCoord;
      varying vec2 v_texCoord;
      void main()             
      {                       
         gl_Position = a_position;
         v_texCoord = a_texCoord;
      }                          
	
      

and the fragment shader will be

	   
      precision mediump float;   
      varying vec2 v_texCoord;   
      uniform sampler2D s_texture;
      void main()                 
      {                             
        gl_FragColor = texture2D( s_texture, v_texCoord );
      }                                                  
	
      

These are all combined into a revised Init function as

	
int Init ( ESContext *esContext )
{
    UserData *userData = esContext->userData;
    GLbyte vShaderStr[] =  
      "attribute vec4 a_position;   \n"
      "attribute vec2 a_texCoord;   \n"
      "varying vec2 v_texCoord;     \n"
      "void main()                  \n"
      "{                            \n"
      "   gl_Position = a_position; \n"
      "   v_texCoord = a_texCoord;  \n"
      "}                            \n";
   
    GLbyte fShaderStr[] =  
      "precision mediump float;                            \n"
      "varying vec2 v_texCoord;                            \n"
      "uniform sampler2D s_texture;                        \n"
      "void main()                                         \n"
      "{                                                   \n"
      "  gl_FragColor = texture2D( s_texture, v_texCoord );\n"
      "}                                                   \n";

   // Load the shaders and get a linked program object
   userData->programObject = esLoadProgram ( vShaderStr, fShaderStr );

   // Get the attribute locations
   userData->positionLoc = glGetAttribLocation ( userData->programObject, "a_position" );
   userData->texCoordLoc = glGetAttribLocation ( userData->programObject, "a_texCoord" );
   
   // Get the sampler location
   userData->samplerLoc = glGetUniformLocation ( userData->programObject, "s_texture" );

   // Load the texture
   userData->textureId = CreateSimpleTexture2D (esContext);

   glClearColor ( 0.0f, 0.0f, 0.0f, 0.0f );
   return GL_TRUE;
}
	
      

Drawing the texture

In order to draw, we need to pass in the vertex co-ordinates. We shall draw a square for the vertices. For the texture co-ordinates, we also need to specify their four co-ordinates. This could be done using two arrays as in

	
   GLfloat vVertices[] = { -0.5f,  0.5f, 0.0f,  // Position 0
                           -0.5f, -0.5f, 0.0f,  // Position 1
                            0.5f, -0.5f, 0.0f,  // Position 2
                            0.5f,  0.5f, 0.0f,  // Position 3
                         };
    GLfloat tVertices[] = {
                            0.0f,  1.0f,        // TexCoord 0 
                            0.0f,  0.0f,        // TexCoord 1
                            1.0f,  0.0f,        // TexCoord 2
                            1.0f,  1.0f         // TexCoord 3
                         };
	
      

More commonly, these arrays would be interleaved as

	
    GLfloat vVertices[] = { -0.5f,  0.5f, 0.0f,  // Position 0
                            0.0f,  1.0f,        // TexCoord 0 
                           -0.5f, -0.5f, 0.0f,  // Position 1
                            0.0f,  0.0f,        // TexCoord 1
                            0.5f, -0.5f, 0.0f,  // Position 2
                            1.0f,  0.0f,        // TexCoord 2
                            0.5f,  0.5f, 0.0f,  // Position 3
                            1.0f,  1.0f         // TexCoord 3
                         };
	
      

(Note that we are rendering the texture upside-down, to compensate for it being upside-down relative to OpenGL ES co-ordinates.)

The question then is how to get - separately - the two sets of vertices. This is where the stride parameter comes into play in the call to glVertexAttribPointer. Basically we want to say

	
Each vertex co-ordinate occurs in a set of three in every five values,
   starting at index zero
Each texture co-ordinate occurs in a set of two in every five values,
   starting at index three
	
      

The code for this is

	
   // Load the vertex position
   glVertexAttribPointer ( userData->positionLoc, 3, GL_FLOAT, 
                           GL_FALSE, 5 * sizeof(GLfloat), &vVertices[0] );
   // Load the texture coordinate
   glVertexAttribPointer ( userData-gt;texCoordLoc, 2, GL_FLOAT,
                           GL_FALSE, 5 * sizeof(GLfloat), &vVertices[3] );
	
      

The drawing code is then

	
void Draw ( ESContext *esContext )
{
   UserData *userData = esContext->userData;
   GLfloat vVertices[] = { -0.5f,  0.5f, 0.0f,  // Position 0
                            0.0f,  1.0f,        // TexCoord 0 
                           -0.5f, -0.5f, 0.0f,  // Position 1
                            0.0f,  0.0f,        // TexCoord 1
                            0.5f, -0.5f, 0.0f,  // Position 2
                            1.0f,  0.0f,        // TexCoord 2
                            0.5f,  0.5f, 0.0f,  // Position 3
                            1.0f,  1.0f         // TexCoord 3
                         };
   GLushort indices[] = { 0, 1, 2, 0, 2, 3 };
      
   // Set the viewport
   glViewport ( 0, 0, esContext->width, esContext->height );
   
   // Clear the color buffer
   glClear ( GL_COLOR_BUFFER_BIT );

   // Use the program object
   glUseProgram ( userData->programObject );

   // Load the vertex position
   glVertexAttribPointer ( userData->positionLoc, 3, GL_FLOAT, 
                           GL_FALSE, 5 * sizeof(GLfloat), vVertices );
   // Load the texture coordinate
   glVertexAttribPointer ( userData->texCoordLoc, 2, GL_FLOAT,
                           GL_FALSE, 5 * sizeof(GLfloat), &vVertices[3] );

   glEnableVertexAttribArray ( userData->positionLoc );
   glEnableVertexAttribArray ( userData->texCoordLoc );

   // Bind the texture
   glActiveTexture ( GL_TEXTURE0 );
   glBindTexture ( GL_TEXTURE_2D, userData->textureId );

   // Set the sampler texture unit to 0
   glUniform1i ( userData->samplerLoc, 0 );

   glDrawElements ( GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, indices );

}
	
      

Complete code for drawing an image

The final code is Simple_Image.c:

	// Authors:   Aaftab Munshi, Dan Ginsburg, Dave Shreiner
// Copyright: the authors and Jan Newmarch

// Simple_Image.c
//
//    This is a simple example that draws an image with a 2D
//    texture image.
//
#include <stdlib.h>
#include <stdio.h>
#include "esUtil.h"

typedef struct
{
   // Handle to a program object
   GLuint programObject;

   // Attribute locations
   GLint  positionLoc;
   GLint  texCoordLoc;

   // Sampler location
   GLint samplerLoc;

   // Texture handle
   GLuint textureId;

   GLubyte *image;
    int width, height;
} UserData;

///
// Create a simple 2x2 texture image with four different colors
//
GLuint CreateSimpleTexture2D(ESContext *esContext)
{
   // Texture object handle
   GLuint textureId;
   UserData *userData = esContext->userData;
   
   GLubyte *pixels = userData->image;
   userData->width = esContext->width;
   userData->height = esContext->height;

   // Use tightly packed data
   glPixelStorei ( GL_UNPACK_ALIGNMENT, 1 );

   // Generate a texture object
   glGenTextures ( 1, &textureId );

   // Bind the texture object
   glBindTexture ( GL_TEXTURE_2D, textureId );

   // Load the texture
   glTexImage2D ( GL_TEXTURE_2D, 0, GL_RGB, 
		  userData->width, userData->height, 
		  0, GL_RGB, GL_UNSIGNED_BYTE, pixels );

   // Set the filtering mode
   glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST );
   glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST );

   return textureId;

}


///
// Initialize the shader and program object
//
int Init ( ESContext *esContext )
{
    UserData *userData = esContext->userData;
    GLbyte vShaderStr[] =  
      "attribute vec4 a_position;   \n"
      "attribute vec2 a_texCoord;   \n"
      "varying vec2 v_texCoord;     \n"
      "void main()                  \n"
      "{                            \n"
      "   gl_Position = a_position; \n"
      "   v_texCoord = a_texCoord;  \n"
      "}                            \n";
   
    GLbyte fShaderStr[] =  
      "precision mediump float;                            \n"
      "varying vec2 v_texCoord;                            \n"
      "uniform sampler2D s_texture;                        \n"
      "void main()                                         \n"
      "{                                                   \n"
      "  gl_FragColor = texture2D( s_texture, v_texCoord );\n"
      "}                                                   \n";

   // Load the shaders and get a linked program object
   userData->programObject = esLoadProgram ( vShaderStr, fShaderStr );

   // Get the attribute locations
   userData->positionLoc = glGetAttribLocation ( userData->programObject, "a_position" );
   userData->texCoordLoc = glGetAttribLocation ( userData->programObject, "a_texCoord" );
   
   // Get the sampler location
   userData->samplerLoc = glGetUniformLocation ( userData->programObject, "s_texture" );

   // Load the texture
   userData->textureId = CreateSimpleTexture2D (esContext);

   glClearColor ( 0.0f, 0.0f, 0.0f, 0.0f );
   return GL_TRUE;
}

///
// Draw a triangle using the shader pair created in Init()
//
void Draw ( ESContext *esContext )
{
   UserData *userData = esContext->userData;
   GLfloat vVertices[] = { -0.5f,  0.5f, 0.0f,  // Position 0
                            0.0f,  1.0f,        // TexCoord 0 
                           -0.5f, -0.5f, 0.0f,  // Position 1
                            0.0f,  0.0f,        // TexCoord 1
                            0.5f, -0.5f, 0.0f,  // Position 2
                            1.0f,  0.0f,        // TexCoord 2
                            0.5f,  0.5f, 0.0f,  // Position 3
                            1.0f,  1.0f         // TexCoord 3
                         };
   GLushort indices[] = { 0, 1, 2, 0, 2, 3 };
      
   // Set the viewport
   glViewport ( 0, 0, esContext->width, esContext->height );
   
   // Clear the color buffer
   glClear ( GL_COLOR_BUFFER_BIT );

   // Use the program object
   glUseProgram ( userData->programObject );

   // Load the vertex position
   glVertexAttribPointer ( userData->positionLoc, 3, GL_FLOAT, 
                           GL_FALSE, 5 * sizeof(GLfloat), vVertices );
   // Load the texture coordinate
   glVertexAttribPointer ( userData->texCoordLoc, 2, GL_FLOAT,
                           GL_FALSE, 5 * sizeof(GLfloat), &vVertices[3] );

   glEnableVertexAttribArray ( userData->positionLoc );
   glEnableVertexAttribArray ( userData->texCoordLoc );

   // Bind the texture
   glActiveTexture ( GL_TEXTURE0 );
   glBindTexture ( GL_TEXTURE_2D, userData->textureId );

   // Set the sampler texture unit to 0
   glUniform1i ( userData->samplerLoc, 0 );

   glDrawElements ( GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, indices );

}

///
// Cleanup
//
void ShutDown ( ESContext *esContext )
{
   UserData *userData = esContext->userData;

   // Delete texture object
   glDeleteTextures ( 1, &userData->textureId );

   // Delete program object
   glDeleteProgram ( userData->programObject );
	
   free(esContext->userData);
}

int main ( int argc, char *argv[] )
{
   ESContext esContext;
   UserData  userData;

   int width, height;
   GLubyte *image;
   
   image = esLoadTGA("jan.tga", &width, &height);
   if (image == NULL) {
       fprintf(stderr, "No such image\n");
       exit(1);
   }
   printf("Width %d height %d\n", width, height);
   
   userData.image = image;
   userData.width = width;
   userData.height = height;
   

   esInitContext ( &esContext );
   esContext.userData = &userData;

   esCreateWindow ( &esContext, "Simple Texture 2D", width, height, ES_WINDOW_RGB );

   if ( !Init ( &esContext ) )
      return 0;

   esRegisterDrawFunc ( &esContext, Draw );

   esMainLoop ( &esContext );

   ShutDown ( &esContext );
}

      

The image used is of me since that is possibly more interesting than a wall tile :-)

Animation: rotating an image

In this section we will give a very brief look at some of the issues and techniques in animating the graphics display. As you will have seen from multiple computer games and digital movies, the animations can be amazingly sophisticated - and also involve much programmer time, program complexity and CPU/GPU time.

We shall do about the simplest: make the image of the previous section rotate about an axis.

Matrices

I cut my teeth on matrices mumblety-mumblety years ago writing programs in Algol 60 to solve simultaneous linear equations. Now it is easier, with libraries to manage most calculations. We shall make use of various ES functions from the Munshi et al book to simplify things.

There are several primary operations you can carry out on graphic images to fit them into scenes: you can rotate them, move them nearer or further, apply perspective views so that that "parallel lines meet at the horizon" or any other effects you may choose.

The operations are carried out on vectors such as the (x, y, z, w) co-ordinates of a vertex. The operations are represented by matrices which are square (or rectangular) arrays of numbers. The main operation is to multiply a vector by a matrix. Any book on linear algebra will give details on how to do this.

esUtil

The principal functions supplied by the esUtil package are

These may remove the need to explicitly specify actual matrices.

Invoking animation

We already have a Draw function. We add to this an Update function to make the changes needed for the next Draw:

	
  esRegisterDrawFunc ( &esContext, Draw );
  esRegisterUpdateFunc ( &esContext, Update );
	
      

The Update function for a simple rotation can be

	
void Update ( ESContext *esContext, float deltaTime )
{
   UserData *userData = (UserData*) esContext->userData;
   
   // Compute a rotation angle based on time to rotate the cube
   userData->angle += ( deltaTime * 40.0f );
   if( userData->angle >= 360.0f )
      userData->angle -= 360.0f;

   // Generate an identity matrix before rotating the square
   esMatrixLoadIdentity(  &userData->rotateMx );

   // Rotate the square about the (1, 0, 1) axis
   esRotate(  &userData->rotateMx, userData->angle, 1.0, 0.0, 1.0 );
}
	
      

This uses a current rotation angle stored in the userData and applies it to an identity matrix about a vector specified here as (x, y, z) = (1.0, 0.0, 1.0).

Uniform parameters: the rotation matrix

If we are rotating an entire image, the rotation has to be applied to every pixel in the drawn image - and it is the same rotation matrix in each case. So this matrix needs to be constant over all of the vertex and fragment shaders. Such a matrix is a uniform parameter and is specified as such in both the vertex and fragment shaders.

Working out the value of a uniform parameter is something that needs to be done once in each drawing iteration; so it is done in the application's Update C code code, not in the shaders. It is then passed in as a parameter on each draw call by glUniformMatrix4fv.

Extra fields are added to the UserData for the rotation angle and rotation matrix. Apart from that and the changes above, the standard program structure holds. The program to rotate an image loaded from a TGA file is Rotate_Image.c:

	//
// Book:      OpenGL(R) ES 2.0 Programming Guide
// Authors:   Aaftab Munshi, Dan Ginsburg, Dave Shreiner
// ISBN-10:   0321502795
// ISBN-13:   9780321502797
// Publisher: Addison-Wesley Professional
// URLs:      http://safari.informit.com/9780321563835
//            http://www.opengles-book.com
//

// Simple_Texture2D.c
//
//    This is a simple example that draws a quad with a 2D
//    texture image. The purpose of this example is to demonstrate 
//    the basics of 2D texturing
//
#include <stdlib.h>
#include <stdio.h>
#include "esUtil.h"

typedef struct
{
   // Handle to a program object
   GLuint programObject;

   // Attribute locations
   GLint  positionLoc;
   GLint  texCoordLoc;

   // Uniform locations
   GLint  rotateLoc;

   // Sampler location
   GLint samplerLoc;

   // Texture handle
   GLuint textureId;

   GLubyte *image;
    int width, height;

   // Rotation angle
   GLfloat   angle;

   // rotate matrix
   ESMatrix  rotateMx;
} UserData;

///
// Create a simple 2x2 texture image with four different colors
//
GLuint CreateSimpleTexture2D(ESContext *esContext)
{
   // Texture object handle
   GLuint textureId;
   UserData *userData = esContext->userData;
   
#if 0
   // 2x2 Image, 3 bytes per pixel (R, G, B)
   GLubyte pixels[4 * 3] =
   {  
      255,   0,   0, // Red
        0, 255,   0, // Green
        0,   0, 255, // Blue
      255, 255,   0  // Yellow
   };
   userData->width = 2;
   userData->height = 2;
#else
   GLubyte *pixels = userData->image;
   userData->width = esContext->width;
   userData->height = esContext->height;
#endif
   // Use tightly packed data
   glPixelStorei ( GL_UNPACK_ALIGNMENT, 1 );

   // Generate a texture object
   glGenTextures ( 1, &textureId );

   // Bind the texture object
   glBindTexture ( GL_TEXTURE_2D, textureId );

   // Load the texture
   glTexImage2D ( GL_TEXTURE_2D, 0, GL_RGB, 
		  userData->width, userData->height, 
		  0, GL_RGB, GL_UNSIGNED_BYTE, pixels );

   // Set the filtering mode
   glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST );
   glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST );

   return textureId;

}


///
// Initialize the shader and program object
//
int Init ( ESContext *esContext )
{
    UserData *userData = esContext->userData;
    GLbyte vShaderStr[] = 
      "uniform mat4 u_rotateMx;  \n" 
      "attribute vec4 a_position;   \n"
      "attribute vec2 a_texCoord;   \n"
      "varying vec2 v_texCoord;     \n"
      "void main()                  \n"
      "{                            \n"
      "   gl_Position = u_rotateMx * a_position; \n"
      "   v_texCoord = a_texCoord;  \n"
      "}                            \n";
   
    GLbyte fShaderStr[] =  
      "precision mediump float;                            \n"
      "varying vec2 v_texCoord;                            \n"
      "uniform sampler2D s_texture;                        \n"
      "void main()                                         \n"
      "{                                                   \n"
      "  gl_FragColor = texture2D( s_texture, v_texCoord );\n"
      "}                                                   \n";

   // Load the shaders and get a linked program object
   userData->programObject = esLoadProgram ( vShaderStr, fShaderStr );

   // Get the attribute locations
   userData->positionLoc = glGetAttribLocation ( userData->programObject, "a_position" );
   userData->texCoordLoc = glGetAttribLocation ( userData->programObject, "a_texCoord" );
   userData->rotateLoc = glGetUniformLocation( userData->programObject, "u_rotateMx" );
   // Starting rotation angle for the square
   userData->angle = 0.0f;
   
   // Get the sampler location
   userData->samplerLoc = glGetUniformLocation ( userData->programObject, "s_texture" );

   // Load the texture
   userData->textureId = CreateSimpleTexture2D (esContext);

   glClearColor ( 0.0f, 0.0f, 0.0f, 0.0f );   

   return GL_TRUE;
}

///
// Update rotate matrix based on time
//
void Update ( ESContext *esContext, float deltaTime )
{
   UserData *userData = (UserData*) esContext->userData;
   
   // Compute a rotation angle based on time to rotate the cube
   userData->angle += ( deltaTime * 40.0f );
   if( userData->angle >= 360.0f )
      userData->angle -= 360.0f;

   // Generate an identity matrix before rotating the square
   esMatrixLoadIdentity(  &userData->rotateMx );

   // Rotate the square
   esRotate(  &userData->rotateMx, userData->angle, 1.0, 0.0, 1.0 );
}

///
// Draw a triangle using the shader pair created in Init()
//
void Draw ( ESContext *esContext )
{
   UserData *userData = esContext->userData;
   GLfloat vVertices[] = { -0.5f,  0.5f, 0.0f,  // Position 0
                            0.0f,  1.0f,        // TexCoord 0 
                           -0.5f, -0.5f, 0.0f,  // Position 1
                            0.0f,  0.0f,        // TexCoord 1
                            0.5f, -0.5f, 0.0f,  // Position 2
                            1.0f,  0.0f,        // TexCoord 2
                            0.5f,  0.5f, 0.0f,  // Position 3
                            1.0f,  1.0f         // TexCoord 3
                         };
   GLushort indices[] = { 0, 1, 2, 0, 2, 3 };
      
   // Set the viewport
   glViewport ( 0, 0, esContext->width, esContext->height );
   
   // Clear the color buffer
   glClear ( GL_COLOR_BUFFER_BIT );

   // Use the program object
   glUseProgram ( userData->programObject );

   // Load the vertex position
   glVertexAttribPointer ( userData->positionLoc, 3, GL_FLOAT, 
                           GL_FALSE, 5 * sizeof(GLfloat), vVertices );
   // Load the texture coordinate
   glVertexAttribPointer ( userData->texCoordLoc, 2, GL_FLOAT,
                           GL_FALSE, 5 * sizeof(GLfloat), &vVertices[3] );

   glEnableVertexAttribArray ( userData->positionLoc );
   glEnableVertexAttribArray ( userData->texCoordLoc );

   // Load the rotate matrix
   glUniformMatrix4fv( userData->rotateLoc, // userData->mvpLoc, 
		       1, GL_FALSE, (GLfloat*) &userData->rotateMx.m[0][0] );

   // Bind the texture
   glActiveTexture ( GL_TEXTURE0 );
   glBindTexture ( GL_TEXTURE_2D, userData->textureId );

   // Set the sampler texture unit to 0
   glUniform1i ( userData->samplerLoc, 0 );

   glDrawElements ( GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, indices );

}

///
// Cleanup
//
void ShutDown ( ESContext *esContext )
{
   UserData *userData = esContext->userData;

   // Delete texture object
   glDeleteTextures ( 1, &userData->textureId );

   // Delete program object
   glDeleteProgram ( userData->programObject );
	
   free(esContext->userData);
}

int main ( int argc, char *argv[] )
{
   ESContext esContext;
   UserData  userData;

   int width, height;
   GLubyte *image;
   
   image = esLoadTGA("jan.tga", &width, &height);
   if (image == NULL) {
       fprintf(stderr, "No such image\n");
       exit(1);
   }
   printf("Width %d height %d\n", width, height);
   
   userData.image = image;
   userData.width = width;
   userData.height = height;
   

   esInitContext ( &esContext );
   esContext.userData = &userData;

   esCreateWindow ( &esContext, "Simple Texture 2D", width, height, ES_WINDOW_RGB );

   if ( !Init ( &esContext ) )
      return 0;

   esRegisterDrawFunc ( &esContext, Draw );
   esRegisterUpdateFunc ( &esContext, Update );

   esMainLoop ( &esContext );

   ShutDown ( &esContext );
}

      

Conclusion

OpenGL ES is an amazingly complex and sophisticated system that we have made a small attempt to explain. This chapter has looked at drawing on the EGL surface discussed in earlier chapters and considered a number or relatively simple cases. These have been standard OpenGL ES - the only RPi specific part has been the EGL surface.

	
      

Copyright © Jan Newmarch, jan@newmarch.name
Creative Commons License
" Programming AudioVideo on the Raspberry Pi GPU " by Jan Newmarch is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License .
Based on a work at https://jan.newmarch.name/RPi/ .

If you like this book, please contribute using PayPal

Or Flattr me:
Flattr this book