Starting from:
$25

$19

Assignment 2 - Hierarchical Transforms

Goal
Learn and apply hierarchical 3D transformations using the matrix stack.
Associated Labs
    • Lab 0 (required): Setting Up Your Development Environment 
    • Lab 3 (optional, tedious!): Transformation Matrices 
    • Lab 4 (recommended): Matrix Stack 
Overview
Write a program that allows you to create a robot that you can manipulate with the keyboard. You are free to create your own robotic character, but there should be at least 10 components in the robot, and the hierarchy should not be flat. For example, in the figures below, we have the following hierarchy:
    • Torso 
        ◦ Head 
        ◦ Upper left arm 
            ▪ Lower left arm 
        ◦ Upper right arm 
            ▪ Lower right arm 
        ◦ Upper left leg 
            ▪ Lower left leg 
        ◦ Upper right leg 
            ▪ Lower right leg 
 
The exact size, placement, and rotation of these components are up to you. These hard-coded values should be used in the init() function, not the render() function.
Task 1
Start from your Lab 0 or Lab 4 code base.
    1. Create your A2 project folder and copy the the lab files and folders into it. 
    2. Modify CMakeLists.txt to change the project name (line 4). 
    3. Add GLM calls so that you can draw transformed squares. There are two choices: 
        a. If you’re starting with Lab 4, then you should already have this done. 
        b. If you’re starting with Lab 0, then replace the teapot with a cube and then try transforming it. 
            ▪ The benefit of starting with Lab 0 is that it contains the helper classes (e.g., Shape, Program, etc.) to help you organize your code. You will be required to do this for later assignments. 
    4. Add support for keyboard input (x/X, y/Y, z/Z) by using the glfwSetCharCallback() function. 
Task 2 (optional)
You may skip ahead to Step 3 if you understand how to draw the components recursively. In this step, we are going to draw just the torso and the head without recursion so that you first understand how the transforms chain together.
In your render() function, first draw the torso and the head without any hierarchy:
prog->bind();
glUniformMatrix4fv(prog->getUniform("P"), 1, GL_FALSE, value_ptr(P));
// Draw the torso
MV->pushMatrix();
    MV->translate(...); // Where is the torso with respect to the world?
    MV->rotate(...); // This rotation applies only to the torso
    MV->scale(...); // This scale applies only to the torso
    glUniformMatrix4fv(prog->getUniform("MV"), 1, GL_FALSE, value_ptr(MV));
    shape->draw(prog);
MV->popMatrix();
// Draw the head
MV->pushMatrix();
    MV->translate(...); // Where is the head with respect to the world?
    MV->rotate(...); // This rotation applies only to the head
    MV->scale(...); // This scale applies only to the head
    glUniformMatrix4fv(prog->getUniform("MV"), 1, GL_FALSE, value_ptr(MV));
    shape->draw(prog);
