The Sun will be rendered as a textured sphere. However, it's not shaded with front and back sides like our Earth. We need to render it unlit or rather unshaded. This means we need to create the UnlitTextureMaterial
.
We have a texture file for the Sun, too (and all the planets as well).We won't show all of them in the chapter although they're included with the downloadable files for the book.
Drag a copy of the sun_tex.png
file onto your res/drawable/
folder.
As we've seen earlier in this book, unlit shaders are much simpler than ones with lighting. In your res/raw/
folder, create files for unlit_tex_vertex.shader
and unlit_tex_fragment.shader
, and then define them, as follows.
File: unlit_tex_vertex.shader
uniform mat4 u_MVP; attribute vec4 a_Position; attribute vec2 a_TexCoordinate; varying vec3 v_Position; varying vec2 v_TexCoordinate; void main() { // pass through the texture coordinate v_TexCoordinate = a_TexCoordinate; // final point in normalized screen coordinates gl_Position = u_MVP * a_Position; }
File: unlit_tex_fragment.shader
precision mediump float; // default medium precision uniform sampler2D u_Texture; // the input texture varying vec3 v_Position; varying vec2 v_TexCoordinate; void main() { // Send the color from the texture straight out gl_FragColor = texture2D(u_Texture, v_TexCoordinate); }
Now, we can write the UnlitTexMaterial
class. Here's the initial code:
public class UnlitTexMaterial extends Material { private static final String TAG = "unlittex"; 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 MVPParam; FloatBuffer vertexBuffer; FloatBuffer texCoordBuffer; ShortBuffer indexBuffer; int numIndices;
Here are the constructor, setupProgram
, and setBuffers
methods:
public UnlitTexMaterial(int resourceId){ super(); setupProgram(); this.textureId = MainActivity.loadTexture(resourceId); } public static void setupProgram(){ if(program != -1) return; //Create shader program program = createProgram(R.raw.unlit_tex_vertex, R.raw.unlit_tex_fragment); //Get vertex attribute parameters positionParam = GLES20.glGetAttribLocation(program, "a_Position"); texCoordParam = GLES20.glGetAttribLocation(program, "a_TexCoordinate"); //Enable them (turns out this is kind of a big deal ;) GLES20.glEnableVertexAttribArray(positionParam); GLES20.glEnableVertexAttribArray(texCoordParam); //Shader-specific parameters textureParam = GLES20.glGetUniformLocation(program, "u_Texture"); MVPParam = GLES20.glGetUniformLocation(program, "u_MVP"); RenderBox.checkGLError("Unlit Texture params"); } public void setBuffers(FloatBuffer vertexBuffer, FloatBuffer texCoordBuffer, ShortBuffer indexBuffer, int numIndices){ //Associate VBO data with this instance of the material this.vertexBuffer = vertexBuffer; this.texCoordBuffer = texCoordBuffer; this.indexBuffer = indexBuffer; this.numIndices = numIndices; }
It will be handy to have getter and setter methods for the texture ID (in later projects, not used here):
public void setTexture(int textureHandle){ textureId = textureHandle; } public int getTexture(){ return textureId; }
Lastly, here's the draw
method:
@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); 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 vertex attributes GLES20.glVertexAttribPointer(positionParam, 3, GLES20.GL_FLOAT, false, 0, vertexBuffer); GLES20.glVertexAttribPointer(texCoordParam, 2, GLES20.GL_FLOAT, false, 0, texCoordBuffer); GLES20.glDrawElements(GLES20.GL_TRIANGLES, numIndices, GLES20.GL_UNSIGNED_SHORT, indexBuffer); RenderBox.checkGLError("Unlit Texture draw"); } }
We're ready to integrate the new material into our Sphere
class and see how it looks.
In Sphere.java
, add a new constructor that takes a boolean
parameter, indicating that the texture should be lighted, and the createUnlitTexMaterial
helper method:
public Sphere(int textureId, boolean lighting){ super(); allocateBuffers(); if (lighting) { createDiffuseMaterial(textureId); } else { createUnlitTexMaterial(textureId); } } public Sphere createUnlitTexMaterial(int textureId){ UnlitTexMaterial mat = new UnlitTexMaterial(textureId); mat.setBuffers(vertexBuffer, texCoordBuffer, indexBuffer, numIndices); material = mat; return this; }
Notice that the way in which we've defined constructors, you can call either new Sphere(texId)
or Sphere(texId, true)
to get lighted renders. But for unlit, you must use the second one as Sphere(texId, false)
. Also note that setting up the whole component in the constructor is not the only way to go. We only do it this way because it keeps our MainActivity
code concise. In fact, as we start expanding our use of RenderBox
and its shader library, it will become necessary to put most of this code into our MainActivity
class. It would be impossible to create a constructor for every type of material. Ultimately, a materials system is necessary to allow you to create and set materials without having to create a new class for each one.
Now, all we need to do is add the Sun sphere to the setup
method of MainActivity
. Let's make it big, say, at a scale of 6.963 (remember that's in millions of kms). This value may seem arbitrary now, but you'll see where it comes from when we run the calculations on the Solar System geometry and scale the planets as well.
Add the following code to the setup
method of MainActivity
:
public void setup() { Transform origin = new Transform(); //Sun Transform sun = new Transform() .setParent(origin, false) .setLocalScale(6.963f, 6.963f, 6.963f) .addComponent(new Sphere(R.drawable.sun_tex, false)); //"Sun" light RenderBox.instance.mainLight.transform.setPosition( origin.getPosition()); RenderBox.instance.mainLight.color = new float[]{1, 1, 0.8f, 1}; //Earth…
We start by defining an origin transform that will be the center of the Solar System. Then, we create the Sun, parented to the origin, with the given scale. Then, add a new sphere component with the Sun texture. We've also given our light a slightly yellowish color, which will blend with the Earth's texture colors.
Here's what the rendered Sun looks like, which seems to illuminate the Earth: