Upcoming Events
Unite 2010
11/10 - 11/12 @ Montréal, Canada

GDC China
12/5 - 12/7 @ Shanghai, China

Asia Game Show 2010
12/24 - 12/27  

GDC 2011
2/28 - 3/4 @ San Francisco, CA

More events...
Quick Stats
111 people currently visiting GDNet.
2406 articles in the reference section.

Help us fight cancer!
Join SETI Team GDNet!
Link to us Events 4 Gamers
Intel sponsors gamedev.net search:

An Introduction to BREW and OpenGL ES


OpenGL ES 1.0

At last we come to the actual topic of this article, OpenGL ES. It taken a while to get here mainly because for a lot of people this will be their first non-PC programming target.

If you have any experience with writing Open GL code you will find your knowledge translates nearly directly to GL ES. There are however a few differences. The startup and shutdown sequence is different from PC based Open GL. Given that there are no floating point functions things have slightly different names. Most of the names simply replace the trailing f (for float), with an x (for fixed). The most significant difference is GL ES does away with the immediate mode glVertexf interface. All renderering is done through the batched glDrawElements interface for improved efficiency.

To get access to the GL ES functions and data types you need to include IGL.h in your code. You will also need to add the file GL.c, which came with the GL ES SDK to your project. Its located at c:\BREW\BREW 3.0.1\sdk\src\GL.c.

There's another header called AEEGL.h which is intended for (the few) people who'd prefer to use OpenGL ES in the same way other BREW features are used: through an interface. So instead of calling

glPushMatrix()

you'd call

IGL_glPushMatrix(pIGL)

where pIGL is a pointer to an IGL interface.

This article sticks to the standard way of using OpenGL.

The Renderer class

Keeping with the oo theme, all the setup and shutdown code is gathered into a class called Renderer. Take a look at the class definition.

// From Renderer.h
class Renderer
{
private:
  IGL *       mIGL;
  IEGL *      mIEGL;

  EGLDisplay  mDisplay;
  EGLConfig   mConfig;
  EGLSurface  mSurface;
  EGLContext  mContext;

public:
  boolean Create(IShell * shell, IDisplay * display);
  void Destroy();

  void FlipToScreen();
};

IGL is an Interface to GL, while IEGL is a platform specific layer to sit between IGL and the underlying architecture.

The other parameters are just as their names suggest. EGLDisplay is the graphics display, EGLConfig is the video mode (there is normally only one mode available, as opposed to a PC graphics card which might have several to choose from). EGLSurface is the actual surface rendering operations write to. EGLContext represents the current state of the GL environment that will be used when you execute commands.

Renderer::Create(..)

Throughout this function its very important to check every function call for errors, and to completely clean up if anything goes wrong. On a PC its maybe a bit annoying if a program spews garbage and you have to reboot, but I have heard several stories of phones locking up and having to be sent for repair after particularly nasty code errors.

  // From Renderer.cpp
  if (ISHELL_CreateInstance(shell, AEECLSID_GL, (void **)&mIGL) != SUCCESS)
  {
    Destroy();
    return FALSE;
  }

  if (ISHELL_CreateInstance(shell, AEECLSID_EGL, (void **)&mIEGL) != SUCCESS)
  {
    Destroy();
    return FALSE;
  }

  IGL_Init(mIGL);
  IEGL_Init(mIEGL);

Using the ISHELL interface we get BREW to create IGL and IEGL objects for us. The IGL_Init() and IEGL_Init() functions are part of a wrapper system that stores pointers to the IGL and IEGL so we can just call the more usual glClear(..) rather than IGL_glClear(mIGL, ...).

  // From Renderer.cpp
  mDisplay = eglGetDisplay(display);

  if (mDisplay == EGL_NO_DISPLAY) 
  {
    Destroy();
    return FALSE;
  }

Get the GL display, based on the current BREW display.

  // From Renderer.cpp
  EGLint major = 0;
  EGLint minor = 0;

  if (eglInitialize(mDisplay, &major, &minor) == FALSE)
  {
    Destroy();
    return FALSE;
  }

  DBGPRINTF(" *** ES version %d.%d", major, minor);

Initialize GL ES, which also sets major and minor to the major and minor version numbers of the current GL ES implementation. At the moment that is going to always say 1.0, but version 1.1 is coming soon. In the future it will be worth checking this the same way you check for various extensions in GL to be able to use more advanced features if they are available. If you really don't care, you can pass NULL for the last two parameters to not retrieve the version information.

  // From Renderer.cpp
  EGLint numConfigs = 1;
  if (eglGetConfigs(mDisplay, &mConfig, 1, &numConfigs) == FALSE)
  {
    Destroy();
    return false;
  }

