The OpenGL Tutorial — Part VI
Entering the Third Dimension
Introduction
Until now, it was all about 3D. This has changed, and we are using the third dimension. Again, small refactorings are needed while we add functions to support 3D. I will only go so deep into the mathematics behind it. If you need a refresher, since it’s too long since you heard about linear algebra last time, there are plenty of good resources. Instead, I have added all the mathematical functions we need in RBMath.cpp so we can focus on the usage. But now, let’s start!
Entering the Third Dimension
Shader changes
Let me start again with the shader code. I have added only a minimal change. There is an additional matrix called the model matrix. Simply put, this is the transform matrix containing the translation, rotation, and scale of an object we want to draw. It would also be possible to do that in code and pass the projection matrix as before. But we save some calls like this since the transform changes in every frame while the projection matrix remains the same.
// Vertex shader
attribute vec4 vertexPosition;
uniform mat4 projectionMatrix;
uniform mat4 modelMatrix;
uniform vec4 vertexColor;
varying vec4 v_color;
void main() {
v_color = vertexColor;
gl_Position = vertexPosition * modelMatrix * projectionMatrix;
}
You will see below how this will be used.
Draw 3D objects — Let’s start with a Cube
I’ve added a function to RBRenderHelper.cpp that is super easy to use:
static void DrawCube(RBVec3D position,
RBVec3D rotation,
RBVec3D scale,
RBColor color);
We now use RBVec3D instead of the two-dimensional vector RBVec2D, which supports an x, y, and z coordinate.
The rest happens like magic behind the scenes. But let’s dive deeper to see how this function is implemented.
// Vertices of a cube
static const GLfloat verticesCube[] = {
-0.5f, 0.5f, 0.5f, // 0: Front-top-left (used)
0.5f, 0.5f, 0.5f, // 1: Front-top-right (used)
-0.5f, -0.5f, 0.5f, // 2: Front-bottom-left (used)
0.5f, -0.5f, 0.5f, // 3: Front-bottom-right (used)
0.5f, -0.5f, -0.5f, // 4: Back-bottom-right
0.5f, 0.5f, 0.5f, // 5: Front-top-right
0.5f, 0.5f, -0.5f, // 6: Back-top-right
-0.5f, 0.5f, 0.5f, // 7: Front-top-left
-0.5f, 0.5f, -0.5f, // 8: Back-top-left
-0.5f, -0.5f, 0.5f, // 9: Front-bottom-left
-0.5f, -0.5f, -0.5f, // 10: Back-bottom-left (used)
0.5f, -0.5f, -0.5f, // 11: Back-bottom-right (used)
-0.5f, 0.5f, -0.5f, // 12: Back-top-left (used)
0.5f, 0.5f, -0.5f // 13: Back-top-right (used)
};
This is called a vertex list, an array of x,y, and z coordinates. The second we need is an index list that describes how and in which order the vertices are connected.
static const GLubyte indicesCube[] = {
2, 3, 3, 1, 1, 0, 0, 2,
10,11,11,13,13,12,12,10,
0,12, 2,10, 1,13, 3,11
};
As you see in the index list, we use vertices 0–4 and 10–13. The remaining vertices are not used now but become handy later when we don’t just use wireframe graphics. In the end, this is the advantage of an index list. I can always use the same vertex data but build different meshes out of this using a different index list. Does that make sense?
That’s almost all we need to draw the cube, which happens in RBShader.cpp
glVertexAttribPointer(m_gl_position, 3, GL_FLOAT, GL_FALSE, 0, vertices); // 1
glEnableVertexAttribArray(m_gl_position); // 2
glDrawElements(GL_LINES, count, GL_UNSIGNED_BYTE, indices); // 3
- First, map the vertex array to the uniform in the shader. Instead of one coordinate, we pass in many, and the shader does the rest with the help of a GPU.
- Then, we enable the vertex array.
- Draw the list of vertices by using the index list.
That’s all needed to draw a cube. But like this, every cube would be in the same position. That’s not what we need. We want to position, rotate, and scale every cube individually. So, we have to set the model matrix before each call of DrawElements().
This is also done in DrawCube():
RBMat4x4 transform = RBMatrixMakeIdentity(); // 1
RBMat4x4 scaling = RBMatrixMakeScale(scale.x, scale.y, scale.z);
RBMat4x4 rotationX = RBMatrixMakeRotationX(RAD_TO_DEG(rotation.x));
RBMat4x4 rotationY = RBMatrixMakeRotationY(RAD_TO_DEG(rotation.y));
RBMat4x4 rotationZ = RBMatrixMakeRotationZ(RAD_TO_DEG(rotation.z));
RBMat4x4 translation = RBMatrixMakeTranslation(position.x, position.y, position.z);
transform = RBMatrixMultiplyMatrix(transform, scaling); // 2
transform = RBMatrixMultiplyMatrix(transform, rotationX); // 3.1
transform = RBMatrixMultiplyMatrix(transform, rotationY); // 3.2
transform = RBMatrixMultiplyMatrix(transform, rotationZ); // 3.3
transform = RBMatrixMultiplyMatrix(transform, translation); // 4
As mentioned earlier, you don’t have to understand the mathematics behind it; you can use these functions if you like. Noteworthy is the order in which the calculation is done:
- Start with an identity matrix.
- Scale it by multiplying it with the identity matrix.
- Rotate it by multiplying it with the x, y, and z rotation matrix.
- Translate it by multiplying it with the translation matrix (position)
- Pass the resulting matrix (model matrix) to the shader.
s_renderer->GetShader()->MapModelMatrix(transform); // 5
s_renderer->GetShader()->DrawElements(&verticesCube[0],
&indicesCube[0],
sizeof(indicesCube)/sizeof(GLubyte));
Tip: You can even move the objects by touching the screen of your Android phone
The Mathematics behind it
Linear algebra is a branch of mathematics focusing on vector spaces and the linear mappings between these spaces. It’s fundamental in computer graphics and more. At its core, linear algebra deals with objects like:
- Vectors: These are quantities that have both magnitude and direction, and they can exist in 2D, 3D, or higher-dimensional spaces.
- Matrices: These are arrays of numbers organized into rows and columns, representing linear transformations or systems of equations.
- Linear transformations: These are operations that move or transform vectors while preserving their straightness and proportion. Examples include rotation, scaling, and translation.
There is already much information and tutorials about linear algebra, from short videos to entire books. One of my favored videos that summarizes the essentials you find here on YouTube
And as always, find the code here on GitHub.
Stay tuned for the next chapter when we use today’s content to write a real 3D game!
Check out my newsletter, ‘The Spatial Projects’ on Substack, where I write about Spatial Computing on Apple’s Vision Pro.