An Introduction to BREW and OpenGL ES
OpenGL ES 1.0At 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.
The Renderer classKeeping 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::DestroyI 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::FlipToScreenWe 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 TriangleAdd 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 TextWe 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! ConclusionHopefully 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! SourceThe 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 readingBooks
Contest
BREW
Open GL ES
Open GL
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 |
|