Drawing 3D Objects Android

Now that you have the OpenGL ES environment working within Android, it’s time to do some actual drawing. This section leads you through a number of examples, each building upon the previous. In doing so, these examples introduce new Android-specific concepts with OpenGL ES.

Drawing Your Vertices

OpenGL ES supports two primary drawing calls, glDrawArrays() and glDrawElements(). Both of these methods require the use of a vertex buffer assigned through a call to glVertexPointer. Because Android runs on top of Java, though, an arbitrary array cannot be passed in as the array contents might move around in memory. Instead, we have to use a ByteBuffer, FloatBuffer, or IntBuffer so the data stays at the same location in memory. Converting various arrays to buffers is common, so we have implemented some helper methods. Here is one for converting a float array into a FloatBuffer:

FloatBuffer getFloatBufferFromFloatArray(float array[]) {
ByteBuffer tempBuffer =
ByteBuffer.allocateDirect(array.length * 4);
tempBuffer.order(ByteOrder.nativeOrder());
FloatBuffer buffer = tempBuffer.asFloatBuffer();
buffer.put(array);
buffer.position(0);
return buffer;
}

This creates a buffer of 32-bit float values with a stride of 0.You can then store the resulting FloatBuffer and assign it to OpenGL calls. Here is an example of doing this, using the triangle we showed previously in this chapter:

float[] vertices = {
-0.559016994f, 0, 0,
0.25f, 0.5f, 0f,
0.25f, -0.5f, 0f
};
mVertexBuffer = getFloatBufferFromFloatArray(vertices);

With the buffer assigned,we can now draw the triangle, as shown here:

void drawTriangle(GL10 gl) {
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mVertexBuffer);
gl.glDrawArrays(GL10.GL_TRIANGLES, 0, 3);
}

We have to enable the GL_VERTEX_ARRAY state, though you could do this in GL configuration, as it is required to draw anything with OpenGL ES. We then assign the vertex buffer through a call to glVertexPointer(), also telling GL that we’re using float values. Fixed point values, through GL_FIXED, can also be used and might be faster with some Android implementations. Finally, a call to glDrawArrays() is made to draw the triangles using three vertices from the first one. The result of this can be seen in Figure.

Coloring Your Vertices

In OpenGL ES, you can use an array of colors to individually assign colors to each vertex that is drawn. This is accomplished by calling the glColorPointer() method with a buffer of colors. The following code sets up a small buffer of colors for three vertices:

float[] colors = {
1f, 0, 0, 1f,
0, 1f, 0, 1f,
0, 0, 1f, 1f
};
mColorBuffer = getFloatBufferFromFloatArray(colors);

With the buffer available, we can now use it to color our triangle, as shown in the following code:

void drawColorful(GL10 gl) {
gl.glEnableClientState(GL10.GL_COLOR_ARRAY);
gl.glColorPointer(4,GL10.GL_FLOAT, 0, mColorBuffer);
draw(gl);
gl.glDisableClientState(GL10.GL_COLOR_ARRAY);
}

First, the client state for GL_COLOR_ARRAY is enabled. Then, calling the glColorPointer method sets the preceding color buffer created. The call to draw() draws the triangle like the colorful one seen in Figure.

A triangle with red, green, and blue vertices smoothly blended.

A triangle with red, green, and blue vertices smoothly blended

Drawing More Complex Objects

A standard cube has eight vertices. However, in OpenGL ES, each of the six faces needs to be drawn with two triangles. Each of these triangles needs three vertices. That’s a total of 36 vertices to draw an object with just 8 of its own vertices. There must be a better way.

OpenGL ES supports index arrays. An index array is a list of vertex indexes from the current vertex array. The index array must be a buffer, and in this example we use a ByteBuffer because we don’t have many vertices to indicate. The index array lists the order that the vertices should be drawn when used with glDrawElements(). Note that the color arrays (and normal arrays that we get to shortly) are still relative to the vertex array and not the index array. Here is some code that draws an OpenGL cube using just eight defined vertices:

