2
\$\begingroup\$

Is this a good method to render sprites (textured quads) or is there a faster or better way?

Example use:

renderer.beginTextureRendering();
for(Enemy enemy : enemies)
{
    renderer.renderTexture(enemy.getX(), enemy.getY(), 0.5f, 0.5f, 0, 0, 1, 1, 1, 1, 1, 1);
}
renderer.endTextureRendering(cameraTransform, gameTexture);

Some members of the Renderer class:

private int vertexBuffer;
private FloatBuffer vertexBatch;

private static final int VERTEX_BATCH_SIZE = 300;

Core render methods of Renderer class:

 // Call this before rendering a few sprites.
        public void beginTextureRendering()
        {
            vertexBatch.rewind();
        }
        public void renderTexture(float xPosition, float yPosition, float xSize, float ySize,
                                  float srcXPosition, float srcYPosition, float srcXSize,
                                  float srcYSize, float red, float green, float blue, float alpha)
        {
            if(vertexBatch.position() - 48 >= VERTEX_BATCH_SIZE)
            {
                Log.e(TAG, "Vertex batch too small !");
            }
            else
            {
                vertexBatch.put(xPosition);
                vertexBatch.put(yPosition);
                vertexBatch.put(srcXPosition);
                vertexBatch.put(srcYPosition + srcYSize);
                vertexBatch.put(red);
                vertexBatch.put(green);
                vertexBatch.put(blue);
                vertexBatch.put(alpha);

                vertexBatch.put(xPosition + xSize);
                vertexBatch.put(yPosition);
                vertexBatch.put(srcXPosition + srcXSize);
                vertexBatch.put(srcYPosition + srcYSize);
                vertexBatch.put(red);
                vertexBatch.put(green);
                vertexBatch.put(blue);
                vertexBatch.put(alpha);

                vertexBatch.put(xPosition + xSize);
                vertexBatch.put(yPosition + ySize);
                vertexBatch.put(srcXPosition + srcXSize);
                vertexBatch.put(srcYPosition);
                vertexBatch.put(red);
                vertexBatch.put(green);
                vertexBatch.put(blue);
                vertexBatch.put(alpha);

                vertexBatch.put(xPosition);
                vertexBatch.put(yPosition);
                vertexBatch.put(srcXPosition);
                vertexBatch.put(srcYPosition + srcYSize);
                vertexBatch.put(red);
                vertexBatch.put(green);
                vertexBatch.put(blue);
                vertexBatch.put(alpha);

                vertexBatch.put(xPosition + xSize);
                vertexBatch.put(yPosition + ySize);
                vertexBatch.put(srcXPosition + srcXSize);
                vertexBatch.put(srcYPosition);
                vertexBatch.put(red);
                vertexBatch.put(green);
                vertexBatch.put(blue);
                vertexBatch.put(alpha);

                vertexBatch.put(xPosition);
                vertexBatch.put(yPosition + ySize);
                vertexBatch.put(srcXPosition);
                vertexBatch.put(srcYPosition);
                vertexBatch.put(red);
                vertexBatch.put(green);
                vertexBatch.put(blue);
                vertexBatch.put(alpha);
            }
        }
// Call this if you finished drawing a few sprites.
        public void endTextureRendering(float[] transform, Texture texture)
        {
            int count = vertexBatch.position();

            if(count > 0)
            {
                vertexBatch.rewind();

                GLES20.glUseProgram(textureProgram);

                GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vertexBuffer);
                GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, count * (Float.SIZE / 8), vertexBatch, GLES20.GL_STATIC_DRAW);

                GLES20.glEnableVertexAttribArray(textureProgramPositionLocation);
                GLES20.glVertexAttribPointer(textureProgramPositionLocation, 2, GLES20.GL_FLOAT, false, 8 * (Float.SIZE / 8), 0 * (Float.SIZE / 8));

                GLES20.glEnableVertexAttribArray(textureProgramUVLocation);
                GLES20.glVertexAttribPointer(textureProgramUVLocation, 2, GLES20.GL_FLOAT, false, 8 * (Float.SIZE / 8), 2 * (Float.SIZE / 8));

                GLES20.glEnableVertexAttribArray(textureProgramColorLocation);
                GLES20.glVertexAttribPointer(textureProgramColorLocation, 4, GLES20.GL_FLOAT, false, 8 * (Float.SIZE / 8), 4 * (Float.SIZE / 8));

                GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
                GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture.getHandle());

                GLES20.glUniform1i(textureProgramTextureUnitLocation, 0);
                GLES20.glUniformMatrix4fv(textureProgramTransformLocation, 1, false, transform, 0);

                GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, count / 8);

                GLES20.glDisableVertexAttribArray(textureProgramPositionLocation);
                GLES20.glDisableVertexAttribArray(textureProgramUVLocation);
                GLES20.glDisableVertexAttribArray(textureProgramColorLocation);
            }
        }

Fragment shader:

uniform sampler2D un_TextureUnit;

varying vec2 fr_UV;
varying vec4 fr_Color;

void main()
{
    gl_FragColor =  vec4(texture2D(un_TextureUnit, fr_UV)) * fr_Color;
}

Vertex shader:

uniform mat4 un_Transform;

