Vertex color lighting material and shaders

This next topic gets a bit complicated. We're going to write new vertex and fragment shaders that handle lighting and write a corresponding class extending Material that makes use of them. Don't worry though, we've already done this once before. We're just going to actually explain it this time.

Let's dive right into it. Locate the res/raw/ folder. Then, for each file, right-click on it, and go to New | File to create new files.

File: res/raw/vertex_color_lighting_vertex.shader

uniform mat4 u_Model;
uniform mat4 u_MVP;
uniform mat4 u_MVMatrix;
uniform vec3 u_LightPos;

attribute vec4 a_Position;
attribute vec4 a_Color;
attribute vec3 a_Normal;

varying vec4 v_Color;

const float ONE = 1.0;
const float COEFF = 0.00001;

void main() {
    vec3 modelViewVertex = vec3(u_MVMatrix * a_Position);
    vec3 modelViewNormal = vec3(u_MVMatrix * vec4(a_Normal, 0.0));

    float distance = length(u_LightPos - modelViewVertex);
    vec3 lightVector = normalize(u_LightPos - modelViewVertex);
    float diffuse = max(dot(modelViewNormal, lightVector), 0.5);

    diffuse = diffuse * (ONE / (ONE + (COEFF * distance * distance)));
    v_Color = a_Color * diffuse;
    gl_Position = u_MVP * a_Position;
}

The vertex shader maps a 3D vertex to 2D screen space using a model-view transformation matrix. Then, it finds the light distance and direction to calculate the light color and intensity at that point. These values are passed through the graphics pipeline. The fragment shader then determines the pixel colors in the raster segment.

// File: res/raw/vertex_color_lighting_fragment.shader
precision mediump float;
varying vec4 v_Color;

void main() {
    gl_FragColor = v_Color;
}

Now, we'll create the Material. In the renderbox/materials/ folder, create a VertexColorLightingMaterial class. Define it so it extends Material, and then declare its buffers and methods for setupProgram and draw. Here's the code in all its gory glory:

public class VertexColorLightingMaterial extends Material {
    private static final String TAG = "vertexcollight";
    static int program = -1;
    //Initialize to a totally invalid value for setup state

    static int positionParam;
    static int colorParam;
    static int normalParam;
    static int MVParam;
    static int MVPParam;
    static int lightPosParam;

    FloatBuffer vertexBuffer;
    FloatBuffer normalBuffer;
    FloatBuffer colorBuffer;
    int numIndices;

    public VertexColorLightingMaterial(){
        super();
        setupProgram();
    }

    public static void setupProgram(){
        //Already setup?
		if (program != -1) return;
        //Create shader program
        program = createProgram(R.raw.vertex_color_lighting_vertex, R.raw.vertex_color_lighting_fragment);

        //Get vertex attribute parameters
        positionParam = GLES20.glGetAttribLocation(program, "a_Position");
        normalParam = GLES20.glGetAttribLocation(program, "a_Normal");
        colorParam = GLES20.glGetAttribLocation(program, "a_Color");

        //Enable vertex attribute parameters
        GLES20.glEnableVertexAttribArray(positionParam);
        GLES20.glEnableVertexAttribArray(normalParam);
        GLES20.glEnableVertexAttribArray(colorParam);

        //Shader-specific parameteters
        MVParam = GLES20.glGetUniformLocation(program, "u_MVMatrix");
        MVPParam = GLES20.glGetUniformLocation(program, "u_MVP");
        lightPosParam = GLES20.glGetUniformLocation(program, "u_LightPos");

        RenderBox.checkGLError("Solid Color Lighting params");
    }
    public void setBuffers(FloatBuffer vertexBuffer, FloatBuffer colorBuffer, FloatBuffer normalBuffer, int numIndices){
        this.vertexBuffer = vertexBuffer;
        this.normalBuffer = normalBuffer;
        this.colorBuffer = colorBuffer;
        this.numIndices = numIndices;
    }

