Implemented Bezier curves and expanded them to also work on 3D surfaces. Then, implemented Phong shading so that meshes can have better shading. Finally, implemented loop subdivision to increase resolution for low resolution polygon meshes by implementing local remesh operations `flipEdge` and `splitEdge`.
In this task, we implemented de Casteljau's algorithm for 1D which is useful for finding Bezier curves.
de Casteljau's algorithm takes the given control points, inserts a point using linear interpolation between each control point, and does this recursively until we are left with only one point. Essentially, it recursively finds intermediate control points between each pair of given control points using linear interpolation until only one point remains. This enables for a smooth curve.
The implementation is straightforward. Simply iterate through the given control points, which is a list of points (x, y) and during the iteration, perform linear interpolation between the current point and the next point and add this interpolated point to a new list of points. This new list containing intermediate points is returned. Eventually, this new list will only contain one final point that lies on the Bezier curve. This final point is the red point shown below in the images.
|
|
|
|
|
|
when parameter t is changed
If we have 6 control points, we would have to do linear interpolation 5 times to get 5 intermediate control points for the first step of de Casteljau's algorithm. Then, with these 5 points, we would have to do linear interpolation 4 times to get 4 intermediate points. This repeats until we have one final control point as shown above in the images. Essentially, given n points, we have to find n-1 intermediate points using linear interpolation until we are left with one final point that lies on the Bezier curve. This algorithm is used to find the bezier curve as indicated by the green curve above.
The parameter t ranges from 0 to 1 and determines the position of the final intermediate point. For example, when t = 0, this corresponds to the starting point. In the gif above, it would be the leftmost and bottommost point whereas when t = 1, this corresponds to the last point which is the rightmost and bottommost point. The parameter t essentially controls how far along the curve the interpolated point should be.
In this task, we extended de Casteljau's algorithm to work with surfaces.
Instead of working with a list of 2D coordinates, we are now given a grid, or matrix, of 3D coordinates. We first find Bezier curve points for each row of points using de Casteljau's algorithm. Then, we find the Bezier surface point on the computed Bezier curve points.
This function is called when we want to evaluate the next step of de Casteljau's algorithm and is a helper function for `evaluate1D`. It is the same logic as the `evaluateStep` function for BezierCurve except we work with 3D coordinates.
This function is called when we want to fully evaluate de Casteljau's algorithm given a list of control points and pararmeter t. It calls `evaluateStep` until we are left with one final point which would be the evaluated curve point.
This function finds us the Bezier surface point given a grid of control points and parameters u and v by making use of the helper functions from above: `evaluate1D` and `evaluateStep`. As mentioned above, we first perform de Casteljau's algorithm on the rows using parameter u to find Bezier curve points. Then, the final surface point is determined by again calling `evaluate1D` but instead with parameter v.
Screenshot of bez/teapot.bez
In this task, we implemented area-weighted normal vectors at vertices. These vertex normals can be used for Phong shading which is a better shading for smooth shading compared to flat shading.
To implement this, we find all the neighboring faces that share the given vertex and for each face, find the area and normal. Then, compute the area weighted norm. We can then sum all the area weighted norms of each face and normalize it. This would be the unit normal at the given vertex.
|
|
In this task, we implemented a remeshing operation on an edge. This operation flips an edge by changing its two endpoints, or vertices, to another pair of vertices.
To implement this, we wrote down a list of all elements: half-edges, vertices, edges, and faces in a simple mesh. The simple mesh consists of two triangles glued together by a half-edge. This list of elements represents what the elements were originally pointing to before the flip operation. This list is important because it ensures that we do not forget to reassign the pointers after the flip operation. Finally, we perform the flip operation by changing the given edge's two vertices to the other two vertices in the simple mesh and reassigning all pointers. Keeping track of all elements and doing so correctly was crucial in ensuring that they were no bugs in this function. This required a complete understanding of the halfedge data structure which we did not have initially. We were confused with halfedge->vertex; we initially thought halfedge->vertex meant the vertex pointed to by the halfedge. After fully understanding how the halfedge datastructure worked, we were able to easily implement the flip operation.
|
|
In this task, we implemented a remeshing operation on an edge. This operation splits the given edge and inserts a new vertex at the midpoint and connects the two opposing vertex that were not originally connected by the given edge.
Implementing this was similar to the implementation of `flipEdge` except now, we have to account for new elements. To implement this, we first drew a diagram of two triangles connected by a half-edge before the split. We labeled every pointer in the diagram before we perform the operation just like we did in `flipEdge`: halfedges, vertices, edges, and faces. In code, we tracked all these pointers. Then, we drew a diagram of what would happen to these two triangles after the split operation and labeled what each pointer would point to. In code, this is done by using `setNeighbor` function; we also had to create new halfedges, new vertices, new edges, and faces which is done by calling `newHalfedge`, `newVertex`, `newEdge`, and `newFace`. Overall, these diagrams were key in helping us implement this function because they helped us easily determine what each pointer should be reassigned to.
|
|
|
In this task, we implement loop subdivision which increases the number of triangles in the mesh. This way, we can convert a low resolution polygon mesh into a higher resolution. This is a similar concept to supersampling in antialiasing.
To implement this,
Following these steps, we were able to make the upsample function work. However, it did not produce the expected results. There were two bugs. Initially, the upsample function creates a mesh with all the faces missing. This was because we did not set any edges to be `isNew` after splitting. After this was fixed, we encountered an error where the upsampled mesh results in a mesh that is missing polygons. This was not a bug from the upsample function but rather a bug from our `splitEdge` function. The problem was fixed by properly reassigning all the pointers, new and old, of the input mesh of the function.
After loop subdivision, sharp corners and edges become smoother since the number of polygons in the mesh are increased. You can reduce this effect by pre-splitting edges near the sharp corners and edges.
|
|
|
|
Similarly, the cube is not symmetrical after repeated subdivisions because originally, for each of its faces, it only has one inner edge which causes it to be asymmetric. The fix is to pre-split this edge for each face and then do subdivision.
|
|
|
|