├── README.md ├── example_data ├── pegasus.obj ├── rocketship.ply ├── terrain8k.obj └── tet_with_self_edge.ply ├── tutorial_completed.py └── tutorial_skeleton.py /README.md: -------------------------------------------------------------------------------- 1 | # Geometry Processing with Intrinsic Triangulations 2 | 3 | Intrinsic triangulations are a powerful technique for computing with 3D surfaces. Among other things, they enable existing algorithms to work "out of the box" on poor-quality triangulations. The basic idea is to represent the geometry of a triangle mesh by edge lengths, rather than vertex positions; this change of perspective unlocks many powerful algorithms with excellent robustness to poor-quality triangulations. 4 | 5 | This course gives an overview of intrinsic triangulations and their use in geometry processing, beginning with a general introduction to the basics and historical roots, then covering recent data structures for encoding intrinsic triangulations, and their application to tasks in surface geometry ranging from geodesics to vector fields to parameterization. 6 | 7 | This course was presented at SIGGRAPH 2021 and IMR 2021. 8 | 9 | - **Course Notes**: [(pdf link)](https://nmwsharp.com/media/papers/int-tri-course/int_tri_course.pdf) 10 | - **Course Video**: [(youtube link)](https://www.youtube.com/watch?v=gcRDdYrgOhg) 11 | - **Authors**: [Nicholas Sharp](https://nmwsharp.com/), [Mark Gillespie](https://markjgillespie.com/), [Keenan Crane](http://keenan.is/here) 12 | 13 | 14 | ## Code Tutorial 15 | 16 | We provide an implementation of *intrinsic triangulations* from scratch in Python 3, alongside Lawson's algorithm for flipping to an (intrinsic) Delaunay triangulation. 17 | Using these intrinsic triangulations, we compute geodesic distance via the the [heat method](http://www.cs.cmu.edu/~kmcrane/Projects/HeatMethod/paper.pdf). 18 | 19 | ![Screenshot](http://www.cs.cmu.edu/~mgillesp/IntTriCourse/img_small/ScreenshotDropshadow.png) 20 | 21 | Install dependencies: 22 | ``` 23 | python -m pip install numpy scipy polyscope potpourri3d 24 | ``` 25 | (`python` might be `python3`, depending on your environment) 26 | 27 | Like most PDE-based methods, the heat method may yield inaccurate solutions on low-quality inputs. Running it on a mesh's intrinsic Delaunay triangulation yields dramatically more accurate solutions. 28 | | Mesh | Distance on Original Mesh | Distance on Intrinsic Delaunay Triangulation | 29 | | ------------- |:-------------:| -----:| 30 | | `terrain8k` | ![Terrain8kBadDistances](http://www.cs.cmu.edu/~mgillesp/IntTriCourse/img_small/terrain8k_input.png) | ![Terrain8kBadDistances](http://www.cs.cmu.edu/~mgillesp/IntTriCourse/img_small/terrain8k_idt.png) | 31 | | `pegasus` | ![Terrain8kBadDistances](http://www.cs.cmu.edu/~mgillesp/IntTriCourse/img_small/pegasus_input.png) | ![Terrain8kBadDistances](http://www.cs.cmu.edu/~mgillesp/IntTriCourse/img_small/pegasus_idt.png) | 32 | | `rocketship` | ![Terrain8kBadDistances](http://www.cs.cmu.edu/~mgillesp/IntTriCourse/img_small/rocketship_input.png) | ![Terrain8kBadDistances](http://www.cs.cmu.edu/~mgillesp/IntTriCourse/img_small/rocketship_idt.png) | 33 | -------------------------------------------------------------------------------- /example_data/rocketship.ply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmwsharp/intrinsic-triangulations-tutorial/a9e0dd9f6386bae50f5298776efc9b22babb3ffa/example_data/rocketship.ply -------------------------------------------------------------------------------- /example_data/tet_with_self_edge.ply: -------------------------------------------------------------------------------- 1 | ply 2 | format ascii 1.0 3 | element vertex 3 4 | property float x 5 | property float y 6 | property float z 7 | element face 2 8 | property list uchar uint vertex_indices 9 | end_header 10 | 1.359723 -0.359795 0.170123 11 | -0.808181 0.367495 -0.199186 12 | -0.640277 -0.359795 0.170123 13 | 3 0 1 2 14 | 3 0 2 1 15 | -------------------------------------------------------------------------------- /tutorial_completed.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | ############################################################## 4 | ### Mesh management and traversal helpers 5 | ############################################################## 6 | 7 | def next_side(fs): 8 | """ 9 | For a given side s of a triangle, returns the next side t. (This method serves mainly to make code more readable.) 10 | 11 | :param fs: A face side (f,s) 12 | :returns: The next face side in the same triangle (f, sn) 13 | """ 14 | return (fs[0], (fs[1]+1)%3) 15 | 16 | 17 | def other(G, fs): 18 | """ 19 | For a given face-side fs, returns the neighboring face-side in some other triangle. 20 | 21 | :param G: |F|x3x2 gluing map G, 22 | :param fs: a face-side (f,s) 23 | :returns: The neighboring face-side (f_opp,s_opp) 24 | """ 25 | return tuple(G[fs]) 26 | 27 | def n_faces(F): 28 | """ 29 | Return the number of faces in the triangulation. 30 | 31 | :param F: |F|x3 array of face-vertex indices 32 | :returns: |F| 33 | """ 34 | return F.shape[0] 35 | 36 | def n_verts(F): 37 | """ 38 | Return the number of vertices in the triangulation. 39 | 40 | Note that for simplicity this function recovers the number of vertices from 41 | the face listing only. As a consequence it is _not_ constant-time, and 42 | should not be called in a tight loop. 43 | 44 | :param F: |F|x3 array of face-vertex indices 45 | :returns: |F| 46 | """ 47 | return np.amax(F)+1 48 | 49 | 50 | ############################################################## 51 | ### Geometric subroutines 52 | ############################################################## 53 | 54 | def face_area(l, f): 55 | """ 56 | Computes the area of the face f from edge lengths 57 | 58 | :param l: |F|x3 array of face-side edge lengths 59 | :param f: An integer index specifying the face 60 | :returns: The area of the face. 61 | """ 62 | # Gather edge lengths 63 | l_a = l[f, 0] 64 | l_b = l[f, 1] 65 | l_c = l[f, 2] 66 | 67 | # Heron's rule 68 | s = (l_a + l_b + l_c) / 2 69 | d = s * (s - l_a) * (s - l_b) * (s - l_c) 70 | return np.sqrt(d) 71 | 72 | def surface_area(F,l): 73 | """ 74 | Compute the surface area of a triangulation. 75 | 76 | :param F: A |F|x3 vertex-face adjacency list F 77 | :param l: F |F|x3 edge-lengths array, giving the length of each face-side 78 | :returns: The surface area 79 | """ 80 | area_tot = 0. 81 | for f in range(n_faces(F)): 82 | area_tot += face_area(l,f) 83 | 84 | return area_tot 85 | 86 | def opposite_corner_angle(l, fs): 87 | """ 88 | Computes triangle corner angle opposite the face-side fs. 89 | 90 | :param l: A |F|x3 array of face-side edge lengths 91 | :param fs: An face-side (f,s) 92 | :returns: The corner angle, in radians 93 | """ 94 | # Gather edge lengths 95 | l_a = l[fs] 96 | l_b = l[next_side(fs)] 97 | l_c = l[next_side(next_side(fs))] 98 | 99 | # Law of cosines (inverse) 100 | d = (l_b**2 + l_c**2 - l_a**2) / (2*l_b*l_c); 101 | return np.arccos(d) 102 | 103 | 104 | def diagonal_length(G, l, fs): 105 | """ 106 | Computes the length of the opposite diagonal of the diamond formed by the 107 | triangle containing fs, and the neighboring triangle adjacent to fs. 108 | 109 | This is the new edge length needed when flipping the edge fs. 110 | 111 | :param G: |F|x3x2 gluing map 112 | :param l: |F|x3 array of face-side edge lengths 113 | :param fs: A face-side (f,s) 114 | :returns: The diagonal length 115 | """ 116 | # Gather lengths and angles 117 | fs_opp = other(G, fs) 118 | u = l[next_side(next_side(fs))] 119 | v = l[next_side(fs_opp)] 120 | theta_A = opposite_corner_angle(l, next_side(fs)) 121 | theta_B = opposite_corner_angle(l, next_side(next_side((fs_opp)))) 122 | 123 | # Law of cosines 124 | d = u**2 + v**2 - 2 * u * v * np.cos(theta_A + theta_B) 125 | return np.sqrt(d) 126 | 127 | 128 | def is_delaunay(G, l, fs): 129 | """ 130 | Test if the edge given by face-side fs satisfies the intrinsic Delaunay property. 131 | 132 | :param G: |F|x3x2 gluing map G, 133 | :param l: |F|x3 array of face-side edge lengths 134 | :param fs: A face-side (f,s) 135 | :returns: True if the edge is Delaunay 136 | """ 137 | 138 | fs_opp = other(G, fs) 139 | 140 | theta_A = opposite_corner_angle(l, fs) 141 | theta_B = opposite_corner_angle(l, fs_opp) 142 | 143 | # Test against PI - eps to conservatively pass in cases where theta_A 144 | # + theta_B \approx PI. This ensures the algorithm terminates even in the 145 | # case of a co-circular diamond, in the presence of floating-point errors. 146 | EPS = 1e-5 147 | return theta_A + theta_B <= np.pi + EPS 148 | 149 | 150 | ############################################################## 151 | ### Construct initial data 152 | ############################################################## 153 | 154 | 155 | def build_edge_lengths(V,F): 156 | """ 157 | Compute edge lengths for the triangulation. 158 | 159 | Note that we store a length per face-side, which means that each edge 160 | length appears twice. This is just to make our code simpler. 161 | 162 | :param V: |V|x3 array of vertex positions 163 | :param F: |F|x3 array of face-vertex indices 164 | :returns: The |F|x3 array of face-side lengths 165 | """ 166 | 167 | # Allocate an empty Fx3 array to fill 168 | l = np.empty((n_faces(F),3)) 169 | 170 | for f in range(n_faces(F)): # iterate over triangles 171 | for s in range(3): # iterate over the three sides 172 | 173 | # get the two endpoints (i,j) of this side 174 | i = F[f,s] 175 | j = F[next_side((f,s))] 176 | 177 | # measure the length of the side 178 | length = np.linalg.norm(V[j] - V[i]) 179 | 180 | l[f,s] = length 181 | 182 | return l 183 | 184 | 185 | def sort_rows(A): 186 | """ 187 | Sorts rows lexicographically, i.e., comparing the first column first, then 188 | using subsequent columns to break ties. 189 | 190 | :param A: A 2D array 191 | :returns: A sorted array with the same dimensions as A 192 | """ 193 | return A[np.lexsort(np.rot90(A))] 194 | 195 | 196 | def glue_together(G, fs1, fs2): 197 | """ 198 | Glues together the two specified face sides. Using this routine (rather 199 | than manipulating G directly) just helps to ensure that a basic invariant 200 | of G is always preserved: if a is glued to b, then b is glued to a. 201 | 202 | The gluing map G is updated in-place. 203 | 204 | :param G: |F|x3x2 gluing map 205 | :param fs1: a face-side (f1,s1) 206 | :param fs2: another face-side (f2,s2) 207 | """ 208 | G[fs1] = fs2 209 | G[fs2] = fs1 210 | 211 | 212 | def build_gluing_map(F): 213 | """ 214 | Builds the gluing map for a triangle mesh. 215 | 216 | :param F: |F|x3 vertex-face adjacency list F describing a manifold, oriented triangle mesh without boundary. 217 | :returns: |F|x3x2 gluing map G, which for each side of each face stores the 218 | face-side it is glued to. In particular, G[f,s] is a pair (f',s') such 219 | that (f,s) and (f',s') are glued together. 220 | """ 221 | 222 | # In order to construct this array, for each side of a triangle, we need to 223 | # find the neighboring side in some other triangle. There are many ways that 224 | # this lookup could be accomplished. Here, we use an array-based strategy 225 | # which constructs an `Sx4` array (where `S` is the number of face-sides), 226 | # where each row holds the vertex indices of a face-side, as well as the face 227 | # it comes from and which side it is. We then sort the rows of this array 228 | # lexicographically, which puts adjacent face-sides next to each other in the 229 | # sorted array. Finally, we walk down the array and populate the gluing map 230 | # with adjacent face-side entries. 231 | 232 | 233 | # Build a temporary list S of all face-sides, given by tuples (i,j,f,s), 234 | # where (i,j) are the vertex indices of side s of face f in sorted order 235 | # (i