    @Override
    public void draw(float[] view, float[] perspective) {
        GLES20.glUseProgram(program);

        GLES20.glUniform3fv(lightPosParam, 1, RenderBox.instance.mainLight.lightPosInEyeSpace, 0);

        Matrix.multiplyMM(modelView, 0, view, 0, RenderObject.lightingModel, 0);

        // Set the ModelView in the shader, used to calculate // lighting
        GLES20.glUniformMatrix4fv(MVParam, 1, false, modelView, 0);

        Matrix.multiplyMM(modelView, 0, view, 0, RenderObject.model, 0);
        Matrix.multiplyMM(modelViewProjection, 0, perspective, 0, modelView, 0);
        // Set the ModelViewProjection matrix in the shader.
        GLES20.glUniformMatrix4fv(MVPParam, 1, false, modelViewProjection, 0);

        // Set the normal positions of the cube, again for shading
        GLES20.glVertexAttribPointer(normalParam, 3, GLES20.GL_FLOAT, false, 0, normalBuffer);
        GLES20.glVertexAttribPointer(colorParam, 4, GLES20.GL_FLOAT, false, 0, colorBuffer);

        // Set the position of the cube
        GLES20.glVertexAttribPointer(positionParam, 3, GLES20.GL_FLOAT, false, 0, vertexBuffer);

        GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, numIndices);
    }

    public static void destroy(){
        program = -1;
    }
}

There's a lot going on here, but you can follow along if you read through it carefully. Mostly, the material code sets up the parameters that we wrote in the shader program.

It is especially important that in the draw() method, we obtain the current position transformation matrix, RenderBox.instance.mainLight.lightPosInEyeSpace of mainLight and the light color, and pass them along to the shader program.

Now is a good time to bring up the calls to GLES20.glEnableVertexAttribArray, which is required for each vertex attribute you are using. Vertex attributes are any data which are specified for each vertex, so in this case, we have positions, normals, and colors. Unlike before, we're now using normal and colors.

Having introduced a new Material, let's follow our pattern of adding it to RenderBox.reset():

    public static void reset(){
        VertexColorMaterial.destroy();
        VertexColorLightingMaterial.destroy();
    }

Finally, in the setup() method of MainActivity, make sure that you pass the lighting parameter to the Cube constructor:

    public void setup() {
        cube = new Transform();
        cube.addComponent(new Cube(true));
        cube.setLocalPosition(2.0f, -2.f, -5.0f);
    }

Run your app. TAADAA!! There, we have it. The difference from the nonlit material view may be subtle, but it's more real, virtually.

Vertex color lighting material and shaders

If you'd like to adjust the shading, you might need to play with the attenuation value used to calculate the diffuse lighting (for example, change COEFF = 0.00001 to 0.001) in vertex_color_lighting_vertex.shader, depending on the scale of your scene. For those still in the dark (pun intended), attenuation is a fancy word for how light intensity diminishes over distance, and actually refers to the same property of any physical signal (for example, light, radio, sound, and so on). If you have a very large scene, you might want a smaller value (so light reaches distant regions) or the inverse (so not everything is in light). You might also want to make the attenuation a uniform float parameter, which can be adjusted and set on a per-material or per-light basis, in order to achieve just the right lighting conditions.

So far, we've been using a single point light to light our scene. A point light is a light source with a position in 3D space, which casts light equally in all directions. Just like a standard light bulb placed at a specific location in a room, all that matters is the distance between it and the object, and the angle at which the ray strikes the surface. Rotation doesn't matter for point lights, unless a cookie is used to apply a texture to the light. We do not implement light cookies in the book, but they're super cool.

Other light sources can be directional lights, which will imitate sunlight on earth, where all of the light rays are going essentially in the same direction. Directional lights have a rotation that affects the direction of the light rays, but they do not have a position, as we assume that the theoretical source is infinitely far away along that direction vector. The third type of light source, from a graphics perspective, is a spotlight, where the light takes a cone shape and casts a circle or ellipse on the surface that it hits. The spotlight will end up working in a similar way to the perspective transformation that we do to our MVP matrix. We will only be using a single point light for the examples in this book. Implementation of other light source types is left as an exercise for the reader.

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

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