MV->popMatrix();
prog->unbind();
Note: In this pseudocode, I’m assuming that P and MV are matrices. In your code, these may instead be pointers to MatrixStack, in which case you need to call glm::value_ptr(P->topMatrix()). (Don’t forget #include <glm/gtc/type_ptr.hpp>.) Alternatively, you can write &P->topMatrix()[0][0].
The indentation between push and pop helps with clarity but is not necessary. The first call to glUniformMatrix4fv() sends the projection matrix to the GPU. Then, we modify the modelview matrix, send it to the GPU, and then draw the shape. With this naive version, changing the position or the rotation of the torso does not modify the head.
To fix this, we now add some pushes and pops. Note that when we rotate the torso, we want the head to also rotate, but when we change the scale of the torso, we do not want to change the scale of the head. Therefore, we use an extra push/pop around the torso scale:
...
// Draw torso
MV->pushMatrix();
    MV->translate(...); // Where is the torso's joint with respect to the world?
    MV->rotate(...); // This rotation applies to torso and its children
    MV->pushMatrix();
        MV->translate(0, 0, 0) // Where is the torso's mesh with respect to the torso's joint?
        MV->scale(...);
        glUniformMatrix4fv(prog->getUniform("MV"), 1, GL_FALSE, value_ptr(MV));
        shape->draw(prog);
    MV->popMatrix();
    // Draw head
    MV->pushMatrix();
        MV->translate(...); // Where is the head's joint with respect to the torso's joint?
        MV->rotate(...); // This rotation applies to head and its children
        MV->pushMatrix();
            MV->translate(...) // Where is the head's mesh with respect to the head's joint?
            MV->scale(...);
            glUniformMatrix4fv(prog->getUniform("MV"), 1, GL_FALSE, value_ptr(MV));
            shape->draw(prog);
        MV->popMatrix();
    MV->popMatrix();
MV->popMatrix();
...
With the code above, translating and rotating the torso should also translate and rotate the head. Note that with this hierarchical version, the numbers used for translation/rotation/scale of the head may be different than with the previous non-hierarchical version, since now we are defining the head with respect to the torso.
Task 3
Now we are going to create a general, hierarchical structure for drawing a robot with multiple limbs.
Create a class that represents a component. This class should contain the necessary member variables so that you can make a tree data structure out of these components. The root of the tree should represent the torso, which means that transforming the torso transforms everything else.
In addition to the member variables required for the tree hierarchy, the class should also have the following:
    • A glm::vec3 representing the translation of the component’s joint with respect to the parent component’s joint. 
    • A glm::vec3 representing the current joint angles about the X, Y, and Z axes of the component’s joint. (You may want to start with Z-rotations only.) 
    • A glm::vec3 representing the translation of the component’s mesh with respect to its joint. 
    • A glm::vec3 representing the X, Y, and Z scaling factors for the mesh. 
    • A member method for drawing itself and its children. 
    • Any additional variable(s) and method(s) you see fit. 
The drawing code should be recursive – in other words, in the render() function in main.cpp, there should be a single draw call on the root component, and all the other components should be drawn recursively from the root. In the main render() function, you should create an instance of the matrix stack class and pass it to the root component’s drawing function. Make sure to pass the matrix stack by reference or as a (smart) pointer.
The component’s rendering method should simply take the current state of the component and draw it. You should not create the robot hierarchy in this method. In other words, the scene setup must be done in main’s init() rather than in main’s render(). In your README, state where in your init() function (which line) should be modified to change the joint angles.
For this assignment, the 3D rotation of the joint should be represented simply as a concatenation of three separate rotation matrices about the x-, y-, and z-axes: Rx * Ry * Rz. The position of the joint should not be at the center of the box. For example, the elbow joint should be positioned between the upper and lower arms.
Task 4
Add the functionality to select components and rotate the joints with the keyboard. When the appropriate key is pressed, the currently selected component, along with all of its descendants, should be rotated about the joint. For example, if the upper right arm is rotated, the lower right arm should rotate with it. The keyboard control should be as follows:
    • . (period): traverse the hierarchy forward 
    • , (comma): traverse the hierarchy backward 
    • x/X: increment/decrement x angle 
    • y/Y: increment/decrement y angle 
    • z/Z: increment/decrement z angle 
By pressing the period and comma keys, you should be able to select different components in the hierarchy. You must draw the selected component so that it is distinguishable from unselected components. The x/X, y/Y, and z/Z keys should change the rotation angle of the selected component. In the left figure above, the torso is the selected component, and in the right figure, one of the lower legs is the selected component.
The traversal of the tree with the period and comma keys should be in depth-first or breadth-first order. Do not hardcode this traversal order – your code should be set up so that it works with any tree.
When drawing the selected component, change its size using the time variable. In the render() function, use the following GLFW call:
double t = glfwGetTime();
This t variable should then be used to change the scale as follows:
where 
is the amplitude, is the frequency, is the time, and is the resulting scale. The following values work well: , and 
. In other words, the scale increases by 5% twice a second. Here is a plot of this function.

Here is a working example:

Task 5
Put a cube at each joint. The cube should be placed exactly where the joint is, so that when the joint is rotated, the cube does not translate. These cubes should not be in the hierarchy. Instead, they are simply extra things to be drawn while traversing the hierarchy.
For bonus, instead of drawing a cube, draw a sphere.

Task 6
Rotate at least two components in place. This rotation should not be propagated to its children. At least one of the two should be a non-leaf component.

HINT: Debugging OpenGL & GLSL
    • Set the Program class to be verbose by calling the setVerbose() function. If there is a GLSL compilation error, then you will see the error in the console. For example, if the varying variables of the vertex shader and the fragment shaders do not match up, it will tell you so. Make sure to set verbose to be false after debugging.
    • Use GLSL::checkError(GET_FILE_LINE); to find which OpenGL call caused an error. This function will assert if there were any OpenGL errors before getting to this line. You can use this to winnow down which OpenGL function is causing an error. For example, if you put this line at the top, the middle, and the bottom of your function, and if the assertion happens in the middle, you know that the error must be happening in the top half of your function. Once find exactly which OpenGL call is causing the error, you can Google the OpenGL function to figure out what caused the error. For example, maybe one of the arguments should not have been zero or null.
    • The GLSL compiler will silently optimize away any variables that are not used in the shader. If you try to access these variables at runtime, the program will crash, since these variables no longer exist in the shader. In this lab, when you move the computation of the normal to the GPU, the aNor variable no longer needs to be passed to the GPU, since it is computed in the shader. Therefore, you will have to comment out any reference to aNor from your C++ runtime code. Or, you can trick the GLSL compiler from optimizing away aNor by using it and disgarding it as follows:
  vec3 nor = aNor.xyz;
  nor.x = ...;
  nor.y = ...;
  nor.z = ...;
Point breakdown
Task 2 is optional. If you have completed Task 3, you will get full points for Task 2.
    • 10 points for Task 2a: drawing the torso and head without hierarchy. 
        ◦ Specify in the README where (which line in the code) to change the rotation angles. 
    • 15 points for Task 2b: drawing the torso and head with hierarchy. 
        ◦ Specify in the README where (which line in the code) to change the rotation angles. 
    • 20 points for Task 3: a functioning hierarchical robot with hard coded angles. 
        ◦ Specify in the README where (which line in the code) to change the rotation angles. 
    • 20 points for Task 4: being able to select different components with the keyboard and for showing the current selection with a different size. 
        ◦ Once this task is implemented, you can remove the note in the README where you specify where to change the joint angles. 
    • 15 points for Task 5: cubes at the joints. 
    • 15 points for Task 6: for animation. 
    • 5 points for coding style and general execution. 
        ◦ Must use recursive, object-oriented design. 
        ◦ Hard-coded values must be set in init(), not render(). 
    • 5 bonus points for drawing spheres at the joints. 
Total: 100 + 5 points
What to hand in
Failing to follow these points may decrease your “general execution” score. On Linux/Mac, make sure that your code compiles and runs by typing:
> mkdir build
> cd build
> cmake ..
> make
> ./A2 ../resources
If you’re on Windows, make sure that you can build your code using the same procedure as in Lab 0.
    • Make sure the arguments are exactly as specified. 
    • Include a README file (ascii only) that includes: 
        ◦ Your name, UID, and email 
        ◦ The highest task you’ve completed 
        ◦ Citations for any downloaded code 
        ◦ Plus anything else of note 
    • Remove unnecessary debug printouts. 
    • Remove unnecessary debug code that has been commented out. 
    • Hand in src/, resources/, CMakeLists.txt, and your readme file. The resources folder should contain the obj files and the glsl files. 
    • Do not hand in: 
        ◦ The build directory 
        ◦ The executable 
        ◦ Old save files (*.~) 
        ◦ Object files (*.o) 
        ◦ Visual Studio files (.vs) 
        ◦ Git folder (.git) 
    • Create a single zip file of all the required files. 
        ◦ The filename of this zip file should be UIN.zip (e.g., 12345678.zip). 
        ◦ The zip file should extract a single top-level folder named UIN/ (e.g. 12345678/). 
        ◦ This top-level folder should contain your README, src/, CMakeLists.txt, etc. 
        ◦ Use the standard .zip format (not .gz, .7z, .rar, etc.). 

More products