CPSC 424 | Computer Graphics | Fall 2025 |
This lab adds the viewing pipeline to the programmable pipeline structure from lab 2, and also introduces the indexed face representation for polygonal meshes.
Successful completion of this lab means that you:
This is an individual lab. You may get technical help from others, but the effort and ideas that go into producing solutions should be yours.
You may use AI as outlined in
the Use of AI policy —
Copilot's inline coding suggestions, explain, fix, review and
comment features but not code generation from English prompts.
Also:
Hand in your work by copying your ~/cs424/lab3 folder into your handin folder (/classes/cs424/handin/username, where username is your username).
Check that the result is that your files are contained in /classes/cs424/handin/username/lab3 — if not, fix it!
Copy the contents of /classes/cs424/lib into your ~/cs424/workspace/lib directory. Make sure that you end up (only) with a bunch of files inside ~/cs424/workspace/lib instead of multiple nested lib directories.
Copy the directory /classes/cs424/lab3 and its contents into your ~/cs424/workspace directory. You should end up with a folder lab3 inside your workspace directory, with files inside of it.
Make sure that your lab3 and lib directories are named exactly like that and are at the same level in your workspace directory. This is important so that the relative path names used to access common files remain the same so your program doesn't break when you hand it in.
The lib directory contents that you copied contains several new files:
gl-matrix.js and gl-matrix-min.js contain the glMatrix library. gl-matrix-min.js is a "minified" version intended for production use — it's a smaller file, but not very human-readable. gl-matrix.js is the human-readable version — better if you need to look at the source for some reason and for debugging. The provided code uses gl-matrix-min.js but you might want to switch to the un-minified version if you run into error messages generated from within the glMatrix library code.
simple-rotator.js and trackball-rotator.js provide mouse-driven rotation.
basic-object-models-IFS.js and teapot-model-IFS.js provide definitions for standard shapes.
Live Preview now appears to be working on all Linux platforms! (VDI, Demarest 002, Lansing 310) Please let me know if you encounter issues.
Live Preview now appears to be working on the dual-boot computers
in Demarest 002 (as well as in the VDI), but for now you'll need to
continue using the workaround (start up Firefox manually) in Lansing
310.
Your best reference sources are the slides from class (which pull out and organize the key points), the examples from class (which put all the pieces together), and the textbook.
The provided viewing.html file contains a WebGL program displaying a multicolored cube. It includes support for viewing and projection transforms.
Run/view viewing.html to see the cube. You can spin it around with the mouse. (Note that all of the vertices are different colors — this will help you determine if you have the right view for some of the projections in the next exercise.)
Review viewing.html, noting in particular the elements related to the viewing pipeline: the JavaScript modelview and projection variables, the vertex shader with its u_modelview and u_projection arguments, and the steps involved in passing the JavaScript modelview and projection matrices to the shader. Also compare viewing.html to hello-webgl.html from lab 2.
In this exercise you'll modify the camera and/or projection transforms in order to implement specific projection types.
The provided projection.html file contains the same scene as viewing.html, but the code has been restructured slightly to accommodate runtime selection of a specific projection type.
Review projection.html — compare it to viewing.html and make sure you understand its code structure. In particular, when the program is run you'll get just a black drawing window even though a shape is being drawn — why? (setView is only called when one of the projection types is selected — what are the values of modelview and projection if draw is called before that?)
Edit projection.html file to provide a showcase of different projection types as described below.
projection.html contains everything needed for the scene...except for the camera and projection setup. Your task is to fill in the various cases of the two switch statements in the setView function to implement standard projection types: multiview orthographic (front, side, plan); axonometric (isometric, dimetric, trimetric); one-, two-, and three-point perspective; and oblique perspective. Note that cavalier, cabinet, and military projections are optional!
In many cases, there is more than one way to specify a projection of a given type. Choose settings where the effect of the projection is clear e.g. you can easily tell which axes have the same scaling and which are different for axonometric projections. Also, to best illustrate the effect of an oblique perspective projection, define it to use the same near clipping plane as your one-point perspective projection and position the camera so that the central cube in the scene is in approximately the same place for both projections.
For extra credit, also implement cavalier, cabinet, and military projections. This addresses oblique projections (including cavalier and cabinet) on pages 21-23. Some notes:
The matrices given also place all transformed points on the view plane (z=0), but the z coordinate is needed for the depth test. Adjust the matrices so that the z coordinate is unchanged — the values along the diagonal should all be 1.
You may need to adjust the signs of the angle α and/or the cos(α) term and/or the sin(α) term to get the shear in the right direction for the conventional cavalier and cabinet views where you see the front, top, and right sides.
The matrices given apply the shear to "square up" the oblique view volume so there are 90 degree angles at the corners, but the projection transform stage of the viewing pipeline also includes normalizing the view volume into the canonical view volume (centered at the origin, with extents between -1 and 1 in all dimensions) — you'll need to include that step as well to get the full projection transform.
Keep in mind that the shear is parallel to the xy plane i.e. the amount of shear is based on the z coordinate. That means anything with z=0 in eye coordinates won't shift when the shear is applied — place the camera strategically so you can use that knowledge to define the view window.
Military involves both the camera and an oblique projection — a hint is to think about which face is undistorted (still a square with 90 degree corners), the orientation of that face, and the scale factor of that face compared to the others.
Additional technical notes for oblique projections:
gl-matrix 2.2.0 doesn't support creating matrices from values, so you'll need a newer version:
Copy gl-matrix-3.4.4.js and/or gl-matrix-3.4.4-min.js from /classes/cs424/lib into your ~/cs424/workspace/lib directory and change the line that includes gl-matrix to use the v3.4.4 file.
Add the following line at the beginning of the JavaScript program (right after "use strict"):
const { vec4, mat4 } = glMatrix;
If you need other types, such as vec3 or mat3, include those in the list as well. (The newer versions of gl-matrix have a different organization, so types like mat4 aren't available without this step.)
To create a 4x4 matrix from values, use
mat4.fromValues(m00, m01, m02, m03, m10, m11, m12, m13, m20, m21, m22, m23, m30, m31, m32, m33)
This returns a 4x4 matrix with the specified elements (column-major order).
basic-object-models-IFS.js and teapot-model-IFS.js provide definitions for many of the standard shapes: cube, sphere, torus, cylinder, cone, and teapot. (There's also a less-common one: ring.) Vertex positions and the face indices are defined explicitly for the teapot — access them as teapotModel.vertexPositions and teapotModel.indices as shown in the comment at the beginning of that file. (The normal vectors and texture coordinates aren't needed at this point.) The other shapes are defined as functions which return an object with the same information, for example
let cubemodel = cube(1);
You can then use cubemodel.vertexPositions and cubemodel.indices. Look for the other function definitions in basic-object-models-IFS.js to find out the names and parameters for each shape. Note that in JavaScript it's not necessary to provide values for the rightmost parameters, so, for example, if you want a sphere of radius 1 with the default number of slices and stacks, you can write uvSphere(1).
An important note is that while these are defined as IFS objects — faces reference vertices by index — the format of the vertexPositions and indices arrays is not the same as the example discussed in class — these are triangular meshes rather than polygonal meshes and so the coordinates and faces lists are flat (1D) arrays of values rather than arrays-of-arrays (2D). Also, the arrays are already the typed arrays needed for passing as shader parameters rather than plain JavaScript arrays. This is easiest to see for the teapot model.
Other technical notes:
Remember that you'll need to include the basic-object-models-IFS.js and teapot-model-IFS.js scripts in order to use their definitions in your code.
Since there's the potential to draw wireframe on top of solid, use the mechanism described on the last page of Monday's slides (polygon offset) to ensure that the wireframe is visible.
The provided geometry.html file is set up to draw one instance of one of the basic shapes at a time. It allows for solid and/or wireframe views. Your task, as detailed in the steps below, is to fill in the actual drawing-of-the-model part.
Review the provided geometry.html file: notice in particular the shaders and their arguments (modelview and projection transforms, along with a color for the primitive), the structure of draw, and the defined view and projection.
Add code in the "draw scene" section (marked with a TODO note) to draw model as a solid object. This will be similar to the steps in the "draw scene" section in viewing.html, but adapted for IFS shapes and slightly different shader parameters. Pick any color.
Add code in the "draw scene" section (marked with a TODO note) to draw model as a wireframe object. Use a contrasting color for the wireframe so it is visible whether or not the solid object is drawn. Avoid repeated code — steps that are the same for both solid and wireframe (such as linking many of the shader parameters) should go before the if statements rather than being written in both.
Replace (most of) the "return teapotModel" lines in getModel so the correct shape is returned for each case. Size the shape so that it fills a reasonable amount of the view volume while still fitting entirely within the view volume. You'll need to include the basic-object-models-IFS.js script to access those model definitions.
The provided scene1.html file is set up with same structure as geometry.html. In addition, a modelview stack (called stack) has been defined and there are push and pop statements in the "draw scene" section illustrating how to use it (along with an example of applying a modeling transform).
Technical notes:
See the setView function in projection.html for an example of how to define functions with parameters in JavaScript.
The various glMatrix mat4.rotate operations take the angle in radians. JavaScript has a constant Math.PI that is helpful here; to convert degrees to radians, multiply by Math.PI/180.
Edit scene1.html to create a "museum of objects" scene as described below. The two TODO comments identify the elements you need to edit/modify — the scene contents and the camera and projection. It is recommended that you start by copying the draw scene code you wrote for the previous exercise into scene1.html and arranging those elements into helper functions (as described below) so that you can then focus on building the scene from objects and modeling transforms.
The "museum of objects" scene should include at least four of the objects defined in basic-object-models-IFS.js and teapot-model-IFS.js, the house on page 5 of Monday's "specifying geometry" slides, and one object of your own where the vertex geometry is specified using an indexed face set representation. (For the latter, your object should include at least 10 faces.) Note that the house is a polygonal mesh and uses an array-of-arrays representation! (It will take slightly different handling to get data in the right format for passing to the shaders. Also, the syntax given on the slides is Java — translate it to JavaScript arrays.) You are encouraged to use a similar representation for your own object. Choose a different color for each object.
As in a real museum, each object should be displayed sitting atop a pedestal. Scale/orient each object in an interesting way — they shouldn't all be uniformly scaled and axis-aligned. Your pedestals can be as simple as tall boxes and/or cylinders, or you can get fancier. The pedestals should all be sitting on the floor, but they can be of varying size and height if you wish. Make all the pedestals the same color.
The pedestals should be solid (filled polygons). The objects can be solid or wireframe. For any solid objects (including the pedestals), also draw a wireframe outline version in a contrasting color to outline the edges of each face. (Otherwise the 3D shape isn't apparent in the absence of lighting.)
Utilize hierarchical modeling — position an object on its pedestal and then place the object+pedestal in the world rather than placing object and pedestal separately. Also, if you have any complex objects (such as a pedestal that is more than just a tall box or cylinder), create a function to place and draw all the shapes making up that object rather than repeating code for each instance of the complex object.
As you add more objects to a scene, the amount of code needed to link shader arguments and draw primitives quickly adds up. Create helper functions to pull all this repetition out of draw — you might consider, for example, a setView function which sets the shader parameters for the modelview and projection, a setModel function which sets the shader parameters for the geometry, and a drawModel which draws the primitives. (Up to you to decide is where to handle setting the color, and how to handle solid vs wireframe if you have both in your scene.)
Design your scene within a world coordinates box ranging from 0 to 20 in each dimension, then position and orient a camera to give a good view of the scene. You can use either an orthographic or a perspective projection. The provided scene1.html is set up with a simple rotator for the camera and a too-big view volume so that you can get your scene set up without having to worry about things not being visible. Adjust the projection at any point as you are building your scene, and set a fixed camera view (instead of the mouse rotator) at the end.
An articulated structure is made up of individual segments connected by joints, so that the segments can move relative to each other. Think of a leg or a crane or Pixar's cute little desk lamp as examples. There is typically a hierarchy present in these structures — bending your knee, for example, causes both your lower leg and foot to move, while flexing your ankle moves only your foot.
For extra credit, edit scene2.html to create an articulated structure with at least two joints and three segments. Each segment must involve at least two shapes and the structure must be modeled so that moving one joint affects everything beyond that. Animate each joint in some way to illustrate this property.
Technical notes:
The provided scene2.html contains the setup for an animation — the key additions to our standard WebGL program structure are a step function which calls draw() to draw one frame and then updates the animation variables for the next frame, and the use of requestAnimationFrame(step) to schedule repeated calls to step. The now parameter to step and the deltaTime computation provide a way to scale the amount of update between frames to the actual elapsed time between frames rather than assuming frames always occur at a fixed interval.
As an example, the provided code defines an animation variable xpos, uses it in draw in a modeling transform (translate), and updates it in step. See the TODO comments to identify these three spots in the program. (Note that no geometry is drawn so you won't see anything if you run the program, but it does print the xpos values to the web console so you can see what kind of numbers are involved. deltaTime can be further scaled to produce appropriate update amounts for your animation variables.)