Day and night material

Honestly though, the back of the Earth looks uncannily dark. I mean, this isn't the 18th century. So much nowadays is 24 x 7, especially our cities. Let's represent this with a separate Earth night texture that has city lights.

We have a file for you to use named earth_night_tex.jpg. Drag a copy of the file into your res/drawable/ folder.

It may be a little difficult to discern on this book's page, but this is what the texture image looks like:

Day and night material

Day/night shader

To support this, we will create a new DayNightMaterial class that takes both versions of the Earth texture. The material will also incorporate the corresponding fragment shader that takes into consideration the normal vector of the surface relative to the light source direction (using dot products, if you're familiar with vector math) to decide whether to render using the day or night texture image.

In your res/raw/ folder, create files for day_night_vertex.shader and day_night_fragment.shader, and then define them, as follows.

File: day_night_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 to 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;
}

Except for the addition of v_Texcoordinate, this is exactly the same as our SolidColorLighting shader.

File: day_night_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 day texture.
uniform sampler2D u_NightTexture;    // the night texture.

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

void main() {
    // 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 ambient = 0.3;
    float dotProd = dot(v_Normal, lightVector);
    float blend = min(1.0, dotProd * 2.0);
    if(dotProd < 0.0){
        //flat ambient level of 0.3
        gl_FragColor = texture2D(u_NightTexture, v_TexCoordinate) * ambient;
    } else {
        gl_FragColor = (
            texture2D(u_Texture, v_TexCoordinate) * blend
            + texture2D(u_NightTexture, v_TexCoordinate) * (1.0 - blend)
        ) * u_LightCol * min(max(dotProd * 2.0, ambient), 1.0);
    }
}

As always, for lighting, we calculate the dot product (dotProd) of the vertex normal vector and the light direction vector. When that value is negative, the vertex is facing away from the light source (the Sun), so we'll render using the night texture. Otherwise, we'll render using the regular daytime earth texture.

The lighting calculations also include a blend value. This is basically a way of squeezing the transitional zone closer around the terminator when calculating the gl_FragColor variable. We are multiplying the dot product by 2.0 so that it follows a steeper slope, but still clamping the blend value between 0 and 1. It's a little complicated, but once you think about the math, it should make some sense.

We are using two textures to draw the same surface. While this might seem unique to this day/night situation, it is actually a very common method known as multitexturing. You may not believe it, but 3D graphics actually got quite far before introducing the ability to use more than one texture at a time. These days, you see multitexturing almost everywhere, enabling techniques such as normal mapping, decal textures, and displacement/parallax shaders, which create greater detail with simpler meshes.

The DayNightMaterial class

Now we can write the DayNightMaterial class. It's basically like the DiffuseLightingMaterial class that we created earlier but supports both the textures. Therefore, the constructor takes two texture IDs. The setBuffers method is identical to the earlier one, and the draw method is nearly identical but with the added binding of the night texture.

Here's the complete code, highlighting the lines that differ from DiffuseLightingMaterial:

public class DayNightMaterial extends Material {
    private static final String TAG = "daynightmaterial";

As with our other materials, declare the variables we'll need, including the texture ID for both the day and night:

    int textureId;
    int nightTextureId;

    static int program = -1; //Initialize to a totally invalid value for setup state
    static int positionParam;
    static int texCoordParam;
    static int textureParam;
    static int nightTextureParam;
    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;

Define the constructor that takes both the resource IDs and the setupProgram helper method:

    public DayNightMaterial(int resourceId, int nightResourceId){
        super();
        setupProgram();
        this.textureId = MainActivity.loadTexture(resourceId);

        this.nightTextureId = MainActivity.loadTexture(nightResourceId);
    }
    
    public static void setupProgram(){
        if(program != -1) return;
        //Create shader program
        program = createProgram(R.raw.day_night_vertex, R.raw.day_night_fragment);

        //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");
        nightTextureParam = GLES20.glGetUniformLocation(program, "u_NightTexture");
        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("Day/Night params");
    }
    
    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, the draw method that cranks it all out to the screen:

    @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);

        GLES20.glActiveTexture(GLES20.GL_TEXTURE1);
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, nightTextureId);

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

        //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("DayNight Texture Color Lighting draw");
    }
}

Rendering with day/night

Now we're ready to integrate the new material into our Sphere component and see how it looks.

In Sphere.java, add a new constructor and the createDayNightMaterial helper method, as follows:

    public Sphere(int textureId, int nightTextureId){
        super();
        allocateBuffers();
        createDayNightMaterial(textureId, nightTextureId);
    }

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

Let's call it from the setup method of MainActivity, and replace the call with the new Sphere instance passing both the textures' resource IDs:

    .addComponent(new Sphere(R.drawable.earth_tex, R.drawable.earth_night_tex));

Run it now. That looks really cool! Classy! Unfortunately, it doesn't make a lot of sense to paste a screenshot here because the city night lights won't show very well. You'll just have to see it for yourself in your own Cardboard viewer. Believe me when I tell you, it's worth it!

Next, here comes the Sun, and I say, it's alright...

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

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