float vertices[] = {
-1,1,1, 1,1,1, 1,-1,1, -1,-1,1,
1,1,-1, -1,1,-1, -1,-1,-1, 1,-1,-1
};
byte indices[] = {
0,1,2, 2,3,0, 1,4,7, 7,2,1, 0,3,6, 6,5,0,
3,2,7, 7,6,3, 0,1,4, 4,5,0, 5,6,7, 7,4,5
};
FloatBuffer vertexBuffer =
getFloatBufferFromFloatArray(vertices);
ByteBuffer indexBuffer =
getByteBufferFromByteArray(indices);
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
gl.glDrawElements(GL10.GL_TRIANGLES, indices.length,
GL10.GL_UNSIGNED_BYTE, indexBuffer);

The vertices define the typical shape for a cube. Then, however, we use the index array to define in what order the vertices are drawn to create the cube out of the 12 triangles that we need (recalling that OpenGL ES does not support quads). Now you have a red shape on your screen that looks (left). It doesn’t actually look much like a cube, though, does it? Without some shading, it looks too much like a random polygon. If, however, you switch the glDrawElements() to GL_LINE_LOOP instead of GL_TRIANGLES, you see a line-drawing version of the shape, like Figure (right). Now you can see that it really is a cube. You can reuse the vertices buffer with different index buffers, too. This is useful if you can define multiple shapes using the same set of vertices and then draw them in their own locations with transformations.

Lighting Your Scene

The last 3D object that we drew was a cube that looked like some strange polygon on your flat 2D screen. The colors of each face could be made different by applying coloring between each call to draw a face. However, that will still produce a fairly flat-looking cube. Instead, why not shine some light on the scene and let the lighting give the cube some additional depth?

Before you can provide lighting on a scene, each vertex of each surface needs a vector applied to it to define how the light will reflect and, thus, how it will be rendered. Although this vector can be anything, most often it is perpendicular to the surface defined by the vertices; this is called the normal of a surface. Recallin our cube from the preceding example, we see now that a cube can’t actually be created out of eight vertices as each vertex can carry only one normal array, and we would need three per vertex because each vertex belongs to three faces. Instead, we have to use a cube that does, in fact, contain the entire lot of 24 vertices. (Technically, you could define a bunch of index arrays and change the normal array between calls to each face, but it’s more commonly done with a large list of vertices and a single list of normal vectors.)

(Left) A solid cube with no shading and (right) the same cube with only lines.

(Left) A solid cube with no shading and (right) the same cube with only lines

Like the color array, the normal array is applied to each vertex in the vertex array in order. Lighting is a fairly complex topic and if it’s unfamiliar, you need to check out the References and More Information section at the end of this chapter where you can learn more. For now, we just give an example of how to use the lighting features of Open GL ES within Android.

Here is some code for enabling simple lighting:

mGL.glEnable(GL10.GL_LIGHTING);
mGL.glEnable(GL10.GL_LIGHT0);
mGL.glLightfv(GL10.GL_LIGHT0, GL10.GL_AMBIENT,
new float[] {0.1f, 0.1f, 0.1f, 1f}, 0);
mGL.glLightfv(GL10.GL_LIGHT0, GL10.GL_DIFFUSE,
new float[] {1f, 1f, 1f, 1f}, 0); mGL.glLightfv(GL10.GL_LIGHT0, GL10.GL_POSITION,
new float[] {10f, 0f, 10f, 1f}, 0);
mGL.glEnable(GL10.GL_COLOR_MATERIAL);
mGL.glShadeModel(GL10.GL_SMOOTH);

This code enables lighting, enables GL_LIGHT0, and then sets the color and brightness of the light. Finally, the light is positioned in 3D space. In addition, we enable GL_COLOR_MATERIAL so the color set for drawing the objects is used with the lighting. We also enable the smooth shading model, which helps remove the visual transition between triangles on the same face. You can use color material definitions for fancier lighting and more realistic-looking surfaces, but that is beyond the scope of this book.

Here is the drawing code for our cube, assuming we now have a full vertex array of all 24 points and an index array defining the order in which they should be drawn:

gl.glEnableClientState(GL10.GL_NORMAL_ARRAY);
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mVertexBuffer);
gl.glNormalPointer(GL10.GL_FLOAT, 0, mNormalBuffer);
gl.glDrawElements(GL10.GL_TRIANGLES, indices.length,
GL10.GL_UNSIGNED_BYTE, mIndexBuffer);

