Following the critically acclaimed Part I and the not-as-good-as-the-first-part-but-still-good-enough Part II, here comes the back-to-the-origins third act of the Android port of our C++ game Andy’s Super Great Park!
Today’s topic is the migration from OpenGL direct mode to GLES. This is the last blocking element in the compilation of the engine and the game. Consequently, this is a very exciting step. As you will see, the changes are quite straightforward, as soon as we know where to go.
After a short reminder of the original OpenGL direct mode API, I will define the target GL API and the acceptable compromises in the engine’s functionalities. Then I will describe the conversion of the existing rendering code using GLES and provide a workaround for the disabled functionalities of the engine.
About OpenGL direct mode and GLES
The direct mode is the old way to describe a scene in OpenGL. It was the only way to render things for a long time and it was deprecated starting with OpenGL 3.0. Still, you may find code samples using the direct mode all over the web, especially in old tutorials and even in recent forum questions.
Due to this wide visibility, it is still considered as the easiest way to start OpenGL programming (unfortunately), especially when you are in the prototyping phase (i.e. when you want to render things, not to render efficiently).
You may recognize this approach by the use of the glBegin()/glEnd()
directives, as in the following code sample:
glBegin( GL_QUADS );
glColor4f(red, green, blue, alpha);
glVertex3f(x[0], y[0], z[0]);
glVertex3f(x[1], y[1], z[1]);
glVertex3f(x[2], y[2], z[2]);
glVertex3f(x[3], y[3], z[3]);
glEnd();
So, this API is now deprecated and even if it still works on desktop applications, it is not available on newer phones and tablets. For them, we have to switch to OpenGL for embedded systems, also known as GLES.
Picking the right API
As I write, there are three versions of GLES. The third version is quite recent and still not widely available, so we will not use it. Thus we have to choose between GLES 1 and GLES 2.
GLES 1 is interesting in the sense that it is widely available and
requires minimum changes in the rendering process. The changes mostly
consist into replacing the glBegin()/glEnd()
sections with calls to
glEnableClientState()
and
glVertexPointer()
and similar. On the other hand, there is no shader support in
GLES 1. Thus we’ll have to disable this part of the engine and find a
way to work without it.
GLES 2 is most recent and also widely available. Consequently this is
the most interesting candidate to update the game engine. Shaders are
supported, which is great, but on the other hand you have to use them
every time (i.e. there is no default vertex nor fragment
shader). Also, the calls to render primitives are a bit more complex
(see
glVertexAttribPointer()
to provide the coordinates of your vertices).
As you may have observed in the previous posts, I am a partisan of small changes and incremental progression. It comes to no surprise, then, that I chose to switch to GLES 1 and to leave the shaders out for now. Indeed, the support for shaders in the engine were added for Tunnel and are absolutely not required for Andy’s Super Great Park. Consequently I will be able to focus on the port of the game rather than on the upgrade of the engine, which will be done later.
Converting OpenGL calls
Let’s code! The engine supports three graphical objects: colored lines, colored polygons and sprites. To render them, we will need the following OpenGL functions:
glEnableClientState()
/glDisableClientState()
to activate/deactivate the OpenGL elements we need (colors, vertices, texture coordinates),glColorPointer()
to provide the colors of the shape,glVertexPointer()
to provide the vertices of the shape,glTexCoordPointer()
to provide the coordinates in the texture,glDrawArrays()
to effectively draw the previously provided elements.
All of the glSomethingPointer(size, type, stride, pointer)
functions
work in the same way. The idea is to provide a pointer
to a memory
area containing several elements (“something”) represented by size
primitive values of a given type
. Adjacent elements are expected to
be separated in memory by stride
bytes.
Before calling a glSomethingPointer()
function, we have to activate
the element by a call to glEnableClientState(cap)
with the right
argument (in our case, one of GL_COLOR_ARRAY
, GL_VERTEX_ARRAY
and
GL_TEXTURE_COORD_ARRAY
).
Then, when everything is well described, we just have to render them
by a call to glDrawArrays(mode, first, count)
. The first argument
tells which primitives to build with the vertices (in our case, one of
GL_LINE_STRIP
or GL_TRIANGLE_FAN
), the second one is the index of
the first element to render in the array and the last one is the
number of elements to render. Obviously the size of the memory area
passed to each of the previous calls to glSomethingPointer()
must be
greater or equal to (first + count) × size
.
Disabling the shaders and other adaptations
In order to have the code to compile without completely dropping the
shaders, I have declared a set of macros to fill the nonexistent
shader interface. For example, the compiler complains about
glCreateShader()
not being defined, thus I put in my code:
#define glCreateShader( a ) 0
And so on for each missing function, until I obtain a dummy shader API.
Also, an important constraint of GLES compared with OpenGL is that
GLdouble
is not available, so are the related functions of the
API. The single floating point type available is thus GLfloat
. In
order to pass easily from the latter to the former, I had to put these
two lines in my code:
typedef GLfloat GLdouble;
#define glOrtho glOrthof
The idea is to replace the nonexistent declaration of GLdouble
and
related functions by an alias to GLfloat
and their
functions. Fortunately for me, there was not a lot of them in my code.
What’s next?
All these changes can be seen in details in the commit of the conversion to GLES. As stated before, it was the last blocking step before being able to compile the game. Once these modifications were done, the engine and the game compiled well.
Before being able to run the game on my phone, a launcher and an Android application must be written, along with a new procedure to load the game’s resources. Several other adjustments will have to be done too, like the handling of the touch events or the translations. Also, the initial performances will be quite low and I will have to do some optimizations in various parts of the code in order to reach an acceptable frame rate. These optimizations will be also described in future notes.