Adding the Earth texture material

Next, we'll terraform our sphere into a globe of the Earth by rendering a texture onto the surface of the sphere.

Shaders can get quite complex, implementing all kinds of specular highlights, reflections, shadows, and so on. A simpler algorithm that still makes use of a color texture and lighting is a diffuse material. This is what we'll use here. The word diffuse refers to the fact that light diffuses across the surface, as opposed to being reflective or shiny (specular lighting).

A texture is just an image file (for example, .jpg) that can be mapped (projected) onto a geometric surface. Since a sphere isn't easily flattened or unpeeled into a two-dimensional map (as centuries of cartographers can attest), the texture image will look distorted. The following is the texture we'll use for the Earth. (A copy of this file is provided with the download files for this book and similar ones can be found on the Internet at http://www.solarsystemscope.com/nexus/textures/):

  • In our application, we plan to make use of the standard practice of packaging image assets into the res/drawable folder. If necessary, create this folder now.
  • Add the earth_tex.png file to it.

The earth_tex texture is shown in the following image:

Adding the Earth texture material

Loading a texture file

We now need a function to load the texture into our app. We can add it to MainActivity. Or, you can add it directly to the RenderObject class of your RenderBox lib. (It's fine in MainActivity for now, and we'll move it along with our other extensions to the library at the end of this chapter.) Add the code, as follows:

    public static int loadTexture(final int resourceId){
        final int[] textureHandle = new int[1];

        GLES20.glGenTextures(1, textureHandle, 0);

        if (textureHandle[0] != 0)
        {
            final BitmapFactory.Options options = new BitmapFactory.Options();
            options.inScaled = false;   // No pre-scaling

            // Read in the resource
            final Bitmap bitmap = BitmapFactory.decodeResource(RenderBox.instance.mainActivity.getResources(), resourceId, options);
            // Bind to the texture in OpenGL
            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureHandle[0]);

            // Set filtering
            GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
            GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST);

            // Load the bitmap into the bound texture.
            GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);

            // Recycle the bitmap, since its data has been loaded // into OpenGL.
            bitmap.recycle();
        }

        if (textureHandle[0] == 0)
        {
            throw new RuntimeException("Error loading texture.");
        }

        return textureHandle[0];
    }

The loadTexture method returns an integer handle that can be used to reference the loaded texture data.

Diffuse lighting shaders

As you may now be familiar, we are going to create a new Material, which uses new shaders. We'll write the shaders now. Create the two files in the res/raw folder named diffuse_lighting_vertex.shader and diffuse_lighting_fragment.shader, and define them as follows.

File: res/raw/diffuse_lighting_vertex.shader

uniform mat4 u_MVP;
uniform mat4 u_MV;

attribute vec4 a_Position;
attribute vec3 a_Normal;
attribute vec2 a_TexCoordinate;

varying vec3 v_Position;
varying vec3 v_Normal;
varying vec2 v_TexCoordinate;

void main() {
    // vertex in eye space
    v_Position = vec3(u_MV * a_Position);

    // pass through the texture coordinate.
    v_TexCoordinate = a_TexCoordinate;

    // normal's orientation in eye space
    v_Normal = vec3(u_MV * vec4(a_Normal, 0.0));

    // final point in normalized screen coordinates
    gl_Position = u_MVP * a_Position;
}

File: res/raw/diffuse_lighting_fragment.shader

precision highp float; // default high precision for floating point ranges of the planets

uniform vec3 u_LightPos;        // light position in eye space
uniform vec4 u_LightCol;
uniform sampler2D u_Texture;    // the input texture

varying vec3 v_Position;
varying vec3 v_Normal;
varying vec2 v_TexCoordinate;

void main() {
    // distance for attenuation.
    float distance = length(u_LightPos - v_Position);

    // lighting direction vector from the light to the vertex
    vec3 lightVector = normalize(u_LightPos - v_Position);

    // dot product of the light vector and vertex normal. // If the normal and light vector are
    // pointing in the same direction then it will get max // illumination.
    float diffuse = max(dot(v_Normal, lightVector), 0.01);

    // Add a tiny bit of ambient lighting (this is outerspace)
    diffuse = diffuse + 0.025;

    // Multiply the color by the diffuse illumination level and // texture value to get final output color
    gl_FragColor = texture2D(u_Texture, v_TexCoordinate) * u_LightCol * diffuse;
}

