Welcome to Scotty3D! This 3D graphics software package includes components for interactive mesh
Welcome to Scotty3D! This 3D graphics software package includes components for interactive mesh
editing, realistic path tracing, and dynamic animation. Implementing functionality in each of these areas
editing, realistic path tracing, and dynamic animation. Implementing functionality in each of these areas
constitutes the majority of the coursework for 15-462/662 (Computer Graphics) at Carnegie Mellon University
constitutes the majority of the coursework for 15-462/662 (Computer Graphics) at Carnegie Mellon University
These pages describe how to set up and use Scotty3D. Start here!
These pages describe how to set up and use Scotty3D. Start here!
-[Git Setup](git): create a private git mirror that can pull changes from Scotty3D.
-[Git Setup](git): create a private git mirror that can pull changes from Scotty3D.
-[Building Scotty3D](build): build and run Scotty3D on various platforms.
-[Building Scotty3D](build): build and run Scotty3D on various platforms.
-[User Guide](guide): learn the intended functionality for end users.
-[User Guide](guide): learn the intended functionality for end users.
...
@@ -48,18 +56,18 @@ solution that meets a few fundamental criteria:
...
@@ -48,18 +56,18 @@ solution that meets a few fundamental criteria:
*[Failing gracefully](https://en.wikipedia.org/wiki/Fault_tolerance) is
*[Failing gracefully](https://en.wikipedia.org/wiki/Fault_tolerance) is
preferable to failing utterly---for instance, if a rare corner case is difficult
preferable to failing utterly---for instance, if a rare corner case is difficult
to handle, it is far better to simply refuse to perform the operation than to
to handle, it is far better to simply refuse to perform the operation than to
let the program crash!
let the program crash!
* Your implementation should follow the [principle of least
* Your implementation should follow the [principle of least
surprise](https://en.wikipedia.org/wiki/Principle_of_least_astonishment). A user
surprise](https://en.wikipedia.org/wiki/Principle_of_least_astonishment). A user
should be able to expect that things behave more or less as they are described
should be able to expect that things behave more or less as they are described
in the User Guide.
in the User Guide.
* You should not use an algorithm whose performance is [asymptotically
* You should not use an algorithm whose performance is [asymptotically
worse](https://en.wikipedia.org/wiki/Asymptotic_computational_complexity) just
worse](https://en.wikipedia.org/wiki/Asymptotic_computational_complexity) just
because it makes your code easier to write (for instance, using [bubble
because it makes your code easier to write (for instance, using [bubble
sort](https://en.wikipedia.org/wiki/Bubble_sort) rather than [merge
sort](https://en.wikipedia.org/wiki/Bubble_sort) rather than [merge
sort](https://en.wikipedia.org/wiki/Merge_sort) on large data sets).
sort](https://en.wikipedia.org/wiki/Merge_sort) on large data sets).
* That being said, when it comes to performance, [premature optimization is
* That being said, when it comes to performance, [premature optimization is
the root of all evil!](https://en.wikipedia.org/wiki/Program_optimization#When_to_optimize) The only way to know whether an optimization matters is to [measure performance](https://en.wikipedia.org/wiki/Profiling_(computer_programming)), and understand [bottlenecks](https://en.wikipedia.org/wiki/Program_optimization#Bottlenecks).
the root of all evil!](https://en.wikipedia.org/wiki/Program_optimization#When_to_optimize) The only way to know whether an optimization matters is to [measure performance](https://en.wikipedia.org/wiki/Profiling_(computer_programming)), and understand [bottlenecks](https://en.wikipedia.org/wiki/Program_optimization#Bottlenecks).
* Finally, you should take pride in your craft. Beautiful things just tend to work better.
* Finally, you should take pride in your craft. Beautiful things just tend to work better.
Just to reiterate the main point above:
Just to reiterate the main point above:
...
@@ -75,5 +83,5 @@ good design choices with you, and you should also feel free to discuss these
...
@@ -75,5 +83,5 @@ good design choices with you, and you should also feel free to discuss these
choices with your classmates. Practically speaking, it is ok for routines to
choices with your classmates. Practically speaking, it is ok for routines to
simply show an error if they encounter a rare and difficult corner case---as long as it
simply show an error if they encounter a rare and difficult corner case---as long as it
does not interfere with successful operation of the program (i.e., if it does
does not interfere with successful operation of the program (i.e., if it does
not crash or yield bizarre behavior). Your main goal here above all else should be
not crash or yield bizarre behavior). Your main goal here above all else should be
to develop _effective tool for modeling, rendering, and animation_.
to develop _effective tool for modeling, rendering, and animation_.
Here we provide some additional detail about the bevel operations and their implementation in Scotty3D. Each bevel operation has two components:
Here we provide some additional detail about the bevel operations and their implementation in Scotty3D. Each bevel operation has two components:
1. a method that modifies the _connectivity_ of the mesh, creating new beveled elements, and
1. a method that modifies the _connectivity_ of the mesh, creating new beveled elements, and
2. a method the updates the _geometry_ of the mesh, insetting and offseting the new vertices according to user input.
2. a method the updates the _geometry_ of the mesh, insetting and offseting the new vertices according to user input.
The methods that update the connectivity are `HalfedgeMesh::bevel_vertex`, `halfedgeMesh::bevel_edge`, and `HalfedgeMesh::bevel_face`. The methods that update geometry are `HalfedgeMesh::bevel_vertex_positions`, `HalfedgeMesh::bevel_edge_positions`, and `HalfedgeMesh::bevel_face_positions`.
The methods that update the connectivity are `HalfedgeMesh::bevel_vertex`, `halfedgeMesh::bevel_edge`, and `HalfedgeMesh::bevel_face`. The methods that update geometry are `HalfedgeMesh::bevel_vertex_positions`, `HalfedgeMesh::bevel_edge_positions`, and `HalfedgeMesh::bevel_face_positions`.
The methods for updating connectivity can be implemented following the general strategy outlined in [edge flip tutorial](edge_flip). **Note that the methods that update geometry will be called repeatedly for the same bevel, in order to adjust positions according to user mouse input. See the gif in the [User Guide](../guide/model).**
The methods for updating connectivity can be implemented following the general strategy outlined in [edge flip tutorial](edge_flip). **Note that the methods that update geometry will be called repeatedly for the same bevel, in order to adjust positions according to user mouse input. See the gif in the [User Guide](../guide/model).**
...
@@ -31,13 +33,13 @@ Also note that we provide code to gather the halfedges contained in the beveled
...
@@ -31,13 +33,13 @@ Also note that we provide code to gather the halfedges contained in the beveled
The reason for storing `new_halfedges` and `start_positions` in an array is that it makes it easy to access positions "to the left" and "to the right" of a given vertex. For instance, suppose we want to figure out the offset from the corner of a polygon. We might want to compute some geometric quantity involving the three vertex positions `start_positions[i-1]`, `start_positions[i]`, and `start_positions[i+1]` (as well as `inset`), then set the new vertex position `new_halfedges[i]->vertex()->pos` to this new value:
The reason for storing `new_halfedges` and `start_positions` in an array is that it makes it easy to access positions "to the left" and "to the right" of a given vertex. For instance, suppose we want to figure out the offset from the corner of a polygon. We might want to compute some geometric quantity involving the three vertex positions `start_positions[i-1]`, `start_positions[i]`, and `start_positions[i+1]` (as well as `inset`), then set the new vertex position `new_halfedges[i]->vertex()->pos` to this new value:
![BevelIndexing](bevel_indexing.png)
<center><imgsrc="bevel_diagram.png"></center>
A useful trick here is _modular arithmetic_: since we really have a "loop" of vertices, we want to make sure that indexing the next element (+1) and the previous element (-1) properly "wraps around." This can be achieved via code like
A useful trick here is _modular arithmetic_: since we really have a "loop" of vertices, we want to make sure that indexing the next element (+1) and the previous element (-1) properly "wraps around." This can be achieved via code like
// Get the number of vertices in the new polygon
// Get the number of vertices in the new polygon
int N = (int)hs.size();
int N = (int)hs.size();
// Assuming we're looking at vertex i, compute the indices
// Assuming we're looking at vertex i, compute the indices
// of the next and previous elements in the list using
// of the next and previous elements in the list using
// modular arithmetic---note that to get the previous index,
// modular arithmetic---note that to get the previous index,
...
@@ -47,7 +49,7 @@ A useful trick here is _modular arithmetic_: since we really have a "loop" of ve
...
@@ -47,7 +49,7 @@ A useful trick here is _modular arithmetic_: since we really have a "loop" of ve
int a = (i+N-1) % N;
int a = (i+N-1) % N;
int b = i;
int b = i;
int c = (i+1) % N;
int c = (i+1) % N;
// Get the actual 3D vertex coordinates at these vertices
// Get the actual 3D vertex coordinates at these vertices
@@ -12,15 +15,15 @@ Here we provide a step-by-step guide to implementing a simplified version of the
...
@@ -12,15 +15,15 @@ Here we provide a step-by-step guide to implementing a simplified version of the
We now consider the case of a triangle-triangle edge flip.
We now consider the case of a triangle-triangle edge flip.
#### PHASE 0: Draw a Diagram
### PHASE 0: Draw a Diagram
Suppose we have a pair of triangles (a,b,c) and (c,b,d). After flipping the edge (b,c), we should now have triangles (a,d,c) and (a,b,d). A good first step for implementing any local mesh operation is to draw a diagram that clearly labels all elements affected by the operation:
Suppose we have a pair of triangles (a,b,c) and (c,b,d). After flipping the edge (b,c), we should now have triangles (a,d,c) and (a,b,d). A good first step for implementing any local mesh operation is to draw a diagram that clearly labels all elements affected by the operation:
![](edge_flip_before_after.png)
<center><imgsrc="edge_flip_diagram.png"></center>
Here we have drawn a diagram of the region around the edge both before and after the edge operation (in this case, "flip"), labeling each type of element (halfedge, vertex, edge, and face) from zero to the number of elements. It is important to include every element affected by the operation, thinking very carefully about which elements will be affected. If elements are omitted during this phase, everything will break---even if the code written in the two phases is correct! In this example, for instance, we need to remember to include the halfedges "outside" the neighborhood, since their "twin" pointers will be affected.
Here we have drawn a diagram of the region around the edge both before and after the edge operation (in this case, "flip"), labeling each type of element (halfedge, vertex, edge, and face) from zero to the number of elements. It is important to include every element affected by the operation, thinking very carefully about which elements will be affected. If elements are omitted during this phase, everything will break---even if the code written in the two phases is correct! In this example, for instance, we need to remember to include the halfedges "outside" the neighborhood, since their "twin" pointers will be affected.
#### PHASE I: Collect elements
### PHASE I: Collect elements
Once you've drawn your diagram, simply collect all the elements from the "before" picture. Give them the same names as in your diagram, so that you can debug your code by comparing with the picture.
Once you've drawn your diagram, simply collect all the elements from the "before" picture. Give them the same names as in your diagram, so that you can debug your code by comparing with the picture.
...
@@ -35,22 +38,22 @@ Once you've drawn your diagram, simply collect all the elements from the "before
...
@@ -35,22 +38,22 @@ Once you've drawn your diagram, simply collect all the elements from the "before
HalfedgeRef h7 = h2->twin();
HalfedgeRef h7 = h2->twin();
HalfedgeRef h8 = h4->twin();
HalfedgeRef h8 = h4->twin();
HalfedgeRef h9 = h5->twin();
HalfedgeRef h9 = h5->twin();
// VERTICES
// VERTICES
VertexRef v0 = h0->vertex();
VertexRef v0 = h0->vertex();
VertexRef v1 = h3->vertex();
VertexRef v1 = h3->vertex();
// ...you fill in the rest!...
// ...you fill in the rest!...
// EDGES
// EDGES
EdgeRef e1 = h5->edge();
EdgeRef e1 = h5->edge();
EdgeRef e2 = h4->edge();
EdgeRef e2 = h4->edge();
// ...you fill in the rest!...
// ...you fill in the rest!...
// FACES
// FACES
FaceRef f0 = h0->face();
FaceRef f0 = h0->face();
// ...you fill in the rest!...
// ...you fill in the rest!...
#### PHASE II: Allocate new elements
### PHASE II: Allocate new elements
If your edge operation requires new elements, now is the time to allocate them. For the edge flip, we don't need any new elements; but suppose that for some reason we needed a new vertex v4\. At this point we would allocate the new vertex via
If your edge operation requires new elements, now is the time to allocate them. For the edge flip, we don't need any new elements; but suppose that for some reason we needed a new vertex v4\. At this point we would allocate the new vertex via
...
@@ -58,7 +61,7 @@ If your edge operation requires new elements, now is the time to allocate them.
...
@@ -58,7 +61,7 @@ If your edge operation requires new elements, now is the time to allocate them.
(The name used for this new vertex should correspond to the label you give it in your "after" picture.) Likewise, new edges, halfedges, and faces can be allocated via the methods `mesh.new_edge()`, `mesh.new_halfedge()`, and `mesh.new_face()`.
(The name used for this new vertex should correspond to the label you give it in your "after" picture.) Likewise, new edges, halfedges, and faces can be allocated via the methods `mesh.new_edge()`, `mesh.new_halfedge()`, and `mesh.new_face()`.
#### PHASE III: Reassign Elements
### PHASE III: Reassign Elements
Next, update the pointers for all the mesh elements that are affected by the edge operation. Be exhaustive! In other words, go ahead and specify every pointer for every element, even if it did not change. Once things are working correctly, you can always optimize by removing unnecessary assignments. But get it working correctly first! Correctness is more important than efficiency.
Next, update the pointers for all the mesh elements that are affected by the edge operation. Be exhaustive! In other words, go ahead and specify every pointer for every element, even if it did not change. Once things are working correctly, you can always optimize by removing unnecessary assignments. But get it working correctly first! Correctness is more important than efficiency.
...
@@ -74,27 +77,27 @@ Next, update the pointers for all the mesh elements that are affected by the edg
...
@@ -74,27 +77,27 @@ Next, update the pointers for all the mesh elements that are affected by the edg
h1->edge() = e3;
h1->edge() = e3;
h1->face() = f0;
h1->face() = f0;
// ...you fill in the rest!...
// ...you fill in the rest!...
// ...and don't forget about the "outside" elements!...
// ...and don't forget about the "outside" elements!...
h9->next() = h9->next(); // didn't change, but set it anyway!
h9->next() = h9->next(); // didn't change, but set it anyway!
h9->twin() = h4;
h9->twin() = h4;
h9->vertex() = v1;
h9->vertex() = v1;
h9->edge() = e1;
h9->edge() = e1;
h9->face() = h9->face(); // didn't change, but set it anyway!
h9->face() = h9->face(); // didn't change, but set it anyway!
// VERTICES
// VERTICES
v0->halfedge() = h2;
v0->halfedge() = h2;
v1->halfedge() = h5;
v1->halfedge() = h5;
v2->halfedge() = h4;
v2->halfedge() = h4;
v3->halfedge() = h3;
v3->halfedge() = h3;
// EDGES
// EDGES
e0->halfedge() = h0; //...you fill in the rest!...
e0->halfedge() = h0; //...you fill in the rest!...
// FACES
// FACES
f0->halfedge() = h0; //...you fill in the rest!...
f0->halfedge() = h0; //...you fill in the rest!...
#### PHASE IV: Delete unused elements
### PHASE IV: Delete unused elements
If your edge operation eliminates elements, now is the best time to deallocate them: at this point, you can be sure that they are no longer needed. For instance, since we do not need the vertex allocated in PHASE II, we could write
If your edge operation eliminates elements, now is the best time to deallocate them: at this point, you can be sure that they are no longer needed. For instance, since we do not need the vertex allocated in PHASE II, we could write
...
@@ -102,7 +105,7 @@ If your edge operation eliminates elements, now is the best time to deallocate t
...
@@ -102,7 +105,7 @@ If your edge operation eliminates elements, now is the best time to deallocate t
You should be careful that this mesh element is not referenced by any other element in the mesh. But if your "before" and "after" diagrams are correct, that should not be an issue!
You should be careful that this mesh element is not referenced by any other element in the mesh. But if your "before" and "after" diagrams are correct, that should not be an issue!
#### Design considerations
### Design considerations
The basic algorithm outlined above will handle most edge flips, but you should also think carefully about possible corner-cases. You should also think about other design issues, like "how much should this operation cost?" For instance, for this simple triangle-triangle edge flip it might be reasonable to:
The basic algorithm outlined above will handle most edge flips, but you should also think carefully about possible corner-cases. You should also think about other design issues, like "how much should this operation cost?" For instance, for this simple triangle-triangle edge flip it might be reasonable to:
@@ -23,4 +27,4 @@ Three subdivision schemes are supported by Scotty3D: [Linear](linear), [Catmull-
...
@@ -23,4 +27,4 @@ Three subdivision schemes are supported by Scotty3D: [Linear](linear), [Catmull-
## Performance
## Performance
All subdivision operations, as well as re-meshing and simplification, should complete almost instantaneously (no more than a second) on meshes of a few hundred polygons or fewer. If performance is worse than this, ensure that implementations are not repeatedly iterating over more elements than needed, or allocating/deallocating more memory than necessary. A useful debugging technique is to print out (or otherwise keep track of, e.g., via an integer counter or a profiler) the number of times basic methods like `Halfedge::next()` or `Halfedge_Mesh::new_vertex()` are called during a single execution of one of the methods; for most methods this number should be some reasonably small constant (no more than, say, 1000!) times the number of elements in the mesh.
All subdivision operations, as well as re-meshing and simplification, should complete almost instantaneously (no more than a second) on meshes of a few hundred polygons or fewer. If performance is worse than this, ensure that implementations are not repeatedly iterating over more elements than needed, or allocating/deallocating more memory than necessary. A useful debugging technique is to print out (or otherwise keep track of, e.g., via an integer counter or a profiler) the number of times basic methods like `Halfedge::next()` or `Halfedge_Mesh::new_vertex()` are called during a single execution of one of the methods; for most methods this number should be some reasonably small constant (no more than, say, 1000!) times the number of elements in the mesh.