Here’s our game plan for the chapter:
• First we’ll learn about OpenGL’s perspective division and how to use the w component to create the illusion of 3D on a 2D screen.• Once we understand the w component, we’ll learn how to set up a perspective projection so that we can see the table in 3D.
6.1 The Art of 3D
People have been fooling people into perceiving a flat two-dimensional painting as a complete third-dimensional scene, one of the tricks they use is called linear
projection, and it works by joining together parallel lines at an imaginary vanishing point to create the illusion of perspective.
6.2 Transforming a Coordinate from the Shader to the Screen
We are now familiar with normalized device coordinates, and let’s take a look at the following flow chart to review how a coordinate gets transformed from the
original gl_Position written by the vertex shader to the final coordinate onscreen:
There are two transformation steps and three different coordinate spaces.
1.Clip Space
When the vertex shader writes a value out to gl_Position, OpenGL expects this to be in clip space. The logic behind clip space is very simple: for any position, the x, y, and
z components all need to be between -w and w for that position. For example, if a position’s w is 1, then the x, y, and z components all need to be between -1 and 1.
Anything outside this range will not be visible on the screen. The reason why it depends on the position’s w will be apparent once we learn about perspective division.
2.Perspective Division
Before a vertex position becomes a normalized device coordinate, OpenGL actually performs an extra step known as perspective division.every visible coordinate will lie in
the range of [-1, 1] for the x, y, and z components, regardless of the size or shape of the rendering area.
To create the illusion of 3D on the screen, OpenGL will take each gl_Position and divide the x, y, and z components by the w component. When the w component is used
to represent distance, this causes objects that are further away to be moved closer to the center of the rendering area, which then acts like a vanishing point. This is how
OpenGL fools us into seeing a scene in 3D, using the same trick that artists have been using for centuries.
In the following image, we can see an example of this effect in action, as a coordinate with the same x, y, and z will be brought ever closer to the center as the w value
increases:
In OpenGL, the 3D effect is linear and done along straight lines. In real life, things are more complicated (imagine a fish-eye lens), but this sort of linear projection is a
reasonable approximation.
3. Homogeneous Coordinates
Because of the perspective division, coordinates in clip space are often referred to as homogeneous coordinates,1 introduced by August Ferdinand Möbius in 1827. The
reason they are called homogeneous is because several coordinates in clip space can map to the same point. For example, take the following points:
(1, 1, 1, 1), (2, 2, 2, 2), (3, 3, 3, 3), (4, 4, 4, 4), (5, 5, 5, 5)
After perspective division, all of these points will map to (1, 1, 1) in normalized device coordinates.
4.The Advantages of Dividing by W
There are additional advantages to adding w as a fourth component. We can decouple the perspective effect from the actual z coordinate, so we can switch between an
orthographic and a perspective projection. There’s also a benefit to preserving the z component as a depth buffer, which we’ll cover in Removing Hidden Surfaces with the
Depth Buffer, on page 245.
5. Viewport Transformation
Before we can see the final result, OpenGL needs to map the x and y components of the normalized device coordinates to an area on the screen that the operating system
has set aside for display, called the viewport; these mapped coordinates are known as window coordinates. We don’t really need to be too concerned about these
coordinates beyond telling OpenGL how to do the mapping. We’re currently doing this in our code with a call to glViewport() in onSurfaceChanged().When OpenGL does
this mapping, it will map the range (-1, -1, -1) to (1, 1, 1) to the window that has been set aside for display. Normalized device coordinates outside of this range will be
clipped. As we learned in Chapter 5, Adjusting to the Screen's Aspect Ratio, on page 77, this range is always the same, regardless of the width or height of the viewport.
6.3 Adding the W Component to Create Perspective
Since we’ll now be specifying the x, y, z, and w components of a position, let’s begin by updating POSITION_COMPONENT_COUNT as follows:
//AirHockey3D/src/com/airhockey/android/AirHockeyRenderer.java
private static final int POSITION_COMPONENT_COUNT = 4;
The next step is to update all of our vertices:
//AirHockey3D/src/com/airhockey/android/AirHockeyRenderer.java
float[] tableVerticesWithTriangles = {
// Order of coordinates: X, Y, Z, W, R, G, B
// Triangle Fan
0f, 0f, 0f, 1.5f, 1f, 1f, 1f,
-0.5f, -0.8f, 0f, 1f, 0.7f, 0.7f, 0.7f,
0.5f, -0.8f, 0f, 1f, 0.7f, 0.7f, 0.7f,
0.5f, 0.8f, 0f, 2f, 0.7f, 0.7f, 0.7f,
-0.5f, 0.8f, 0f, 2f, 0.7f, 0.7f, 0.7f,
-0.5f, -0.8f, 0f, 1f, 0.7f, 0.7f, 0.7f,
// Line 1
-0.5f, 0f, 0f, 1.5f, 1f, 0f, 0f,
0.5f, 0f, 0f, 1.5f, 1f, 0f, 0f,
// Mallets
0f, -0.4f, 0f, 1.25f, 0f, 0f, 1f,
0f, 0.4f, 0f, 1.75f, 1f, 0f, 0f
};
We added a z and a w component to our vertex data. We’ve updated all of the vertices so that the ones near the bottom of the screen have a w of 1 and the ones near the
top of the screen have a w of 2; we also updated the line and the mallets to have a fractional w that’s in between. This should have the effect of making the top part of the
table appear smaller than the bottom, as if we were looking into the distance. We set all of our z components to zero, since we don’t need to actually have anything in z to
get the perspective effect.
OpenGL will automatically do the perspective divide for us using the w values that we’ve specified, and our current orthographic projection will just copy these w values
over.
what if we wanted to make things more dynamic, like changing the angle of the table or zooming in and out? Instead of hard-coding the w values, we’ll use matrices to
generate the values for us. Go ahead and revert the changes that we’ve made; in the next section, we’ll learn how to use a perspective projection matrix to generate the w
values automatically.
6.4 Moving to a Perspective Projection
Have a look again.
6.5 Defining a Perspective Projection
To recreate the magic of 3D, our perspective projection matrix needs to work together with the perspective divide. The projection matrix can’t do the perspective divide by
itself, and the perspective divide needs something to work with.
An object should move toward the center of the screen and decrease in size as it gets further away from us, so the most important task for our projectionmatrix is to create
the proper values for w so that when OpenGL does the perspective divide, far objects will appear smaller than near objects. One of the ways that we can do that is by
using the z component as the distance from the focal point and then mapping this distance to w. The greater the distance, the greater the w and the smaller the resulting
object.
1.Adjusting for the Aspect Ratio and Field of Vision
Let’s take a look at a more general-purpose projection matrix, which will allow us to adjust for the field of vision as well as for the screen’s aspect ratio:
6.6 Creating a Projection Matrix in Our Code
Android’s Matrix class contains two methods for this, frustumM() and perspectiveM(). Unfortunately, frustumM() has a bug that affects some types of projections,3 and
perspectiveM() was only introduced in Android Ice Cream Sandwich and is not available on earlier versions of Android. We could simply target Ice Cream Sandwich and
above, but then we’d be leaving out a large part of the market that still runs earlier versions of Android.
Instead, we can create our own method to implement the matrix as defined in the previous section. Open up the project we created back at the beginning of this chapter
and add a new class called MatrixHelper to the package com.airhockey.android.util. We’ll implement a method very similar to the perspectiveM() in Android’s Matrix class.
1.Creating Our Own perspectiveM
Add the following method signature to the beginning of MatrixHelper:
//AirHockey3D/src/com/airhockey/android/util/MatrixHelper.java
public static void perspectiveM(float[] m, float yFovInDegrees, float aspect,
float n, float f) {
2.Calculating the Focal Length
The first thing we’ll do is calculate the focal length, which will be based on the field of vision across the y-axis. Add the following code just
after the method signature:
//AirHockey3D/src/com/airhockey/android/util/MatrixHelper.java
final float angleInRadians = (float) (yFovInDegrees * Math.PI / 180.0);
final float a = (float) (1.0 / Math.tan(angleInRadians / 2.0));
We use Java’s Math class to calculate the tangent, and since it wants the angle in radians, we convert the field of vision from degrees to radians. We then calculate the
focal length as described in the previous section.
We use Java’s Math class to calculate the tangent, and since it wants the angle in radians, we convert the field of vision from degrees to radians. We then calculate the
focal length as described in the previous section.
Writing Out the Matrix
We can now write out the matrix values. Add the following code to complete the method:
//AirHockey3D/src/com/airhockey/android/util/MatrixHelper.java
m[0] = a / aspect;
m[1] = 0f;
m[2] = 0f;
m[3] = 0f;
m[4] = 0f;
m[5] = a;
m[6] = 0f;
m[7] = 0f;
m[8] = 0f;
m[9] = 0f;
m[10] = -((f + n) / (f - n));
m[11] = -1f;
m[12] = 0f;
m[13] = 0f;
m[14] = -((2f * f * n) / (f - n));
m[15] = 0f;}
This writes out the matrix data to the floating-point array defined in the argument m, which needs to have at least sixteen elements. OpenGL stores matrix data in column-
major order, which means that we write out data one column at a time rather than one row at a time. The first four values refer to the first column, the second four values to
the second column, and so on.
We’ve now finished our perspectiveM(), and we’re ready to use it in our code.Our method is very similar to the one found in the Android source code,4 with a few slight
changes to make it more readable.
6.7 Switching to a Projection Matrix
We’ll now switch to using the perspective projection matrix. Open up AirHockeyRenderer and remove all of the code from onSurfaceChanged(), except for the call to
glViewport(). Add the following code:
//AirHockey3D/src/com/airhockey/android/AirHockeyRenderer.java
MatrixHelper.perspectiveM(projectionMatrix, 45, (float) width
/ (float) height, 1f, 10f);
This will create a perspective projection with a field of vision of 45 degrees. The frustum will begin at a z of -1 and will end at a z of -10.
After adding the import for MatrixHelper, go ahead and run the program. You’ll probably notice that our air hockey table has disappeared! Since we didn’t specify a z
position for our table, it’s located at a z of 0 by default. Since our frustum begins at a z of -1, we won’t be able to see the table unless we move it into the distance.
Instead of hard-coding the z values, let’s use a translation matrix to move the table out before we project it using the projection matrix. By convention, we’ll call this matrix
the model matrix.
1.Moving Objects Around with a Model Matrix
Let’s add the following matrix definition to the top of the class:
//AirHockey3D/src/com/airhockey/android/AirHockeyRenderer.java
private final float[] modelMatrix = new float[16];
We’ll use this matrix to move the air hockey table into the distance. At the end of onSurfaceChanged(), add the following code:
//AirHockey3D/src/com/airhockey/android/AirHockeyRenderer.java
setIdentityM(modelMatrix, 0);
translateM(modelMatrix, 0, 0f, 0f, -2f);
This sets the model matrix to the identity matrix and then translates it by -2 along the z-axis. When we multiply our air hockey table coordinates with this matrix, they will
end up getting moved by 2 units along the negative z-axis.
2.Multiplying Once Versus Multiplying Twice
3.Updating the Code to Use One Matrix
Let’s wrap up the new matrix code and add the following to onSurfaceChanged() after the call to translateM():
//AirHockey3D/src/com/airhockey/android/AirHockeyRenderer.java
final float[] temp = new float[16];
multiplyMM(temp, 0, projectionMatrix, 0, modelMatrix, 0);
System.arraycopy(temp, 0, projectionMatrix, 0, temp.length);
Whenever we multiply two matrices, we need a temporary area to store the result. If we try to write the result directly, the results are undefined!
We first create a temporary floating-point array to store the temporary result; then we call multiplyMM() to multiply the projection matrix and model matrix together into this
temporary array. Next we call System.arraycopy() to store the result back into projectionMatrix, which now contains the combined effects of the model matrix and the
projection matrix.
Pushing the air hockey table into the distance brought it into our frustum, but the table is still standing upright. After a quick recap, we’ll learn how to rotate the table so that
we see it from an angle rather than upright.
6.8 Adding Rotation
1.The Direction of Rotation
To figure out how an object would rotate around a given axis, we’ll use the right-hand rule.Try this out with the x-, y-, and z-axes. If we rotate around the y-axis, our
table will spin horizontally around its top and bottom ends. If we rotate around the z-axis, the table will spin around in a circle. What we want to do is rotate the table
backward around the x-axis, as this will bring the table more level with our eyes.
2.Rotation Matrices
To do the actual rotation, we’ll use a rotation matrix. Matrix rotation uses the trigonometric functions of sine and cosine to convert the rotation angle into scaling factors.
The following is the matrix definition for a rotation around the x-axis:
Then you have a matrix for a rotation around the y-axis:
Finally, there’s also one for a rotation around the z-axis:
3.Adding the Rotation to Our Code
We’re now ready to add the rotation to our code. Go back to onSurfaceChanged(), and adjust the translation and add a rotation as follows:
//AirHockey3D/src/com/airhockey/android/AirHockeyRenderer.java
translateM(modelMatrix, 0, 0f, 0f, -2.5f);
rotateM(modelMatrix, 0, -60f, 1f, 0f, 0f);
We push the table a little farther, because once we rotate it the bottom end will be closer to us. We then rotate it by -60 degrees around the x-axis, which brings the table at
a nice angle, as if we were standing in front of it. The table should now look like the following: