CS184/284A Spring 2025 Homework 2 Write-Up

Names: Yuhe Qin & Henry Michaelson

Link to webpage: hw-webpages-yuhe-henry-webpage
Link to GitHub repository: hw2-meshedit-yuhe-henry
The Joys of Meshedit
The Joys of Meshedit!

Overview

In this homework assignment we implemented various means of representing geometric objects with primitives. In particular, we started by representing curved lines and surface patches through a tight-knit series of points defined by the de Casteljau algorithm. We then turned our focus to triangle meshes and implemented vertex normal vectors to assist with Phong shading. Lastly, we implemented the loop subdivision algorithm (and its primary edge flip and edge split processes) to allow for mesh upsampling. This homework was very beautiful as it showed the power of simple iterative operations to model complex geometry. However, the highly iterative nature of this project meant that many bugs did not show themselves immediately, and thus required a lot of tracing down.

Section I: Bezier Curves and Surfaces

Part 1: Bezier curves with 1D de Casteljau subdivision

In this assignment, we completed a single recursive step in the de Casteljau algorithm to evaluate a Bezier curve. The secret of this algorithm lies in its highly recursive nature where each step requires every set of contiguous points to be lerped together as such: left_point * (1-t) + right_point * (t). This produces a new resultant point for each pair of input points. At each step of this algorithm, the total number of points to blend decreases by 1. Once we have only 1 point left, the algorithm terminates and the one point is returned as the final blending of all the input points. It is useful to know that the de Casteljau algorithm uses a simple recursive mechanic to compute Bezier curves for said input points.

Here are images showing the de Casteljau algorithm evaluating a septic polynomial based on 6 control points.

This represents the first iteration of the algorithm in which control points are initially blended to create the first intermediate points.
This represents the second iteration of the algorithm in which the first intermediate points are blended to create a second set of intermediate points.
This represents the third iteration of the algorithm in which the second intermediate points are blended to create a third set of intermediate points.
This represents the fourth iteration of the algorithm in which the third intermediate points are blended to create a fourth set of intermediate points.
At this step, there is only one point inputted into the algorithm. This point is result of running de Casteljau at a given t and should lie of the final curve that we display.
This is the final curve which displays the result of evaluating the de Casteljau algorithm many samples of t.
This is the final curve for the same control points but for another value of t.
This is the final curve for the same control points but for yet another value of t.
This is the final curve for a different pair of control points.

Part 2: Bezier surfaces with separable 1D de Casteljau

After implementing the simple, de Casteljau algorithm operating on points in 2 dimensions (x,y) space to draw a line, we extended our approach in two meaningful ways:
This image shows a variety of Bezier patches strung together to create a teapot. This looks very similar to the teapot that is operated on later on in this homework, but is created via a series of patches instead of triangle meshes.

Section II: Triangle Meshes and Half-Edge Data Structure

Part 3: Area-weighted vertex normals

For this part of the assignment we needed to compute the area-weighted vertex normal for a given vertex. This is very important so we can get a reliable estimate of each pixel's normal vector which is required for Phong shading. To do this, we performed the following:
Triangle mesh with flat shading.
Triangle mesh with Phong shading.
Triangle mesh with flat shading from a different angle.
Triangle mesh with Phong shading from a different angle.

Part 4: Edge flip

To implement the edge flip we had to update the elements in the halfedge data structure to point to the right places.
Specifically, we needed to update the vertices that a given edge pointed to so that it could be flipped. Luckily, flipping an edge does not add / remove any elements. That being said, this implementation was anything but simple.

There were many elements (vertices, halfedges, edges, and faces) that had to be reassigned and a single mistake could leave a dangling pointer which could break the mesh at a later point. Even more frustrating is that said issues may not arise immediately on the first edge flip, but could show themselves much later, after a few flips have occurred.

Therefore, a critical piece of debugging this function was to use the visual inspector to see what each half edge, face, vertex, and edge pointed to after each flip operation. It was also important to try several flip operations to reverse said changes and ensure that after 2 (or more) flips the elements were in their original (or equivalent) state.

Additionally, we took extra care to draw out sample triangles and painstakingly detailed what each element should do during the transformation. We then made sure to update every element in the flip step, including those elements that did not have their values changed. This was to make sure that we could see every state change that was occurring to allow us to make sure the final state matched our diagram.

There were a few small bugs on a few halfedges, however, the UI tool allowed us to inspect visually.

These processes ensured that we did not get too lost at this step.

Teapot mesh before edge flips.
Teapot mesh with only one edge flipped.
Teapot mesh with two edges flipped.
Teapot mesh with three edges flipped.
Teapot mesh viewed from the bottom without any flips.
Teapot mesh viewed from the bottom with one flip.

Part 5: Edge split