Retrieve a valid configuration based on the display.

  // From Renderer.cpp
  IBitmap *  DeviceBitmap = NULL; 
  IDIB *    DIB = NULL;

  if (IDISPLAY_GetDeviceBitmap(display, &DeviceBitmap) != SUCCESS) 
  {
    Destroy();
    return FALSE;
  }

  if (IBITMAP_QueryInterface(DeviceBitmap, AEECLSID_DIB, (void**)&DIB) != SUCCESS) 
  {
    IBITMAP_Release(DeviceBitmap);
    Destroy();
    return FALSE;
  }

Using the BREW IDISPLAY interface, get the current device bitmap. From this, use the IBITMAP interface to query for a device dependant bitmap (a bitmap in the native phone format). This will be our front buffer.

  // From Renderer.cpp
  mSurface = eglCreateWindowSurface(mDisplay, mConfig, DIB, NULL);

  IDIB_Release(DIB);
  IBITMAP_Release(DeviceBitmap);

  if (mSurface == EGL_NO_SURFACE) 
  {
    Destroy();
    return FALSE;
  }

Create the surface we will be rendering to. This is our back buffer which when we issue an eglSwapBuffers will be copied to the font buffer. We can release the bitmaps we acquired earlier, they have served their purpose.

  // From Renderer.cpp
  mContext = eglCreateContext(mDisplay, mConfig, NULL, NULL); 

  if (mContext == EGL_NO_CONTEXT)
  {
    Destroy();
    return FALSE;
  }

  if (eglMakeCurrent(mDisplay, mSurface, mSurface, mContext) == FALSE)
  {
    Destroy();
    return FALSE;
  }

Create a context, and then lastly make our display, surface and context current so they are the target of any rendering we do.

Assuming we got this far with no errors, the basic GL ES system is up and ready to be used.

Renderer::Destroy

I have mentioned the importance of cleaning up correctly several times, so lets take a look at the Destroy function that takes care of shutting everything down.

  // From Renderer.cpp
  eglMakeCurrent(EGL_NO_DISPLAY, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);

  if (mContext)
  {
    eglDestroyContext(mDisplay, mContext);
    mContext = NULL;
  }

  if (mSurface)
  {
    eglDestroySurface(mDisplay, mSurface);
    mSurface = NULL;
  }

  if (mDisplay)
  {
    eglTerminate(mDisplay);
    mDisplay = NULL;
  }

  if (mIEGL)
  {
    IEGL_Release(mIEGL);
    mIEGL = NULL;
  }

  if (mIGL)
  {
    IGL_Release(mIGL);
    mIGL = NULL;
  }

First we deactivate our display, surface and context, then take each in turn and destroy or release them depending on how they were created.

Renderer::FlipToScreen

We are nearly finished with the Renderer class now, lets take a look at the final function FlipToScreen, and then move onto actually getting something on screen.

// From Renderer.cpp
void Renderer::FlipToScreen()
{
  eglSwapBuffers(mDisplay, mSurface);
}

That is the entire function, it just calls eglSwapBuffers to copy our backbuffer to the screen.

A Spinning Triangle

Add an instance of Renderer to the Game class. Also add an int called mRotateAngle to record the current rotation of the triangle. In Game::Create, at the end, we have this:

  mRenderer.Create(mShell, mDisplay);

  // Enable the zbuffer
  glEnable(GL_DEPTH_TEST);

  // Set the view port size to the window size
  glViewport(0, 0, GetWidth(), GetHeight());

  // Setup the projection matrix
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();

  // Diable lighting and alpha blending
  glDisable(GL_LIGHTING);
  glDisable(GL_BLEND);

  // Set the fustrum clipping planes
  glFrustumx(ITOFP(-5), ITOFP(5), ITOFP(-5), ITOFP(5), ITOFP(10),  ITOFP(100));

  // Set the model view to identity
  glMatrixMode(GL_MODELVIEW );
  glLoadIdentity();

  // Enable the arrays we want used when we glDrawElements(..)
  glEnableClientState(GL_VERTEX_ARRAY);
  glEnableClientState(GL_COLOR_ARRAY);

  mRotateAngle = 0;

