Advanced Computer Graphics

• Assignment 1 - Progressive Meshes

Due: February 12

0. Starter code

- I compiled the started code from source under Mac OS 10.5. I had to include the OpenGL and GLUT frameworks in order to do so in XCode. Later on in the project I use the CBLAS library which also needed to be linked. Here is a simple screenshot of the resulting code running on my machine

1. Mesh Decimation

- Now we get to the really really hard part! The first thing I did was convert the input OFF files into something more useful for mesh operations. I chose the half-edge data structure slthough in retrospect I think the progressive mesh representation outlined by Hoppe et al would have been a better choice. The algorithm to compute the half-edge adjacency information has two major steps
- For every face in the mesh, add its vertices to the adjacency's vertices table, loop around the face and add each edge to the edge table, connect all edges via the next_edge field of edge struct, attach each vertex to an edge and vice versa. Also, the face was linked to one edge and each edge was linked to that face. Here are the three basic structs that were used to accomplish this:

struct he_edge

{

he_vertex* vertex_begin;

he_edge* next_edge;

he_edge* paired_edge;

he_face* left_face;

}

struct he_vertex

{

point* coordinate;

he_edge* edge;

}

struct he_face

{

he_edge* edge;

}

- By connecting the edges, faces, and vertices in this way, the entire mesh can be built into the half-edge data structure. One note is that vertices may be redundant from face to face so in order to ensure they are unique, before being inserted into the vertex table they are checked against all current vertices and dropped if they match anywhere.
- The second step is the edge matching portion of the algorithm. This step requires us to traverse the entire mesh and test each edge's vertices with all other edge's vertices. If the vertices of one edge mathces that of another, they are paired together with each's paired_edge pointer. Once this step has been completed all edges point to a vertex, a face, the next edge around the face, and the edge pointing in the opposite direction.
- I implemented a simple tool to allow a user to step through all of the half edges of a mesh after this conversion has been performed. If you would like to try it out, load a mesh and press the 'X' key to eXplore the edges. You can control the edge you are currently on using the 'w' and 's' keys.

Courtesy: Jason Lawrence

- For every face in the mesh, add its vertices to the adjacency's vertices table, loop around the face and add each edge to the edge table, connect all edges via the next_edge field of edge struct, attach each vertex to an edge and vice versa. Also, the face was linked to one edge and each edge was linked to that face. Here are the three basic structs that were used to accomplish this:
- The next step was to write a method that would collapse a given edge. This is a rather hard function to explain but I will include an illustration at the end to assist me. The first step in the function is to aggregate all of the edges that touch both of the vertices of the edge that is to be collapsed. These are stored in a list. A new vertex position is either passed in as an argument or the average of the two vertices of the edge is used to generate a new vertex. The two old vertices' edge pointers are set to NULL to exclude them from the mesh. For each edge in the aforementioned adjacent edges list, their beginning vertex is set to this new vertex. The next step is to adjoin certain edges around the collapse. All of the edges on the inside of the two adjacent faces to the collapsing edge are removed from the edge list (actually their vertex pointers are set to NULL). Now, the four outer edges of the two adjacent faces (i.e. the outer edges of all of the face's edges less the collapsing edge) are matched. The two outer edges from one face are matched and the two outer edges from the other face are matched. Finally, the two faces' edge pointers are set to NULL, effectively removing them from the mesh.
- This is what the edge looks like before it is collapsed and the associated operations that will happen once it is collapsed. With this function we can collapse any number of edges in a mesh built from the half-edge data structure in O(1) time because we are just following pointers and re-assigning them.
- One additional step to the face collapse is to check for degenerate faces after an edge collapses. This can actually be a bit time consuming because each face that touches the newly created vertex must be compared to every other one to make sure none are degenerate. This operation is easy enough to reason about, but the function to collapse degenerate faces unfortunately tends to produce even more degenerate faces. That being said, the function did work for the test case and usually works. It often produces poor results once a mesh has been decimated a lot. Here is a screenshot of the testpatch.off file after the edge noted in the assignment has been removed. You can see that the new vertex has a degree of 8 as it should since we removed the fins that resulted from the edge collapse.
- The next goal was to compute the quadric error metric for every possible edge collapse and create a heap that would allow the removal of edges whose quadric error was the smallest in turn. For this, an STL priority_queue was used with a comparison function that ensured elements with the least error would be on the top. The heap elements themselves consisted of the following data struct:

struct error_quadric

{

he_edge* edge;

double* vbar;

double error;

bool valid;

}

