Thursday, May 15, 2014

OpenGL|ES 2.0 on Android #3 - Colors

Some colors added to our shapes.
Previously we rendered a few shapes so we can see... something being rendered. The color was hard-coded to white. So now let's try adding some colors.

Changes to Shader.java

import android.opengl.GLES20;

// Added
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

public class Shader {
...
    // Added
    public static String loadShaderFromFile(final InputStream is) {
        BufferedReader reader;
        String line, output = new String();

        try {
            reader = new BufferedReader(new InputStreamReader(is));

            while ((line = reader.readLine()) != null) {
                output += line;
            }

            reader.close();
        } catch (IOException err) {
            err.printStackTrace();
        }

        return output;
    }
}

I've added a new function to load shaders from file. This should help keep things more manageable. On that note, I've moved out the shader code to .glsl files stored in assets/shaders folder called vertex_shader.glsl and fragment_shader.glsl.

Here's the vertex shader code:

attribute vec4 vPosition;
attribute vec4 vColor;    // Added

varying vec4 aColor;      // Added
uniform mat4 uMVPMatrix;

void main() {
   gl_Position = uMVPMatrix * vPosition;
   aColor = vColor;    // Added
}

... and the fragment shader code:

precision mediump float;

// Added
varying vec4 aColor;    
uniform vec4 sColor;
uniform bool isSquare;

void main() {
   // Changed
   if (isSquare) {
       gl_FragColor = sColor;
   } else {
       gl_FragColor = aColor;
   }
}

Changes to GLActivity.java

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import java.io.IOException;    // Added

Opening files always means handling IOExceptions in Java.

@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
    GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);    // Black background

    geometry = new GeometryEngine();

    // Added
    final String vertexShader, fragmentShader;

    try {
        vertexShader = Shader.loadShaderFromFile(getResources().getAssets().open("shaders/vertex_shader.glsl"));
        fragmentShader = Shader.loadShaderFromFile(getResources().getAssets().open("shaders/fragment_shader.glsl"));
        geometry.initShaders(vertexShader, fragmentShader);
    } catch (IOException err) {
        err.printStackTrace();
    }
}

So now we're reading in plain text files containing our shader code instead of hard-coding them. We're also calling initShaders from GLActivity now so the GeometryEngine class constructor no longer calls initShaders. GLActivity.java remains otherwise unchanged.

Changes in GeometryEngine.java

private static float triangleGeometry[] = {     // in counterclockwise order
        0.0f, 0.5f, 0.0f,                       // top
        -0.5f, -0.5f, 0.0f,                     // bottom left
        0.5f, -0.5f, 0.0f                       // bottom right
};
// Added
private static float triangleColors[] = {      // per-vertex colors
        1.0f, 0.0f, 0.0f,                       // red
        0.0f, 1.0f, 0.0f,                       // green
        0.0f, 0.0f, 1.0f                        // blue
};
private static float squareGeometry[] = {
        -0.5f, 0.5f, 0.0f,                      // top left
        -0.5f, -0.5f, 0.0f,                     // bottom left
        0.5f, 0.5f, 0.0f,                       // top right
        0.5f, -0.5f, 0.0f,                      // bottom right
};

// Added
private static float squareColor[] = {
        1.0f, 0.0f, 1.0f,                       // purple
};

private FloatBuffer triangleColorBuffer;
private FloatBuffer squareColorBuffer;

I've added arrays for storing color values for the triangle and square along with buffers to hold those values when rendering.

private void initGeometry() {
    triangleBuffer = createFloatBuffer(triangleGeometry.length * 4);
    triangleBuffer.put(triangleGeometry)
            .position(0);
    // Added
    triangleColorBuffer = createFloatBuffer(triangleColors.length * 4);
    triangleColorBuffer.put(triangleColors)
            .position(0);

    squareBuffer = createFloatBuffer(squareGeometry.length * 4);
    squareBuffer.put(squareGeometry)
            .position(0);
    // Added
    squareColorBuffer = createFloatBuffer(squareColor.length * 4);
    squareColorBuffer.put(squareColor)
            .position(0);
}

