$19
Goals
To become proficient with the use of 3D transforms and projections in OpenGL and to continue working with Blinn-Phong shading. Specifically, you will:
• Create a 3D world you can walk through, with multiple transformed objects.
• Implement freelook for camera motion.
• Create a HUD (heads up display).
• Show a top-down view of the world showing the camera and the frustum.
• Use a textured material with Blinn-Phong lighting.
Associated Labs
• Lab 0 (required): Setting Up Your Development Environment
• Lab 8 (L08’s Task
is recommended for A4’s Task
• ): Texture mapping
• Lab 9 (L09’s Task is recommended for A4’s Task
• ): Texture mapping and lighting
Task 1: Setting up the World
There is no base code provided for this assignment. Please start with your previous lab/assignment code. Please come see the instructor or the TA if you need help completing your previous assignment.
Write some code so that you can add multiple objects to the world. Each object should have some member variables so that you can easily translate/rotate/scale/shear the object in the world. (Remember that to transform the normals properly, you’ll need to send the inverse transpose matrix as a parameter to the shader if you include shear or non-uniform scales.) For this initial stage, since we’re using the default camera from previous assignments/labs, the objects that you add to the world may have to be scaled small so that they’re visible from the camera. Once you implement freelook later in this assignment, you may need to re-transform these objects to distribute them in the world the way you want.
You should include at least two types of shapes. For example, here I’m using the bunny and the teapot shapes. You can use any model, but please try to keep the file sizes reasonable. If you download a model, please put a citation in your readme. Note that some OBJ files may not come with normals or texture coordinates.
You should add at least 100 things in the scene and a ground plane. These objects should be distributed (roughly) equally on the ground plane. (E.g., do not put them in a straight line.) It is important to load each OBJ file just once, even if you are going to draw that shape multiple times. One good way to do this is to create a new class that has a pointer/reference to a Shape instance. Each object must be translated so that the bottom of the object touches the ground. To do this, you will need the most negative Y coordinate of the mesh.
Use your Blinn-Phong shader from your previous assignment to shade the models. The objects should be assigned random colors. Unlike the last assignment, the light position should be fixed in the world. This means that the light position is no longer a constant in camera space. You need to choose a world space position for the light and then multiply this position by the view matrix (modelview matrix before adding any modeling transforms) to transform the light position into camera space. The camera-space light position should then be sent to the fragment shader as a uniform variable.
Finally, draw a sphere (the sun) where the light is. The lighting on the objects in the scene should match this location of the light source. In the image below, note how the left side of the green bunny is correctly lit by the light in the upper left corner of the image.
The shader for drawing the light can be the same Blinn-Phong shader as for the other models. You can set the ambient color to be the color of the sun, and diffuse and specular colors to be zero.
Task 2: Freelook Camera
Now replace the Camera class with your own class that implements freelook. You can reuse the applyProjectionMatrix() method, but you’ll need to modify the applyViewMatrix() method. To implement freelook, the new camera class needs to keep track of its position, yaw, and pitch. From these three quantities, you need to come up with the correct arguments for the glm::lookAt() function.
The general setup of your code should be as follows:
• When a key is pressed or the mouse is moved, the camera’s translation, yaw, and pitch should be updated. For event handling, use your previous labs and assignments, as well as the GLFW Input guide.
• In the render() method in main.cpp, the first matrix in the modelview matrix stack should be filled by the applyViewMatrix() method of your new freelook camera class. In the applyViewMatrix() method, you should use the glm::lookAt() method to create this view matrix.
The eye, target, and up arguments of the lookAt() function are:
• eye: camera position
• target: camera position + “forward” direction
• up: the Y vector,
• , (assuming Y-up)
Position
I suggest fixing yaw and pitch first so that you can get basic translation correct with no rotation. The initial position of the camera should incorporate the height of the camera off of the ground.
Add keyboard hooks for WASD for translation:
• w: move forward
• a: move left
• s: move backward
• d: move right
Pressing these keys should update the translation of the camera.
For now, the “forward” direction can be set to the negative Z direction, which is the default camera direction in OpenGL. When the w key is pressed, the camera should move along this “forward” direction. In the applyViewMatrix() method of your camera class, feed this new translation value into the lookAt() function.
Yaw
Now add yaw to your freelook camera, which allows you to look right and left. The X-motion of the mouse should be tied to the yaw angle of the camera. Look at the previous camera class from the labs to see how mouse inputs are handled.
The “forward” direction should be computed based on the yaw angle. If the ground is on the
plane, then the forward direction is
where
is the yaw angle. The w key should now move the camera along this new “forward” direction, rather than the negative Z direction. The “left” direction (or the “right” direction) can be computed using the cross product: “right” = “forward” cross “up”. If this is working properly, pressing the w key should move the camera forward, and pressing the d key should move the camera to the right, no matter which way it is facing with respect to the world.
Pitch
Finally, add the pitch angle, which allows you to look up and down. The Y-motion of the mouse should be tied to the pitch angle of the camera. Unlike the yaw angle, the pitch angle should be capped at some reasonable limits (e.g., -60 to +60 degrees). The pitch angle should change the “target” argument of the lookAt() method, since it changes the “forward” direction that the camera is looking at. In other words, when computing the “target” argument, the “forward” vector should have a Y component that depends on the pitch angle. However, the pitch angle should not change the direction of motion. Even if the camera is looking up or down, pressing w should not change the height of the camera. (The camera should not lift off of or go into the ground.)
Zoom
Add zooming functionality to the camera using the z/Z keys (zoom in and zoom out). Pressing these keys should change the field of view in Y (FOVY) of the camera. The field of view should be capped between
degrees and degrees. These correspond to roughly mm and
mm lenses for full-frame cameras.
Depending on which skeleton code you start from, the z key may already be mapped to enable wireframe display mode. If so, delete or comment out the code to change the glPolygonMode(...) in the render function.
Task 3: Transform Objects with Time
Transform the objects over time using the glfwGetTime() function. (See the image below.)
• Apply a scale using sine/cosine so that the objects grow and shrink over time.
• Translate the objects appropriately so that the object is just touching the ground plane at all times.
Task 4: Heads Up Display
Add a head-up display (HUD) that shows objects (e.g., bunnies) on the two upper corners of the screen.
• These objects should rotate in place.
• These objects should stay in the upper corners when the window size is changed.
• Use the Blinn-Phong shader for the HUD objects, with the light defined in camera space.
Task 5: Top-Down View
Now we’re going to add a “top-down” orthographic view. As the camera moves around the world, this top-down view should show where the camera is and which direction it is pointing.
Add Another Viewport
First, add a second viewport to the lower left of the screen. This viewport should be activated/deactivated by pressing the t key. Within this viewport, draw the scene objects (models, ground, sun) again but with different projection and view matrices. The pseudocode for the render() function is as follows:
// Main viewport (your current code should be doing something like this already)
glViewport(0, 0, width, height);
P->pushMatrix();
MV->pushMatrix();
APPLY PROJECTION MATRIX FOR MAIN VIEWPORT
APPLY VIEW MATRIX FOR MAIN VIEWPORT
DRAW SCENE
MV->popMatrix();
P->popMatrix();
// Top-down viewport (new code for this task)
if top-down view activated
double s = 0.5;
glViewport(0, 0, s*width, s*height);
glEnable(GL_SCISSOR_TEST);
glScissor(0, 0, s*width, s*height);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glDisable(GL_SCISSOR_TEST);
P->pushMatrix();
MV->pushMatrix();
APPLY PROJECTION MATRIX FOR TOP-DOWN VIEWPORT
APPLY VIEW MATRIX FOR TOP-DOWN VIEWPORT
DRAW SCENE
MV->popMatrix();
P->popMatrix();
end
Some notes about this pseudocode:
• The width and height variables can be obtained with glfwGetFramebufferSize(window, &width, &height);.
• OpenGL’s “scissor” test is used to clear just a portion of the screen corresponding to the 2nd viewport.
• The lines “apply projection matrix” and “apply view matrix” for the main viewport correspond to the methods in your camera class from the previous tasks.
• The projection matrix for the top-down viewport should be constructed with a call to P->multMatrix(glm::ortho(...)) (see the documentation on the ortho function here).
• The view matrix for the top-down viewport should be constructed manually with MV->translate(...) and MV->rotate(...). By default, the camera is at the origin, looking down the negative Z-axis. For this viewport, you need the camera’s origin to be above the X-Z plane, looking down the negative Y-axis.
• Do not draw the HUD for the top-down viewport.
Draw the Frustum
Next, draw the view frustum in the top-down view. To create the mesh for the frustum, we’ll be using OpenSCAD. Download, install, and start the application, and then copy the following code into the editor.
s = 0.9;
difference() {
translate([0, 0, -1]) {
difference() {
rotate([0, 0, 45])
cylinder(h = 1, r1 = sqrt(2), r2 = 0, $fn = 4);
translate([0, 0, -0.5*(1 - s)])
rotate([0, 0, 45])
cylinder(h = 1, r1 = sqrt(2), r2 = 0, $fn = 4);
}
}
translate([0, 0, -s - 0.5*(1 - s)]) {
scale([2, s, s])
rotate([0, 0, 45])
cylinder(h = 1, r1 = sqrt(2), r2 = 0, $fn = 4);
scale([s, 2, s])
rotate([0, 0, 45])
cylinder(h = 1, r1 = sqrt(2), r2 = 0, $fn = 4);
}
}
Then follow these steps in OpenSCAD:
1. ‘View’ –> ‘View All’ (optional, to see the object better)
2. ‘Design’ –> ‘Render’
3. ‘File’ –> ‘Export’ –> ‘Export as STL…’
Open the STL file in MeshLab. Uncheck the option to ‘Unify Duplicated Vertices in STL files’ because we want to retain the hard corners. Then:
1. ‘File’ –> ‘Export Mesh As…’
2. Choose ’Alias Wavefront Object (*.obj)’
The left image below shows the exported mesh.
First, draw this mesh in the top-down viewport without applying any model transform to it. It should appear at the world origin, looking down the negative Z axis. Now, to account for the translation and the rotation of the camera, use the inverse of the view matrix (i.e., the camera matrix) as the model matrix when drawing the camera. This camera matrix should be multiplied onto the matrix stack before drawing the frustum. The following pseudocode should be added to the DRAW SCENE line of the top-down viewport portion of the first pseudocode of this task:
pushMatrix()
Camera matrix = inverse of the view matrix
multMatrix(Camera matrix)
Send the top matrix to the GPU
Draw the frustum
popMatrix()
The inverse of the view matrix can be obtained by inverting the output matrix of the glm::lookAt(...) function.
As shown in the right image above, the frustum’s extent is
to in Y (as well as X) and to in Z, which means that the shape of this frustum is correct only when the field of view is
degrees. To fix this, the following scales should be applied in X and Y:
where
is the aspect ratio of the frame buffer. The angle, , is the field of view of the main camera, which depends on the current zoom level. The scale values and
should be applied to the modelview matrix stack before drawing the frustum.
Optionally, to make visible the parts of the frustum below the ground, disable the depth test while drawing the frustum:
glDisable(GL_DEPTH_TEST);
Transform and draw the frustum
glEnable(GL_DEPTH_TEST);
Task 6: Texture
Add a texture to the ground. You can use the vertex buffer approach from Lab 8 or use square.obj from Lab 9. You can also download a mesh (and add a citation in your readme), but note that some OBJ files may not come with normals or texture coordinates.
Point breakdown
• 20 points for Task 1
◦ 10 points for populating the world with multiple objects (
types,
◦ total objects) and the ground. Objects must be properly grounded.
◦ 5 points for Blinn-Phong with world light position. Objects must have random colors.
◦ 5 points for drawing the sun.
• 30 points for Task 2
◦ 5 points for freelook translation.
◦ 10 points for freelook yaw.
◦ 10 points for freelook pitch. The camera must not go into or fly off of the ground.
◦ 5 points for freelook zoom.
• 10 points for Task 3: transforming objects with time. Objects must remain properly grounded.
• 10 points for Task 4: HUD.
• 20 points for Task 5
◦ 10 points for top-down viewport.
◦ 10 points for camera frustum.
• 5 points for Task 6: texturing the ground.
• 5 points for coding style and general execution (e.g., loading each OBJ only once, etc.).
Total: 100 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
> ./A4 ../resources
If you’re on Windows, make sure that you can build your code using the same procedure as in Lab 0.
For this assignment, there should be only one argument. You can hard code all your input files (e.g., obj files) in the resources directory.
• Make sure the arguments are exactly as specified.
• Include an ASCII README file that includes:
◦ Your name, UID, and email
◦ The highest task you’ve completed
◦ Citations for any downloaded code
◦ Plus anything else of note
• Make sure you don’t get any compiler warnings.
• 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.).