Time for action – initializing OpenGL ES

Let's now see how to implement a sprite batch in DroidBlaster:

  1. Modify 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 screen
    • loadShaderProgram() to load a vertex and fragment shader and link them together into an OpenGL program
    • registerComponent() which records a list of GraphicsComponent to initialize and render

    Create 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;
    };
    ...
  2. Then, define a few new methods in GraphicsManager:
    • getProjectionMatrix() which provides an OpenGL matrix to project 2D graphics on screen
    • loadShaderProgram() to load a vertex and fragment shader and link them together into an OpenGL program
    • registerComponent() which records a list of GraphicsComponent to initialize and render

    Create 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
  3. Open 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;
    }
    ...
  4. Amend 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.

    Tip

    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;
        ...
    }
    ...
  5. Free any resources loaded with 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.
        ...
    }
    ...
  6. Render any registered components in 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) {
        ...
    }
    ...
  7. Create the new method loadShader(). Its role is to compile and load the given shaders passed as a human-readable GLSL program. To do so:
    • Generate a new vertex shader with glCreateShader().
    • Upload the vertex shader source into OpenGL with glShaderSource().
    • Compile the shader with 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;
        }
    ...
  8. Once compiled, link the compiled vertex and fragment shaders together. To do so:
    • Create a program object with glCreateProgram().
    • Specify the shaders to use glAttachShader().
    • Link them together with 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().
    • Finally, get rid of the shaders as they are useless once linked in a program.
      ...
          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;
      }
      ...
  9. Create 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:

    • Sprite animation can be updated and retrieved with setAnimation() and animationEnded(). Location is publicly available for simplicity purposes.
    • Give privileged access to a component that we are going to define later, named 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);
      ...
  10. Finally, define a few properties:
    • A texture containing the sprite sheet and its corresponding resource
    • Sprite frame data: mWidth and mHeight, horizontal, vertical, and total number of frames in mFrameXCount, mFrameYCount, and mFrameCount
    • Animation data: first and total number of frames of an animation in 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
  11. Write the 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)
    {}
    ...
  12. Frame information (horizontal, vertical, and total number of frames) needs to be recomputed in 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;
    }
    ...
  13. An animation starts from a given in the sprite sheet and ends after a certain amount of frames, whose number changes according to speed. An animation can loop to restart from the beginning when it is over:
    ...
    void Sprite::setAnimation(int32_t pStartFrame,
        int32_t pFrameCount, float pSpeed, bool pLoop) {
        mAnimStartFrame = pStartFrame;
        mAnimFrame = 0.0f, mAnimSpeed = pSpeed, mAnimLoop = pLoop;
        mAnimFrameCount = pFrameCount;
    }
    ...
  14. In 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);
    ...
  15. A sprite is composed of four vertices drawn in an output array, 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;
    }
  16. Specify jni/SpriteBatch.hpp with methods such as:
    • registerSprite() to add a new sprite to draw
    • load() to initialize all the registered sprites
    • draw() to effectively render all the registered sprites

    We are going to need member variables:

    • A set of sprites to draw in mSprites and mSpriteCount
    • mVertices, mVertexCount, mIndexes, and mIndexCount, which define a vertex and an index buffer
    • A shader program identified by mShaderProgram

    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
  17. Implement the 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];
        }
    }
    ...
  18. The index buffer is rather static. We can precompute its content when a sprite is registered. Each index points to a vertex in the vertex buffer (0 representing the very first vertex, 1 the 2nd, and so on). As a sprite is represented by 2 triangles of 3 vertices (to form a quad), we need 6 indexes per sprite:
    ...
    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++];
    }
    ...
  19. Write the GLSL vertex and fragment shaders as constant strings.

    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);
    "
        "}";
    ...
  20. Load the shader program and retrieve the shader attributes and uniform identifiers in 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;
    }
    ...
  21. Write 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);
    ...

    Tip

    For more information about the blending modes provided by OpenGL, have a look at https://www.opengl.org/wiki/Blending.

  22. We can now start the rendering loop to render all sprites in a batch.

    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));
    ...
  23. Each time the texture changes, render the bunch of sprites with 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);
    }
  24. Update 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
  25. In 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);
        }
    }
    ...
  26. We do not need to load textures manually in 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();
    }
    ...

What just happened?

Launch DroidBlaster. You should now see an animated ship surrounded by frightening rotating asteroids:

What just happened?

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.

Tip

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 versus Vertex Buffer Object

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:

  • In main memory (that is, in RAM), we talk about Vertex Arrays (abbreviated VA). Vertex arrays are transmitted from the CPU to the GPU for each draw call. As a consequence, they are slower to render, but also much easier to update. Thus, they are appropriate when a mesh of vertices is changing frequently. This explains the decision to use a vertex array to implement sprite batches; each sprite is updated each time a new frame is rendered (position, as well as texture coordinates, to switch to a new frame).
  • In driver memory (generally in GPU memory or VRAM), we talk about Vertex Buffers Objects. Vertex buffers are faster to draw but more expensive to update. Thus, they are often used to render static data that never changes. You can still transform it with vertex shaders, which we are going to see in the next part. Note that some hints can be provided to the driver during initialization (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:

  • As lists 3 by 3 (which can lead to vertex duplication), in fans, in strips, and so on; in which case, we use glDrawArrays().
  • Using an index buffers which specifies 3 by 3, where vertices are connected together. Index buffers are often the best way to achieve better performance. Indices need to be sorted to favor caching. Indices are drawn with their associated VBO or VA using glDrawElements().
    Vertex Arrays versus Vertex Buffer Object

Some good practices to remember when you're dealing with vertices are:

  • Pack as many vertices in each buffer as you can, even from multiple meshes. Indeed, switching from one set of vertices to another, either a VA or a VBO, is slow.
  • Avoid updating static vertex buffers at runtime.
  • Make vertex structure the size of a power of 2 (in bytes) to favor data alignment. It is often preferred to pad data rather than to transmit unaligned data because of the way GPU processes it.

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.149.243.32