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:
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.
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"); } }
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...