Translation

The simplest space transformation operation is translation. Indeed, we have not only seen this transform before, it has been used in all of the tutorials with a perspective projection. Recall this line from the vertex shaders:

vec4 cameraPos = position + vec4(offset.x, offset.y, 0.0, 0.0);

This is a translation transformation: it is used to position the origin point of the initial space relative to the destination space. Since all of the coordinates in a space are relative to the origin point of that space, all a translation needs to do is add a vector to all of the coordinates in that space. The vector added to these values is the location of where the user wants the origin point relative to the destination coordinate system.

Figure 6.2. Coordinate System Translation in 2D

Coordinate System Translation in 2D

Here is a more concrete example. Let us say that an object which in its model space is near its origin. This means that, if we want to see that object in front of the camera, we must position the origin of the model in front of the camera. If the extent of the model is only [-1, 1] in model space, we can ensure that the object is visible by adding this vector to all of the model space coordinates: (0, 0, -3). This puts the origin of the model at that position in camera space.

Translation is ultimately just that simple. So let's make it needlessly complex. And the best tool for doing that: matrices. Oh, we could just use a 3D uniform vector to pass an offset to do the transformation. But matrices have hidden benefits we will explore very soon.

All of our position vectors are 4D vectors, with a final W coordinate that is always 1.0. In Tutorial 04, we took advantage of this with our perspective transformation matrix. The equation for the Z coordinate needed an additive term, so we put that term in the W column of the transformation matrix. Matrix multiplication causes the value in the W column to be multiplied by the W coordinate of the vector (which is 1) and added to the sum of the other terms.

But how do we keep the matrix from doing something to the other terms? We only want this matrix to apply an offset to the position. We do not want to have it modify the position in some other way.

This is done by modifying an identity matrix. An identity matrix is a matrix that, when performing matrix multiplication, will return the matrix (or vector) it was multiplied with. It is sort of like the number 1 with regular multiplication: 1*X = X. The 4x4 identity matrix looks like this:

Equation 6.2. Identity Matrix

1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1

To modify the identity matrix into one that is suitable for translation, we simply put the offset into the W column of the identity matrix.

Equation 6.3. Translation Matrix

Translation = x y z 1 0 0 x 0 1 0 y 0 0 1 z 0 0 0 1

The tutorial project cleverly titled Translation performs translation operations.

This tutorial renders 3 of the same object, all in different positions. One of the objects is positioned in the center of the screen, and the other two's positions orbit it at various speeds.

Because of the prevalence of matrix math, this is the first tutorial that uses the GLM math library. So let's take a look at the shader program initialization code to see it in action.

Example 6.1. Translation Shader Initialization

void InitializeProgram()
{
    std::vector<GLuint> shaderList;
    
    shaderList.push_back(Framework::LoadShader(GL_VERTEX_SHADER,
        "PosColorLocalTransform.vert"));
    shaderList.push_back(Framework::LoadShader(GL_FRAGMENT_SHADER,
        "ColorPassthrough.frag"));
    
    theProgram = Framework::CreateProgram(shaderList);
    
    positionAttrib = glGetAttribLocation(theProgram, "position");
    colorAttrib = glGetAttribLocation(theProgram, "color");
    
    modelToCameraMatrixUnif = glGetUniformLocation(theProgram,
        "modelToCameraMatrix");
    cameraToClipMatrixUnif = glGetUniformLocation(theProgram,
        "cameraToClipMatrix");
    
    float fzNear = 1.0f; float fzFar = 45.0f;
    
    cameraToClipMatrix[0].x = fFrustumScale;
    cameraToClipMatrix[1].y = fFrustumScale;
    cameraToClipMatrix[2].z = (fzFar + fzNear) / (fzNear - fzFar);
    cameraToClipMatrix[2].w = -1.0f;
    cameraToClipMatrix[3].z = (2 * fzFar * fzNear) / (fzNear - fzFar);
    
    glUseProgram(theProgram);
    glUniformMatrix4fv(cameraToClipMatrixUnif, 1, GL_FALSE,
        glm::value_ptr(cameraToClipMatrix));
    glUseProgram(0);
}

GLM takes a unique approach for a vector/matrix math library. It attempts to emulate GLSL's approach to vector operations where possible. It uses C++ operator overloading to effectively emulate GLSL. In many cases, GLM-based expressions would compile in GLSL.

The matrix cameraToClipMatrix is defined as a glm::mat4, which has the same properties as a GLSL mat4. Array indexing of a mat4, whether GLM or GLSL, returns the zero-based column of the matrix as a vec4.

The glm::value_ptr function is used to get a direct pointer to the matrix data, in column-major order. This is useful for uploading data to OpenGL, as shown with the call to glUniformMatrix4fv.

With the exception of getting a second uniform location (for our model transformation matrix), this code functions exactly as it did in previous tutorials.

There is one important note: fFrustumScale is not 1.0 anymore. Until now, the relative sizes of objects were not particularly meaningful. Now that we are starting to deal with more complex objects that have a particular scale, picking a proper field of view for the perspective projection is very important.

The new fFrustumScale is computed with this code:

Example 6.2. Frustum Scale Computation

float CalcFrustumScale(float fFovDeg)
{
    const float degToRad = 3.14159f * 2.0f / 360.0f;
    float fFovRad = fFovDeg * degToRad;
    return 1.0f / tan(fFovRad / 2.0f);
}

const float fFrustumScale = CalcFrustumScale(45.0f);

The function CalcFrustumScale computes the frustum scale based on a field-of-view angle in degrees. The field of view in this case is the angle between the forward direction and the direction of the farmost-extent of the view.

This project, and many of the others in this tutorial, uses a fairly complex bit of code to manage the transform matrices for the various object instances. There is an Instance object for each actual object; it has a function pointer that is used to compute the object's offset position. The Instance object then takes that position and computes a transformation matrix, based on the current elapsed time, with this function:

Example 6.3. Translation Matrix Generation

glm::mat4 ConstructMatrix(float fElapsedTime)
{
    glm::mat4 theMat(1.0f);
    
    theMat[3] = glm::vec4(CalcOffset(fElapsedTime), 1.0f);
    
    return theMat;
}

The glm::mat4 constructor that takes only a single value constructs what is known as a diagonal matrix. That is a matrix with all zeros except for along the diagonal from the upper-left to the lower-right. The values along that diagonal will be the value passed to the constructor. An identity matrix is just a diagonal matrix with 1 as the value along the diagonal.

This function simply replaces the W column of that identity matrix with the offset value.

This all produces the following:

Figure 6.3. Translation Project

Translation Project

Fork me on GitHub