Notice that the normal array and normal mode are now turned on. Without this, the lighting won’t look right. As with the other arrays, this has to be assigned through a fixed buffer in Java, as this code demonstrates:

float normals[] = {
// front
0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1,
// back
0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1,
// top
0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0,
// bottom
0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0,
// right
1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0,
// left
-1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0 };
mNormalBuffer = getFloatBufferFromFloatArray(normals);

The preceding code uses one of the helper methods we talked about previously to create a FloatBuffer. We use a floating point array for the normals. This also shows the normals and how each vertex must have one. (Recall that we now have 24 vertices for the cube.) You can create various lighting effects by making the normals not actually perpendicular to the surface, but for more accurate lighting, it’s usually better to just increase the polygon count of your objects or add textures. Figure shows the solid cube, now shaded to show depth better.

Texturing Your Objects

Texturing surfaces, or putting images on surfaces, is a rather lengthy and complex topic. It’s enough for our purposes to focus on learning how to texture with Android, so we use the previously lit and colored cube and texture it.

A cube with a light shining from the right to shade it.

A cube with a light shining from the right to shade it.

First, texturing needs to be enabled, as shown in the following code:

mGL.glEnable(GL10.GL_TEXTURE_2D);
int[] textures = new int[1];
mGL.glGenTextures(1, textures, 0);

This code enables texturing and creates an internally named slot for one texture. We use this slot to tell OpenGL what texture we operate on in the next block of code:

gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);
Bitmap bitmap = BitmapFactory.decodeResource(
c.getResources(), R.drawable.android);
Bitmap bitmap256 = Bitmap.createScaledBitmap(
bitmap, 256, 256, false);
GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap256, 0);
bitmap.recycle();
bitmap256.recycle();

You’ve probably begun to wonder what happened to Android-specific code. Well, it’s back. OpenGL ES needs bitmaps to use as textures. Lucky for us, Android comes with a Bitmap class that can read in nearly any format of image, including PNG, GIF, and JPG files. You can do this straight from a Drawable resource identifier, too, as demonstrated in the preceding code. OpenGL requires that textures be square and have sides that are powers of two, such as 64×64 or 256×256. Because the source image might or might not be in one of these exact sizes, we scale it again with just a single Android method call. If the source image weren’t square, though, the original aspect ratio is not kept. Sometimes it is easier to scale down with the original aspect ratio and add colored padding around the edges of the image instead of stretching it, but this is beyond the scope of this example.

Finally, GLUtils.texImage2D() assigns an Android Bitmap to an OpenGL texture. OpenGL keeps the image internally, so we can clean up the Bitmap objects with a call to the recycle() method.

Now that OpenGL ES knows about the texture, the next step is to tell it where to draw the texture. You can accomplish this through using a texture coordinate buffer. This is similar to all the other buffer arrays in that it must be assigned to a fixed Java buffer and enabled. Here is the code to do this with our cube example:

float texCoords[] = {
1,0, 1,1, 0,1, 0,0,
1,0, 1,1, 0,1, 0,0,
1,0, 1,1, 0,1, 0,0,
1,0, 1,1, 0,1, 0,0,
1,0, 1,1, 0,1, 0,0,
1,0, 1,1, 0,1, 0,0,
};
mCoordBuffer = getFloatBufferFromFloatArray(texCoords);
gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, mCoordBuffer);
draw(gl);

As promised, this code creates a fixed buffer for the texture coordinates. We set the same ones on each face of the cube, so each vertex has a texture coordinate assigned to it. (0,0 is the lower-left portion of the texture and 1,1 is the upper-right.) Next, we enable the GL_TEXTURE_COORD_ARRAY state and then tell OpenGL which buffer to use. Finally, we draw the cube. Now, we left the code the same as before, which produces the output you see in Figure (left).The coloring does still apply, even with textures. If coloring is not applied, the output looks like what you see in Figure (right).


All rights reserved © 2018 Wisdom IT Services India Pvt. Ltd DMCA.com Protection Status

Android Topics