ESPerf Renderer

          
package com.vizit;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import javax.microedition.khronos.egl.EGLConfig;
import java.nio.FloatBuffer;
import javax.microedition.khronos.opengles.GL10;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import java.nio.IntBuffer;
import android.util.Log;
import android.opengl.Matrix;
import android.graphics.PointF;

/**
 * Render a simple triangle using the simplest possible OpenGL ES 2.
 */
public class TriangleRenderer
             implements GLSurfaceView.Renderer
{
    private static final boolean DEBUG              = false;

    private final String      appName               = "ESPerf";

    /** How many bytes per float. */
    private final int         bytesPerFloat         = 4;
    /** R, G, B, A for color at each vertex. */
    private final int         colorElementSize      = 4;
    private       int         colorLocation;
    private       int         modelViewLocation;
    /** Number of elements per vertex. */
    private final int         nElements             = 3;
    /** Each position element has three values, x,y, and z coordinates. */
    private final int         positionElementSize   = 3;
    private       int         positionLocation;
    /** Offset of the position data. */
    private final int         positionOffset        = 0;
    private       int         program;
    /** Translation for the second triangle has floating point x and y components. */
    private       PointF      translation           = new PointF();
    // Vertex buffer for the first triangle.
    private       int         vertexBuffer;
    private       float[]     vertices;
    /** Translate from world space to the view. */
    private       float[]     viewMatrix;


    public TriangleRenderer()
    {
        vertices     = new float[] {
                                    // Vertex coordinate,  3 floats (XYZ)
                                    -1.0f,   -1.0f,  0.0f,
                                    // Vertex color, four floats (RGBA)
                                     1.0f,    0.0f,  0.0f, 1.0f,
                                     1.0f,   -1.0f,  0.0f,
                                     0.0f,    1.0f,  0.0f, 1.0f,
                                     0.0f,    1.0f,  0.0f,
                                     0.0f,    0.0f,  1.0f, 1.0f,
                                    -1.0f,   -1.0f,  0.5f,
                                     0.0f,    0.0f,  1.0f, 1.0f,
                                     1.0f,   -1.0f,  0.5f,
                                     0.0f,    1.0f,  0.0f, 1.0f,
                                     0.0f,    1.0f,  0.5f,
                                     1.0f,    0.0f,  0.0f, 1.0f
                                   };
        
    }

    /**
     * Compile a shader of the given type.
     * @param shaderSource A string containing GLSL code for the shader.
     * @param shaderType   The type of shader: GL_VERTEX_SHADER or GL_FRAGMENT_SHADER for OpenGL ES 2.
     *
     * @return The compiled shader.
     */
    protected int compileShader(String shaderSource, int shaderType)
    {
        IntBuffer logStatus;
        int       shader;
        String    compileLog;
        int[]     compileStatus;

        shader = GLES20.glCreateShader(shaderType);
        // Pass in the shader source.
        GLES20.glShaderSource(shader, shaderSource);
        // Compile the shader.
        GLES20.glCompileShader(shader);

        // Refactor #1 - Some argue that you should skip this as shader syntax errors will show up in dev & test.
        if (DEBUG)
        {
          // Get the compilation status.
          compileStatus = new int[1];
          GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compileStatus, 0);

          // Error compile status, get the relevant log.
          if (compileStatus[0] != GLES20.GL_TRUE)
          {
              logStatus     = ByteBuffer.allocateDirect(4).order(ByteOrder.nativeOrder()).asIntBuffer();
              GLES20.glGetShaderiv(shader, GLES20.GL_INFO_LOG_LENGTH, logStatus);
              if (logStatus.get(0) > 0)
              {
                  compileLog = GLES20.glGetShaderInfoLog(shader);
                  Log.d(appName, "Shader compilation failed with, " + compileLog);
              }
              else
              {
                  // Workaround for issue 9953, where GL_INFO_LOG_LENGTH is zero.
                  Log.d(appName, "Shader compilation failed for an unknown reason.");
              }
              GLES20.glDeleteShader(shader);
              shader = 0;
          }
        }
        return shader;
    }

    /**
     * Given the handles to two previously compiled shaders, assemble them into a program.
     *
     * @param vertexShader   The handle for the vertex shader for this program.
     * @param fragmentShader The handle for ths fragment shader for this program.
     *
     * @return int A handle on the linked program.
     */
    protected int createProgram(int vertexShader, int fragmentShader)
    {
        String    loaderLog;
        IntBuffer logStatus;
        int       program;

        program = GLES20.glCreateProgram();

        GLES20.glAttachShader(program, vertexShader);

        // Bind the fragment shader to the program.
        GLES20.glAttachShader(program, fragmentShader);

        GLES20.glLinkProgram(program);

        if (DEBUG)
        {
          // Get the link status.
          final int[] linkStatus = new int[1];
          GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0);

          // If the link failed, delete the program.
          if (linkStatus[0] != GLES20.GL_TRUE)
          {
              logStatus     = ByteBuffer.allocateDirect(4).order(ByteOrder.nativeOrder()).asIntBuffer();
              GLES20.glGetProgramiv(program, GLES20.GL_INFO_LOG_LENGTH, logStatus);
              if (logStatus.get(0) > 0)
              {
                  loaderLog = GLES20.glGetProgramInfoLog(program);
                  Log.d(appName, "Program linking failed with, " + loaderLog);
              }
              else
              {
                  // Workaround for issue 9953, where GL_INFO_LOG_LENGTH is zero.
                  Log.d(appName, "Program linking failed for an unknown reason.");
              }
              GLES20.glDeleteProgram(program);
              program = 0;
          }
        }

        GLES20.glUseProgram(program);

        return program;
    }

    /**
     * Generate a matrix corresponding to an eye at eye(x,y,z) looking toward
     * look (x,y,z), with the up direction up (x,y,z).
     *
     * @return A matrix that will transform the scene according to the given view.
     */
    public float[] generateViewMatrix()
    {
        final float[] viewMatrix = new float[16];

        // Position the eye behind the origin.
        final float   eyeX       = 0.0f;
        final float   eyeY       = 0.0f;
        final float   eyeZ       = 0.0f;

        // We are looking in the -z direction
        final float   lookX      =  0.0f;
        final float   lookY      =  0.0f;
        final float   lookZ      = -1.0f;

        // Up is the y axis.
        final float   upX        = 0.0f;
        final float   upY        = 1.0f;
        final float   upZ        = 0.0f;

        // Set the view matrix. This matrix can be said to represent the camera position.
        Matrix.setLookAtM(viewMatrix, 0, eyeX, eyeY, eyeZ, lookX, lookY, lookZ, upX, upY, upZ);

        return viewMatrix;
    }

    /**
     * Load vertex data into a buffers so that it can be read into vertex shader attributes.
     *
     * @param vertexData An array of floats containing per vertex data.
     */
    protected int createBuffer(float[] vertexData)
    {
        int[] vertexBuffer = new int[1];

        /** Store our model data in a float buffer. */
        FloatBuffer vertexBufferData;

        // The OpenGL Driver expects floating point data in native byte order.
        vertexBufferData = ByteBuffer.allocateDirect(vertexData.length * bytesPerFloat)
                                     .order(ByteOrder.nativeOrder()).asFloatBuffer();

        vertexBufferData.put(vertexData).position(0);

        // Generate a single buffer handle into element 0 of the array.
        GLES20.glGenBuffers(1, vertexBuffer, 0);

        // Binding an object in Open GL creates it, and makes it the target of subsequent manipulations.
        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vertexBuffer[0]);

        // loads the current buffer, the vertexBuffer found above, with the vertex data.
        GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, vertexBufferData.capacity() * bytesPerFloat,
                            vertexBufferData, GLES20.GL_STATIC_DRAW);

        return vertexBuffer[0];
    }

    /**
     * Setup pointer for attribute to read data into shader attribute.
     * @param vertexBuffer      The FloatBuffer within this program space containing the data to be read.
     * @param attributeLocation An integer identifying the attribute..
     * @param size              The number of entries from the buffer that make up each attribute value.
     * @param type              The type of each entry.
     * @param stride            An in giving the begin-begin spacing of values in bytes. 0 for adjacent values.
     * @param offset            An in giving the offset into the FloatBuffer where we begin reading data.
     */
    protected void bindBuffer(int vertexBuffer, int attributeLocation, int size, int type, int stride, int offset)
    {
        // Binding an object makes it the target of subsequent manipulations.
        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vertexBuffer);

        GLES20.glVertexAttribPointer(attributeLocation, size, type, false, stride, offset);

        GLES20.glEnableVertexAttribArray(attributeLocation);
    }

    /**
     * Invoked after the rendering surface is setup - the OpenGL context is created.
     * It is now safe to compile shaders, build programs, setup vertex buffers, etc.
     */
    @Override
    public void onSurfaceCreated(GL10 glUnused, EGLConfig config)
    {
        int stride;
        // Enable depth tests to draw only nearer objects, less depth.
        GLES20.glEnable(GLES20.GL_DEPTH_TEST);
        GLES20.glDepthFunc(GLES20.GL_LEQUAL);
        GLES20.glDepthMask(true);

        final String vertexShaderSource   = "attribute vec3 position;"
                                            + "attribute vec4 color;"
                                            + "uniform   mat4 modelViewMatrix;"
                                            + ""
                                            + "varying vec4 vColor;"
                                            + ""
                                            + "void main()"
                                            + "{"
                                            + "    gl_Position = modelViewMatrix * vec4(position, 1);"
                                            + "    vColor      = color;"
                                            + "}";

        // The fragment shader can be thought of for now as doing per pixel computations. It is the
        // fragment shader the colors each pixel in a 3d scene.
        final String fragmentShaderSource = "precision mediump float;"
                                            + ""
                                            + "varying vec4 vColor;"
                                            + ""
                                            + "void main()"
                                            + "{"
                                            + "    gl_FragColor = vColor;"
                                            + "}";

        int vertexShader   = compileShader(vertexShaderSource,   GLES20.GL_VERTEX_SHADER);
        int fragmentShader = compileShader(fragmentShaderSource, GLES20.GL_FRAGMENT_SHADER);

        program            = createProgram(vertexShader, fragmentShader);

        vertexBuffer       = createBuffer(vertices);

        viewMatrix         = generateViewMatrix();

        positionLocation   = GLES20.glGetAttribLocation(program, "position");
        colorLocation      = GLES20.glGetAttribLocation(program, "color");
        modelViewLocation  = GLES20.glGetUniformLocation(program, "modelViewMatrix");

        stride       = bytesPerFloat*(colorElementSize + positionElementSize);

        bindBuffer(vertexBuffer, positionLocation,                                     positionElementSize,
                   GLES20.GL_FLOAT, stride,   positionOffset*bytesPerFloat);
        bindBuffer(vertexBuffer, colorLocation,                                        colorElementSize,
                   GLES20.GL_FLOAT, stride,  (positionOffset + positionElementSize)*bytesPerFloat);
    }

    /**
     * Height or width changed, usually portrait <=> landscape.
     */
    @Override
    public void onSurfaceChanged(GL10 glUnused, int width, int height)
    {
        // Resize our drawing surface to the new height and width.
        GLES20.glViewport(0, 0, width, height);
    }

    /**
     * Draws each frame. Invoked when the surface first becomes drawable,
     * and again whenever motion is detected.
     * @param glUnused
     */
    @Override
    public void onDrawFrame(GL10 glUnused)
    {
        GLES20.glClearColor(.9255f, .8941f, .8275f, 1.0f);
        GLES20.glClear( GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);

        // Location, count (1 matrix), transposed, values, offset
        GLES20.glUniformMatrix4fv(modelViewLocation, 1, false, viewMatrix, 0);

        GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, nElements);

        // Take into account translations of the second triangle, if any. (does NOT need be be recreated here).a
        float[] newModelView = new float[16];
        // load newModelView with x and y translation from the detected motion.
        Matrix.translateM(newModelView, 0, viewMatrix, 0, translation.x, translation.y, 0);
        GLES20.glUniformMatrix4fv(modelViewLocation, 1, false, newModelView, 0);

        GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 3, nElements);
    }

    /**
     * Invoked by the surfaced view with normalized change in x and y
     * @param deltaX A float giving how far the touch moved in the x direction.
     * @param deltaY A float giving how far the touch moved in the y direction.
     */
    public void motion(float deltaX, float deltaY)
    {
        translation.x += deltaX;
        translation.y += deltaY;
    }
}


          
        