These shaders add attributes to a light source and utilize geometry normal vectors on the vertices to calculate the shading. You might have noticed that the difference between this and the solid color shader is the use of texture2D, which is a sampler function. Also, note that we declared u_Texture as sampler2D. This variable type and function make use of the texture units, which are built into the GPU hardware, and can be used with UV coordinates to return the color values from a texture image. There are a fixed number of texture units, depending on graphics hardware. You can query the number of texture units using OpenGL. A good rule of thumb for mobile GPUs is to expect eight texture units. This means that any shader may use up to eight textures simultaneously.

Diffuse lighting material

Now we can write a Material to use a texture and shaders. In the materials/ folder, create a new Java class, DiffuseLightingMaterial, as follows:

public class DiffuseLightingMaterial extends Material {
    private static final String TAG = "diffuselightingmaterial";

Add the variables for the texture ID, program references, and buffers, as shown in the following code:

    int textureId;
    static int program = -1; //Initialize to a totally invalid value for setup state
    static int positionParam;
    static int texCoordParam;
    static int textureParam;
    static int normalParam;
    static int MVParam;    
    static int MVPParam;
    static int lightPosParam;
    static int lightColParam;

    FloatBuffer vertexBuffer;
    FloatBuffer texCoordBuffer;
    FloatBuffer normalBuffer;
    ShortBuffer indexBuffer;
    int numIndices;

Now we can add a constructor, which sets up the shader program and loads the texture for the given resource ID, as follows:

    public DiffuseLightingMaterial(int resourceId){
        super();
        setupProgram();
        this.textureId = MainActivity.loadTexture(resourceId);
    }

As we've seen earlier, the setupProgram method creates the shader program and obtains references to its parameters:

    public static void setupProgram(){
        //Already setup?
        if (program != -1) return;

        //Create shader program
        program = createProgram(R.raw.diffuse_lighting_vertex, R.raw.diffuse_lighting_fragment);
        RenderBox.checkGLError("Diffuse Texture Color Lighting shader compile");

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

        //Enable them (turns out this is kind of a big deal ;)
        GLES20.glEnableVertexAttribArray(positionParam);
        GLES20.glEnableVertexAttribArray(normalParam);
        GLES20.glEnableVertexAttribArray(texCoordParam);

        //Shader-specific parameters
        textureParam = GLES20.glGetUniformLocation(program, "u_Texture");
        MVParam = GLES20.glGetUniformLocation(program, "u_MV");
        MVPParam = GLES20.glGetUniformLocation(program, "u_MVP");
        lightPosParam = GLES20.glGetUniformLocation(program, "u_LightPos");
        lightColParam = GLES20.glGetUniformLocation(program, "u_LightCol");

        RenderBox.checkGLError("Diffuse Texture Color Lighting params");
    }

Likewise, we add a setBuffers method that is called by the RenderObject component (Sphere):

    public void setBuffers(FloatBuffer vertexBuffer, FloatBuffer normalBuffer, FloatBuffer texCoordBuffer, ShortBuffer indexBuffer, int numIndices){
        //Associate VBO data with this instance of the material
        this.vertexBuffer = vertexBuffer;
        this.normalBuffer = normalBuffer;
        this.texCoordBuffer = texCoordBuffer;
        this.indexBuffer = indexBuffer;
        this.numIndices = numIndices;
    }

Lastly, add the draw code, which will be called from the Camera component, to render the geometry prepared in the buffers (via setBuffers). The draw method looks like this:

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

