Required Features for Menger Sponge Milestone 1
Menger Sponge Only Milesone
Due: Feb 27, 2024
The assignment is broken down into several features described below. Each feature is worth a number of points and will be graded separately (although later feature may build on and require a working implementation of earlier features). Your code should be correct and free of segfaults, deadlocks, or other buggy behavior. You will not be graded on software engineering (use design patterns and unit tests if they help you) but unreadable code is less likely to receive partial credit. You should not worry too much about performance or optimizing your code, but egregiously inefficient implementations of any of the features will be penalized.
(0 pts) Build
Your submitted archive should build smoothly when make-menger.py
is run and should yield a web package that executes without fatal erros on common browsers (Chrome; Firefox; etc).
Code that does not build or run will lose anywhere between 5 to 100 points, depending on the time and effort it requires the TA to fix your code.
It is your responsibility to thoroughly test your submission.
(30 pts) Create the Menger Sponge
Your first task in this assignment is to procedurally generate the Menger sponge. The sponge
is an example of a fractal---its geometry is self-similar across scales, so that if you zoom in on one part of
the sponge, it looks the same as the whole sponge. The full sponge is thus infinitely detailed, and cannot be
rendered and manufactured; however, better and better approximations of the sponge can be created using
a recursive algorithm. Let L
be the nesting level of the sponge; as L
increases, the sponge becomes more
detailed. Given L
, the algorithm for building the L
-level sponge is listed below.
Sponge generation code
Start with a single cube
for i = 2 to L do
for each cube do
Subdivide cube into 27 subcubes of one third the side length
Delete the 7 inner subcubes
end for
end for
Notice that for \( L = 1 \), the Menger sponge is simply a cube. You will write code to generate the Menger
sponge for \( 1\le L \le 4 \) , and modify the keyboard callback so that pressing any key from 1
to 4
draws
the appropriate sponge on the screen. All sponges should fill a bounding box whose diametrically-opposite
corners are \( (m, m, m) \) and \( (M, M, M) \), where \( m = -0.5 \) and \( M = 0.5 \).
Implementation Details and Hints
You will want to write a function which, given a list of existing vertices (in homogeneous coordinates) and triangle indices, appends the vertices and triangles necessary to draw a cube with diametrically-opposite corners at arbitrary locations (minx, miny, minz) and (maxx, maxy, maxz). This will entail appending the eight cube vertices, and enough triangles to draw all six faces of the cube.
You will be asked to flat-shade the cube (see below) and so you will need to create a "triangle soup" (where each vertex belongs to exactly one triangle; corners of the cube consist of multiple coincident vertices) and compute a normal for each vertex (perpendicular to the face to which that vertex belongs). You will need to ensure that the vertices you create have outward-pointing normals. Incorrectly-oriented triangles can be fixed by permuting the order of the triangle's vertex indices. If triangles are not oriented correctly, they will appear incorrectly shaded (probably black) when rendered, because they are "inside out."
Finally, modify MengerSponge.ts to create, given L
, the Menger sponge of level L
, properly positioned
and scaled in space as described above, and to populate vertices, faces, and normals with the vertices and
triangles of the sponge. The simplest implementation of the above pseudocode is probably to first
compute where to place all cubes, and then independently add each cube's vertices, faces, and normals using
the helper function suggested above.
Ensure that pressing any key from 1
to 4
generates and displays a Menger sponge of the
appropriate level L
. The geometry should only be recreated when one of these keys is pressed---do
not procedurally generate the cube every frame! (Hint: any time you change the vertex or triangle
list, you need to inform OpenGL about the new data by binding the vertex and triangle VBOs using
glBindBuffer
, passing the new data to the GPU using glBufferData
, etc.) The skeleton code will
do most of this for you, if you slot in your Menger-generation code in the right place.
You will need to implement camera controls to fully appreciate the Menger sponge; however, the sample code provides a hard-coded perspective camera, so that even before moving onto the next part of the assignment, you can get a sense for if the sponge is being generated correctly.
(45 pts) Camera Controls
Next, you will implement keyboard and mouse controls that let you navigate around the world. As discussed in class, the major pieces needed for setting up a camera are
- a view matrix, which transforms from world coordinates to camera coordinates; as the camera moves around the world, the view matrix changes;
- a perspective matrix, which transforms from 3D camera coordinates to 2D screen coordinates using a perspective projection;
- logic to move the camera in response to user input.
The camera's position and orientation is encoded using a set of points and vectors in world space:
- the eye, or position of the camera in space;
- the look direction that the camera is pointing;
- the look direction is not enough to specify the orientation of the camera completely, since the camera is still free to spin about the look axis. The up vector pins down this rotation by specifying the direction that should correspond to "up" in screen coordinate (negative y direction);
- the tangent or "right" direction of the camera. Notice that this direction is completely determined by the cross product of the look and up directions;
- the camera distance, the distance from the camera to the point that the camera is focusing on; this distance controls the camera zoom;
- the center point, the point on which the camera is focusing. Notice that \( \vec{center} = \vec{eye} + d \cdot \vec{look} \), in which \( d \) is camera distance.
The following default values should be used to initialize the camera at program start. It will ensure that the Menger sponge begins within the camera's field of view
- eye = \( (0,0,d) \);
- look = \( (0,0,-1) \);
- up = \( (0,1,0) \).
The other default values can be inferred from these. The starter code should already create a camera at the correct location and with correct orientation.
You will implement a "first-person shooter" (FPS) camera, where the eye is fixed and the center moves around as the user moves the mouse.
(20 pts) Rotation
Implement camera rotation when left-clicking the mouse and dragging. The starter contains a
mouse callback that detects mouse clicks and mouse drags. Track the mouse position during dragging, and compute the mouse drag direction
based on the difference in mouse position on two consecutive frames. Calculate the corresponding vector in world
coordinates; the camera should swivel in the direction of dragging, i.e. should rotate about an axis
perpendicular to this vector and to the look direction. Rotate by an angle of rotationSpeed
radians
each frame that the mouse is dragged. You should be able to use the provided camera starter code to swivel the camera.
(5 pts) Forward/Back
Implement the behavior of the w
and s
keys. Pressing the w
or s
key should translate both the eye and center by zoomSpeed
times the look direction.
(5 pts) Strafe
Implement the behavior of holding down the a
and d
keys. The keys should strafe, i.e. translate the eye by panSpeed
times the camera tangent direction, left or right, respectively.
(10 pts) Roll
Pressing the left and right arrow keys should roll the camera: spin it counterclockwise or
clockwise (respectively) by rollSpeed
radians per frame.
(5 pts) Up/Down
Pressing the down the up or down arrow keys should translate the camera position, as with the a
and d
keys, but this time in the up
direction (with the translation speed being again panSpeed
units per frame).
(25 pts) Some Basic Shaders
You will do some more shader programming, to liven up the rendering of the cube, and place a ground plane on the screen so that the user has a point of reference when moving around the world.
(5 pts) Better Cube Shading
Fix the fragment shader so that it computes diffuse shading using per-vertex normals. Color the different faces of the cube based on what direction the face normal is pointing in world coordinates. Notice that the face normals will be axis-aligned no matter how the user moves or spins the camera, since transforming the camera does not move the cube in world coordinates. Refer to the following table for how to color the faces:
Normal | Color |
---|---|
\( (1,0,0) \) | red |
\( (0,1,0) \) | green |
\( (0,0,1) \) | blue |
\( (-1,0,0)\) | red |
\( (0,-1,0)\) | green |
\( (0,0,-1)\) | blue |
All coloring code should take place in the fragment shader. The above colors are the base colors; you'll shade the cube based on the orientation of the face relative to the light source, as described by the diffuse term in the Phong illumination model.
(10 pts) Ground Plane
Add a ground plane to the scene. This plane should be placed at \( y = -2.0 \) and extend effectively infinitely in the x and z directions. Rendering of this plane should be completely separate from that of the cube: create new lists of vertices and triangles for the infinite plane, new VBOs for sending this geometry to the GPU, and a new shader program for rendering the plane. This shader program can reuse the cube's vertex shader, but should have its own fragment shader. You can use the cube shaders as skeletons for writing the floor shader.
You should be able to represent the geometry of the plane using only a small \( (\approx 4) \) number of triangles.
(10 pts) Ground Plane Shading
Write a fragment shader for the ground plane, so that the plane is colored using a checkerboard pattern. The checkerboard should be aligned to the x and z axes (in world coordinates), and each grid cell should have size \( 5.0 \times 5.0 \). The cell base color should alternate white and black (again, these are base colors, and you should diffuse-shade the floor to dim pixels facing away from the light, as in the cube's fragment shader).
There are many ways of coding the checkerboard pattern; use any method you like. You may find
the following GLSL functions useful: mod
, clamp
, floor
, sin
. Hint: the fragment shader, by default,
does not have access to the position of pixels in world coordinates (since that information was lost in
the vertex shader, when the vertex positions are multiplied by the view and perspective matrices).
You may find it useful to pass the untransformed world coordinates from the vertex shader to the
fragment shader.
Build-in Tests
To help you debug your Menger Sponge implementation, we have provided some built-in unit tests. You will find these on the right side of the screen; each test mocks some inputs into your code, and then compares the image that your code produced to the "correct" image from a reference solution.
Extra Credit
The list below contains pre-approved optional features (worth five points per chime and ten points per bell), but this is not an exhaustive list---feel free to implement additional features (you can ask us to assess the point value of proposed features during office hours.) Additional optional features you implement will be awarded extra credit.
All optional features must be fully described in your README file (including instructions for how to invoke the feature behavior) to receive credit.
🎐 Implement a couple of additional types of volumetric fractal (with the ability to view them at different levels of resolution), such as the Jerusalem cube, tetrix, Koch snowball, or novel fractals of your own invention.
🎐 Find some aesthetically interesting textures online, and use OpenGL's texture-mapping support to replace the flat colors on the cube sides with textures. Use visually distinct textures for the six different cube faces. For an extra 🎐, also implement normal mapping and draw the cube with engraved/embossed sides.
🎐 Implement basic animation of the cube: allow the user to "grab" the cube with the mouse and spin it in any direction by dragging the mouse (as if the cube were pinned at the center of a set of gimbals). Slow the rotation of the cube over time after the user stops dragging.
🎐 Show a simple projected shadow of the Menger sponge on the floor surface.
🔔 Create a "skybox" that wraps your scene. You may reuse the cube mapping texture files from the ray-tracer project.
🔔 🎐 Implement a unit cube of the space-filling Morton fractal curve (with the ability to view it at different levels of resolution). To give the Z-order curve volume, you'll need to realize its geometry as thin square or circular cylinders made of triangles. Color the curve using a continuous HSV gradient along its arclength to convey intuition about why this curve is useful for constructing a bottom-up BVH.
🔔 🔔 Give the user the ability to slice through your Menger sponge using an arbitrary plane. Draw a good-looking cross-section by exploiting the depth buffer, in a way similar to depth peeling.
🔔 🔔 Turn your checkerboard into mountainous terrain. Replace the infinite plane by a finite (but very large) square grid, and procedurally generate the mountain geometry using Perlin noise. Use a color map to make your mountains snow-covered with verdant valleys.
🔔 🔔 Turn your checkerboard into an ocean. Replace the infinite plane by a finite (but very large) square grid, and approximate the (time-varying) height of the ocean surface by assuming that the height function is given as a superposition of Fourier modes. Calculate the height of each triangle vertex in the tessellated square and set the vertex positions and normals accordingly. Tips and details can be found in GPU Gems 1 Chapter 1. For an extra 🔔, make the water look good by implementing specular highlighting and per-vertex normals that depend on the ocean's slope at each vertex (math hints are available in Section 1.2.2 of GPU Gems 1 Chapter 1).
🔔 🔔 🔔 Implement a fluid dynamics simulation of the ocean's surface: spawn wave packets as the user wades through the water to get a realistic "ripple" effect.
Up to 🔔 🔔 🔔 Create a YouTube video explaining fractals, self-similarity, fractal dimension, or other similar concepts to a general audience, using visual aids created with your Menger sponge code. Publish the video on social media. Amount of credit will depend on the depth of the content covered and editing/production values.