Let's now see how to implement a sprite batch in DroidBlaster:
jni/GraphicsManager.hpp
. Create the class GraphicsComponent
, which defines a common interface for all rendering techniques starting with sprite batches. Define a few new methods such as:getProjectionMatrix()
which provides an OpenGL matrix to project 2D graphics on screenloadShaderProgram()
to load a vertex and fragment shader and link them together into an OpenGL programregisterComponent()
which records a list of GraphicsComponent
to initialize and renderCreate the RenderVertex
private structure representing the structure of an individual sprite vertex.
Also, declare a few new member variables such as:
mProjectionMatrix
to store an orthographic projection (as opposed to a perspective projection used in 3D games).mShaders
, mShaderCount
, mComponents
, and mComponentCount
to keep trace of all resources.Finally, get rid of all the GraphicsElement
stuff used in the previous chapter to render raw graphics, as shown in the following code:
... class GraphicsComponent { public: virtual status load() = 0; virtual void draw() = 0; }; ...
GraphicsManager
:getProjectionMatrix()
which provides an OpenGL matrix to project 2D graphics on screenloadShaderProgram()
to load a vertex and fragment shader and link them together into an OpenGL programregisterComponent()
which records a list of GraphicsComponent to initialize and renderCreate the RenderVertex
private structure representing the structure of an individual sprite vertex.
Also, declare a few new member variables such as:
mProjectionMatrix
to store an orthographic projection (as opposed to a perspective projection used in 3D games)mShaders
, mShaderCount
, mComponents
, and mComponentCount
to keep trace of all resources.Finally, get rid of all the GraphicsElement
stuff used in the previous chapter to render raw graphics:
... class GraphicsManager { public: GraphicsManager(android_app* pApplication); ~GraphicsManager(); int32_t getRenderWidth() { return mRenderWidth; } int32_t getRenderHeight() { return mRenderHeight; } GLfloat* getProjectionMatrix() { return mProjectionMatrix[0]; } void registerComponent(GraphicsComponent* pComponent); status start(); void stop(); status update(); TextureProperties* loadTexture(Resource& pResource); GLuint loadShader(const char* pVertexShader, const char* pFragmentShader); private: struct RenderVertex { GLfloat x, y, u, v; }; android_app* mApplication; int32_t mRenderWidth; int32_t mRenderHeight; EGLDisplay mDisplay; EGLSurface mSurface; EGLContext mContext; GLfloat mProjectionMatrix[4][4]; TextureProperties mTextures[32]; int32_t mTextureCount; GLuint mShaders[32]; int32_t mShaderCount; GraphicsComponent* mComponents[32]; int32_t mComponentCount; }; #endif
jni/GraphicsManager.cpp
.Update the constructor initialization list and the destructor. Again, get rid of everything related to GraphicsElement
.
Also implement registerComponent()
in place of registerElement()
:
... GraphicsManager::GraphicsManager(android_app* pApplication) : mApplication(pApplication), mRenderWidth(0), mRenderHeight(0), mDisplay(EGL_NO_DISPLAY), mSurface(EGL_NO_CONTEXT), mContext(EGL_NO_SURFACE), mProjectionMatrix(), mTextures(), mTextureCount(0), mShaders(), mShaderCount(0), mComponents(), mComponentCount(0) { Log::info("Creating GraphicsManager."); } GraphicsManager::~GraphicsManager() { Log::info("Destroying GraphicsManager."); } void GraphicsManager::registerComponent(GraphicsComponent* pComponent) { mComponents[mComponentCount++] = pComponent; } ...
onStart()
to initialize the Orthographic projection matrix array with display dimensions (we will see how to compute matrices more easily using GLM in Chapter 9, Porting Existing Libraries to Android) and load components.A projection matrix is a mathematical way to project 3D objects composing a scene into a 2D plane, which is the screen. In orthographic projection, a projection is perpendicular to the display surface. That means that an object has exactly the same size whether it is close or far away from the point of view. Orthographic projection is appropriate for 2D games. Perspective projection, in which objects look smaller the farther they are, is rather used for 3D games.
For more information, have a look at http://en.wikipedia.org/wiki/Graphical_projection.
... status GraphicsManager::start() { ... glViewport(0, 0, mRenderWidth, mRenderHeight); glDisable(GL_DEPTH_TEST); // Prepares the projection matrix with viewport dimesions. memset(mProjectionMatrix[0], 0, sizeof(mProjectionMatrix)); mProjectionMatrix[0][0] = 2.0f / GLfloat(mRenderWidth); mProjectionMatrix[1][1] = 2.0f / GLfloat(mRenderHeight); mProjectionMatrix[2][2] = -1.0f; mProjectionMatrix[3][0] = -1.0f; mProjectionMatrix[3][1] = -1.0f; mProjectionMatrix[3][2] = 0.0f; mProjectionMatrix[3][3] = 1.0f; // Loads graphics components. for (int32_t i = 0; i < mComponentCount; ++i) { if (mComponents[i]->load() != STATUS_OK) { return STATUS_KO; } } return STATUS_OK; ... } ...
loadShaderProgram()
in stop()
.... void GraphicsManager::stop() { Log::info("Stopping GraphicsManager."); for (int32_t i = 0; i < mTextureCount; ++i) { glDeleteTextures(1, &mTextures[i].texture); } mTextureCount = 0; for (int32_t i = 0; i < mShaderCount; ++i) { glDeleteProgram(mShaders[i]); } mShaderCount = 0; // Destroys OpenGL context. ... } ...
update()
after the display is cleared but before it is refreshed:... status GraphicsManager::update() { glClear(GL_COLOR_BUFFER_BIT); for (int32_t i = 0; i < mComponentCount; ++i) { mComponents[i]->draw(); } if (eglSwapBuffers(mDisplay, mSurface) != EGL_TRUE) { ... } ...
loadShader()
. Its role is to compile and load the given shaders passed as a human-readable GLSL program. To do so:glCreateShader()
.glShaderSource()
.glCompileShader()
and check the compilation status with glGetShaderiv()
. The compilation errors can be read with glGetShaderInfoLog()
.Repeat the operation for the given fragment shader:
... GLuint GraphicsManager::loadShader(const char* pVertexShader, const char* pFragmentShader) { GLint result; char log[256]; GLuint vertexShader, fragmentShader, shaderProgram; // Builds the vertex shader. vertexShader = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertexShader, 1, &pVertexShader, NULL); glCompileShader(vertexShader); glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &result); if (result == GL_FALSE) { glGetShaderInfoLog(vertexShader, sizeof(log), 0, log); Log::error("Vertex shader error: %s", log); goto ERROR; } // Builds the fragment shader. fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragmentShader, 1, &pFragmentShader, NULL); glCompileShader(fragmentShader); glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &result); if (result == GL_FALSE) { glGetShaderInfoLog(fragmentShader, sizeof(log), 0, log); Log::error("Fragment shader error: %s", log); goto ERROR; } ...
glCreateProgram()
.glAttachShader()
.glLinkProgram()
to create the final program. Shader consistencies and compatibility with the hardware is checked at that point. The result can be checked with glGetProgramiv()
.... shaderProgram = glCreateProgram(); glAttachShader(shaderProgram, vertexShader); glAttachShader(shaderProgram, fragmentShader); glLinkProgram(shaderProgram); glGetProgramiv(shaderProgram, GL_LINK_STATUS, &result); glDeleteShader(vertexShader); glDeleteShader(fragmentShader); if (result == GL_FALSE) { glGetProgramInfoLog(shaderProgram, sizeof(log), 0, log); Log::error("Shader program error: %s", log); goto ERROR; } mShaders[mShaderCount++] = shaderProgram; return shaderProgram; ERROR: Log::error("Error loading shader."); if (vertexShader > 0) glDeleteShader(vertexShader); if (fragmentShader > 0) glDeleteShader(fragmentShader); return 0; } ...
jni/Sprite.hpp
, which defines a class with all the necessary data to animate and draw a single sprite.Create a Vertex
structure which defines the content of a sprite vertex. We need a 2D position and texture coordinates which delimit the sprite picture.
Then, define a few methods:
setAnimation()
and animationEnded()
. Location is publicly available for simplicity purposes.SpriteBatch
. It can load()
and draw()
sprites.#ifndef _PACKT_GRAPHICSSPRITE_HPP_ #define _PACKT_GRAPHICSSPRITE_HPP_ #include "GraphicsManager.hpp" #include "Resource.hpp" #include "Types.hpp" #include <GLES2/gl2.h> class SpriteBatch; class Sprite { friend class SpriteBatch; public struct Vertex { GLfloat x, y, u, v; }; Sprite(GraphicsManager& pGraphicsManager, Resource& pTextureResource, int32_t pHeight, int32_t pWidth); void setAnimation(int32_t pStartFrame, int32_t pFrameCount, float pSpeed, bool pLoop); bool animationEnded() { return mAnimFrame > (mAnimFrameCount-1); } Location location; protected: status load(GraphicsManager& pGraphicsManager); void draw(Vertex pVertex[4], float pTimeStep); ...
mWidth
and mHeight
, horizontal, vertical, and total number of frames in mFrameXCount
, mFrameYCount
, and mFrameCount
mAnimStartFrame
and mAnimFrameCount
, animation speed in mAnimSpeed
, the currently shown frame in mAnimFrame
, and a looping indicator in mAnimLoop
:... private: Resource& mTextureResource; GLuint mTexture; // Frame. int32_t mSheetHeight, mSheetWidth; int32_t mSpriteHeight, mSpriteWidth; int32_t mFrameXCount, mFrameYCount, mFrameCount; // Animation. int32_t mAnimStartFrame, mAnimFrameCount; float mAnimSpeed, mAnimFrame; bool mAnimLoop; }; #endif
jni/Sprite.cpp
constructor and initialize the members to default values:#include "Sprite.hpp" #include "Log.hpp" Sprite::Sprite(GraphicsManager& pGraphicsManager, Resource& pTextureResource, int32_t pHeight, int32_t pWidth) : location(), mTextureResource(pTextureResource), mTexture(0), mSheetWidth(0), mSheetHeight(0), mSpriteHeight(pHeight), mSpriteWidth(pWidth), mFrameCount(0), mFrameXCount(0), mFrameYCount(0), mAnimStartFrame(0), mAnimFrameCount(1), mAnimSpeed(0), mAnimFrame(0), mAnimLoop(false) {} ...
load()
as texture dimensions are known only at load time:... status Sprite::load(GraphicsManager& pGraphicsManager) { TextureProperties* textureProperties = pGraphicsManager.loadTexture(mTextureResource); if (textureProperties == NULL) return STATUS_KO; mTexture = textureProperties->texture; mSheetWidth = textureProperties->width; mSheetHeight = textureProperties->height; mFrameXCount = mSheetWidth / mSpriteWidth; mFrameYCount = mSheetHeight / mSpriteHeight; mFrameCount = (mSheetHeight / mSpriteHeight) * (mSheetWidth / mSpriteWidth); return STATUS_OK; } ...
... void Sprite::setAnimation(int32_t pStartFrame, int32_t pFrameCount, float pSpeed, bool pLoop) { mAnimStartFrame = pStartFrame; mAnimFrame = 0.0f, mAnimSpeed = pSpeed, mAnimLoop = pLoop; mAnimFrameCount = pFrameCount; } ...
draw()
, first update the frame to draw according to the sprite animation and the time spent since the last frame. What we need is the indices of the frame in the spritesheet:... void Sprite::draw(Vertex pVertices[4], float pTimeStep) { int32_t currentFrame, currentFrameX, currentFrameY; // Updates animation in loop mode. mAnimFrame += pTimeStep * mAnimSpeed; if (mAnimLoop) { currentFrame = (mAnimStartFrame + int32_t(mAnimFrame) % mAnimFrameCount); } else { // Updates animation in one-shot mode. if (animationEnded()) { currentFrame = mAnimStartFrame + (mAnimFrameCount-1); } else { currentFrame = mAnimStartFrame + int32_t(mAnimFrame); } } // Computes frame X and Y indexes from its id. currentFrameX = currentFrame % mFrameXCount; // currentFrameY is converted from OpenGL coordinates // to top-left coordinates. currentFrameY = mFrameYCount - 1 - (currentFrame / mFrameXCount); ...
pVertices
. Each of these vertices is composed of a sprite position (posX1
, posY1
, posX2
, posY2
) and texture coordinates (u1
, u2
, v1
, v2
). Compute and generate these vertices dynamically in the memory buffer, pVertices
, provided in the parameter. This memory buffer will be given later to OpenGL to render the sprite:... // Draws selected frame. GLfloat posX1 = location.x - float(mSpriteWidth / 2); GLfloat posY1 = location.y - float(mSpriteHeight / 2); GLfloat posX2 = posX1 + mSpriteWidth; GLfloat posY2 = posY1 + mSpriteHeight; GLfloat u1 = GLfloat(currentFrameX * mSpriteWidth) / GLfloat(mSheetWidth); GLfloat u2 = GLfloat((currentFrameX + 1) * mSpriteWidth) / GLfloat(mSheetWidth); GLfloat v1 = GLfloat(currentFrameY * mSpriteHeight) / GLfloat(mSheetHeight); GLfloat v2 = GLfloat((currentFrameY + 1) * mSpriteHeight) / GLfloat(mSheetHeight); pVertices[0].x = posX1; pVertices[0].y = posY1; pVertices[0].u = u1; pVertices[0].v = v1; pVertices[1].x = posX1; pVertices[1].y = posY2; pVertices[1].u = u1; pVertices[1].v = v2; pVertices[2].x = posX2; pVertices[2].y = posY1; pVertices[2].u = u2; pVertices[2].v = v1; pVertices[3].x = posX2; pVertices[3].y = posY2; pVertices[3].u = u2; pVertices[3].v = v2; }
jni/SpriteBatch.hpp
with methods such as:registerSprite()
to add a new sprite to drawload()
to initialize all the registered spritesdraw()
to effectively render all the registered spritesWe are going to need member variables:
mSprites
and mSpriteCount
mVertices
, mVertexCount
, mIndexes
, and mIndexCount
, which define a vertex and an index buffermShaderProgram
The vertex and fragment shader parameters are:
aPosition
, which is one of the sprite corner positions.aTexture
, which is the sprite corner texture coordinate. It defines the sprite to display in the sprite sheet.uProjection
, is the orthographic projection matrix.uTexture
, contains the sprite picture.#ifndef _PACKT_GRAPHICSSPRITEBATCH_HPP_ #define _PACKT_GRAPHICSSPRITEBATCH_HPP_ #include "GraphicsManager.hpp" #include "Sprite.hpp" #include "TimeManager.hpp" #include "Types.hpp" #include <GLES2/gl2.h> class SpriteBatch : public GraphicsComponent { public: SpriteBatch(TimeManager& pTimeManager, GraphicsManager& pGraphicsManager); ~SpriteBatch(); Sprite* registerSprite(Resource& pTextureResource, int32_t pHeight, int32_t pWidth); status load(); void draw(); private: TimeManager& mTimeManager; GraphicsManager& mGraphicsManager; Sprite* mSprites[1024]; int32_t mSpriteCount; Sprite::Vertex mVertices[1024]; int32_t mVertexCount; GLushort mIndexes[1024]; int32_t mIndexCount; GLuint mShaderProgram; GLuint aPosition; GLuint aTexture; GLuint uProjection; GLuint uTexture; }; #endif
jni/SpriteBach.cpp
constructor to initialize the default values. The component must register with GraphicsManager
to be loaded and rendered.In the destructor, the allocated sprites must be freed when the component is destroyed.
#include "SpriteBatch.hpp" #include "Log.hpp" #include <GLES2/gl2.h> SpriteBatch::SpriteBatch(TimeManager& pTimeManager, GraphicsManager& pGraphicsManager) : mTimeManager(pTimeManager), mGraphicsManager(pGraphicsManager), mSprites(), mSpriteCount(0), mVertices(), mVertexCount(0), mIndexes(), mIndexCount(0), mShaderProgram(0), aPosition(-1), aTexture(-1), uProjection(-1), uTexture(-1) { mGraphicsManager.registerComponent(this); } SpriteBatch::~SpriteBatch() { for (int32_t i = 0; i < mSpriteCount; ++i) { delete mSprites[i]; } } ...
... Sprite* SpriteBatch::registerSprite(Resource& pTextureResource, int32_t pHeight, int32_t pWidth) { int32_t spriteCount = mSpriteCount; int32_t index = spriteCount * 4; // Points to 1st vertex. // Precomputes the index buffer. GLushort* indexes = (&mIndexes[0]) + spriteCount * 6; mIndexes[mIndexCount++] = index+0; mIndexes[mIndexCount++] = index+1; mIndexes[mIndexCount++] = index+2; mIndexes[mIndexCount++] = index+2; mIndexes[mIndexCount++] = index+1; mIndexes[mIndexCount++] = index+3; // Appends a new sprite to the sprite array. mSprites[mSpriteCount] = new Sprite(mGraphicsManager, pTextureResource, pHeight, pWidth); return mSprites[mSpriteCount++]; } ...
The shader code is written inside a main()
function similar to what can be coded in C. As any normal computer program, shaders require variables to process data: attributes (per-vertex data like the position), uniforms (global parameters per draw call), and varying (values interpolated per fragment like the texture coordinates).
Here, texture coordinates are passed to the fragment shader in vTexture
. The vertex position is transformed from a 2D vector to a 4D vector into a predefined GLSL variable gl_Position
. The fragment shader retrieves interpolated texture coordinates in vTexture
. This information is used as an index in the predefined function texture2D()
to access the texture color. Color is saved in the predefined output variable gl_FragColor
, which represents the final pixel:
... static const char* VERTEX_SHADER = "attribute vec4 aPosition; " "attribute vec2 aTexture; " "varying vec2 vTexture; " "uniform mat4 uProjection; " "void main() { " " vTexture = aTexture; " " gl_Position = uProjection * aPosition; " "}"; static const char* FRAGMENT_SHADER = "precision mediump float; " "varying vec2 vTexture; " "uniform sampler2D u_texture; " "void main() { " " gl_FragColor = texture2D(u_texture, vTexture); " "}"; ...
load()
. Then, initialize sprites, as shown in the following code:... status SpriteBatch::load() { GLint result; int32_t spriteCount; mShaderProgram = mGraphicsManager.loadShader(VERTEX_SHADER, FRAGMENT_SHADER); if (mShaderProgram == 0) return STATUS_KO; aPosition = glGetAttribLocation(mShaderProgram, "aPosition"); aTexture = glGetAttribLocation(mShaderProgram, "aTexture"); uProjection = glGetUniformLocation(mShaderProgram,"uProjection"); uTexture = glGetUniformLocation(mShaderProgram, "u_texture"); // Loads sprites. for (int32_t i = 0; i < mSpriteCount; ++i) { if (mSprites[i]->load(mGraphicsManager) != STATUS_OK) goto ERROR; } return STATUS_OK; ERROR: Log::error("Error loading sprite batch"); return STATUS_KO; } ...
draw()
, which executes the OpenGL sprite rendering logic.First, select the sprite shader and pass its parameters: the matrix and the texture uniforms:
... void SpriteBatch::draw() { glUseProgram(mShaderProgram); glUniformMatrix4fv(uProjection, 1, GL_FALSE, mGraphicsManager.getProjectionMatrix()); glUniform1i(uTexture, 0); ...
Then, indicate to OpenGL how the position and UV coordinates are stored in the vertex buffer with glEnableVertexAttribArray()
and glVertexAttribPointer()
. These calls basically describe the mVertices
structure. Note how vertex data is linked to shader attributes:
... glEnableVertexAttribArray(aPosition); glVertexAttribPointer(aPosition, // Attribute Index 2, // Size in bytes (x and y) GL_FLOAT, // Data type GL_FALSE, // Normalized sizeof(Sprite::Vertex),// Stride &(mVertices[0].x)); // Location glEnableVertexAttribArray(aTexture); glVertexAttribPointer(aTexture, // Attribute Index 2, // Size in bytes (u and v) GL_FLOAT, // Data type GL_FALSE, // Normalized sizeof(Sprite::Vertex), // Stride &(mVertices[0].u)); // Location ...
Activate transparency using a blending function to draw sprites over the background, or other sprites:
... glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); ...
For more information about the blending modes provided by OpenGL, have a look at https://www.opengl.org/wiki/Blending.
The first outer loop basically iterates over textures. Indeed, the pipeline state changes in OpenGL are costly. Methods like glBindTexture()
should be called as little as possible to guarantee performance:
... const int32_t vertexPerSprite = 4; const int32_t indexPerSprite = 6; float timeStep = mTimeManager.elapsed(); int32_t spriteCount = mSpriteCount; int32_t currentSprite = 0, firstSprite = 0; while (bool canDraw = (currentSprite < spriteCount)) { // Switches texture. Sprite* sprite = mSprites[currentSprite]; GLuint currentTexture = sprite->mTexture; glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, sprite->mTexture); ...
The inner loop generates vertices for all sprites with the same texture:
... // Generate sprite vertices for current textures. do { sprite = mSprites[currentSprite]; if (sprite->mTexture == currentTexture) { Sprite::Vertex* vertices = (&mVertices[currentSprite * 4]); sprite->draw(vertices, timeStep); } else { break; } } while (canDraw = (++currentSprite < spriteCount)); ...
glDrawElements()
. The vertex buffer specified earlier is combined with the index buffer given here to render the right sprites with the right texture. At this point, draw calls are sent to OpenGL, which executes the shader program:... glDrawElements(GL_TRIANGLES, // Number of indexes (currentSprite - firstSprite) * indexPerSprite, GL_UNSIGNED_SHORT, // Indexes data type // First index &mIndexes[firstSprite * indexPerSprite]); firstSprite = currentSprite; } ...
When all sprites are rendered, restore the OpenGL state:
... glUseProgram(0); glDisableVertexAttribArray(aPosition); glDisableVertexAttribArray(aTexture); glDisable(GL_BLEND); }
jni/Ship.hpp
with the new sprite system. You can remove the previous GraphicsElement
stuff:#include "GraphicsManager.hpp"
#include "Sprite.hpp"
class Ship {
public:
...
void registerShip(Sprite* pGraphics);
...
private:
GraphicsManager& mGraphicsManager;
Sprite* mGraphics;
};
#endif
The file jni/Ship.cpp
does not change much apart from the Sprite
type:
...
void Ship::registerShip(Sprite* pGraphics) {
mGraphics = pGraphics;
}
...
Include the new SpriteBatch
component in jni/DroidBlaster.hpp
:
... #include "Resource.hpp" #include "Ship.hpp" #include "SpriteBatch.hpp" #include "TimeManager.hpp" #include "Types.hpp" class DroidBlaster : public ActivityHandler { ... private: ... Asteroid mAsteroids; Ship mShip; SpriteBatch mSpriteBatch; }; #endif
jni/DroidBlaster.cpp
, define some new constants with animation properties.Then, use the
SpriteBatch
component to register the ship and asteroids graphics.
Remove the previous stuff related to GraphicsElement
again:
... static const int32_t SHIP_SIZE = 64; static const int32_t SHIP_FRAME_1 = 0; static const int32_t SHIP_FRAME_COUNT = 8; static const float SHIP_ANIM_SPEED = 8.0f; static const int32_t ASTEROID_COUNT = 16; static const int32_t ASTEROID_SIZE = 64; static const int32_t ASTEROID_FRAME_1 = 0; static const int32_t ASTEROID_FRAME_COUNT = 16; static const float ASTEROID_MIN_ANIM_SPEED = 8.0f; static const float ASTEROID_ANIM_SPEED_RANGE = 16.0f; DroidBlaster::DroidBlaster(android_app* pApplication): ... mAsteroids(pApplication, mTimeManager, mGraphicsManager, mPhysicsManager), mShip(pApplication, mGraphicsManager), mSpriteBatch(mTimeManager, mGraphicsManager) { Log::info("Creating DroidBlaster"); Sprite* shipGraphics = mSpriteBatch.registerSprite(mShipTexture, SHIP_SIZE, SHIP_SIZE); shipGraphics->setAnimation(SHIP_FRAME_1, SHIP_FRAME_COUNT, SHIP_ANIM_SPEED, true); mShip.registerShip(shipGraphics); // Creates asteroids. for (int32_t i = 0; i < ASTEROID_COUNT; ++i) { Sprite* asteroidGraphics = mSpriteBatch.registerSprite( mAsteroidTexture, ASTEROID_SIZE, ASTEROID_SIZE); float animSpeed = ASTEROID_MIN_ANIM_SPEED + RAND(ASTEROID_ANIM_SPEED_RANGE); asteroidGraphics->setAnimation(ASTEROID_FRAME_1, ASTEROID_FRAME_COUNT, animSpeed, true); mAsteroids.registerAsteroid( asteroidGraphics->location, ASTEROID_SIZE, ASTEROID_SIZE); } } ...
onActivate()
anymore. Sprites will handle this for us.Finally, release the graphic resources in onDeactivate()
:
...
status DroidBlaster::onActivate() {
Log::info("Activating DroidBlaster");
if (mGraphicsManager.start() != STATUS_OK) return STATUS_KO;
// Initializes game objects.
mAsteroids.initialize();
mShip.initialize();
mTimeManager.reset();
return STATUS_OK;
}
void DroidBlaster::onDeactivate() {
Log::info("Deactivating DroidBlaster");
mGraphicsManager.stop();
}
...
Launch DroidBlaster. You should now see an animated ship surrounded by frightening rotating asteroids:
In this part, we have seen how to draw a sprite efficiently with the help of the Sprite Batch technique. Indeed, a common cause of bad performance in OpenGL programs lies in state changes. Changing the OpenGL device state (for example, binding a new buffer or texture, changing an option with glEnable()
, and so on) is a costly operation and should be avoided as much as possible. Thus, a good practice to maximize OpenGL performance is to order draw calls and change only the needed states.
One of the best OpenGL ES documentation is available from the Apple developer site at https://developer.apple.com/library/IOS/documentation/3DDrawing/Conceptual/OpenGLES_ProgrammingGuide/.
But first, let's see more about the way OpenGL stores vertices in memory and the basics of OpenGL ES shaders.
Vertex Arrays (VA) and Vertex Buffer Objects (VBO) are the two main ways to manage vertices in OpenGL ES. Like with textures, multiple VAs/VBOs can be bound simultaneously to one vertex shader.
There are two main ways to manage vertices in OpenGL ES:
GL_DYNAMIC_DRAW
) to allow fast updates but at the price of more complex buffer management (that is, multiple buffering).After transformation, the vertices are connected together during the primitive assembly stage. They can be assembled in the following ways:
glDrawArrays()
.glDrawElements()
.Some good practices to remember when you're dealing with vertices are:
For more information about vertex management, have a look at the OpenGL.org wiki at http://www.opengl.org/wiki/Vertex_Specification and http://www.opengl.org/wiki/Vertex_Specification_Best_Practices.
3.149.243.32