In this project, we have successfully implemented two key sections: Section 1 focusing on Bezier Curves and
Surfaces, and Section 2 dedicated to Triangle Meshes and the Half-Edge Data Structure. In the first section, we've
developed Bezier Curves utilizing the 1D de Casteljau Subdivision method, along with Bezier Surfaces through the
application of Separable 1D de Casteljau techniques. The second section features the implementation of
Area-Weighted Vertex Normals, Edge Flip, Edge Split, and Loop Subdivision for Mesh Upsampling.
Bezier curves and surfaces are fundamental in modeling smooth curves and surfaces, with Bezier surfaces offering
the benefit of representing complex forms using less memory compared to triangle meshes. However, rendering Bezier
curves directly presents challenges. This is primarily due to the extensive number of control points and weights
that Bezier surfaces comprise, leading to significant computational and storage demands, especially for surfaces
of higher order. As a practical solution, Bezier surfaces are typically converted into triangle meshes prior to
rendering. Given this, triangle meshes are often favored for representing 3D geometric models.
A common method to store triangle meshes involves using a vertex list and a corresponding list of triangles that
index these vertices. Yet, this structure proves inefficient for meaningful mesh traversal. For example,
identifying all triangles adjacent to a specific triangle necessitates iterating through the entire list, a
time-consuming task. To address this issue more efficiently, we have employed the half-edge data structure. This
approach efficiently captures the connectivity information among mesh elements, streamlining operations such as
neighbor identification and traversal within the mesh.
The de Casteljau algorithm is adeptly designed to process 'n' control points alongside a parameter 't' to define a
Bezier curve. The control points collectively outline the shape of the curve, while the parameter 't', which
varies between 0 and 1, is crucial for evaluating the curve at specific points. Central to this algorithm is the
technique of linear interpolation, utilized to compute 'n – 1' intermediate control points for each subdivision
level. These points, denoted as p’1, p’2, …, p’n-1, are calculated using the formula p’i = (1 – t) * pi + t *
p(i+1). By recursively applying this process, the algorithm ultimately converges to a singular point that
precisely corresponds to the parameter 't' on the Bezier curve.
Our implementation of this algorithm iteratively processes the input, represented as a std::vector
Show screenshots of each step / level of the evaluation from the original control points down to the final evaluated point. Press E to step through. Toggle C to show the completed Bezier curve as well.
|
|
|
|
|
The input of the algorithm changes to n x n original control points Pij, where i and j are row and column index,
and the two parameters u and v. For each row of n control points, we can define a Bezier curve parameterized by u.
We can recursively use the algorithm in Part 1 to evaluate the final single point Pi on Bezier curve i. Then we
consider that Pi for n rows define a Bezier curve parameterized by v. We can again recursively use the algorithm
in Part 1 to evaluate the final, single point P on the Bezier curve parameterized by v. The final, single point P,
lies on the Bezier surface parameterized by the u and v.
For the BezierPatch::evaluateStep function, we extend the coordinates from 2D space to 3D space. For i = 0 to n –
2, we compute the n – 1 intermediate points by Vector3D lerp = (1.0 - t) * points[i] + t * points[i + 1]. If there
is only one point in the input, we directly return that point. The BezierPatch::evaluate1D function recursively
call itself until it generates the final, single point lying on the Bezier curve. In function
BezierPatch::evaluate, we first call BezierPatch::evaluate1D n times to compute the n final, single points
parameterized by u. Then we call BezierPatch::evaluate1D again, using these n points and the parameter v as input.
Eventually we generate the final single point lying on the Bezier surface at the given parameter u and v.
Our process begins with a specific halfedge, labeled as s2, the vector from v0 to v2 (v2 - v0), originating from
vertex v0. To determine the final normal vector, it's essential to traverse all the halfedges connected to vertex
v0 and compute the normal vector for each adjacent face. We employ the “next()” and “vertex()” methods to
ascertain the positions of the two neighboring vertices in a given face, for instance, vertices v1 and v2 in our
case.
Once these vertices are identified, we calculate the edge vectors: s1, which is the vector from v0 to v1
(expressed as v1 - v0), and s2, the vector from v0 to v2 (v2 - v0). These vectors are pivotal for computing the
face's normal vector through the cross-product operation, denoted as s1×s2. Additionally, the normal vector is
assigned a weight corresponding to the area of the face, which is also derived using the cross-product magnitude
(||s1×s2||/ 2).
This procedure is repeated for the other halfedges and faces connected to vertex v0, culminating in the
accumulation of the normal vectors. The final step involves normalizing the sum of these vectors to a norm of 1,
thus yielding the resultant final normal vector.
|
|
The process is based on the figure below:
|
|
|
The execution of this task bears resemblance to the approach we adopted in task 4. Fundamentally, it involves modifying pointers and attributes of various elements, including halfedges, vertices, edges, and faces. This process is methodically guided by the diagram provided below:
|
|
|
|
During the debugging phase, we frequently encountered an issue where selecting an edge for splitting inadvertently caused additional edges to split, and some segments of the mesh darkened. This phenomenon was traced back to an inadequacy in our previous variable update process. A notable example is the failure to update the halfedge of a face, leading to a cascade of errors in the model. Consequently, when executing the loop subdivision, a variety of unexpected and anomalous images emerged, indicating inconsistencies in our geometric representation.
In this task, we leverage our previously implemented edge flip and split functions to facilitate the comprehensive
subdivision of Bezier surfaces.
General process
(1)First, we calculate new positions for all original vertices in the input mesh. The formula used is: (1 - n * u)
* original_position + u * original_neighbor_position_sum. We begin by iterating over all vertices in the mesh,
starting from "mesh.verticesBegin()" and ending at "mesh.verticesEnd()". Each vertex is accessed through its
halfedge, enabling us to locate and sum up the positions of all neighboring vertices. We then apply the
aforementioned formula to compute the new position for each vertex.
(2)This step mirrors the previous one, but focuses on traversing all edges to pre-calculate the positions of new
edge points.
(3)In this phase, we split edges in the mesh, ensuring that only edges with both vertices being old are selected
for splitting. This criterion was established during the first process.
(4)We proceed to flip any new edge that connects an old vertex to a new one. This is achieved using an XOR logical
expression to validate the condition. Additionally, we check to confirm that the edge in question is indeed a new
edge, as old edges are not eligible for flipping.
(5)Finally, we assign new positions to the vertices based on the values computed and stored during the first two
processes.
The loop subdivision results for quad ball, cube, and beans are as follows:
|
|
|
|
|
|
(1) The results of the cube image after loop subdivision are as follows:
|
|
|
|
|
|
|