This initializes our renderer with our stored ISHELL and IDISPLAY. It sets various initial GL states. Note the use of the ITOFP macro to convert values into 16.16 fixed point. Start the triangle with no rotation, facing the camera. Don't forget to add a matching call to Renderer::Destroy to Game::Destroy to clean up when the program exits.

  glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

  glPushMatrix();
  glLoadIdentity();

  glTranslatex(0,  0, ITOFP(-15));

  if (mKeysDown[AVK_LEFT - AVK_FIRST] == TRUE)
  {
    mRotateAngle -= 3;
  }
  if (mKeysDown[AVK_RIGHT - AVK_FIRST] == TRUE)
  {
    mRotateAngle += 3;
  }
  
  if (mRotateAngle < 0) mRotateAngle += 360;
  if (mRotateAngle > 360) mRotateAngle -= 360;

  glRotatex(ITOFP(mRotateAngle), ITOFP(0), ITOFP(1), ITOFP(0));

  int FaceData[9] =
  {
    -ITOFP(2), -ITOFP(2), ITOFP(0),    // First vertex position
    ITOFP(2),  -ITOFP(2), ITOFP(0),    // Second vertex position
    -ITOFP(0),  ITOFP(2), ITOFP(0)    // Third vertex position
  };

  int ColorData[12] =
  {
    ITOFP(1), ITOFP(0), ITOFP(0), ITOFP(0),  // First vertex color
    ITOFP(0), ITOFP(1), ITOFP(0), ITOFP(0),  // Second vertex color
    ITOFP(0), ITOFP(0), ITOFP(1), ITOFP(0)  // Third vertex color
  };

  uint8 IndexData[3] = {0, 1, 2};

  glVertexPointer(3, GL_FIXED, 0, FaceData);  // Set the vertex (position) data source
  glColorPointer(4, GL_FIXED, 0, ColorData);  // Set the color data source

  glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_BYTE, IndexData);  // Draw the triangle

  glPopMatrix();

  mRenderer.FlipToScreen();

As I said earlier if you have any existing OpenGL knowledge this should look very familiar to you. Note again the use of the ITOFP macro to convert values to 16.16 fixed point, and the introduction of the new data type GL_FIXED as a parameter to various functions.

Adding Text

We can use the BREW functions to draw text to screen, as long as we do it after ending GL rendering for the frame. At the end of Game::Tick, after calling mRenderer.EndFrame(), add this:

  AECHAR buffer[16];
  WSPRINTF(buffer, 16, L"FPS: %d", 1000 / timeElapsed);
  IDISPLAY_DrawText(mDisplay, AEE_FONT_BOLD, buffer, -1, 5, 5, NULL, 0);

  IDISPLAY_Update(mDisplay);

The last call, IDISPLAY_Update only needs to be called once at the very end for however much text or other data you want to put on screen. BREW is entirely UNICODE (except for filenames), so we need to use the wide version of sprintf. To declare a string constant as a wide string simply precede it with an L.

Run!

If you compile and run you should have a triangle with three different colored corners. The FPS should be in the top left corner, and pressing the left and right arrows (either use the keyboard, or click the emulator buttons) should rotate the triangle. Congratulations, you just wrote your first Open GL ES program!

Conclusion

Hopefully if you have been following along you have managed to install the BREW SDK, set up the emulator and have built your first OpenGL ES program.

Once you have OpenGL ES up and running you can use nearly any existing OpenGL books or websites for information. Just bear in mind the restrictions of the hardware, and don't forget to convert all your values to 16.16 fixed point!

Source

The source code, including a Visual C++ 2003 project file, that accompanies this article is available here (10k).

Where now?

I hope you are aware of the great contest Gamedev.net and Qualcomm are running. This article provides enough information to get you started writing the contest winner, and next cult classic 3d game for mobile phones.

Further reading

Books
OpenGL ES Game Development by Dave Astle and Dave Durnil

Contest
Gamedev.net & Qualcomm OpenGL ES development contest

BREW
Register for a free BREW developer account
Install the BREW SDK
Get the BREW GL ES SDK (and Visual C++ addon)
BREW developer forums

Open GL ES
Official OpenGL ES web site

Open GL
OpenGL.org
NeHe OpenGL tutorials

Fixed Point Math
Fixed point math article
More fixed point math

"The road must be trod, but it will be very hard. And neither strength nor wisdom will carry us far upon it. This quest may be attempted by the weak with as much hope as the strong. Yet such is oft the course of deeds that move the wheels of the world: small hands do them because they must, while the eyes of the great are elsewhere." J.R.R. Tolkein, The Fellowship of the Ring


Contents
  Introduction
  Data storage, timers, math and input
  OpenGL ES

  Printable version
  Discuss this article