attribute vec2 ve_Position;
attribute vec2 ve_UV;
attribute vec4 ve_Color;

varying vec2 fr_UV;
varying vec4 fr_Color;

void main()
{
    fr_UV = ve_UV;
    fr_Color = ve_Color;
    gl_Position = un_Transform * vec4(ve_Position.xy, 0, 1);
}
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

I don't usually code with Java, but I often play with OpenGL and shaders, so I'll focus more on that for this review.


Interface:

renderer.beginTextureRendering();
for(Enemy enemy : enemies)
{
    renderer.renderTexture(enemy.getX(), enemy.getY(), 0.5f, 0.5f, 0, 0, 1, 1, 1, 1, 1, 1);
}
renderer.endTextureRendering(cameraTransform, gameTexture);

Overall, the way you've implemented the API seems fine to me. I have used similar approaches myself. Just make sure to avoid calling [begin/end]TextureRendering all the time and consolidate all possible 2D sprite drawings under the same begin/end pair. One of the biggest offenders when it comes to rendering performance is render state changes.

Now the parameter list on renderTexture() is crazy long! It would be nice to aggregate all those parameters in a couple types. In C or C++ I'd use a struct for that, but since Java doesn't have the concept of plain data aggregates, I guess a class would be the way to go. You could group the RGBA color into a Color class and the other parameters into a Sprite class. That would clean up your method call to something like:

renderer.renderTexture(enemy.getX(), enemy.getY(), enemy,getSprite(), enemy.getColor());

A little more readable, IMHO. You can go further an also group the position into a Point2D type, so the first two parameters change to one getPosition().


Looking into renderTexture() ...

if(vertexBatch.position() - 48 >= VERTEX_BATCH_SIZE)
{
    Log.e(TAG, "Vertex batch too small !");
}
else
{
    ... many, many more lines ...

Two things in the excerpt above that are universal to any programming language:

  • The magic number 48: If I guess it correctly, this is the number of floats in a single sprite quadrilateral in the vertex batch. So why not name it properly like NUM_FLOATS_IN_A_SPRITE. It might also be nice to provide a method to test if the batch is full, so a test could instead look like: if (vertexBatchIsFull()) { ... }.

  • Second issue is the unnecessary nesting of the else part, which is very big. Since the first if is a terminal condition to the method, just return and avoid nesting the rest of the code inside the else:

    if (vertexBatchIsFull())
    {
        Log.e(TAG, "Vertex batch too small !");
        return;
    }
    // The previous code that was inside the 'else' part.
    

    In endTextureRendering() you have the same issue of needless nesting. You could've written instead:

    int count = vertexBatch.position();
    if (count <= 0)
    {
        return;
    }
    

Looking at the GL calls inside endTextureRendering() ...

  • I'd suggest using GL_DYNAMIC_DRAW or even GL_STREAM_DRAW as the last parameter of glBufferData. GL_STATIC_DRAW is for buffers that are created once and rarely or never get updated. DYNAMIC is for buffers updated once per frame or less. STREAM is for buffers that are updated more than once per frame. In your case, if you only have a single endTextureRendering() call per frame, then GL_DYNAMIC_DRAW should be the best match.

  • In the glVertexAttribPointer calls, you have 8 * (Float.SIZE / 8). That's just the same as Float.SIZE.

  • Disabling the attribute indexes after drawing is not strictly necessary, the same way you don't unbind the textures or shader. They will get overwritten the next time you draw something. Those lines could go away:

    GLES20.glDisableVertexAttribArray(textureProgramPositionLocation);
    GLES20.glDisableVertexAttribArray(textureProgramUVLocation);
    GLES20.glDisableVertexAttribArray(textureProgramColorLocation);
    

Shaders:

Your shaders are okay to me. I have only a couple minor point to mention here:

  • Prefer qualifying decimal literals with the dot and zero. Some lame mobile drivers might complain that you are trying to assign an integer constant to a float/vector type. So this line should be:

    gl_Position = un_Transform * vec4(ve_Position.xy, 0.0, 1.0);
    //                                                ^^^~~^^^~~~ Notice the explicit '.0' here
    
  • The naming convention you've adopted is unusual. I understand the logic though, ve_ is for Vertex Shader and fr_ is for Fragment Shader. A more usual notation is instead to append a_ for vertex attributes and v_ for varyings, u_ for uniforms and s_ for samplers. But since there is no universal agreement in the matter, this is left to personal choice. I for one have stopped using these prefixes as of late, since I couldn't really see any benefits towards readability, while it adds maintenance overhead.

\$\endgroup\$
2
  • \$\begingroup\$ But the constant Float.SIZE returns the size of float in bits. So if I print Float.SIZE in the console I get 32 as output. But the glBufferData function needs the size in bytes so I have to divide the 32 with 8. And with this approach the sprites get rendered correctly. \$\endgroup\$ Commented Jul 30, 2015 at 11:02
  • \$\begingroup\$ @MichaelBuschmann, Ah, okay, my mistake then, I thought it was the size in bytes. Clearly a result of my lack of knowledge about java ;) I'll remove that part then. Hope the rest was of use to you! \$\endgroup\$
    – glampert
    Commented Jul 30, 2015 at 17:48

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.