// Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions // are met: // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // * Neither the name of NVIDIA CORPORATION nor the names of its // contributors may be used to endorse or promote products derived // from this software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ''AS IS'' AND ANY // EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR // PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR // CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, // EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, // PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY // OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // // Copyright (c) 2008-2025 NVIDIA Corporation. All rights reserved. #include "ExtOctreeTetrahedralizer.h" #include "foundation/PxSort.h" #include "foundation/PxQuat.h" #include "CmRandom.h" using namespace physx; using namespace Ext; // ------------------------------------------------------------------------------------- static const PxI32 childRelPos[8][3] = { {0,0,0}, {1,0,0},{0,1,0},{1,1,0}, {0,0,1}, {1,0,1},{0,1,1},{1,1,1} }; static const PxI32 cubeCorners[8][3] = { {0,0,0}, {1,0,0},{1,1,0},{0,1,0}, {0,0,1}, {1,0,1},{1,1,1},{0,1,1} }; //static const PxI32 cubeEdges[12][2] = { {0,1}, {1,2},{2,3},{3,0}, {0,4},{1,5},{2,6},{3,7},{4,5},{5,6},{6,7},{7,4} }; static const PxI32 tetFaces[4][3] = { {2,1,0}, {0,1,3}, {1,2,3}, {2,0,3} }; static const PxI32 cubeTets[6][4] = { {0,1,2,5}, {0,4,5,2}, {2,4,5,6}, {4,7,6,3}, {2,6,3,4}, {0,2,3,4} }; static const PxI32 cubeTetNeighbors[6][4] = { {-1,-1,-1,1}, {-1,5,2,0}, {1,4,-1,-1}, {-1,-1,-1,4}, {-1,2,3,5}, {-1,1,4,-1} }; // ------------------------------------------------------------------------------------- OctreeTetrahedralizer::OctreeTetrahedralizer() { clear(); } // ------------------------------------------------------------------------------------- void OctreeTetrahedralizer::clearTets() { tetVerts.clear(); tetIds.clear(); firstFreeTet = -1; currentTetMark = 0; tetMarks.clear(); tetNeighbors.clear(); renderVerts.clear(); renderTriIds.clear(); firstABBVert = 0; } // ------------------------------------------------------------------------------------- void OctreeTetrahedralizer::clear() { surfaceVerts.clear(); surfaceTriIds.clear(); tetIds.clear(); tetNeighbors.clear(); cells.clear(); clearTets(); prevClip = -1.0f; prevScale = -1.0f; } // ----------------------------------------------------------------------------------- void OctreeTetrahedralizer::createTree() { Bounds3 bounds; bounds.setEmpty(); for (PxI32 i = 0; i < PxI32(surfaceVerts.size()); i++) { const PxVec3& v = surfaceVerts[i]; bounds.include(PxVec3d(PxF64(v.x), PxF64(v.y), PxF64(v.z))); } bounds.expand(0.01); PxVec3d dims = bounds.getDimensions(); PxF64 size = PxMax(dims.x, PxMax(dims.y, dims.z)); // create root cells.resize(1); cells.front().init(); cells.front().orig = bounds.minimum; cells.front().size = size; cells.front().depth = 0; // insert vertices PxI32 numVerts = PxI32(surfaceVerts.size()); for (PxI32 i = 0; i < numVerts; i++) { treeInsertVert(0, i); } } // ------------------------------------------------------------------------------------- PxI32 OctreeTetrahedralizer::Cell::getChildNr(const PxVec3d& p) { if (firstChild < 0) return -1; PxI32 nr = 0; if (p.x > orig.x + 0.5 * size) nr |= 1; if (p.y > orig.y + 0.5 * size) nr |= 2; if (p.z > orig.z + 0.5 * size) nr |= 4; return firstChild + nr; } // ------------------------------------------------------------------------------------- void OctreeTetrahedralizer::treeInsertVert(PxI32 cellNr, PxI32 vertNr) { // inner node if (cells[cellNr].firstChild >= 0) { treeInsertVert(cells[cellNr].getChildNr(surfaceVerts[vertNr]), vertNr); return; } // add vertsOfCell.add(cellNr, vertNr); cells[cellNr].numVerts++; if (cells[cellNr].numVerts <= maxVertsPerCell || cells[cellNr].depth >= maxTreeDepth) return; // split PxI32 firstChild = cells.size(); cells[cellNr].firstChild = firstChild; cells.resize(cells.size() + 8); for (PxI32 i = 0; i < 8; i++) { Cell& child = cells[firstChild + i]; child.init(); child.depth = cells[cellNr].depth + 1; child.size = cells[cellNr].size * 0.5; child.orig = cells[cellNr].orig + PxVec3d( childRelPos[i][0] * child.size, childRelPos[i][1] * child.size, childRelPos[i][2] * child.size); } PxI32 iterator, id; vertsOfCell.initIteration(cellNr, iterator); while (vertsOfCell.iterate(id, iterator)) treeInsertVert(cells[cellNr].getChildNr(surfaceVerts[id]), id); vertsOfCell.removeAll(cellNr); cells[cellNr].numVerts = 0; } // ----------------------------------------------------------------------------------- static PxVec3d jitter(const PxVec3d& p, Cm::RandomR250& random) { PxF64 eps = 0.001; return PxVec3d( p.x - eps + 2.0 * eps * PxF64(random.rand(0.0f, 1.0f)), p.y - eps + 2.0 * eps * PxF64(random.rand(0.0f, 1.0f)), p.z - eps + 2.0 * eps * PxF64(random.rand(0.0f, 1.0f))); } // ----------------------------------------------------------------------------------- void OctreeTetrahedralizer::createTetVerts(bool includeOctreeNodes) { tetVerts.clear(); insideTester.init(surfaceVerts.begin(), PxI32(surfaceVerts.size()), surfaceTriIds.begin(), PxI32(surfaceTriIds.size()) / 3); for (PxI32 i = 0; i < PxI32(surfaceVerts.size()); i++) { const PxVec3& v = surfaceVerts[i]; tetVerts.pushBack(PxVec3d(PxF64(v.x), PxF64(v.y), PxF64(v.z))); } if (includeOctreeNodes) { PxArray treeVerts; for (PxI32 i = 0; i < PxI32(cells.size()); i++) { PxF64 s = cells[i].size; for (PxI32 j = 0; j < 8; j++) { PxVec3d p = cells[i].orig + PxVec3d( s * cubeCorners[j][0], s * cubeCorners[j][1], s * cubeCorners[j][2]); treeVerts.pushBack(p); } } // remove duplicates PxF64 eps = 1e-8; struct Ref { PxF64 d; PxI32 vertNr; bool operator < (const Ref& r) const { return d < r.d; } }; PxI32 numTreeVerts = PxI32(treeVerts.size()); PxArray refs(numTreeVerts); for (PxI32 i = 0; i < numTreeVerts; i++) { PxVec3d& p = treeVerts[i]; refs[i].d = p.x + 0.3 * p.y + 0.1 * p.z; refs[i].vertNr = i; } PxSort(refs.begin(), refs.size()); PxArray duplicate(numTreeVerts, false); PxI32 nr = 0; Cm::RandomR250 random(0); while (nr < numTreeVerts) { Ref& r = refs[nr]; nr++; if (duplicate[r.vertNr]) continue; PxVec3d& p = treeVerts[r.vertNr]; PxVec3d v = jitter(p, random); if (insideTester.isInside(PxVec3(PxReal(v.x), PxReal(v.y), PxReal(v.z)))) tetVerts.pushBack(jitter(p, random)); PxI32 i = nr; while (i < numTreeVerts && fabs(refs[i].d - r.d) < eps) { PxVec3d& q = treeVerts[refs[i].vertNr]; if ((p - q).magnitude() < eps) duplicate[refs[i].vertNr] = true; i++; } } } } // ----------------------------------------------------------------------------------- PxVec3d OctreeTetrahedralizer::getTetCenter(PxI32 tetNr) const { return (tetVerts[tetIds[4 * tetNr]] + tetVerts[tetIds[4 * tetNr + 1]] + tetVerts[tetIds[4 * tetNr + 2]] + tetVerts[tetIds[4 * tetNr + 3]]) * 0.25; } // ----------------------------------------------------------------------------------- void OctreeTetrahedralizer::treeInsertTet(PxI32 tetNr) { PxVec3d center = getTetCenter(tetNr); PxI32 cellNr = 0; while (cellNr >= 0) { Cell& c = cells[cellNr]; if (c.closestTetNr < 0) c.closestTetNr = tetNr; else { PxVec3d cellCenter = c.orig + PxVec3d(c.size, c.size, c.size) * 0.5; PxVec3d closest = getTetCenter(c.closestTetNr); if ((cellCenter - center).magnitudeSquared() < (cellCenter - closest).magnitudeSquared()) c.closestTetNr = tetNr; } cellNr = cells[cellNr].getChildNr(center); } } // ----------------------------------------------------------------------------------- void OctreeTetrahedralizer::treeRemoveTet(PxI32 tetNr) { PxVec3d center = getTetCenter(tetNr); PxI32 cellNr = 0; while (cellNr >= 0) { Cell& c = cells[cellNr]; if (c.closestTetNr == tetNr) c.closestTetNr = -1; cellNr = cells[cellNr].getChildNr(center); } } static void resizeFast(PxArray& arr, PxU32 newSize, PxI32 value = 0) { if (newSize < arr.size()) arr.removeRange(newSize, arr.size() - newSize); else { while (arr.size() < newSize) arr.pushBack(value); } } // ----------------------------------------------------------------------------------- PxI32 OctreeTetrahedralizer::getNewTetNr() { PxI32 newTetNr; if (firstFreeTet >= 0) { // take from free list newTetNr = firstFreeTet; firstFreeTet = tetIds[4 * firstFreeTet]; } else { // append newTetNr = PxI32(tetIds.size()) / 4; resizeFast(tetIds, tetIds.size() + 4); resizeFast(tetMarks, newTetNr + 1, 0); resizeFast(tetNeighbors, tetIds.size(), -1); } return newTetNr; } // ----------------------------------------------------------------------------------- void OctreeTetrahedralizer::removeTetNr(PxI32 tetNr) { // add to free list tetIds[4 * tetNr] = firstFreeTet; tetIds[4 * tetNr + 1] = -1; tetIds[4 * tetNr + 2] = -1; tetIds[4 * tetNr + 3] = -1; firstFreeTet = tetNr; } // ----------------------------------------------------------------------------------- bool OctreeTetrahedralizer::findSurroundingTet(const PxVec3d& p, PxI32 startTetNr, PxI32& tetNr) { currentTetMark++; tetNr = startTetNr; bool found = false; while (!found) { if (tetNr < 0 || tetMarks[tetNr] == currentTetMark) // circular, something went wrong break; tetMarks[tetNr] = currentTetMark; PxVec3d c = getTetCenter(tetNr); PxI32* ids = &tetIds[4 * tetNr]; PxF64 minT = DBL_MAX; PxI32 minFaceNr = -1; for (PxI32 i = 0; i < 4; i++) { const PxVec3d& p0 = tetVerts[ids[tetFaces[i][0]]]; const PxVec3d& p1 = tetVerts[ids[tetFaces[i][1]]]; const PxVec3d& p2 = tetVerts[ids[tetFaces[i][2]]]; PxVec3d n = (p1 - p0).cross(p2 - p0); n = n.getNormalized(); PxF64 hp = (p - p0).dot(n); PxF64 hc = (c - p0).dot(n); PxF64 t = hp - hc; if (t == 0.0) continue; t = -hc / t; // time when c -> p hits the face if (t >= 0.0 && t < minT) { // in front and new min minT = t; minFaceNr = i; } } if (minT >= 1.0) found = true; else tetNr = tetNeighbors[4 * tetNr + minFaceNr]; } return found; } // ----------------------------------------------------------------------------------- bool OctreeTetrahedralizer::findSurroundingTet(const PxVec3d& p, PxI32& tetNr) { PxI32 startTet = 0; PxI32 cellNr = 0; while (cellNr >= 0) { if (cells[cellNr].closestTetNr >= 0) startTet = cells[cellNr].closestTetNr; cellNr = cells[cellNr].getChildNr(p); } return findSurroundingTet(p, startTet, tetNr); } // ----------------------------------------------------------------------------------- static PxVec3d getCircumCenter(PxVec3d& p0, PxVec3d& p1, PxVec3d& p2, PxVec3d& p3) { PxVec3d b = p1 - p0; PxVec3d c = p2 - p0; PxVec3d d = p3 - p0; PxF64 det = 2.0 * (b.x*(c.y*d.z - c.z*d.y) - b.y*(c.x*d.z - c.z*d.x) + b.z*(c.x*d.y - c.y*d.x)); if (det == 0.0) return p0; else { PxVec3d v = c.cross(d)*b.dot(b) + d.cross(b)*c.dot(c) + b.cross(c)*d.dot(d); v /= det; return p0 + v; } } // ----------------------------------------------------------------------------------- bool OctreeTetrahedralizer::meshInsertTetVert(PxI32 vertNr) { const PxVec3d& p = tetVerts[vertNr]; PxI32 surroundingTetNr; if (!findSurroundingTet(p, surroundingTetNr)) return false; // find violating tets violatingTets.clear(); stack.clear(); currentTetMark++; stack.pushBack(surroundingTetNr); while (!stack.empty()) { PxI32 tetNr = stack.back(); stack.popBack(); if (tetMarks[tetNr] == currentTetMark) continue; tetMarks[tetNr] = currentTetMark; violatingTets.pushBack(tetNr); for (PxI32 i = 0; i < 4; i++) { PxI32 n = tetNeighbors[4 * tetNr + i]; if (n < 0 || tetMarks[n] == currentTetMark) continue; // Delaunay condition test PxI32* ids = &tetIds[4 * n]; PxVec3d c = getCircumCenter(tetVerts[ids[0]], tetVerts[ids[1]], tetVerts[ids[2]], tetVerts[ids[3]]); PxF64 r2 = (tetVerts[ids[0]] - c).magnitudeSquared(); if ((p - c).magnitudeSquared() < r2) stack.pushBack(n); } } // remove old tets, create new ones edges.clear(); Edge e; for (PxI32 i = 0; i < PxI32(violatingTets.size()); i++) { PxI32 tetNr = violatingTets[i]; // copy information before we delete it PxI32 ids[4], ns[4]; for (PxI32 j = 0; j < 4; j++) { ids[j] = tetIds[4 * tetNr + j]; ns[j] = tetNeighbors[4 * tetNr + j]; } // delete the tetrahedron treeRemoveTet(tetNr); removeTetNr(tetNr); // visit neighbors for (PxI32 j = 0; j < 4; j++) { PxI32 n = ns[j]; if (n < 0 || tetMarks[n] != currentTetMark) { // no neighbor or neighbor is not-violating -> we are facing the border // create new tetrahedron PxI32 newTetNr = getNewTetNr(); PxI32 id0 = ids[tetFaces[j][2]]; PxI32 id1 = ids[tetFaces[j][1]]; PxI32 id2 = ids[tetFaces[j][0]]; tetIds[4 * newTetNr] = id0; tetIds[4 * newTetNr + 1] = id1; tetIds[4 * newTetNr + 2] = id2; tetIds[4 * newTetNr + 3] = vertNr; treeInsertTet(newTetNr); tetNeighbors[4 * newTetNr] = n; if (n >= 0) { for (PxI32 k = 0; k < 4; k++) { if (tetNeighbors[4 * n + k] == tetNr) tetNeighbors[4 * n + k] = newTetNr; } } // will set the neighbors among the new tetrahedra later tetNeighbors[4 * newTetNr + 1] = -1; tetNeighbors[4 * newTetNr + 2] = -1; tetNeighbors[4 * newTetNr + 3] = -1; e.init(id0, id1, newTetNr, 1); edges.pushBack(e); e.init(id1, id2, newTetNr, 2); edges.pushBack(e); e.init(id2, id0, newTetNr, 3); edges.pushBack(e); } } // next neighbor } // next violating tetrahedron // fix neighbors PxSort(edges.begin(), edges.size()); PxI32 nr = 0; while (nr < PxI32(edges.size())) { Edge& e0 = edges[nr]; nr++; if (nr < PxI32(edges.size()) && edges[nr] == e0) { Edge& e1 = edges[nr]; tetNeighbors[4 * e0.tetNr + e0.faceNr] = e1.tetNr; tetNeighbors[4 * e1.tetNr + e1.faceNr] = e0.tetNr; nr++; } } return true; } // ----------------------------------------------------------------------------------- static PxF64 tetQuality(const PxVec3d& p0, const PxVec3d& p1, const PxVec3d& p2, const PxVec3d& p3) { PxVec3d d0 = p1 - p0; PxVec3d d1 = p2 - p0; PxVec3d d2 = p3 - p0; PxVec3d d3 = p2 - p1; PxVec3d d4 = p3 - p2; PxVec3d d5 = p1 - p3; PxF64 s0 = d0.magnitudeSquared(); PxF64 s1 = d1.magnitudeSquared(); PxF64 s2 = d2.magnitudeSquared(); PxF64 s3 = d3.magnitudeSquared(); PxF64 s4 = d4.magnitudeSquared(); PxF64 s5 = d5.magnitudeSquared(); PxF64 ms = (s0 + s1 + s2 + s3 + s4 + s5) / 6.0; PxF64 rms = sqrt(ms); static const PxF64 s = 12.0 / sqrt(2.0); PxF64 vol = d0.dot(d1.cross(d2)) / 6.0; return s * vol / (rms * rms * rms); // 1.0 for regular tetrahedron } // ----------------------------------------------------------------------------------- void OctreeTetrahedralizer::pruneTets() { insideTester.init(surfaceVerts.begin(), PxI32(surfaceVerts.size()), surfaceTriIds.begin(), PxI32(surfaceTriIds.size()) / 3); static PxF64 minQuality = 0.01; PxI32 numTets = tetIds.size() / 4; PxI32 num = 0; for (PxI32 i = 0; i < numTets; i++) { bool remove = false; PxI32* ids = &tetIds[4 * i]; for (PxI32 j = 0; j < 4; j++) { if (ids[j] >= firstABBVert) remove = true; } if (ids[0] < 0 || ids[1] < 0 || ids[2] < 0 || ids[3] < 0) remove = true; if (!remove) { PxVec3d c = getTetCenter(i); if (!insideTester.isInside(PxVec3(PxReal(c.x), PxReal(c.y), PxReal(c.z)))) remove = true; if (tetQuality(tetVerts[ids[0]], tetVerts[ids[1]], tetVerts[ids[2]], tetVerts[ids[3]]) < minQuality) continue; } if (remove) continue; for (PxI32 j = 0; j < 4; j++) tetIds[4 * num + j] = ids[j]; num++; } tetIds.resize(4 * num); } // ----------------------------------------------------------------------------------- void OctreeTetrahedralizer::createTetMesh(const PxArray& verts, const PxArray& triIds, bool includeOctreeNodes, PxI32 _maxVertsPerCell, PxI32 _maxTreeDepth) { this->surfaceVerts = verts; surfaceTriIds.resize(triIds.size()); for (PxU32 i = 0; i < triIds.size(); i++) this->surfaceTriIds[i] = triIds[i]; this->maxVertsPerCell = _maxVertsPerCell; this->maxTreeDepth = _maxTreeDepth; createTree(); clearTets(); if (cells.empty()) return; createTetVerts(includeOctreeNodes); if (tetVerts.empty()) return; for (PxI32 i = 0; i < PxI32(cells.size()); i++) cells[i].closestTetNr = -1; // create aabb tets Bounds3 bounds; bounds.setEmpty(); for (PxI32 i = 0; i < PxI32(tetVerts.size()); i++) bounds.include(tetVerts[i]); bounds.expand(bounds.getDimensions().magnitude() * 0.1); firstABBVert = PxI32(tetVerts.size()); PxVec3d dims = bounds.getDimensions(); for (PxI32 i = 0; i < 8; i++) { tetVerts.pushBack(bounds.minimum + PxVec3d( cubeCorners[i][0] * dims.x, cubeCorners[i][1] * dims.y, cubeCorners[i][2] * dims.z)); } for (PxI32 i = 0; i < 6; i++) { for (PxI32 j = 0; j < 4; j++) { tetIds.pushBack(firstABBVert + cubeTets[i][j]); tetNeighbors.pushBack(cubeTetNeighbors[i][j]); } treeInsertTet(i); } tetMarks.resize(6, 0); for (PxI32 i = 0; i < firstABBVert; i++) { meshInsertTetVert(i); } pruneTets(); renderTriIds.clear(); renderVerts.clear(); } // ----------------------------------------------------------------------------------- void OctreeTetrahedralizer::readBack(PxArray &outputTetVerts, PxArray &outputTetIds) { outputTetVerts.resize(tetVerts.size()); for (PxU32 i = 0; i < tetVerts.size(); i++) { PxVec3d &v = tetVerts[i]; outputTetVerts[i] = PxVec3(PxReal(v.x), PxReal(v.y), PxReal(v.z)); } outputTetIds.resize(tetIds.size()); for (PxU32 i = 0; i < tetIds.size(); i++) outputTetIds[i] = PxU32(tetIds[i]); }