1 /// Generic 3d mesh representation 2 module nwn.trn.genericmesh; 3 4 import std.stdint; 5 import std.conv: to; 6 import std.algorithm; 7 import std.math; 8 import std.traits; 9 import std.string; 10 import std.stdio: File; 11 import std.exception: enforce; 12 import std.array: array; 13 import std.typecons: Nullable; 14 debug import std.stdio: stderr, write, writeln, writefln; 15 import std.container.dlist; 16 17 import nwnlibd.geometry; 18 import gfm.math.vector; 19 20 /// 21 struct GenericMesh { 22 vec3f[] vertices; 23 24 static struct Triangle{ 25 uint32_t[3] vertices; /// Vertex indices composing the triangle 26 uint32_t material = uint32_t.max; 27 } 28 Triangle[] triangles; 29 30 uint32_t[][] lines; 31 static struct Material{ 32 float[3] ambientColor; /// Ka 33 string ambientTexture; /// map_Ka 34 float[3] diffuseColor; /// Kd 35 string diffuseTexture; /// map_Kd 36 float[3] specularColor; /// Ks 37 string specularTexture; /// map_Ks 38 float specularWeight; /// Ns 39 float transparency; /// Tr ; 0 = opaque 40 string bumpTexture; /// bump 41 string dispTexture; /// disp 42 string decalTexture; /// decal 43 enum Illumniation : ubyte { 44 None = ubyte.max, 45 ColorOnAmbientOff = 0, /// Color on and Ambient off 46 ColorOnAmbientOn = 1, /// Color on and Ambient on 47 HilightOn = 2, /// Highlight on 48 ReflectRaytrace = 3, /// Reflection on and Ray trace on 49 GlassRaytrace = 4, /// Transparency: Glass on, Reflection: Ray trace on 50 FresnelRaytrace = 5, /// Reflection: Fresnel on and Ray trace on 51 RefractRaytrace = 6, /// Transparency: Refraction on, Reflection: Fresnel off and Ray trace on 52 RefractFresnelRaytrace = 7, /// Transparency: Refraction on, Reflection: Fresnel on and Ray trace on 53 Reflect = 8, /// Reflection on and Ray trace off 54 Glass = 9, /// Transparency: Glass on, Reflection: Ray trace off 55 Shadows = 10, /// Casts shadows onto invisible surfaces 56 } 57 Illumniation illumination = Illumniation.None; /// illum 58 } 59 Material[] materials; 60 61 GenericMesh dup() const { 62 return GenericMesh(vertices.dup, triangles.dup, lines.dup.map!(a => a.dup).array, materials.dup); 63 } 64 65 /// Throw an exception if mesh contains invalid indices 66 void validate(){ 67 foreach(i, ref t ; triangles) 68 foreach(vi ; t.vertices) 69 enforce(vi < vertices.length, 70 "Triangle "~i.to!string~" contains invalid vertex index ("~vi.to!string~")"); 71 } 72 73 /// Shuffle all data, while keeping the same 3d model 74 void shuffle(){ 75 // Shuffle all triangles & vertices 76 uint32_t[] vertTransTable, triTransTable; 77 vertTransTable.length = vertices.length; 78 triTransTable.length = triangles.length; 79 foreach(i, ref val ; vertTransTable) val = cast(uint32_t)i; 80 foreach(i, ref val ; triTransTable) val = cast(uint32_t)i; 81 82 import std.random: randomShuffle; 83 vertTransTable.randomShuffle(); 84 triTransTable.randomShuffle(); 85 86 auto oldVertices = this.vertices.idup; 87 auto oldTriangles = this.triangles.idup; 88 89 foreach(oldVIdx, newVIdx ; vertTransTable) 90 vertices[newVIdx] = vec3f(oldVertices[oldVIdx]); 91 92 foreach(oldTIdx, newTIdx ; triTransTable){ 93 auto oldTri = &oldTriangles[oldTIdx]; 94 triangles[newTIdx] = Triangle(oldTri.vertices); 95 96 foreach(ref v ; triangles[newTIdx].vertices[].randomShuffle) 97 v = vertTransTable[v]; 98 } 99 } 100 101 void removeUnusedVertices(){ 102 bool[] usedVertices; 103 usedVertices.length = vertices.length; 104 usedVertices[] = false; 105 106 foreach(ref t ; triangles) 107 t.vertices.each!(a => usedVertices[a] = true); 108 foreach(ref l ; lines) 109 l.each!(a => usedVertices[a] = true); 110 111 uint32_t[] vertexTransTable; 112 vertexTransTable.length = vertices.length; 113 114 uint32_t newIndex = 0; 115 foreach(oldIndex, used ; usedVertices){ 116 if(used){ 117 vertexTransTable[oldIndex] = newIndex; 118 vertices[newIndex] = vertices[oldIndex]; 119 newIndex++; 120 } 121 } 122 vertices.length = newIndex; 123 124 foreach(ref t ; triangles) 125 t.vertices.each!((ref a) => vertexTransTable[a]); 126 127 foreach(ref l ; lines) 128 l.each!((ref a) => vertexTransTable[a]); 129 } 130 131 132 private static struct TriCut{ 133 uint32_t triangle; 134 // Vertices created by cutting this triangle 135 uint32_t[2] cutVertices = [uint32_t.max, uint32_t.max]; 136 // 0 1 or 2 if the cutVertices[i] is on edge 0 1 or 2 137 ubyte[2] verticesEdge = [ubyte.max, ubyte.max]; 138 } 139 private static struct Cut { 140 TriCut[] trianglesCut; 141 vec3f[] newVertices; 142 } 143 private auto lineCut(vec2f[2] line) const { 144 Cut ret; 145 146 alias EDGE = uint32_t[2]; 147 uint32_t[EDGE] verticesOnEdge; 148 uint32_t addVertex(in vec3f vertex, in EDGE edge = [uint32_t.max, uint32_t.max]){ 149 if(edge[0] != uint32_t.max && edge[1] != uint32_t.max){ 150 EDGE sortedEdge = edge[].dup.sort.array; 151 if(auto v = sortedEdge in verticesOnEdge){ 152 return *v; 153 } 154 auto index = ret.newVertices.length.to!uint32_t; 155 verticesOnEdge[sortedEdge] = index; 156 ret.newVertices ~= vertex; 157 return index; 158 } 159 else{ 160 auto index = ret.newVertices.length.to!uint32_t; 161 ret.newVertices ~= vertex; 162 return index; 163 } 164 } 165 166 167 auto lineVec = vec2f(line[1] - line[0]).normalized; 168 169 auto lineVecPer = vec2f(lineVec[1], -lineVec[0]); 170 auto lineDist = line[0].dot(lineVecPer); 171 172 foreach(i, ref t ; triangles){ 173 // TODO: optimize by checking against an AABB 174 175 vec3f[3] triVertices = t.vertices[] 176 .map!(a => vertices[a]) 177 .array[0 .. 3]; 178 vec2f[3] tri2DVertices = t.vertices[] 179 .map!(a => vec2f(vertices[a].v[0 .. 2])) 180 .array[0 .. 3]; 181 182 183 // Check if the triangle collides with the infinite line 184 float[3] ndot; 185 static foreach(j ; 0 .. 3){ 186 // we calculate the distance between the cutting line and each 187 // point of the triangle. 188 // If the distance will be >0 or <0 depending on which side of 189 // the line is the point. 190 ndot[j] = vec2f(tri2DVertices[j]).dot(lineVecPer) - lineDist; 191 } 192 ubyte[] cutEdges;// will contain the list of edges cut by an infinite line 193 static foreach(j ; 0 .. 3){ 194 // ndot[j] * ndot[(j + 1) % 3] is <0 if the two vertices of 195 // a triangle edge are on each side of the cutting line 196 if(ndot[j] * ndot[(j + 1) % 3] < 0) 197 cutEdges ~= j; 198 } 199 200 if(cutEdges.length == 0) 201 continue; // The cutting line does not cross any triangle edge 202 203 assert(cutEdges.length != 1, "Cutting on triangle vertex not supported yet"); 204 assert(cutEdges.length == 2, 205 "Non-Euclidean geometry: You succeeded at cutting "~cutEdges.length.to!string~" edges of a triangle with a line, well done !"); 206 207 auto triDirection = isTriangleClockwise(tri2DVertices); 208 209 // bool[cut edge 0/1][line start/end] 210 bool[2][2] edgeDirections; 211 foreach(iLinePoint, linePoint ; line){ 212 foreach(j, edge ; cutEdges){ 213 edgeDirections[j][iLinePoint] = isTriangleClockwise([tri2DVertices[edge], tri2DVertices[(edge + 1) % 3], linePoint]); 214 } 215 } 216 217 auto triCut = TriCut(cast(uint32_t)i); 218 //stderr.writefln("Triangle collides infinite line on %d edges", cutEdges.length); 219 foreach(j, edge ; cutEdges){ 220 // value == triDirection <=> point is inside of the triangle edge 221 if(edgeDirections[j][0] != edgeDirections[j][1]){ 222 // Calculate intersection point 223 auto edgePos = [tri2DVertices[edge], tri2DVertices[(edge + 1) % 3]]; 224 225 auto intersection = getLineIntersection(edgePos[0..2], line); 226 assert(intersection.intersect, "Should intersect"); 227 228 auto altitude = getAltitudeOnPlane(triVertices, intersection.position); 229 auto intersectIndex = addVertex( 230 vec3f(intersection.position.x, intersection.position.y, altitude), 231 [t.vertices[edge], t.vertices[(edge + 1) % 3]], 232 ); 233 234 triCut.cutVertices[j] = intersectIndex; 235 triCut.verticesEdge[j] = edge; 236 //stderr.writeln(" cut through edge ", j, " => verticesEdge=", triCut.verticesEdge); 237 } 238 } 239 foreach(j ; 0 .. 2){ 240 if(edgeDirections[0][j] == triDirection && edgeDirections[1][j] == triDirection){ 241 242 auto altitude = getAltitudeOnPlane(triVertices, line[j]); 243 auto linePointIndex = addVertex( 244 vec3f(line[j].x, line[j].y, altitude), 245 ); 246 247 ubyte insertPos = triCut.cutVertices[0] == uint32_t.max? 0 : 1; 248 assert(triCut.cutVertices[insertPos] == uint32_t.max); 249 250 triCut.cutVertices[insertPos] = linePointIndex; 251 //stderr.writefln(" Line point %d (%s) is inside the triangle %s => verticesEdge=", j, line[j], triVertices, triCut.verticesEdge); 252 } 253 } 254 255 //if(triCut.cutEdges.length == 0){ 256 // continue;// TODO: handle inner cuts 257 //} 258 if(triCut.cutVertices[0] == uint32_t.max && triCut.cutVertices[1] == uint32_t.max){ 259 continue; 260 } 261 assert(triCut.cutVertices[0] != uint32_t.max && triCut.cutVertices[1] != uint32_t.max); 262 263 264 debug{ 265 foreach(j, e ; triCut.verticesEdge){ 266 if(e != ubyte.max){ 267 vec2f[2] edge = [vertices[t.vertices[e]].toVec2f, vertices[t.vertices[(e + 1)%3]].toVec2f]; 268 vec2f intersection = ret.newVertices[triCut.cutVertices[j]].toVec2f; 269 270 assert(distance(edge, intersection) < 0.01, 271 "New vertex is not on the correct edge: distance="~distance(edge[0..2], intersection).to!string); 272 } 273 } 274 } 275 276 ret.trianglesCut ~= triCut; 277 } 278 return ret; 279 280 } 281 282 283 284 void polygonCut(vec2f[] polygon, bool removeInside = true){ 285 validate(); 286 287 Cut mergedCut; 288 uint32_t[float[2]] insertedPolygonVertices; 289 size_t[][uint32_t] triangleToTriCutIndices; 290 foreach(polyVertIdx ; 0 .. polygon.length){ 291 auto nextCut = lineCut([polygon[polyVertIdx], polygon[(polyVertIdx + 1) % polygon.length]]); 292 293 // Add new vertices as needed, and construct a vertex translate table 294 uint32_t[] vertTransTable;// index in nextCut.newVertices => index in this.vertices 295 vertTransTable.length = nextCut.newVertices.length; 296 foreach(ni, ref nv ; nextCut.newVertices){ 297 float[2] newVertex2D = nv.v[0 .. 2]; 298 if(newVertex2D == polygon[polyVertIdx] || newVertex2D == (polygon[(polyVertIdx + 1) % polygon.length])){ 299 // The vertex is one of the polygon's vertices 300 if(auto ov = (newVertex2D in insertedPolygonVertices)){ 301 // Vertex is already added, link back to it 302 vertTransTable[ni] = *ov; 303 } 304 else{ 305 // Add vertex 306 auto pos = (vertices.length + mergedCut.newVertices.length).to!uint32_t; 307 vertTransTable[ni] = pos; 308 insertedPolygonVertices[newVertex2D] = pos; 309 mergedCut.newVertices ~= nv; 310 } 311 } 312 else{ 313 // The vertex is on a triangle edge 314 vertTransTable[ni] = (vertices.length + mergedCut.newVertices.length).to!uint32_t; 315 mergedCut.newVertices ~= nv; 316 } 317 } 318 319 // Translate nextCut vertex indices 320 foreach(j, ref tc ; nextCut.trianglesCut){ 321 // Keep a list of cuts for a given triangle 322 if(tc.triangle in triangleToTriCutIndices) 323 triangleToTriCutIndices[tc.triangle] ~= mergedCut.trianglesCut.length + j; 324 else 325 triangleToTriCutIndices[tc.triangle] = [mergedCut.trianglesCut.length + j]; 326 327 // Translate vertex indices 328 foreach(ref v ; tc.cutVertices) 329 v = vertTransTable[v]; 330 } 331 332 // Append TriCut 333 mergedCut.trianglesCut ~= nextCut.trianglesCut; 334 } 335 vertices ~= mergedCut.newVertices; 336 mergedCut.newVertices.length = 0; 337 338 foreach(triangleIndex, const ref triangleCutIndices ; triangleToTriCutIndices){ 339 340 // Merge cut segments into chains (multi-vertex cut lines) 341 // and build tables for relationship between vertices, triangle edges and polygon cut ends 342 Chains!uint32_t cutChains; 343 foreach(ref tcut ; triangleCutIndices.map!(a => mergedCut.trianglesCut[a])){ 344 assert(tcut.cutVertices[0] != uint32_t.max && tcut.cutVertices[1] != uint32_t.max); 345 cutChains.extend(tcut.cutVertices); 346 } 347 uint32_t[][3] verticesOnEdge; // Ordered list of vertices on one edge 348 size_t[uint32_t] vertexToCutChainIndex; // Vertex => polygon cut offset 349 ubyte[uint32_t] vertexToEdge; // vertex ID => edge offset 350 foreach(ref tcut ; triangleCutIndices.map!(a => mergedCut.trianglesCut[a])){ 351 static foreach(i ; 0 .. 2){{ 352 immutable edgeIndex = tcut.verticesEdge[i]; 353 if(edgeIndex != ubyte.max){ 354 immutable vertexIndex = tcut.cutVertices[i]; 355 356 auto cutChainIndex = cutChains.countUntil!(a => a.front == vertexIndex || a.back == vertexIndex); 357 assert(cutChainIndex >= 0); 358 359 verticesOnEdge[edgeIndex] ~= vertexIndex; 360 vertexToCutChainIndex[vertexIndex] = cutChainIndex; 361 vertexToEdge[vertexIndex] = edgeIndex; 362 } 363 }} 364 } 365 366 debug{ 367 foreach(ref cut ; cutChains){ 368 assert(cut.front in vertexToEdge, format!"Cut %s first vertex is not on a triangle edge (incomplete cut)"(cut.array)); 369 assert(cut.back in vertexToEdge, format!"Cut %s last vertex is not on a triangle edge (incomplete cut)"(cut.array)); 370 } 371 } 372 373 // Sort verticesOnEdge by distance to triangle edge 374 foreach(i, ref voe ; verticesOnEdge){ 375 auto start = &vertices[ triangles[triangleIndex].vertices[i] ]; 376 voe = voe 377 .sort!((a, b) => start.squaredDistanceTo(vertices[a]) < start.squaredDistanceTo(vertices[b])) 378 .array; 379 } 380 381 ubyte[] exploredCuts;// 0b01 for triangle-oriented direction, 0b10 for reverse direction 382 exploredCuts.length = cutChains.length; 383 uint32_t[] exploreCut(size_t cutIndex, byte direction){ 384 assert(direction == -1 || direction == 1); 385 uint32_t[] resPolygon = cutChains[cutIndex].array; 386 ubyte directionMask = direction == 1? 0b01 : 0b10; 387 388 assert((exploredCuts[cutIndex] & directionMask) == 0, "Already explored cut " ~ cutIndex.to!string); 389 exploredCuts[cutIndex] |= directionMask; 390 391 while(1){ 392 // Explore last vertex of resPolygon 393 394 auto nextCutIndex = size_t.max; 395 bool orderedCut;// true=normal, false=reversed 396 while(nextCutIndex == size_t.max){ 397 auto startVertex = resPolygon[$-1]; 398 399 if(startVertex in vertexToEdge){ 400 // startVertex is a vertex on the triangle edge 401 402 auto edgeIndex = vertexToEdge[startVertex]; 403 auto indexOnEdge = verticesOnEdge[edgeIndex].countUntil(startVertex).to!uint32_t; 404 405 if(direction > 0 ? indexOnEdge + 1 < verticesOnEdge[edgeIndex].length : indexOnEdge > 0){ 406 // nextEdgeVertex will be a cut vertex on the same edge 407 auto nextEdgeVertex = verticesOnEdge[edgeIndex][indexOnEdge + direction]; 408 if(nextEdgeVertex == resPolygon[0]) 409 return resPolygon; 410 nextCutIndex = vertexToCutChainIndex[nextEdgeVertex]; 411 orderedCut = nextEdgeVertex == cutChains[nextCutIndex].front; 412 debug if(!orderedCut) assert(nextEdgeVertex == cutChains[nextCutIndex].back); 413 } 414 else{ 415 // Add next triangle vertex 416 auto nextTriangleVertexIndex = direction > 0 ? (edgeIndex + 1) % 3 : edgeIndex; 417 resPolygon ~= triangles[triangleIndex].vertices[nextTriangleVertexIndex]; 418 } 419 } 420 else{ 421 // startVertex is a triangle vertex 422 423 auto triangleVertexIndex = triangles[triangleIndex].vertices[].countUntil(startVertex).to!uint32_t; 424 auto nextEdge = direction > 0 ? triangleVertexIndex : ((triangleVertexIndex + 2) % 3); 425 426 if(verticesOnEdge[nextEdge].length > 0){ 427 // there are cuts on nextEdge, follow them 428 auto nextEdgeVertex = verticesOnEdge[nextEdge][direction > 0 ? 0 : $-1]; 429 if(nextEdgeVertex == resPolygon[0]) 430 return resPolygon; 431 nextCutIndex = vertexToCutChainIndex[nextEdgeVertex]; 432 orderedCut = nextEdgeVertex == cutChains[nextCutIndex].front; 433 debug if(!orderedCut) assert(nextEdgeVertex == cutChains[nextCutIndex].back); 434 } 435 else{ 436 // There are no cuts on nextEdge, add next triangle vertex 437 auto nextTriangleVertexIndex = (triangleVertexIndex + 3 + direction) % 3; 438 resPolygon ~= triangles[triangleIndex].vertices[nextTriangleVertexIndex]; 439 } 440 441 } 442 } 443 444 if(orderedCut){ 445 resPolygon ~= cutChains[nextCutIndex].array; 446 assert((exploredCuts[nextCutIndex] & directionMask) == 0); 447 exploredCuts[nextCutIndex] |= directionMask; 448 } 449 else{ 450 resPolygon ~= cutChains[nextCutIndex].array.reverse.array; 451 assert((exploredCuts[nextCutIndex] & (directionMask ^ 0b11)) == 0); 452 exploredCuts[nextCutIndex] |= directionMask ^ 0b11; 453 } 454 455 assert(resPolygon.dup.sort.uniq.array.length == resPolygon.length, "Loop detected in polygon " ~ resPolygon.to!string); 456 } 457 } 458 459 uint32_t[][] finalPolygons; 460 foreach(polygonCutIndex ; 0 .. cutChains.length){ 461 if((exploredCuts[polygonCutIndex] & 0b01) == 0){ 462 finalPolygons ~= exploreCut(polygonCutIndex, 1); 463 } 464 if((exploredCuts[polygonCutIndex] & 0b10) == 0){ 465 finalPolygons ~= exploreCut(polygonCutIndex, -1); 466 } 467 } 468 469 foreach(ref p ; finalPolygons){ 470 triangulatePolygon(p); 471 } 472 473 474 // Mark triangle for removal 475 triangles[triangleIndex].vertices[0] = uint32_t.max; 476 } 477 478 if(removeInside){ 479 // Remove triangles which center point is inside the polygon 480 foreach(i, ref t ; triangles){ 481 if(t.vertices[0] == uint32_t.max) 482 continue; 483 484 auto center = (vertices[t.vertices[0]] + vertices[t.vertices[1]] + vertices[t.vertices[2]]) / 3.0; 485 if(!isPointInsidePolygon(vec2f(center[0..2]), polygon) == false){ 486 t.vertices[0] = uint32_t.max; 487 } 488 } 489 } 490 triangles = triangles.filter!(a => a.vertices[0] != uint32_t.max).array; 491 } 492 493 unittest { 494 GenericMesh simpleMesh; 495 simpleMesh.vertices = [ 496 vec3f(0, 0, 0), 497 vec3f(0, 1, 0), 498 vec3f(1, 0, 0), 499 ]; 500 simpleMesh.triangles ~= GenericMesh.Triangle([0, 1, 2]); 501 502 GenericMesh mesh; 503 mesh = simpleMesh.dup(); 504 mesh.polygonCut([ 505 vec2f(2, 0.2), 506 vec2f(0.2, 0.2), 507 vec2f(0.2, 2), 508 ]); 509 assert(mesh.vertices.length == 6); 510 assert(mesh.triangles.length == 4); 511 512 mesh = simpleMesh.dup(); 513 mesh.polygonCut([ 514 vec2f(2, -1), 515 vec2f(0.2, 0.2), 516 vec2f(0.2, 2), 517 ]); 518 mesh.removeUnusedVertices(); 519 assert(mesh.vertices.length == 5); 520 assert(mesh.triangles.length == 3); 521 522 mesh = simpleMesh.dup(); 523 mesh.polygonCut([ 524 vec2f(0.5, 1), 525 vec2f(-0.5, 0), 526 vec2f(0, -0.5), 527 vec2f(1, 0.5), 528 ]); 529 mesh.removeUnusedVertices(); 530 assert(mesh.vertices.length == 6); 531 assert(mesh.triangles.length == 2); 532 533 mesh = simpleMesh.dup(); 534 mesh.polygonCut([ 535 vec2f(0.2, 2), 536 vec2f(0.2, 0.2), 537 vec2f(0.4, 2), 538 vec2f(0.5, -0.5), 539 vec2f(1, 3), 540 ]); 541 assert(mesh.vertices.length == 10); 542 assert(mesh.triangles.length == 6); 543 544 //// polygon on triangle vertex 545 //mesh = simpleMesh.dup(); 546 //mesh.polygonCut([ 547 // vec2f(0, 0), 548 // vec2f(0.2, 0.2), 549 // vec2f(0.4, 2), 550 // vec2f(0.5, -0.5), 551 // vec2f(1, 3), 552 //]); 553 //mesh.removeUnusedVertices(); 554 //assert(mesh.vertices.length == 10); 555 //assert(mesh.triangles.length == 6); 556 } 557 558 /** 559 Build a generic mesh by reading a Wavefront OBJ file 560 561 Params: 562 obj = OBJ file to read 563 objectName = If null, the first object will be used, if a string, will import the mesh from the given object name 564 */ 565 static GenericMesh fromObj(File obj, string objectName = null){ 566 GenericMesh mesh; 567 bool registerData = false; 568 foreach(line ; obj.byLine.map!strip.filter!(a => a[0] != '#')){ 569 import std.format; 570 571 string type; 572 line.formattedRead!"%s "(type); 573 574 575 switch(type){ 576 case "o":{ 577 string name; 578 line.formattedRead!"%s"(name); 579 if(objectName is null || objectName == name){ 580 registerData = true; 581 objectName = name; 582 } 583 else 584 registerData = false; 585 } 586 break; 587 case "v":{ 588 vec3f v; 589 line.formattedRead!"%f %f %f"(v.x, v.y, v.z); 590 mesh.vertices ~= v; 591 } 592 break; 593 case "f":{ 594 string data; 595 line.formattedRead!"%s"(data); 596 597 Triangle t; 598 auto abc = data.split(" "); 599 enforce(abc.length == 3, "Wrong number of vertices for a face. Every face must be a triangle"); 600 601 foreach(i, s ; abc){ 602 t.vertices[i] = s.split("/")[0].to!uint32_t - 1; 603 } 604 mesh.triangles ~= t; 605 606 } 607 break; 608 case "l":{ 609 string data; 610 line.formattedRead!"%s"(data); 611 mesh.lines ~= data.split(" ").map!(a => a.to!uint32_t - 1).array; 612 } 613 break; 614 615 default: break; 616 617 } 618 619 } 620 return mesh; 621 } 622 623 624 void toObj(in string file, in string objectName = "genericmesh") const { 625 auto obj = File(file, "w"); 626 File mtl; 627 if(materials.length > 0) 628 mtl = File(file ~ ".mtl", "w"); 629 630 // OBJ definition 631 if(materials.length > 0) 632 obj.writefln("mtllib %s", file ~ ".mtl"); 633 obj.writeln("o ",objectName); 634 foreach(ref v ; vertices){ 635 obj.writefln("v %(%f %)", v.v); 636 } 637 638 auto currentMaterial = uint32_t.max; 639 foreach(ref t ; triangles){ 640 if(t.material != currentMaterial){ 641 if(t.material == uint32_t.max) 642 obj.writeln("usemtl"); 643 else 644 obj.writefln("usemtl mtl%d", t.material); 645 currentMaterial = t.material; 646 } 647 648 if(isTriangleClockwise(t.vertices[].map!(a => vec2f(vertices[a].v[0..2])).array[0 .. 3])) 649 obj.writefln("f %s %s %s", t.vertices[2]+1, t.vertices[1]+1, t.vertices[0]+1); 650 else 651 obj.writefln("f %s %s %s", t.vertices[0]+1, t.vertices[1]+1, t.vertices[2]+1); 652 } 653 foreach(ref l ; lines){ 654 obj.writefln("l %(%s %)", l.map!(a => a + 1).array); 655 } 656 657 // Material lib 658 foreach(i, ref material ; materials){ 659 mtl.writefln("newmtl mtl%d", i); 660 if(material.ambientColor[0] != float.nan) mtl.writefln("Ka %(%f %)", material.ambientColor); 661 if(material.ambientTexture !is null) mtl.writefln("map_Ka %s", material.ambientTexture); 662 if(material.diffuseColor[0] != float.nan) mtl.writefln("Kd %(%f %)", material.diffuseColor); 663 if(material.diffuseTexture !is null) mtl.writefln("map_Kd %s", material.diffuseTexture); 664 if(material.specularColor[0] != float.nan) mtl.writefln("Ks %(%f %)", material.specularColor); 665 if(material.specularTexture !is null) mtl.writefln("map_Ks %s", material.specularTexture); 666 if(material.specularWeight != float.nan) mtl.writefln("Ns %f", material.specularWeight); 667 if(material.transparency != float.nan) mtl.writefln("Tr %f", material.transparency); 668 if(material.bumpTexture !is null) mtl.writefln("bump %s", material.bumpTexture); 669 if(material.dispTexture !is null) mtl.writefln("disp %s", material.dispTexture); 670 if(material.decalTexture !is null) mtl.writefln("decal %s", material.decalTexture); 671 if(material.illumination != Material.Illumniation.None) mtl.writefln("illum %d", material.illumination); 672 } 673 } 674 675 /// Triangulates a set of vertices and adds the created triangles to the mesh (ear clipping algorithm) 676 void triangulatePolygon(in uint32_t[] polygon){ 677 assert(polygon.length >= 3); 678 679 auto remainingPolygon = polygon.dup; 680 while(remainingPolygon.length > 3){ 681 debug auto prevLen = remainingPolygon.length; 682 683 foreach(i ; 0 .. remainingPolygon.length){ 684 auto aIndex = i; 685 auto bIndex = (i + 1) % remainingPolygon.length; 686 auto cIndex = (i + 2) % remainingPolygon.length; 687 688 auto potentialTriangle = [aIndex, bIndex, cIndex].map!(a => vec2f(vertices[remainingPolygon[a]][0..2])).array; 689 690 int isEar = true; 691 foreach(v ; remainingPolygon){ 692 // Check if any polygon vertices are contained by ear triangle 693 if(v == remainingPolygon[aIndex] || v == remainingPolygon[bIndex] || v == remainingPolygon[cIndex]) 694 continue; 695 696 if(isPointInsidePolygon( 697 vec2f(vertices[v][0..2]), 698 potentialTriangle[0..3], 699 )){ 700 isEar = false; 701 break; 702 } 703 } 704 if(isEar){ 705 // Check if ear vertex is inside potential polygon 706 auto potentialPolygon = remainingPolygon.dup().remove(bIndex); 707 if(isPointInsidePolygon( 708 vec2f(vertices[remainingPolygon[bIndex]][0..2]), 709 potentialPolygon.map!(a => vec2f(vertices[a][0..2])).array, 710 )){ 711 isEar = false; 712 } 713 } 714 715 if(isEar){ 716 // it is an ear 717 triangles ~= Triangle([remainingPolygon[aIndex], remainingPolygon[bIndex], remainingPolygon[cIndex]]); 718 remainingPolygon = remainingPolygon.remove(bIndex); 719 break; 720 } 721 } 722 debug assert(remainingPolygon.length < prevLen); 723 } 724 assert(remainingPolygon.length == 3); 725 triangles ~= Triangle(remainingPolygon[0 .. 3]); 726 } 727 } 728 729 730 731 unittest{ 732 import nwn.trn; 733 import std.algorithm; 734 import std.math; 735 import std.file; 736 import std.path; 737 auto trn = new Trn(cast(ubyte[])import("TestImportExportTRN.trx")); 738 739 foreach(ref TrnNWN2WalkmeshPayload aswm ; trn){ 740 auto mesh = aswm.toGenericMesh(); 741 mesh.validate; 742 743 auto file = tempDir.buildPath("test.obj"); 744 mesh.toObj(file); 745 746 auto mesh2 = GenericMesh.fromObj(File(file)); 747 748 assert(mesh.vertices.length == mesh2.vertices.length); 749 //foreach(i ; 0 .. mesh.vertices.length){ 750 // if(!equal!approxEqual(mesh.vertices[i][], mesh2.vertices[i][])) 751 // writeln(mesh.vertices[i], "!=", mesh2.vertices[i]); 752 //} 753 754 assert(equal!((a,b)=>equal!approxEqual(a[], b[]))(mesh.vertices, mesh2.vertices)); 755 assert(equal!((a,b)=>isPermutation(a.vertices[], b.vertices[]))(mesh.triangles, mesh2.triangles)); 756 } 757 758 } 759 760 unittest{ 761 import nwn.trn; 762 import nwn.fastgff; 763 import std.range; 764 765 auto git = new FastGff("unittest/WalkmeshObjects.git"); 766 auto trn = new Trn("unittest/WalkmeshObjects.trn"); 767 768 alias WMCutter = vec2f[]; 769 WMCutter[] wmCutters; 770 foreach(trigger ; git["TriggerList"].get!GffList){ 771 772 if(trigger["Type"].get!GffInt == 3){ 773 // Walkmesh cutter 774 WMCutter cutter; 775 776 auto pos = [trigger["XPosition"].get!GffFloat, trigger["YPosition"].get!GffFloat]; 777 778 // what about: XOrientation YOrientation ZOrientation ? 779 foreach(point ; trigger["Geometry"].get!GffList){ 780 cutter ~= vec2f( 781 point["PointX"].get!GffFloat + pos[0], 782 point["PointY"].get!GffFloat + pos[1], 783 ); 784 } 785 786 wmCutters ~= cutter; 787 } 788 } 789 790 foreach(ref TrnNWN2WalkmeshPayload aswm ; trn){ 791 auto mesh = aswm.toGenericMesh(); 792 793 // Cut mesh 794 foreach(i, ref wmCutter ; wmCutters){ 795 mesh.polygonCut(wmCutter); 796 } 797 798 799 aswm.setGenericMesh(mesh); 800 aswm.bake(); 801 aswm.validate(); 802 803 804 805 } 806 807 808 } 809 810