        // Set the active texture unit to texture unit 0.
        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);

        // Bind the texture to this unit.
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);

        // Tell the texture uniform sampler to use this texture in // the shader by binding to texture unit 0.
        GLES20.glUniform1i(textureParam, 0);

        //Technically, we don't need to do this with every draw //call, but the light could move.
        //We could also add a step for shader-global parameters //which don't vary per-object
        GLES20.glUniform3fv(lightPosParam, 1, RenderBox.instance.mainLight.lightPosInEyeSpace, 0);
        GLES20.glUniform4fv(lightColParam, 1, RenderBox.instance.mainLight.color, 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 for eye position.
        GLES20.glUniformMatrix4fv(MVPParam, 1, false, modelViewProjection, 0);

        //Set vertex attributes
        GLES20.glVertexAttribPointer(positionParam, 3, GLES20.GL_FLOAT, false, 0, vertexBuffer);
        GLES20.glVertexAttribPointer(normalParam, 3, GLES20.GL_FLOAT, false, 0, normalBuffer);
        GLES20.glVertexAttribPointer(texCoordParam, 2, GLES20.GL_FLOAT, false, 0, texCoordBuffer);

        GLES20.glDrawElements(GLES20.GL_TRIANGLES, numIndices, GLES20.GL_UNSIGNED_SHORT, indexBuffer);

        RenderBox.checkGLError("Diffuse Texture Color Lighting draw");
    }
}

Comparing this with the SolidColorLightingMaterial class that we defined earlier, you will notice that it's quite similar. We've replaced the single color with a texture ID, and we've added the requirements for a texture coordinate buffer (texCoordBuffer) given by a Sphere component. Also, note that we are setting the active texture unit to GL_TEXTURE0 and binding the texture.

Adding diffuse lighting texture to a Sphere component

To add the new material to the Sphere component, we'll make an alternative constructor that receives a texture handle. It then creates an instance of the DiffuseLightingMaterial class and sets the buffers from the sphere.

Let's add the material to the Sphere component by defining a new constructor (Sphere) that takes the texture ID and calls a new helper method named createDiffuseMaterial, as follows:

    public Sphere(int textureId){
        super();
        allocateBuffers();
        createDiffuseMaterial(textureId);
    }

    public Sphere createDiffuseMaterial(int textureId){
        DiffuseLightingMaterial mat = new DiffuseLightingMaterial(textureId);
        mat.setBuffers(vertexBuffer, normalBuffer, texCoordBuffer, indexBuffer, numIndices);
        material = mat;
        return this;
    }

Now, we can use the textured material.

Viewing the Earth

To add the Earth texture to our sphere, modify the setup method of MainActivity to specify the texture resource ID instead of a color, as follows:

    @Override
    public void setup() {
        sphere = new Transform();
        sphere.addComponent(new Sphere(R.drawable.earth_tex));
        sphere.setLocalPosition(2.0f, -2.f, -2.0f);
    }

There you have it, Home Sweet Home!

Viewing the Earth

That looks really cool. Oops, it's upside down! Although there's not really a specific up versus down in outer space, our Earth looks upside down from what we're used to seeing. Let's flip it in the setup method so that it starts at the correct orientation, and while we're at it, let's take advantage of the fact that the Transform methods return themselves, so we can chain the calls, as follows:

    public void setup() {
        sphere = new Transform()
            .setLocalPosition(2.0f, -2.f, -2.0f)
            .rotate(0, 0, 180f)
            .addComponent(new Sphere(R.drawable.earth_tex));
    }

Naturally, the Earth is supposed to spin. Let's animate it to rotate it like we'd expect the Earth to do. Add this to the preDraw method, which gets called before each new frame. It uses the Time class's getDeltaTime method, which returns the current fraction of a second change since the previous frame. If we want it to rotate, say, -10 degrees per second, we use -10 * deltaTime:

    public void preDraw() {
        float dt = Time.getDeltaTime();
        sphere.rotate( 0, -10f * dt, 0);
    }

That looks good to me! How about you?

Changing the camera position

One more thing. We seem to be looking at the Earth in line with the light source. Let's move the camera view so that we can see the Earth from the side. That way, we can see the lighted shading better.

Suppose we leave the light source position at the origin, (0,0,0) as if it were the Sun at the center of the Solar System. The Earth is 147.1 million km from the Sun. Let's place the sphere that many units to the right of the origin, and place the camera at the same relative position. Now, the setup method looks like the following code:

    public void setup() {
        sphere = new Transform()
            .setLocalPosition(147.1f, 0, 0)
            .rotate(0, 0, 180f)
            .addComponent(new Sphere(R.drawable.earth_tex));
        RenderBox.mainCamera.getTransform().setLocalPosition(147.1f, 2f, 2f);
    }

Run it and this is what you will see:

Changing the camera position

Does that look virtually realistic or what? NASA would be proud!

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

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