Notes

Now we return to the ESMotion example, to point out some useful refactorings to bring it a bit closer to code you might use in production.

Uniform Locations

The locations for attributes and uniforms do not change after the program is linked. The code, however, fetches the values over and over each time the model is rendered. Fetch the locations after the createProgram method returns, and use the values over and over. We can completely eliminate the bindMatrixUniform method.

Multiple Objects in a Vertex Array

Remember when we put the vertex positions and colors into one array and interleaved the data? We can do even better, by combining both objects into a single array. This eliminates half the calls that load the vertices, and attach them to the attributes.

In this case we cut the calls in half because the layout of the data is the same for both objects. Even if the layout is different, we can still use the one large vertex array, but use multiple bindBuffer calls to bind the data to the attributes.

Move the Binding

We can do still one better. Because the object data never changes, we can do the binding once, and never again. Move the bindBuffer method calls out of onDrawFrame, and into onSurfaceCreated. Look at how much shorter onDrawFrame is than it was previously. This is critical as onDrawFrame is executed repeatedly, and onSurfaceCreated is executed but once.

Error Checking

Some folks recommend against checking the status of the shader compile and program linking operations on mature production code. This example has a debug flag that when false deactivates these checks. The logic behind this is that shaders are compiled according to the same rules everywhere, so failures are caught in dev and test cycles. I put this last because I am not completely sure that I buy this one.