1 module beziermeshmaker.datastructures.quadmesh; 2 3 import std.math; 4 import std.container; 5 import std.conv; 6 import std.algorithm; 7 import std.format; 8 9 public import beziermeshmaker.datastructures.meshpoint; 10 public import beziermeshmaker.datastructures.quadcell; 11 import beziermeshmaker.datastructures.input.polygon; 12 import beziermeshmaker.datastructures.input.polymesh; 13 import beziermeshmaker.datastructures.vec3; 14 15 /* 16 * The algorithm implemented here is from the paper Biquartic C1 Spline Surfaces Over Irregular Meshes, by Jörg Peters 1994 17 */ 18 class QuadMesh { 19 QuadCell[] cells; 20 21 MeshPoint[vec3] meshPoints; 22 23 float gammaBlend = 0.125; //Recommended default 24 25 /* 26 * Takes a polygon mesh and converts it into a linked bezier quilt. Bezier surfaces on the edge of the quilt are underspecified 27 * and will be dropped, though if fixEdges is true they will be treated as though there were additional neighboring polygons with 28 * the same side length and normal. 29 */ 30 this(PolyMesh polyMesh, float gammaBlend, bool fixEdges = true) { 31 this.gammaBlend = gammaBlend; 32 this(polyMesh, fixEdges); 33 } 34 this(PolyMesh polyMesh, bool fixEdges = true) { 35 //Construct all of the quad cells, making sure that any common corners map to the same MeshPoint 36 subdivideMeshAndLinkVertices(polyMesh); 37 38 //At this point, all of the MeshPoints' surrounding quads are set, and all of the QuadCell vertices are set. 39 //It remains only to find the QuadCells' neighbors. 40 linkQuads(); 41 42 if (fixEdges) { 43 //The algorithm has to drop the quads on the edges, since they're otherwise underspecified. To get around this, we add 44 //an extra set of edge quads to expand the mesh first. 45 addEdgeQuads(); 46 } 47 48 //If adjacent quads have inconsistent vertex orders/normals, it breaks the connection guarantee Pi(0, t) = Pi+1(t, 0) 49 //This can still happen for non-orientable surfaces, though all quads for a given input polygon will share the same final orientation. 50 //This means that transitioning between quads with different orientations can only happen when X or Y equals 0, and the coordinate transformation 51 //becomes source.xy = dest.xy, rather than source.xy = dest.yx 52 orientNormals(); 53 54 //All neighbors of a mesh point are guaranteed to share it as a vertex, but they may be out of order (i.e. point.neighbors[0] and point.neighbors[1] don't have a common edge. 55 foreach (MeshPoint point ; meshPoints) { 56 sortMeshPointNeighbors(point); 57 } 58 59 //Now that everything is linked, we can start setting the intermediate control points. 60 foreach (QuadCell cell ; cells) { 61 cell.calculateCenter(); 62 } 63 foreach (QuadCell cell ; cells) { 64 cell.calculatePrimes(); 65 } 66 //It's more trouble than it's worth to precalculate bValues 67 //Honestly there's not much point in precalculating the others either... 68 69 calculateControlPoints(); 70 } 71 72 /* 73 * This subdivides the mesh such that each polygon becomes N quad patches, where N is the number of sides it had. 74 * This also links the vertices with their connected cells and vice versa. 75 */ 76 private void subdivideMeshAndLinkVertices(PolyMesh polyMesh) { 77 foreach (Polygon poly ; polyMesh.polygons) { 78 MeshPoint polyCenter = getOrDefineMeshPoint(poly.centroid, MeshPoint.P_TYPE_CENTROID); 79 for (int i = 1; i <= poly.vertices.length; i++) { 80 vec3 prev = poly.vertices[i - 1]; 81 vec3 curr = poly.vertices[i % poly.vertices.length]; 82 vec3 next = poly.vertices[(i + 1) % poly.vertices.length]; 83 84 MeshPoint originalVertex = getOrDefineMeshPoint(curr, MeshPoint.P_TYPE_ORIGINAL); 85 MeshPoint mid1 = getOrDefineMeshPoint(curr.interpolate(0.5, next), MeshPoint.M_TYPE); 86 MeshPoint mid2 = getOrDefineMeshPoint(curr.interpolate(0.5, prev), MeshPoint.M_TYPE); 87 88 QuadCell cell = new QuadCell(); 89 cell.vertices = [originalVertex, mid1, polyCenter, mid2]; 90 cells ~= [cell]; 91 92 cell.metadata = poly.metadata.dup; 93 cell.metadata[QuadCell.VERTEX_METADATA_KEY] = to!string(i); 94 95 //Each cell will be added exactly once to each of the mesh points 96 //They won't necessarily be in order, but that doesn't impact the rest of the algorithm 97 foreach (MeshPoint point ; cell.vertices) { 98 point.neighbors ~= [cell]; 99 } 100 } 101 } 102 } 103 private MeshPoint getOrDefineMeshPoint(vec3 pt, int type) { 104 if (pt in meshPoints) { 105 return meshPoints[pt]; 106 } 107 else { 108 meshPoints[pt] = new MeshPoint(pt, type); 109 return meshPoints[pt]; 110 } 111 } 112 113 /* 114 * Goes through the list of quads and makes sure adjacent quads are linked to each other. 115 */ 116 private void linkQuads() { 117 foreach (QuadCell cell ; cells) { 118 for(int i = 0; i < 4; i++) { 119 MeshPoint curr = cell.vertices[i]; 120 MeshPoint next = cell.vertices[ (i+1) % 4]; 121 122 //We know that the cell that should be connected has two vertices in common, so iterate through the cells with a common vertex and search for one with a second matching vertex. 123 foreach (QuadCell sharedVertexCell ; curr.neighbors) { 124 if (cell != sharedVertexCell && sharedVertexCell.containsVertex(next)) { 125 cell.setNeighbor(i, sharedVertexCell); 126 break; 127 } 128 } 129 } 130 } 131 } 132 133 /* 134 * Adds additional quads so that the edges of the input mesh won't be dropped. 135 * After this, the new quads will be the only ones missing neighbors, so they'll be dropped during the last part of 136 * the algorithm, leaving exactly the set of quads that were part of the original mesh. 137 */ 138 private void addEdgeQuads() { 139 foreach (QuadCell cell ; cells) { 140 for (int i = 0; i < 4; i++) { 141 QuadCell neighbor = cell.getNeighbor(i); 142 if (neighbor !is null) { 143 continue; 144 } 145 146 MeshPoint sharedA = cell.vertices[i]; 147 MeshPoint sharedB = cell.vertices[ (i+1) % 4]; 148 149 MeshPoint oppositeA = cell.vertices[ (i+3) % 4]; 150 MeshPoint oppositeB = cell.vertices[ (i+2) % 4]; 151 152 //Extend the edges between sharedA and sharedB in the other direction to form the missing vertices of the new quad 153 MeshPoint newPointOppositeA = new MeshPoint(sharedA.pt + (sharedA.pt - oppositeA.pt), oppositeA.ptType); 154 MeshPoint newPointOppositeB = new MeshPoint(sharedB.pt + (sharedB.pt - oppositeB.pt), oppositeB.ptType); 155 156 QuadCell edgeCell = new QuadCell(); 157 //There are really only two cases where this can happen (i == 0 and i == 3), since sides 0 and 3 are the ones that 158 //are on the edge of the original polygon. 159 if (sharedA.ptType == MeshPoint.P_TYPE_ORIGINAL) { 160 edgeCell.vertices = [sharedA, sharedB, newPointOppositeB, newPointOppositeA]; 161 } 162 else { 163 edgeCell.vertices = [sharedB, newPointOppositeB, newPointOppositeA, sharedA]; 164 } 165 cells ~= [edgeCell]; 166 sharedA.neighbors ~= [edgeCell]; 167 sharedB.neighbors ~= [edgeCell]; 168 cell.setNeighbor(i, edgeCell); 169 edgeCell.setNeighbor(i, cell); 170 } 171 } 172 } 173 174 /* 175 * Ensures that all quads have consistent orientation. Note that there will still be inconsistencies if the surface is non-orientable. 176 */ 177 private void orientNormals() { 178 bool[QuadCell] seen; 179 180 QuadCell first = cells[0]; 181 182 DList!QuadCell toProcess; 183 toProcess.insertFront(first); 184 185 while(!toProcess.empty) { 186 QuadCell current = toProcess.removeAny(); 187 assert(current !is null); 188 seen[current] = true; 189 190 for (int i = 0; i < 4; i++) { 191 QuadCell neighbor = current.getNeighbor(i); 192 if (neighbor !is null && neighbor !in seen) { 193 if(neighbor.getNeighbor(i) == current) { 194 //This ensures that we flip all quads that were part of an original polygon at the same time. 195 //This means that even for non-orientable surfaces, there's a consistent rule for the coordinate transformation 196 //between quads. 197 MeshPoint pCentroid = neighbor.getCentroidPoint(); 198 foreach (QuadCell centroidNeighbor ; pCentroid.neighbors) { 199 flipQuadVertexOrder(centroidNeighbor); 200 } 201 } 202 seen[neighbor] = true; 203 toProcess.insertFront(neighbor); 204 } 205 } 206 } 207 } 208 private void flipQuadVertexOrder(QuadCell cell) { 209 MeshPoint temp = cell.vertices[3]; 210 cell.vertices[3] = cell.vertices[1]; 211 cell.vertices[1] = temp; 212 213 QuadCell tempNeighbor = cell.getNeighbor(0); 214 cell.setNeighbor(0, cell.getNeighbor(3)); 215 cell.setNeighbor(3, tempNeighbor); 216 tempNeighbor = cell.getNeighbor(1); 217 cell.setNeighbor(1, cell.getNeighbor(2)); 218 cell.setNeighbor(2, tempNeighbor); 219 } 220 221 /* 222 * Goes through the set of quads and calculates the coefficients for their bezier patches. 223 */ 224 private void calculateControlPoints() { 225 foreach (QuadCell cell ; cells) { 226 if (!cell.hasAllNeighbors() ) { 227 continue; 228 } 229 230 calculateControlPoints(cell, cell.vertices[0], cell.vertices[1], false); 231 calculateControlPoints(cell, cell.vertices[0], cell.vertices[3], true); 232 calculateControlPoints(cell, cell.vertices[2], cell.vertices[1], true); 233 calculateControlPoints(cell, cell.vertices[2], cell.vertices[3], false); 234 } 235 } 236 private void calculateControlPoints(QuadCell cell, MeshPoint pPoint, MeshPoint mPoint, bool flipIJ) { 237 //The [0][0] control point is always at the P_TYPE_MESH point, and the [4][4] control point at the P_TYPE_CENTROID point 238 bool invertIndices = (pPoint.ptType == MeshPoint.P_TYPE_CENTROID); 239 240 float c = cos( (2 * PI) / pPoint.neighbors.length); 241 //These are used because we don't have guarantees about the order of the neighbors of a point 242 QuadCell mNotPNeighbor = cell.getNeighborSharingANotB(mPoint, pPoint); 243 QuadCell mpNeighbor = cell.getNeighborSharingBoth(mPoint, pPoint); 244 245 vec3 b00 = cell.pPrime[pPoint]; 246 vec3 b01 = (3 * cell.getB1(pPoint, mPoint) + b00) / 4; 247 vec3 b02 = (3 * cell.getB2(pPoint, mPoint) + 3 * cell.getB1(pPoint, mPoint)) / 6; 248 249 vec3 b04 = new vec3(0,0,0); 250 for (int i = 0; i < mPoint.neighbors.length; i++) { 251 b04 = b04 + mPoint.neighbors[i].center / 4; 252 } 253 vec3 b03 = (3 * cell.getB2(pPoint, mPoint) + b04) / 4; 254 255 vec3 b11 = ((3 * c) / 8) * cell.center + ((6 - (3*c)) / 8) * cell.cPrime[pPoint] + b00 / 4; 256 vec3 b12 = (c / 16) * mNotPNeighbor.center + ((8-c)/16) * cell.center + cell.cPrime[pPoint] / 2 257 - gammaBlend * (cell.center - mpNeighbor.center + cell.cPrime[pPoint] - mpNeighbor.cPrime[pPoint]); 258 vec3 b13 = (3 * cell.center + b04) / 4; 259 260 vec3 b22 = cell.center; 261 262 setControlPoint(cell, b00, 0, 0, flipIJ, invertIndices); 263 setControlPoint(cell, b01, 0, 1, flipIJ, invertIndices); 264 setControlPoint(cell, b02, 0, 2, flipIJ, invertIndices); 265 setControlPoint(cell, b03, 0, 3, flipIJ, invertIndices); 266 setControlPoint(cell, b04, 0, 4, flipIJ, invertIndices); 267 setControlPoint(cell, b11, 1, 1, flipIJ, invertIndices); 268 setControlPoint(cell, b12, 1, 2, flipIJ, invertIndices); 269 setControlPoint(cell, b13, 1, 3, flipIJ, invertIndices); 270 setControlPoint(cell, b22, 2, 2, flipIJ, invertIndices); 271 } 272 private void setControlPoint(QuadCell cell, vec3 value, int i, int j, bool flipIJ, bool invertIndices) { 273 if (invertIndices) { 274 i = 4 - i; 275 j = 4 - j; 276 } 277 if (flipIJ) { 278 int temp = i; 279 i = j; 280 j = temp; 281 } 282 283 if (cell.surfacePatch.controlPoints[i][j] !is null && cell.surfacePatch.controlPoints[i][j] != value) { 284 throw new Exception(format("Control point reassigned: was %s, became: %s", cell.surfacePatch.controlPoints[i][j], value)); 285 } 286 287 cell.surfacePatch.controlPoints[i][j] = value; 288 } 289 290 //This ensures that the neighbors of a MeshPoint are in order corresponding to their spatial orientation around the point. 291 //When C' is calculated, the cosine term ensures the nearby neighbors in either direction have a scaled effect on the result. Because 292 //it's a cosine, the order doesn't matter, it just needs to be in order 293 private void sortMeshPointNeighbors(MeshPoint point) { 294 //3 is the minimum number of choices for there to be multiple orderings. 295 if (point.neighbors.length <= 2) { 296 return; 297 } 298 299 QuadCell[] newNeighbors; 300 QuadCell current = null; 301 302 //If this is a vertex where two of the quads don't meet each other, we have to start with one of the quads with a single neighbor 303 //This happens in cases where there's a cleft in the surface. 304 for (int i = 0; i < point.neighbors.length; i++) { 305 QuadCell quad = point.neighbors[i]; 306 int neighborCount = 0; 307 foreach (QuadCell testCell; point.neighbors) { 308 neighborCount += (quad.getNeighborIndex(testCell) != -1 ? 1 : 0); 309 } 310 if (neighborCount == 1) { 311 current = quad; 312 point.neighbors = point.neighbors[0 .. i] ~ point.neighbors[i+1 .. $]; 313 break; 314 } 315 } 316 317 if (current is null) { 318 current = point.neighbors[0]; 319 point.neighbors = point.neighbors[1..$]; 320 } 321 newNeighbors ~= [current]; 322 323 while (point.neighbors.length > 0) { 324 bool found = false; 325 for (int i = 0; i < point.neighbors.length; i++) { 326 QuadCell testCell = point.neighbors[i]; 327 328 if (current.getNeighborIndex(testCell) != -1) { 329 newNeighbors ~= [testCell]; 330 current = testCell; 331 point.neighbors = point.neighbors[0 .. i] ~ point.neighbors[i+1 .. $]; 332 found = true; 333 break; 334 } 335 } 336 if (!found) { 337 //This only happens if the vertex is adjacent to multiple quads, but there are at least two separate 338 //groups of them that have no common boundaries. 339 throw new Exception("Mesh vertex has two neighboring non-continuous faces"); 340 } 341 } 342 343 point.neighbors = newNeighbors; 344 } 345 }