Hello, cube!
A flat triangle floating in 3D space may be amazing, but it's nothing compared to what we're going to do next: a 3D cube!
The cube model data
The triangle, with just three vertices, was declared in the MainActivity
class to keep the example simple. Now, we will introduce more complex geometry. We'll put it in a class named Cube
.
Okay, it's just a cube that is composed of eight distinct vertices, forming six faces, right?
Well, GPUs prefer to render triangles rather than quads, so subdivide each face into two triangles; that's 12 triangles in total. To define each triangle separately, that's a total of 36 vertices, with proper winding directions, defining our model, as shown in CUBE_COORDS
. Why not just define eight vertices and reuse them? We'll show you how to do this later.
In Android Studio, in the Android project hierarchy pane on the left-hand side, find your Java code folder (such as com.cardbookvr.cardboardbox
). Right-click on it, and go to New | Java Class. Then, set Name: Cube, and click on OK. Then, edit the file, as follows (remember that the code for the projects in this book are available for download from the publisher's website and from the book's public GitHub repositories):
package com.cardbookvr.cardboardbox; public class Cube { public static final float[] CUBE_COORDS = new float[] { // Front face -1.0f, 1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, // Right face 1.0f, 1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 1.0f, 1.0f, -1.0f, 1.0f, -1.0f, 1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, // Back face 1.0f, 1.0f, -1.0f, 1.0f, -1.0f, -1.0f, -1.0f, 1.0f, -1.0f, 1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, -1.0f, // Left face -1.0f, 1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, -1.0f, 1.0f, 1.0f, // Top face -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f, // Bottom face 1.0f, -1.0f, -1.0f, 1.0f, -1.0f, 1.0f, -1.0f, -1.0f, -1.0f, 1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, -1.0f, }; }
Cube code
Returning to the MainActivity
file, we'll just copy/paste/edit the triangle code and reuse it for the cube. Obviously, this isn't ideal, and once we see a good pattern, we can abstract out some of this into reusable methods. Also, we'll use the same shaders as those of the triangle, and then in the next section, we'll replace them with a better lighting model. That is to say, we'll implement lighting or what a 2D artist might call shading, which we haven't done so far.
Like the triangle, we declare a bunch of variables that we are going to need. The vertex count, obviously, should come from the new Cube.CUBE_COORDS
array:
// Model variables private static float cubeCoords[] = Cube.CUBE_COORDS; private final int cubeVertexCount = cubeCoords.length / COORDS_PER_VERTEX; private float cubeColor[] = { 0.8f, 0.6f, 0.2f, 0.0f }; // yellow-ish private float[] cubeTransform; private float cubeDistance = 5f; // Viewing variables private float[] cubeView; // Rendering variables private FloatBuffer cubeVerticesBuffer; private int cubeProgram; private int cubePositionParam; private int cubeColorParam; private int cubeMVPMatrixParam;
Add the following code to onCreate
:
cubeTransform = new float[16]; cubeView = new float[16];
Add the following code to onSurfaceCreated
:
prepareRenderingCube();
Write the prepareRenderingCube
method, as follows:
private void prepareRenderingCube() { // Allocate buffers ByteBuffer bb = ByteBuffer.allocateDirect(cubeCoords.length * 4); bb.order(ByteOrder.nativeOrder()); cubeVerticesBuffer = bb.asFloatBuffer(); cubeVerticesBuffer.put(cubeCoords); cubeVerticesBuffer.position(0); // Create GL program cubeProgram = GLES20.glCreateProgram(); GLES20.glAttachShader(cubeProgram, simpleVertexShader); GLES20.glAttachShader(cubeProgram, simpleFragmentShader); GLES20.glLinkProgram(cubeProgram); GLES20.glUseProgram(cubeProgram); // Get shader params cubePositionParam = GLES20.glGetAttribLocation(cubeProgram, "a_Position"); cubeColorParam = GLES20.glGetUniformLocation(cubeProgram, "u_Color"); cubeMVPMatrixParam = GLES20.glGetUniformLocation(cubeProgram, "u_MVP"); // Enable arrays GLES20.glEnableVertexAttribArray(cubePositionParam); }
We will position the cube 5 units away and rotate it 30 degrees on a diagonal axis of (1, 1, 0). Without the rotation, we'll just see the square of the front face. Add the following code to initializeScene
:
// Rotate and position the cube Matrix.setIdentityM(cubeTransform, 0); Matrix.translateM(cubeTransform, 0, 0, 0, -cubeDistance); Matrix.rotateM(cubeTransform, 0, 30, 1, 1, 0);
Add the following code to onDrawEye
to calculate the MVP matrix, including the cubeTransform
matrix, and then draw the cube:
Matrix.multiplyMM(cubeView, 0, view, 0, cubeTransform, 0); Matrix.multiplyMM(modelViewProjection, 0, perspective, 0, cubeView, 0); drawCube();
Write the drawCube
method, which is very similar to the drawTri
method, as follows:
private void drawCube() { GLES20.glUseProgram(cubeProgram); GLES20.glUniformMatrix4fv(cubeMVPMatrixParam, 1, false, modelViewProjection, 0); GLES20.glVertexAttribPointer(cubePositionParam, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, 0, cubeVerticesBuffer); GLES20.glUniform4fv(cubeColorParam, 1, cubeColor, 0); GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, cubeVertexCount); }
Build and run it. You will now see a 3D view of the cube, as shown in the following screenshot. It needs shading.