// Changed
public void initShaders(final String vertexShader, final String fragmentShader) {
    int vertex_shader = Shader.loadShader(GLES20.GL_VERTEX_SHADER, vertexShader);
    int fragment_shader = Shader.loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShader);
    shaderProgram = Shader.createProgram(vertex_shader, fragment_shader);
}

initGeometry now has a few extra lines of code to add in vertex color data alongside the vertex position data. initShaders is now modified to accept Strings containing vertex and fragment shader code. Instead of creating a String object inside GeometryEngine, we can just pass the code in from GLActivity after it reads the .glsl files.

public void draw(final float[] mvpMatrix) {
    GLES20.glUseProgram(shaderProgram);

    // 1.
    int stateHandle = GLES20.glGetUniformLocation(shaderProgram, "isSquare");
    GLES20.glUniform1i(stateHandle, 0);

    int positionHandle = GLES20.glGetAttribLocation(shaderProgram, "vPosition");
    GLES20.glEnableVertexAttribArray(positionHandle);
    GLES20.glVertexAttribPointer(positionHandle, POSITION_PER_VERTEX, GLES20.GL_FLOAT, false, VERTEX_STRIDE, triangleBuffer);

    int colorHandle = GLES20.glGetAttribLocation(shaderProgram, "vColor");
    GLES20.glEnableVertexAttribArray(colorHandle);
    GLES20.glVertexAttribPointer(colorHandle, POSITION_PER_VERTEX, GLES20.GL_FLOAT, false, VERTEX_STRIDE, triangleColorBuffer);

    // Shift to the left and draw our triangle there
    float [] scratch = new float[16];
    Matrix.translateM(scratch, 0, mvpMatrix, 0, 1.0f, 0.0f, 0.0f);
    int matrixHandle = GLES20.glGetUniformLocation(shaderProgram, "uMVPMatrix");
    GLES20.glUniformMatrix4fv(matrixHandle, 1, false, scratch, 0);
    GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, TRIANGLE_VERTEX_COUNT);     // Draw triangle
    GLES20.glDisableVertexAttribArray(colorHandle);

    // 2.
    GLES20.glVertexAttribPointer(positionHandle, POSITION_PER_VERTEX, GLES20.GL_FLOAT, false, VERTEX_STRIDE, squareBuffer);
    colorHandle = GLES20.glGetUniformLocation(shaderProgram, "sColor");
    GLES20.glUniform4fv(colorHandle, 1, squareColorBuffer);

    // Use sColor to set uniform color for our square instead of using per-vertex color. Set isSquare to true.
    // false if 0 or 0.0f else true
    GLES20.glUniform1i(stateHandle, 1);
    
    // Shift to the right and draw there
    Matrix.translateM(scratch, 0, mvpMatrix, 0, -1.0f, 0.0f, 0.0f);
    GLES20.glUniformMatrix4fv(matrixHandle, 1, false, scratch, 0);

    GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, SQUARE_VERTEX_COUNT);  // Draw square
    GLES20.glDisableVertexAttribArray(positionHandle);
}

The draw method is now pretty big. So let's take it a step at a time.

We get the isSquare boolean uniform and set it to false. GL|ES apparently doesn't allow passing boolean values into shaders, so we'll have to follow C conventions (0 is false, anything else is true). We then proceed to pass in vertex position and color data, and finally render the triangle to screen. Nothing really new there.
Next up, we pass in vertex position and color data for the square. Instead of an array of values, we'll be using a single color for the whole square. Hence, we make use of the sColor uniform. We want to use this instead of the color array used by the triangle so here we also set isSquare to true and let the if.. else block in the shader handle the rest.

The final render should be as in the screenshot at the top.