- This associates each edge with a particular error (which is used to sort the heap), a variable v_{bar}, and a flag indicating whether the element is valid. I will explain the last two in the following sections. For now, it is just important to note that each edge has a particular error associated with its collapse.
- The error that is associated with each edge is derived via the method proposed by Garland et. al. in 1997. For each vertex in the mesh, an error quadric is constructed that is the sum of each adjacent face's plane's outer product. This means that for each adjacent face to a vertex, the 4x1 vector associated with that plane is used to construct a 4x4 matrix K
_{p}= pp^{T}. The quadric error Q of a vertex is the sum of all K_{p}adjacent to it. For each vertex pair in the mesh, a new vertex poisition v_{new}can be solved for by minimizing the sum of the two quadrics associated with the two vertices in the pair. This amounts to solving a linear equation where Q.v_{new}= (0 0 0 1)^{T}or v_{new}= Q^{-1}(0 0 0 1). Since the bottom row of Q does not matter because v is in 3-space, not 4, Q has an analytic closed form solution that can be hard coded. This means that each edge's optimal new vertex position can be quickly calculated by summing the quadrics associated with the vertices that make up that edge, inverting via the analytic form, and multiplying by that (0 0 0 1) vector. The error in using v_{new}is the inner product of v_{new}with the matrix Q, i.e. v_{new}= v_{new}^{T}.Q.v_{new}. EASY! - Now all we do is go through all edges, compute their v
_{new}, compute their error, and add them to the heap. Once that is done we simply pop successive entries off the heap and collapse them using v_{new}as the new vertex. One bit of implementation detail that is of note: When an edge is collapsed, all edge that are in the Ring-1 neighborhood of v_{new}must have their error_quadric struct updated. This means that there are now error_quadrics in the heap that are invalid. When we update the error_quadrics what we really do is create totally new error_quadrics for each new vertex pair with v_{new}and set the valid flags of the old error_quadrics to false so that when we see them come off the heap we just throw them away. - As a last note on this section, the BLAS library (notable the CBLAS library) was used for all matrix operations accept to invert Q, because there is an analytic solution to that. Everything was computed using the double precision routines contained therein.
- In order to decimate a mesh press the 'd' key. This will compute the quadrics for all vertices, create theheap and decimate a preset number of EDGES. If you want to control the number of edges that are decimated at each press of 'd' pass a third argument to the binary that is the number of edges you wish to decimate at each press. A word of warning, some models do not reduce to 1 face without returning an error. You can usually get VERY close (5 faces for the larger models) but few will actually decimate all the way.
- Here are some screenshots of meshes that were decimated using this approach along with their timing results as reported by the program (this is just to decimate the mesh, NOT create the heap although that takes only a few seconds at the very most):

Fins are present after collapse |
Fins removed by the degenerate face collapsing function |

Another view showing a lot of information after collapse |

Decimation from 69451 faces to 866 faces took: 13.854117 seconds |
||

Decimation from 7776 faces to 1776 faces took: 0.167539 seconds |
||

Decimation from 20000 faces to 8129 faces took: 0.467083 seconds |
||

Decimation from 572 faces to 172 faces took: 0.008040 seconds |
||

Decimation from 15214 faces to 3225 faces took: 0.429621 seconds |
||

Decimation from 5804 faces to 1606 faces took: 0.153325 seconds |
||

Decimation from 35840 faces to 9856 faces took: 0.867568 seconds |

2. Progressive Meshes

- In order to view how a mesh decimates a progressive mesh viewer was implemented. This allows a user to step the amount of decimation up or down. In order to accomplish this using a half-edge data structure special care must be taken since all information is stored as a pointer. In order to actually store meshes at different decimation levels the entire mesh must be traversed and saved off so that the interconnectivity can be re-computed easily. For this a new file format was created called the EDG format. A series of decimations was built and saved off into auxillary files and then read in depending on the users selection level. Of course these need to actually be saved to file, but it makes the viewer capable of reloading alread existing decimation data. If you want to try this out for yourself, load the program and press the 'h' key to create the decimation Hierarchy. Then you can use the 'g' and 'b' keys to step up and down respectively through the levels.

3. Bellzzzz and Whistlezzzz

- The new EDG format was mentioned in section 2. This format allows a program to read in the half-edge data structure directly in order to avoid the very expensive precomputation step needed to build it. The code that is included can read in these files, and it is advised that you use the .edg file whenever possible as it will dramatically reduce load times especially for larger models. The format of the EDG file is as follows:

- The first 3 characters must be EDG followed immediately by the number of vertices, faces, and half edges in that order. After that, each vertex's position is enumerated as well as each vertex's edge. The vk_edge item is an integer specifying the index of the edge in this file, so if v0_edge = 0, then v0's edge pointer should point to whatever memory address holds the e0 elements. For a more information on exactly how these are read and wrote please see the functions read_edg and save_edg in the code.

EDG

numVerts numFaces numEdges

v0_x v0_y v0_z v0_edge

.

.

.

vn_x vn_y vn_z vn_edge

f0_edge

.

.

.

fn_edge

e0_vertex e0_pair e0_next e0_face

.

.

.

en_vertex en_pair en_next en_face

numVerts numFaces numEdges

v0_x v0_y v0_z v0_edge

.

.

.

vn_x vn_y vn_z vn_edge

f0_edge

.

.

.

fn_edge

e0_vertex e0_pair e0_next e0_face

.

.

.

en_vertex en_pair en_next en_face

© 2008 Sean M. Arietta

University of Virginia