466 lines
16 KiB
C++
466 lines
16 KiB
C++
// 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.
|
|
// Copyright (c) 2004-2008 AGEIA Technologies, Inc. All rights reserved.
|
|
// Copyright (c) 2001-2004 NovodeX AG. All rights reserved.
|
|
|
|
#include "extensions/PxTetrahedronMeshExt.h"
|
|
#include "foundation/PxMathUtils.h"
|
|
#include "foundation/PxHashMap.h"
|
|
#include "GuTetrahedronMesh.h"
|
|
#include "GuBox.h"
|
|
#include "GuBV4.h"
|
|
#include "GuBV4_Common.h"
|
|
#include "GuDistancePointTetrahedron.h"
|
|
|
|
#include "GuAABBTreeNode.h"
|
|
#include "GuAABBTree.h"
|
|
#include "GuAABBTreeBounds.h"
|
|
#include "GuAABBTreeQuery.h"
|
|
|
|
using namespace physx;
|
|
|
|
namespace
|
|
{
|
|
struct TetrahedronFinderCallback
|
|
{
|
|
PxVec3 mQueryPoint;
|
|
PxI32 mTetId;
|
|
PxVec4 mBary;
|
|
|
|
const PxVec3* mVertices;
|
|
const PxU32* mTets;
|
|
|
|
PxReal mTolerance = 1e-6f;
|
|
|
|
TetrahedronFinderCallback(const PxVec3& queryPoint, const PxVec3* vertices, const PxU32* tets, const PxReal tolerance = 1e-6f) :
|
|
mQueryPoint(queryPoint), mTetId(-1), mVertices(vertices), mTets(tets), mTolerance(tolerance) {}
|
|
|
|
PX_FORCE_INLINE bool testPrimitive(const PxU32 primitiveStartId, const PxU32 numPrimitives)
|
|
{
|
|
for (PxU32 i = 0; i < numPrimitives; ++i)
|
|
{
|
|
const PxU32* tet = &mTets[4 * (primitiveStartId + i)];
|
|
PxComputeBarycentric(mVertices[tet[0]], mVertices[tet[1]], mVertices[tet[2]], mVertices[tet[3]], mQueryPoint, mBary);
|
|
|
|
if (mBary.x >= -mTolerance && mBary.x <= 1 + mTolerance && mBary.y >= -mTolerance && mBary.y <= 1 + mTolerance &&
|
|
mBary.z >= -mTolerance && mBary.z <= 1 + mTolerance && mBary.w >= -mTolerance && mBary.w <= 1 + mTolerance)
|
|
{
|
|
mTetId = PxI32(primitiveStartId + i);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
PX_FORCE_INLINE bool testBox(const float boxMinX, const float boxMinY, const float boxMinZ, const float boxMaxX, const float boxMaxY, const float boxMaxZ)
|
|
{
|
|
return mQueryPoint.x >= boxMinX && mQueryPoint.y >= boxMinY && mQueryPoint.z >= boxMinZ &&
|
|
mQueryPoint.x <= boxMaxX && mQueryPoint.y <= boxMaxY && mQueryPoint.z <= boxMaxZ;
|
|
}
|
|
};
|
|
|
|
struct ClosestTetrahedronFinderCallback
|
|
{
|
|
PxVec3 mQueryPoint;
|
|
PxI32 mTetId;
|
|
PxVec4 mBary;
|
|
PxReal mDist = 1.84467e+19f; // sqrtf(FLT_MAX)
|
|
|
|
const PxVec3* mVertices;
|
|
const PxU32* mTets;
|
|
PxReal mTolerance = 1e-6f;
|
|
|
|
ClosestTetrahedronFinderCallback(const PxVec3& queryPoint, const PxVec3* vertices, const PxU32* tets) :
|
|
mQueryPoint(queryPoint), mTetId(-1), mVertices(vertices), mTets(tets) {}
|
|
|
|
PX_FORCE_INLINE bool testPrimitive(const PxU32 primitiveStartId, const PxU32 numPrimitives)
|
|
{
|
|
for (PxU32 i = 0; i < numPrimitives; ++i)
|
|
{
|
|
PxVec4 bary;
|
|
const PxU32* tet = &mTets[4 * (primitiveStartId + i)];
|
|
PxComputeBarycentric(mVertices[tet[0]], mVertices[tet[1]], mVertices[tet[2]], mVertices[tet[3]], mQueryPoint, bary);
|
|
|
|
if (bary.x >= -mTolerance && bary.x <= 1 + mTolerance && bary.y >= -mTolerance && bary.y <= 1 + mTolerance &&
|
|
bary.z >= -mTolerance && bary.z <= 1 + mTolerance && bary.w >= -mTolerance && bary.w <= 1 + mTolerance)
|
|
{
|
|
mTetId = PxI32(primitiveStartId + i);
|
|
mBary = bary;
|
|
mDist = 0;
|
|
return true;
|
|
}
|
|
|
|
PxVec3 closest = Gu::closestPtPointTetrahedron(mQueryPoint, mVertices[tet[0]], mVertices[tet[1]], mVertices[tet[2]], mVertices[tet[3]]);
|
|
PxReal distSq = (closest - mQueryPoint).magnitudeSquared();
|
|
if (distSq < mDist * mDist)
|
|
{
|
|
mTetId = PxI32(primitiveStartId + i);
|
|
mBary = bary;
|
|
mDist = PxSqrt(distSq);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
PX_FORCE_INLINE bool testBox(const float boxMinX, const float boxMinY, const float boxMinZ, const float boxMaxX, const float boxMaxY, const float boxMaxZ)
|
|
{
|
|
if (mQueryPoint.x >= boxMinX && mQueryPoint.y >= boxMinY && mQueryPoint.z >= boxMinZ &&
|
|
mQueryPoint.x <= boxMaxX && mQueryPoint.y <= boxMaxY && mQueryPoint.z <= boxMaxZ)
|
|
return true;
|
|
PxVec3 closest = mQueryPoint;
|
|
closest.x = PxClamp(closest.x, boxMinX, boxMaxX);
|
|
closest.y = PxClamp(closest.y, boxMinY, boxMaxY);
|
|
closest.z = PxClamp(closest.z, boxMinZ, boxMaxZ);
|
|
PxReal distSq = (closest - mQueryPoint).magnitudeSquared();
|
|
return distSq < mDist * mDist;
|
|
}
|
|
};
|
|
}
|
|
|
|
template<typename T, PxU32 i>
|
|
static int process(PxU32* stack, PxU32& stackSize, const Gu::BVDataSwizzledNQ* node, T& callback)
|
|
{
|
|
if (callback.testBox(node->mMinX[i], node->mMinY[i], node->mMinZ[i], node->mMaxX[i], node->mMaxY[i], node->mMaxZ[i]))
|
|
{
|
|
if (node->isLeaf(i))
|
|
{
|
|
PxU32 primitiveIndex = node->getPrimitive(i);
|
|
const PxU32 numPrimitives = Gu::getNbPrimitives(primitiveIndex);
|
|
if(callback.testPrimitive(primitiveIndex, numPrimitives)) //Returns true if the query should be terminated immediately
|
|
return 1;
|
|
}
|
|
else
|
|
stack[stackSize++] = node->getChildData(i);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
template<typename T>
|
|
static void traverseBVH(const Gu::BV4Tree& tree, T& callback)
|
|
{
|
|
const Gu::BVDataPackedNQ* root = static_cast<const Gu::BVDataPackedNQ*>(tree.mNodes);
|
|
|
|
PxU32 stack[GU_BV4_STACK_SIZE];
|
|
PxU32 stackSize = 0;
|
|
stack[stackSize++] = tree.mInitData;
|
|
|
|
while (stackSize > 0)
|
|
{
|
|
const PxU32 childData = stack[--stackSize];
|
|
const Gu::BVDataSwizzledNQ* node = reinterpret_cast<const Gu::BVDataSwizzledNQ*>(root + Gu::getChildOffset(childData));
|
|
|
|
const PxU32 nodeType = Gu::getChildType(childData);
|
|
|
|
if (nodeType > 1)
|
|
if (process<T, 3>(stack, stackSize, node, callback)) return;
|
|
if (nodeType > 0)
|
|
if (process<T, 2>(stack, stackSize, node, callback)) return;
|
|
if (process<T, 1>(stack, stackSize, node, callback)) return;
|
|
if (process<T, 0>(stack, stackSize, node, callback)) return;
|
|
}
|
|
}
|
|
|
|
PxI32 PxTetrahedronMeshExt::findTetrahedronContainingPoint(const PxTetrahedronMesh* mesh, const PxVec3& point, PxVec4& bary, PxReal tolerance)
|
|
{
|
|
TetrahedronFinderCallback callback(point, mesh->getVertices(), static_cast<const PxU32*>(mesh->getTetrahedrons()), tolerance);
|
|
traverseBVH(static_cast<const Gu::BVTetrahedronMesh*>(mesh)->getBV4Tree(), callback);
|
|
bary = callback.mBary;
|
|
return callback.mTetId;
|
|
}
|
|
|
|
PxI32 PxTetrahedronMeshExt::findTetrahedronClosestToPoint(const PxTetrahedronMesh* mesh, const PxVec3& point, PxVec4& bary)
|
|
{
|
|
ClosestTetrahedronFinderCallback callback(point, mesh->getVertices(), static_cast<const PxU32*>(mesh->getTetrahedrons()));
|
|
const Gu::BV4Tree& tree = static_cast<const Gu::BVTetrahedronMesh*>(mesh)->getBV4Tree();
|
|
if (tree.mNbNodes) traverseBVH(tree, callback);
|
|
else callback.testPrimitive(0, mesh->getNbTetrahedrons());
|
|
bary = callback.mBary;
|
|
return callback.mTetId;
|
|
}
|
|
|
|
namespace
|
|
{
|
|
class ClosestDistanceToTetmeshTraversalController
|
|
{
|
|
private:
|
|
PxReal mClosestDistanceSquared;
|
|
const PxU32* mTetrahedra;
|
|
const PxVec3* mPoints;
|
|
const Gu::BVHNode* mNodes;
|
|
PxVec3 mQueryPoint;
|
|
PxVec3 mClosestPoint;
|
|
PxI32 mClosestTetId;
|
|
|
|
public:
|
|
PX_FORCE_INLINE ClosestDistanceToTetmeshTraversalController() {}
|
|
|
|
PX_FORCE_INLINE ClosestDistanceToTetmeshTraversalController(const PxU32* tetrahedra, const PxVec3* points, Gu::BVHNode* nodes) :
|
|
mTetrahedra(tetrahedra), mPoints(points), mNodes(nodes), mQueryPoint(0.0f), mClosestPoint(0.0f), mClosestTetId(-1)
|
|
{
|
|
initialize(tetrahedra, points, nodes);
|
|
}
|
|
|
|
void initialize(const PxU32* tetrahedra, const PxVec3* points, Gu::BVHNode* nodes)
|
|
{
|
|
mTetrahedra = tetrahedra;
|
|
mPoints = points;
|
|
mNodes = nodes;
|
|
mQueryPoint = PxVec3(0.0f);
|
|
mClosestPoint = PxVec3(0.0f);
|
|
mClosestTetId = -1;
|
|
mClosestDistanceSquared = PX_MAX_F32;
|
|
}
|
|
|
|
PX_FORCE_INLINE void setQueryPoint(const PxVec3& queryPoint)
|
|
{
|
|
this->mQueryPoint = queryPoint;
|
|
mClosestDistanceSquared = FLT_MAX;
|
|
mClosestPoint = PxVec3(0.0f);
|
|
mClosestTetId = -1;
|
|
}
|
|
|
|
PX_FORCE_INLINE const PxVec3& getClosestPoint() const
|
|
{
|
|
return mClosestPoint;
|
|
}
|
|
|
|
PX_FORCE_INLINE PxReal distancePointBoxSquared(const PxBounds3& box, const PxVec3& point)
|
|
{
|
|
PxVec3 closestPt = box.minimum.maximum(box.maximum.minimum(point));
|
|
return (closestPt - point).magnitudeSquared();
|
|
}
|
|
|
|
PX_FORCE_INLINE Gu::TraversalControl::Enum analyze(const Gu::BVHNode& node, PxI32)
|
|
{
|
|
if (distancePointBoxSquared(node.mBV, mQueryPoint) >= mClosestDistanceSquared)
|
|
return Gu::TraversalControl::eDontGoDeeper;
|
|
|
|
if (node.isLeaf())
|
|
{
|
|
const PxI32 j = node.getPrimitiveIndex();
|
|
const PxU32* tet = &mTetrahedra[4 * j];
|
|
|
|
PxVec4 bary;
|
|
PxComputeBarycentric(mPoints[tet[0]], mPoints[tet[1]], mPoints[tet[2]], mPoints[tet[3]], mQueryPoint, bary);
|
|
|
|
const PxReal tolerance = 0.0f;
|
|
if (bary.x >= -tolerance && bary.x <= 1 + tolerance && bary.y >= -tolerance && bary.y <= 1 + tolerance &&
|
|
bary.z >= -tolerance && bary.z <= 1 + tolerance && bary.w >= -tolerance && bary.w <= 1 + tolerance)
|
|
{
|
|
mClosestDistanceSquared = 0;
|
|
mClosestTetId = j;
|
|
mClosestPoint = mQueryPoint;
|
|
return Gu::TraversalControl::eAbort;
|
|
}
|
|
|
|
PxVec3 closest = Gu::closestPtPointTetrahedron(mQueryPoint, mPoints[tet[0]], mPoints[tet[1]], mPoints[tet[2]], mPoints[tet[3]]);
|
|
PxReal d2 = (closest - mQueryPoint).magnitudeSquared();
|
|
if (d2 < mClosestDistanceSquared)
|
|
{
|
|
mClosestDistanceSquared = d2;
|
|
mClosestTetId = j;
|
|
mClosestPoint = closest;
|
|
}
|
|
return Gu::TraversalControl::eDontGoDeeper;
|
|
}
|
|
|
|
const Gu::BVHNode& nodePos = mNodes[node.getPosIndex()];
|
|
const PxReal distSquaredPos = distancePointBoxSquared(nodePos.mBV, mQueryPoint);
|
|
const Gu::BVHNode& nodeNeg = mNodes[node.getNegIndex()];
|
|
const PxReal distSquaredNeg = distancePointBoxSquared(nodeNeg.mBV, mQueryPoint);
|
|
|
|
if (distSquaredPos < distSquaredNeg)
|
|
{
|
|
if (distSquaredPos < mClosestDistanceSquared)
|
|
return Gu::TraversalControl::eGoDeeper;
|
|
}
|
|
else
|
|
{
|
|
if (distSquaredNeg < mClosestDistanceSquared)
|
|
return Gu::TraversalControl::eGoDeeperNegFirst;
|
|
}
|
|
return Gu::TraversalControl::eDontGoDeeper;
|
|
}
|
|
|
|
PxI32 getClosestTetId() const { return mClosestTetId; }
|
|
|
|
void setClosestStart(const PxReal closestDistanceSquared, PxI32 closestTetrahedron, const PxVec3& closestPoint)
|
|
{
|
|
mClosestDistanceSquared = closestDistanceSquared;
|
|
mClosestTetId = closestTetrahedron;
|
|
mClosestPoint = closestPoint;
|
|
}
|
|
|
|
private:
|
|
PX_NOCOPY(ClosestDistanceToTetmeshTraversalController)
|
|
};
|
|
}
|
|
|
|
static void buildTree(const PxU32* tetrahedra, const PxU32 numTetrahedra, const PxVec3* points, PxArray<Gu::BVHNode>& tree, PxF32 enlargement = 1e-4f)
|
|
{
|
|
//Computes a bounding box for every triangle in triangles
|
|
Gu::AABBTreeBounds boxes;
|
|
boxes.init(numTetrahedra);
|
|
for (PxU32 i = 0; i < numTetrahedra; ++i)
|
|
{
|
|
const PxU32* tri = &tetrahedra[4 * i];
|
|
PxBounds3 box = PxBounds3::empty();
|
|
box.include(points[tri[0]]);
|
|
box.include(points[tri[1]]);
|
|
box.include(points[tri[2]]);
|
|
box.include(points[tri[3]]);
|
|
box.fattenFast(enlargement);
|
|
boxes.getBounds()[i] = box;
|
|
}
|
|
|
|
Gu::buildAABBTree(numTetrahedra, boxes, tree);
|
|
}
|
|
|
|
void PxTetrahedronMeshExt::createPointsToTetrahedronMap(const PxArray<PxVec3>& tetMeshVertices, const PxArray<PxU32>& tetMeshIndices,
|
|
const PxArray<PxVec3>& pointsToEmbed, PxArray<PxVec4>& barycentricCoordinates, PxArray<PxU32>& tetLinks)
|
|
{
|
|
barycentricCoordinates.resize(0);
|
|
tetLinks.resize(0);
|
|
|
|
if (tetMeshVertices.size() == 0 || tetMeshIndices.size() == 0)
|
|
{
|
|
PxGetFoundation().error(PxErrorCode::eINVALID_PARAMETER, PX_FL, "Point in Tetmesh embedding: Input mesh does not have any tetrahedra or points.");
|
|
return;
|
|
}
|
|
|
|
if (pointsToEmbed.size() == 0)
|
|
return;
|
|
|
|
PxArray<Gu::BVHNode> tree;
|
|
buildTree(tetMeshIndices.begin(), tetMeshIndices.size() / 4, tetMeshVertices.begin(), tree);
|
|
|
|
if (tree.size() == 0)
|
|
return;
|
|
|
|
ClosestDistanceToTetmeshTraversalController cd(tetMeshIndices.begin(), tetMeshVertices.begin(), tree.begin());
|
|
|
|
barycentricCoordinates.resize(pointsToEmbed.size());
|
|
tetLinks.resize(pointsToEmbed.size());
|
|
for (PxU32 i = 0; i < pointsToEmbed.size(); ++i)
|
|
{
|
|
cd.setQueryPoint(pointsToEmbed[i]);
|
|
Gu::traverseBVH(tree.begin(), cd);
|
|
|
|
const PxU32* tet = &tetMeshIndices[4 * cd.getClosestTetId()];
|
|
PxVec4 bary;
|
|
PxComputeBarycentric(tetMeshVertices[tet[0]], tetMeshVertices[tet[1]], tetMeshVertices[tet[2]], tetMeshVertices[tet[3]], cd.getClosestPoint(), bary);
|
|
|
|
barycentricCoordinates[i] = bary;
|
|
tetLinks[i] = cd.getClosestTetId();
|
|
}
|
|
}
|
|
|
|
namespace
|
|
{
|
|
struct SortedTriangle
|
|
{
|
|
public:
|
|
PxU32 A;
|
|
PxU32 B;
|
|
PxU32 C;
|
|
PxI32 TetIndex;
|
|
bool Flipped;
|
|
|
|
PX_FORCE_INLINE SortedTriangle(PxU32 a, PxU32 b, PxU32 c, PxI32 tetIndex = -1)
|
|
{
|
|
A = a; B = b; C = c; Flipped = false; TetIndex = tetIndex;
|
|
if (A > B) { PxSwap(A, B); Flipped = !Flipped; }
|
|
if (B > C) { PxSwap(B, C); Flipped = !Flipped; }
|
|
if (A > B) { PxSwap(A, B); Flipped = !Flipped; }
|
|
}
|
|
};
|
|
|
|
struct TriangleHash
|
|
{
|
|
PX_FORCE_INLINE std::size_t operator()(const SortedTriangle& k) const
|
|
{
|
|
return k.A ^ k.B ^ k.C;
|
|
}
|
|
|
|
PX_FORCE_INLINE bool equal(const SortedTriangle& first, const SortedTriangle& second) const
|
|
{
|
|
return first.A == second.A && first.B == second.B && first.C == second.C;
|
|
}
|
|
};
|
|
}
|
|
|
|
static const PxU32 tetFaces[4][3] = { {0, 2, 1}, {0, 1, 3}, {0, 3, 2}, {1, 2, 3} };
|
|
|
|
void PxTetrahedronMeshExt::extractTetMeshSurface(const void* tetrahedra, PxU32 numTetrahedra, bool sixteenBitIndices, PxArray<PxU32>& surfaceTriangles, PxArray<PxU32>* surfaceTriangleToTet, bool flipTriangleOrientation)
|
|
{
|
|
PxHashMap<SortedTriangle, PxU32, TriangleHash> tris;
|
|
|
|
const PxU32* tets32 = reinterpret_cast<const PxU32*>(tetrahedra);
|
|
const PxU16* tets16 = reinterpret_cast<const PxU16*>(tetrahedra);
|
|
|
|
PxU32 l = 4 * numTetrahedra;
|
|
for (PxU32 i = 0; i < l; i += 4)
|
|
{
|
|
for (PxU32 j = 0; j < 4; ++j)
|
|
{
|
|
SortedTriangle tri(sixteenBitIndices ? tets16[i + tetFaces[j][0]] : tets32[i + tetFaces[j][0]],
|
|
sixteenBitIndices ? tets16[i + tetFaces[j][1]] : tets32[i + tetFaces[j][1]],
|
|
sixteenBitIndices ? tets16[i + tetFaces[j][2]] : tets32[i + tetFaces[j][2]], i);
|
|
if (const PxPair<const SortedTriangle, PxU32>* ptr = tris.find(tri))
|
|
tris[tri] = ptr->second + 1;
|
|
else
|
|
tris.insert(tri, 1);
|
|
}
|
|
}
|
|
|
|
surfaceTriangles.clear();
|
|
if (surfaceTriangleToTet)
|
|
surfaceTriangleToTet->clear();
|
|
for (PxHashMap<SortedTriangle, PxU32, TriangleHash>::Iterator iter = tris.getIterator(); !iter.done(); ++iter)
|
|
{
|
|
if (iter->second == 1) {
|
|
surfaceTriangles.pushBack(iter->first.A);
|
|
if (iter->first.Flipped != flipTriangleOrientation)
|
|
{
|
|
surfaceTriangles.pushBack(iter->first.C);
|
|
surfaceTriangles.pushBack(iter->first.B);
|
|
}
|
|
else
|
|
{
|
|
surfaceTriangles.pushBack(iter->first.B);
|
|
surfaceTriangles.pushBack(iter->first.C);
|
|
}
|
|
if (surfaceTriangleToTet)
|
|
surfaceTriangleToTet->pushBack(iter->first.TetIndex);
|
|
}
|
|
}
|
|
}
|
|
|
|
void PxTetrahedronMeshExt::extractTetMeshSurface(const PxTetrahedronMesh* mesh, PxArray<PxU32>& surfaceTriangles, PxArray<PxU32>* surfaceTriangleToTet, bool flipTriangleOrientation)
|
|
{
|
|
extractTetMeshSurface(mesh->getTetrahedrons(), mesh->getNbTetrahedrons(), mesh->getTetrahedronMeshFlags() & PxTetrahedronMeshFlag::e16_BIT_INDICES, surfaceTriangles, surfaceTriangleToTet, flipTriangleOrientation);
|
|
}
|