In this project, we explore curve and mesh representations. It's interesting to see how we can define a Bezier curve using 4 points and how we can operate meshes by flipping, spliting, and loop subdivision to change their shapes. The mean idea that we utilize for generating Bazier curve is called de Casrekjau Algorithm, and it takes recursive linear interpoliations. The mean idea that we utilize for mesh operation is called halfedge, and we can use it to traverse all triangles, edges, and vertices in a mesh.
De Casteljau's algorithm finds points on curve by recursive linear interpolation. We connect neighboring points, and find new n-1 points on every line at a fixed relative position (with ratio t) as points for next loop, until there is only 1 point left. Changing the value t continuously, we can get a smooth curve.The equation for every point position with parameter t is as below:
Below is an example with 6 control points. We do recursion step by step. We can also move control points to change the curve shape or change parameter value t to change the final point on the curve.
Step 1 ![]() |
Step 2 ![]() |
Step 3 ![]() |
Step 4 ![]() |
Step 5 ![]() |
Step 6 ![]() |
Original ![]() |
|
moving the original control points ![]() |
modifying the parameter t ![]() |
To get 2D Bezier surfaces, we first use de Casteljau to evaluate Bezier curves at fixed v values. Then we find corresponding points on those curves as control points for another Bezier curve at a fixed u value.
Below is an example of generating Bezier surface of a teapot.
In this section, we use notations as below. For a specific healfedge, we have attributes: vertex, twin(halfdege), edge, next(halfedge), face.
Phone shading provides smoother shading by shading every pixel compared to flat shading by shading every triangle, and it's implemented by interpolating area-weighted normals across each triangle. The normal at a vertex is calculated by normalized sum of all area-weighted normals of faces incident to the vertex. Starting from the root half edge of the vertex, we find the face of the half-edge and its three vertices to calculate its normal and area used as weight. Then we traverse all incident faces utilizing next and twin attribute of halfedges until we find the new half-edge is the root half-edge of the vertex. Finally we return normalized sum of all area-weighted normals.
Below is an example of smooth Phong Shading on the teapot.
Flat Shading ![]() |
Phong Shading using area-weighted normals ![]() |
To flip an edge, we collect all the elements that related to the edge(e0), including halfedges, vertices, edges, and faces. During the implementation, we found out it would be easy if we draw out the graph and just follow the graph to update all attributes. See the following graph: we have h0 to h9, v0 to v3, e1 to e4, and f0 to f1. Then, we update pointers that are affected by the edge flip operation for all elements. Notice boundary edges can't be flipped.
Before Flip ![]() |
After Flip ![]() |
Below is an example of performing random edge flips on the teapot.
Before Flip ![]() |
After Flip ![]() |
To split an edge, we collect all the elements that are related to the edge and those two triangles containing the edge, including halfedges, vertices, edges, and faces. For splitting, we add 6 halfedges, 1 vertex, 3 edges, and 2 faces to the mesh. The new vertex is put in the middle of the splited edge on default (which should be changed for loop subdivision in part 6). Similar to edge flip, we update pointers for every element based on the following graph.
Before slpit ![]() |
After split ![]() |
We also want to updated isNew attribute for edges, which is useful for part 6. The two edges we got from breaking the original edge are considered old (e0 ane e5), and the other two newly added edges (e6 and e7) are considered new. e1, e2, e3, e4 should have unchanged isNew value. During our first implementation, we wrongly set e1, e2, e3, e4 all to be false and e0, e5, e6, e7 all to be true, which cause infinite loop in part 6, because the algorithm will keep spliting new edges.
Before ![]() |
After some edge splits ![]() |
Before ![]() |
After a combination of both edge splits and edge flips ![]() |
We draw the helper that split the boundary and write the function base on that. When we select a boundary edge e1, we make sure that halfedge h1 is on side that would be split to 2.
Before boundary split ![]() |
After boundary split ![]() |
Before boundary slpit ![]() |
After boundary split ![]() |
To improve the mesh quality, we implement mesh upsampling by loop subdivision. To make sure every triangle is divided evenly into subtriangles, we follow the following 5 steps.
Below is an example of loop subdivision. Every time we perform one loop subdivision, the sharp edges and corners become smoother. This effect can be reduced by performing pre-splitting. If we split edges around a corner before loop subdivision, the corner will not be averaged by original nerighbor vertices as much as it would be before.
Original mesh ![]() |
After 1 loop subdivision ![]() |
After 2 loop subdivisions ![]() |
Pre-splitting ![]() |
After 2 loop subdivisions ![]() |
Sideview ![]() |
Below is another example of loop subdivision on a cube. We can notice that the subdivision result is not symmetric. That's because the edges on every cube face causes there to be different number of edges on every vertex, and different partterns cause the subdivision algorithm to produce different results. In order to fix the asymmetric problem, we can split the edges on every face, so there are diagonal edges on every square face. In this way, the pattern on every vertex corner will be same, and subdivision wil produce symmetric result.
Original mesh ![]() |
After 1 loop subdivision ![]() |
After 2 loop subdivisions ![]() |
After more loop subdivisions ![]() |
Pre-splitting ![]() |
After 1 loop subdivisions ![]() |
After 2 loop subdivisions ![]() |
After more loop subdivisions ![]() |
Extra Credit: Dealing with Boundary
To deal with boundaries, we need add equations for boundary vertices and edges in step 1&2. The equation for bounary old vertice position is: \(\frac{1}{8}*(U1_{pos}+U2_{pos}+\frac{3}{4}*V_{pos})\), where U1 and U2 are neighbor vertices of V on the boundary. The equation for bounary new vertice position is: \(\frac{1}{2}*(A_{pos}+B_{pos})\), where where A and B are vertices on 2 ends of the edge.
Because we have succesfully implemented boundary split, we can just split boundary edges in the same way as we do for normal edges. For edge flip, it gets nothing to do with boundary edges. Finally, we can update position for all vertices.
Below is an example of boundary loop subdivision on beetle. We can see the boudaries are more smoother with more triangles after loop subdivision.
Original mesh ![]() |
After 1 loop subdivision ![]() |
Original mesh ![]() |
After 1 loop subdivision ![]() |
If you are not participating in the optional mesh competition, don't worry about this section!