First, we began by hand-drawing the original mesh topology on paper, marking each halfedge, edge, vertex, and face along with their key properties (connectivity pointers, face/edge references, etc.). Then, we sketched the ideal result after an edge split: we added two new halfedges, one new edge, and the split vertex; partitioned the original face into two smaller faces; and updated all the next, twin, vertex, face, and edge pointers accordingly.

At first glance, our pencil sketch matched the textbook diagrams, so we dove straight into coding Task-6. Unfortunately, the first few runs produced malformed meshes: unexpected holes, dangling edges, and faces that did not close. In our implementation, some new halfedges were not correctly paired with twins, and a few face loops did not reconnect properly, causing triangles to go missing and boundary loops to collapse.

The wrong mesh 1
The wrong mesh 2
This problem was quite a challenge to debug and there were two key issues in our original solution to this problem.
Teapot mesh before edge splits.
Teapot mesh with only one edge split.
Teapot mesh with two edges split.
Teapot mesh with three edges split.
Teapot mesh with four edges split.
Teapot viewed from the other side before any split and flip operations.
Teapot mesh with a few faces in a column manually subdivided via the loop subdivision algorithm.
Teapot mesh with all faces in a column manually subdivided via the loop subdivision algorithm.

Part 6: Loop subdivision for mesh upsampling

First, we computed new positions for every original vertex: for each v we set v->isNew = false, traversed its one-ring via h = v->halfedge() and h = h->twin()->next(), summed neighbor positions, counted valence n, computed u = (n == 3 ? 3.0/16.0 : 3.0/(8.0*n)), and stored the result in v->newPosition.

Next, we updated edge-based positions: for each EdgeIter e, we identified its two endpoints v0 and v1 plus the two opposite vertices v2 and v3, then applied the four-point stencil e->newPosition = 3.0/8.0*(v0+v1) + 1.0/8.0*(v2+v3).

To prepare for splitting, we collected all original edges in a vector originalEdges , marking each e->isNew = false so we could iterate only over those originals. We then looped over that snapshot, called mesh.splitEdge(e) on each, placed the returned vertex at e->newPosition, ensured its halfedge pointed back to itself, and flagged newV->isNew = true.

After splitting, we refined the triangulation by flipping: we scanned every edge in the mesh and flipped those with e->isNew == true that connected exactly one new and one original vertex, using mesh.flipEdge(e) to improve triangle quality.

Finally, we finalized the subdivision by copying each v->newPosition back into v->position, yielding the smoothly upsampled mesh.

During development, we relied heavily on the isNew flags to control splitting and flipping, used a pre-split edge snapshot to avoid infinite loops, and added assertion checks on face loops and twin pairs to catch pointer errors early.


You can see in the images below that loop subdivision rounds sharp corners and can distort the overall geometry of the shape if the topology is not balanced. The cube is a great example showcasing both of these attributes of loop subdivision. You can see in the first set of cube subdivisions, the shape becomes irregular and becomes slightly longer at the vertices where the original edge was not present. We were able to fix this by adjusting the topological mesh structure of the cube by manually splitting all edges so that every vertex had the name degree. Once this was complete, we split that cube and the final shape was much more even. You can see all the images attached of both cubes + subdivisions. We also attached some of the other objects with their appropriate subdivisions.

You can see in the two images at the bottom with the irregularly split cube, that the vertex locations are noticeably better preserved than in the versions of the subdivided cube with fewer edge splits prior to subdivision. This is because having a greater number of edges prior to subdivision means that each triangle is smaller and therefore each old vertex can move less in the readjustment phase of subdivision. You will see that the overall shape may be strange for these cubes, but this reflects the random nature of the splits that we performed.
Lopsided topological cube with 0 rounds of loop subdivision complete.
Lopsided topological cube with 1 round of loop subdivision complete.
Lopsided topological cube with 2 rounds of loop subdivision complete.
Lopsided topological cube with 3 rounds of loop subdivision complete.
Lopsided topological cube with 4 rounds of loop subdivision complete.
Lopsided topological cube with 5 rounds of loop subdivision complete.
Lopsided topological cube with 6 rounds of loop subdivision complete.
Lopsided topological cube with 7 rounds of loop subdivision complete.
Balanced topological cube with 0 rounds of loop subdivision complete.
Balanced topological cube with 1 round of loop subdivision complete.
Balanced topological cube with 2 rounds of loop subdivision complete.
Balanced topological cube with 3 rounds of loop subdivision complete.
Balanced topological cube with 4 rounds of loop subdivision complete.
Balanced topological cube with 5 rounds of loop subdivision complete.
Balanced topological cube with 5 rounds of loop subdivision complete from a different angle.
Bean object with loop subdivision performed.
Cow object with loop subdivision performed.
Quadball object with loop subdivision performed.
'weird' object with loop subdivision performed.
Cube object with many irregular pre-splits performed with 1 level of subdivision.
Cube object with many irregular pre-splits performed with 2 levels of subdivision.