We currently have an aspect ratio problem with our air hockey table. Our table is squashed in landscape mode!
In this chapter, we’re going to learn why our table appears squashed and how we can use a projection to fix the problem. Here’s our game plan:
• First we’ll review some basic linear algebra and learn how to multiply a matrix and a vector together.
• Then we’ll learn how to define and use a projection with a matrix, which will let us compensate for the screen’s orientation so that our table doesn’t appear squashed.
5.1 We Have an Aspect Ratio Problem
Coordinates in this range are known as normalized device coordinates and are independent of the actual size or shape of the screen. Because they are independent of the
actual screen dimensions, we can run into problems if we use them directly, such as a squashed table in landscape mode.
5.2 Working with a Virtual Coordinate Space
To adjust the coordinate space so that we can take screen orientation into account, we need to stop working directly in normalized device coordinates and start working in a
virtual coordinate space. We then need to find some way of converting coordinates from our virtual space back into normalized device coordinates so that OpenGL can render
them correctly.
5.3 Linear Algebra 101
1.Vector
A vector is a one-dimensional array of elements. In OpenGL, a position is usually a four-element vector, as is a color.
2.Matrices
A matrix is a two-dimensional array of elements. In OpenGL, we generally use matrices to project vectors using an orthographic or perspective projection, and we can also
use them to do rotations, translations, and scaling of an object.
3.Matrix-Vector Multiplication
The following is an example of a complete matrix-vector multiply:
4.The Identity Matrix
An identity matrix looks like the following:
5.Translations Using a Matrix
let’s look at a very simple type of matrix that gets used quite often in OpenGL: the translation matrix.With this type of matrix, we can move one of our objects along a
distance that we specify. This matrix looks just like an identity matrix, with three additional elements specified on the right-hand side:
Let’s look at an example with a position of (2, 2), with a default z of 0 and a default w of 1. We want to translate the vector by 3 along the x-axis and 3 along the y-axis, so
we’ll put 3 for xtranslation and 3 for ytranslation. Here’s the result:
=
The position is now at (5, 5), which is what we expected.
We’ve learned just enough about vector and matrix math to get us on our feet; let’s go ahead and learn how to define an orthographic projection.
5.4 Defining an Orthographic Projection
To define an orthographic projection, we’ll use Android’s Matrix class, which resides in the android.opengl package. In that class there’s a method called orthoM(), which
will generate an orthographic projection for us. We’ll use that projection to adjust the coordinate space,and as we’ll soon see, an orthographic projection is very similar to
a translation matrix.
method : orthoM(float[] m, int mOffset, float left, float right, float bottom, float top, float near, float far). When we call this method, it should produce the following orthographic
projection matrix:
5.5 Adding an Orthographic Projection
1.Updating the Shader
The first thing we need to do is update the shader so that it uses our matrix to transform our positions.
//AirHockeyOrtho/res/raw/simple_vertex_shader.glsl
uniform mat4 u_Matrix;
attribute vec4 a_Position;
attribute vec4 a_Color;
varying vec4 v_Color;
void main()
{
v_Color = a_Color;
gl_Position = u_Matrix * a_Position;
gl_PointSize = 10.0;
}
We’ve added a new uniform definition, u_Matrix, and we’ve defined it as a mat4, meaning that this uniform will represent a 4 x 4 matrix. We’ve also updated the line that
assigns the position as follows: gl_Position = u_Matrix * a_Position;
The matrix will transform the coordinates from this virtual coordinate space back into normalized device coordinates.
2.Adding the Matrix Array and a New Uniform
//AirHockeyOrtho/src/com/airhockey/android/AirHockeyRenderer.java
private static final String U_MATRIX = "u_Matrix";
This holds the name of the new uniform that we defined in our vertex shader.
We’ll also need a floating point array to store the matrix:
//AirHockeyOrtho/src/com/airhockey/android/AirHockeyRenderer.java
private final float[] projectionMatrix = new float[16];
We’ll also need an integer to hold the location of the matrix uniform:
//AirHockeyOrtho/src/com/airhockey/android/AirHockeyRenderer.java
private int uMatrixLocation;
Then we just need to add the following to onSurfaceCreated():
//AirHockeyOrtho/src/com/airhockey/android/AirHockeyRenderer.java
uMatrixLocation = glGetUniformLocation(program, U_MATRIX);
3.Creating the Orthographic Projection Matrix
The next step will be to update onSurfaceChanged(). Add the following lines after the call to glViewport():
//AirHockeyOrtho/src/com/airhockey/android/AirHockeyRenderer.java
final float aspectRatio = width > height ?
(float) width / (float) height :
(float) height / (float) width;
if (width > height) {
// Landscape
orthoM(projectionMatrix, 0, -aspectRatio, aspectRatio, -1f, 1f, -1f, 1f);
} else {
// Portrait or square
orthoM(projectionMatrix, 0, -1f, 1f, -aspectRatio, aspectRatio, -1f, 1f);
}
This code will create an orthographic projection matrix that will take the screen’s current orientation into account. It will set up a virtual coordinate space the way we
describedThere’s more than one Matrix class in Android, so you’ll want to make sure that you’re importing android.opengl.Matrix.First we calculate the aspect ratio by
taking the greater of the width and height and dividing it by the smaller of the width and height. This value will be the same regardless of whether we’re in portrait or
landscape.
We then call orthoM(float[] m, int mOffset, float left, float right, float bottom, float top, float near, float far). If we’re in landscape mode, we’ll expand the coordinate space of
the width so that instead of ranging from -1 to 1, the width will range from-aspectRatio to aspectRatio. The height will stay from -1 to 1. If we’re in portrait
mode, we expand the height instead and keep the width at -1 to 1.
4.Sending the Matrix to the Shader
The last change to do in AirHockeyRenderer is to send the orthographic projection
matrix to the shader. We do that by adding the following line of code to
onDrawFrame(), just after the call to glClear():
//AirHockeyOrtho/src/com/airhockey/android/AirHockeyRenderer.java
glUniformMatrix4fv(uMatrixLocation, 1, false, projectionMatrix, 0);