CS 184: Computer Graphics and Imaging, Spring 2021

Project 2: Mesh Editor

Cameron Hui and Saketh Kollu, p2-meshedit-sp21-team-6



Overview

Give a high-level overview of what you implemented in this project. Think about what you've built as a whole. Share your thoughts on what interesting things you've learned from completing the project.

Section I: Bezier Curves and Surfaces

Part 1: Bezier curves with 1D de Casteljau subdivision

de Casteljau’s algorithm linearly interpolates a point between the edges joining each control point at parameter t (a proportion of length along the line segment). Then, by recursively running the algorithm on the points we just interpolated at, we can finally arrive at a point along the bezier curve at parameter t. By adjusting the parameter between 0 and 1, we can find all points along the final bezier curve parameterized by t.

Note: This is a GIF animation of our curve.

6 Point Bezier curve and moving control points and modifying parameter

Part 2: Bezier surfaces with separable 1D de Casteljau subdivision

The de Casteljau algorithm can be applied to Bezier surfaces, as each point in a given 2D square matrix can be considered control points that correspond to 3D vertices. The algorithm creates edges between each vertex by interpolating with respect to rows and then the columns of the matrix. After the rows are implemented, if the matrix is of size n x n, we are left with a vector of n Bezier points that are just like the final points from part 1. This final vector of n Bezier points is used as the control points vector in the column dimension, and is interpolated in this dimension one last time. Once we have a Bezier point for parameters u,v for a Bezier patch, we can “fill” the patch in by trying all combinations of parameters u and v, creating the faces of our Bezier surface.

Note: This is a GIF animation of our teapot.

Teapot rendered correctly using Bezier patches

Section II: Sampling

Part 3: Average normals for half-edge meshes

We traversed two edges to get the two other points in a triangle that contained our target vertex. We then used the distances from our vertex to each point to calculate the triangle vectors and then the normal of the face by computing the cross product of the triangle vectors and finding the unit vector of that. We then divided that by the area of each triangle (computed using the cross product again) to weight our normal vector. We repeated this process for all adjacent vertices by traversing along the half edges until we returned to our starting point. We then returned the averaged normal vector of all adjacent faces.

Note: This is a GIF animation of our teapot.

Teapot with averaged normals (wireframe on and off) (normals on and off)

Shading off
Shading on

Part 4: Half-edge flip

We took a half-edge labeled triangle and named each half edge, vertex, and face, taking inspiration from the website diagram below. We then applied the edge flip on paper while keeping in mind each variable name, helping us keep track of which variables and their relationships with one another were changed. By creating this diagram, we were able to simply hard code the edge flip as we observed on paper, as the structure would be applicable to any two triangles connected by an edge. This also made our implementation run in O(1) constant time.

Citing: GUIDE TO IMPLEMENTING EDGE OPERATIONS...

Pre-flip
Post-flips

Part 5: Half-edge split

Similar to Part 4, we drew a diagram and figured out which new vertices, edges, and half edges to add. We then created those new objects and assigned them based on our diagram, hard coding the new assignments once again to make our function run in O(1) time.

We ran into an issue where our vertex was placed at the origin of our 3D space, basically extruding our faces into the teapot. We realized that we were initializing the new vertex without assigning its position in relation to the vertices of our faces. We fixed this by assigning the position of our newly created vertex to the linearly interpolated position between the points of our triangle at t=0.5 which is basically finding the midpoint of the edge we are splitting.

Pre-split
Post-splits
We made this diagram to help reassign pointers

Part 6: Loop subdivision for mesh upsampling

We implemented subdivision by following the steps in the specification. We began by computing the new vertex positions for all the original vertices in the mesh. Then, we used the old vertex weighting formula from lecture slides that is a function of the degree of the vertex.

When doing loop subdivision, we knew that new vertices would be inserted at the midpoint of every single edge in the mesh during the split phase - so for each edge in the mesh, we computed the new vertex position that the midpoint had to be adjusted to, based on the new vertex weighting formula from lecture slides. Since during this step we hadn’t created the new vertex yet (would happen in the next step), we stored the new weighted vertex position on the edge that would have a midpoint inserted during the split. However, after running into bugs, we did not end up using this stored position; instead, we stored the midpoint where the split would be created as well as the final weighted vertex position in two separate vectors. From these two vectors, we essentially could map between the midpoint and the final weighted vertex position which we used in the final step.

The next step required us to call splitEdge on every edge in the mesh. We needed to create a counter to ensure we were only splitting the first n original edges in the mesh and not the edges that are added during the splitEdge function.

In the splitEdge function, we had to add a small modification by setting the flags isNew to the new vertex we created, and setting the flag for the two perpendicular edges that were created to the original edge. The new edge along the original edge that was split didn’t get its flag set because we wouldn’t want to flip that edge. We iterated over all the edges one more time, but this time checked if the edge was new and between a new and old vertex. If it passed this check, we flipped the edge.

Finally we copied the new vertex positions into the final vertex positions for new and old vertices. Iterating over the vertices, if the vertex was new (inserted at the midpoint during splitEdge) we got its position, figured out which new weighted vertex position it mapped to using the two vectors we created in step 2, and then updated its position to a weighted position from our map. For non new vertices, we had already computed the newPosition and stored them in step 1, so we wrote newPosition to that vertex’s position. We also cleared any isNew flags for vertices and edges so it wouldn’t mess up with further subdivisions.

Note: We included GIF animations of our subdivisions

Icosahedron being subdivided

Sharp corners where more edges meet will appear more pointy after subdivision compared to corners where there are less edges meeting, which will be more flat. In the below pictures on the cube subdivision, the original asymmetrical cube has 5 or 6 edges meeting in some corners and has 3 edges meeting in other corners, thus the greater edge corners are more defined in the final subdivision while the 3 edge corners are smoothed out to look flatter. This is a result from the vertex weighting we did, because the vertex weights for old vertices are a function of the degree of a vertex.

You can reduce this effect by pre-splitting edges so that most of the vertices have the same degree.

We realized that the triangles do not all have connecting diagonal edges, such that if you unfolded the model into a UV map, the unfolding would not be symmetrical. We decided to flip certain diagonal edges so that there are symmetrical edge splits (the diagonal edges would connect to another diagonal edge in some way). This helped alleviate the effects, but in the process, made the subdivided mesh look more like a reuleaux tetrahedron than a cube.

Note: We included GIF animations of our subdivisions

Pre-flip
Post-flip
Subdivision w/o flips
Subdivision w/ flips and symmetry
An example reuleaux tetrahedron

Section III: Optional Extra Credit

Part 7: Design your own mesh!

I did not follow the tutorial - I lightly followed the tutorial from UCBUGG’s fox modeling lab (here) and customized it using different reference pictures. The intention here was to create a shiba inu that I could then rig into the “bonk” meme. I first started with a box and extruded it multiple times to fit the shape of the body. Then, I cut extra edge loops to create better topology for the limbs, and was able to extrude the tail from there. Finally, I moved individual vertices and extruded certain faces to create the face. I would have put a shader on it, but I used Pixar’s Renderman and that didn’t translate very well from Maya to Blender. But here’s what it would have looked like with the texture I made and lighting:

The bonk dog we modeled (GIF)


How the shiba should have looked with correct shading