Files
UnrealEngine/Engine/Source/Developer/PhysicsUtilities/Private/SkinnedLevelSetBuilder.cpp
2025-05-18 13:04:45 +08:00

355 lines
14 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "SkinnedLevelSetBuilder.h"
#include "SkinnedBoneTriangleCache.h"
#include "LevelSetHelpers.h"
#include "BoneWeights.h"
#include "Chaos/ArrayND.h"
#include "Chaos/Levelset.h"
#include "Chaos/Plane.h"
#include "Chaos/UniformGrid.h"
#include "Chaos/Math/Poisson.h"
#include "DynamicMesh/DynamicMesh3.h"
#include "Engine/SkeletalMesh.h"
#include "Implicit/SweepingMeshSDF.h"
#include "Implicit/Solidify.h"
#include "PhysicsEngine/BodySetup.h"
#include "PhysicsEngine/SkinnedLevelSetElem.h"
#include "Rendering/SkeletalMeshModel.h"
#include "Rendering/SkeletalMeshRenderData.h"
FSkinnedLevelSetBuilder::FSkinnedLevelSetBuilder(const USkeletalMesh& InSkeletalMesh, const FSkinnedBoneTriangleCache& InTriangleCache, const int32 InRootBoneIndex)
:SkeletalMesh(InSkeletalMesh)
, TriangleCache(InTriangleCache)
, RootBoneIndex(InRootBoneIndex)
, StaticLODModel(SkeletalMesh.GetImportedModel())
, RenderData(SkeletalMesh.GetResourceForRendering())
{
check(StaticLODModel);
check(RenderData);
}
bool FSkinnedLevelSetBuilder::InitializeSkinnedLevelset(const FPhysAssetCreateParams& Params, const TArray<int32>& BoneIndices, TArray<uint32>& OutOrigIndices)
{
check(SkeletalMesh.GetRefSkeleton().IsValidRawIndex(RootBoneIndex));
TArray<FVector3f> Positions;
TArray<uint32> Indices;
if (Params.VertWeight == EVW_AnyWeight)
{
// Want to use EVW_DominantWeight for initial levelset
// Build a different cache
FPhysAssetCreateParams ParamsCopy;
ParamsCopy.VertWeight = EVW_DominantWeight;
FSkinnedBoneTriangleCache DominantCache(SkeletalMesh, ParamsCopy);
DominantCache.BuildCache();
DominantCache.GetVerticesAndIndicesForBones(RootBoneIndex, BoneIndices, Positions, Indices, OutOrigIndices);
}
else
{
TriangleCache.GetVerticesAndIndicesForBones(RootBoneIndex, BoneIndices, Positions, Indices, OutOrigIndices);
}
if (Positions.Num())
{
UE::Geometry::FDynamicMesh3 DynamicMesh;
LevelSetHelpers::CreateDynamicMesh(Positions, Indices, DynamicMesh);
const bool bOK = LevelSetHelpers::CreateLevelSetForMesh(DynamicMesh, Params.LevelSetResolution, LevelSet);
if (bOK)
{
GenerateGrid(Params.LatticeResolution, LevelSet->BoundingBox());
}
else
{
FMessageLog EditorErrors("EditorErrors");
EditorErrors.Warning(NSLOCTEXT("PhysicsAssetUtils", "SkinnedLevelSetError", "An error occurred creating root skinned level set."));
EditorErrors.Open();
}
return bOK;
}
FMessageLog EditorErrors("EditorErrors");
EditorErrors.Warning(NSLOCTEXT("PhysicsAssetUtils", "LevelSetNoPositions", "Unable to create a level set for the given bone as there are no vertices associated with the bone."));
EditorErrors.Open();
return false;
}
static float CalculateTriangleWeight(const UE::Geometry::FTriangle3d& Triangle, const FVector3f& TriangleWeights, const Chaos::TVector<double, 3>& Location)
{
if (TriangleWeights.X == 0.f)
{
if (TriangleWeights.Y == 0.f)
{
check(TriangleWeights.Z != 0.f);
return TriangleWeights.Z;
}
else if (TriangleWeights.Z == 0.f)
{
check(TriangleWeights.Y != 0.f);
return TriangleWeights.Y;
}
else
{
double Alpha;
Chaos::FindClosestPointAndAlphaOnLineSegment(Chaos::FVec3(Triangle.V[1]), Chaos::FVec3(Triangle.V[2]), Location, Alpha);
return (1. - Alpha) * TriangleWeights.Y + Alpha * TriangleWeights.Z;
}
}
else if (TriangleWeights.Y == 0.f)
{
if (TriangleWeights.Z == 0.f)
{
check(TriangleWeights.X != 0.f);
return TriangleWeights.X;
}
else
{
double Alpha;
Chaos::FindClosestPointAndAlphaOnLineSegment(Chaos::FVec3(Triangle.V[0]), Chaos::FVec3(Triangle.V[2]), Location, Alpha);
return (1. - Alpha) * TriangleWeights.X + Alpha * TriangleWeights.Z;
}
}
else if (TriangleWeights.Z == 0.f)
{
double Alpha;
Chaos::FindClosestPointAndAlphaOnLineSegment(Chaos::FVec3(Triangle.V[0]), Chaos::FVec3(Triangle.V[1]), Location, Alpha);
return (1. - Alpha) * TriangleWeights.X + Alpha * TriangleWeights.Y;
}
else
{
// Get Barycentric coordinates for closest point
UE::Geometry::TDistPoint3Triangle3<double> TriQuery(Location, Triangle);
TriQuery.ComputeResult();
return TriQuery.TriangleBaryCoords.X * TriangleWeights.X +
TriQuery.TriangleBaryCoords.Y * TriangleWeights.Y + TriQuery.TriangleBaryCoords.Z * TriangleWeights.Z;
}
}
void FSkinnedLevelSetBuilder::AddBoneInfluence(int32 PrimaryBoneIndex, const TArray<int32>& AllBonesForInfluence)
{
// Need to generate weights for grid points corresponding with this bone.
// Will laplacian diffuse the weights from surface to bone interior.
// Get Triangles that correspond with these bones.
TArray<FVector3f> AllVerts;
TArray<uint32> AllIndices;
TArray<uint32> AllOrigIndices;
TriangleCache.GetVerticesAndIndicesForBones(RootBoneIndex, AllBonesForInfluence, AllVerts, AllIndices, AllOrigIndices);
// Strip any vertices that are outside the levelset.
TArray<FVector3f> Verts;
Verts.Reserve(AllVerts.Num());
TArray<uint32> Indices;
Indices.Reserve(AllIndices.Num());
TArray<uint32> OrigIndices;
OrigIndices.Reserve(AllOrigIndices.Num());
TArray<int32> AllVertToVerts;
AllVertToVerts.SetNumUninitialized(AllVerts.Num());
const double MaxInsidePhi = LevelSet->GetGrid().Dx().GetMax() * .5;
for (int32 AllVertIdx = 0; AllVertIdx < AllVerts.Num(); ++AllVertIdx)
{
if (LevelSet->SignedDistance(Chaos::FVec3(AllVerts[AllVertIdx])) < MaxInsidePhi)
{
const int32 VertIdx = Verts.Add(AllVerts[AllVertIdx]);
OrigIndices.Add(AllOrigIndices[AllVertIdx]);
AllVertToVerts[AllVertIdx] = VertIdx;
}
else
{
AllVertToVerts[AllVertIdx] = INDEX_NONE;
}
}
check(AllIndices.Num() % 3 == 0);
const int32 NumTriangles = AllIndices.Num() / 3;
for (int32 AllTriIdx = 0; AllTriIdx < NumTriangles; ++AllTriIdx)
{
const int32 VertIdx0 = AllVertToVerts[AllIndices[AllTriIdx * 3]];
const int32 VertIdx1 = AllVertToVerts[AllIndices[AllTriIdx * 3 + 1]];
const int32 VertIdx2 = AllVertToVerts[AllIndices[AllTriIdx * 3 + 2]];
if (VertIdx0 != INDEX_NONE && VertIdx1 != INDEX_NONE && VertIdx2 != INDEX_NONE)
{
Indices.Add(VertIdx0);
Indices.Add(VertIdx1);
Indices.Add(VertIdx2);
}
}
const Chaos::TUniformGrid<double, 3>& LatticeGrid = GetGrid();
const Chaos::TUniformGrid<double, 3>& LevelsetGrid = LevelSet->GetGrid();
// Surface is almost certainly not closed, but we want to diffuse differently inside vs outside, so we
// need a closed surface. Using solidify to do this.
UE::Geometry::FDynamicMesh3 DynamicMesh;
LevelSetHelpers::CreateDynamicMesh(Verts, Indices, DynamicMesh);
UE::Geometry::TMeshAABBTree3<UE::Geometry::FDynamicMesh3> Spatial(&DynamicMesh);
UE::Geometry::TFastWindingTree<UE::Geometry::FDynamicMesh3> FastWinding(&Spatial);
UE::Geometry::TImplicitSolidify<UE::Geometry::FDynamicMesh3> Solidify(&DynamicMesh, &Spatial, &FastWinding);
constexpr int32 NumExpandCells = 1;
const double SolidifyCellSize = LevelsetGrid.Dx().Min();
Solidify.ExtendBounds = NumExpandCells * SolidifyCellSize;
Solidify.MeshCellSize = SolidifyCellSize;
Solidify.WindingThreshold = 0.5;
Solidify.SurfaceSearchSteps = 3;
Solidify.bSolidAtBoundaries = true;
const UE::Geometry::FDynamicMesh3 SolidMesh(&Solidify.Generate());
UE::Geometry::TMeshAABBTree3<UE::Geometry::FDynamicMesh3> SolidSpatial(&SolidMesh);
UE::Geometry::TFastWindingTree<UE::Geometry::FDynamicMesh3> SolidFastWinding(&SolidSpatial);
constexpr int32 OuterWeightExpandCells = 4; // Diffuse out this far outside mesh
constexpr int32 SurfaceExpandCells = 1; // Consider points this far from the surface of the mesh to be on the surface (dirichlet boundary)
UE::Geometry::IMeshSpatial::FQueryOptions QueryOptions;
QueryOptions.MaxDistance = (double)LatticeGrid.Dx().Max() * FMath::Max(OuterWeightExpandCells, SurfaceExpandCells);// Used for initial query against solidified mesh
const double OuterWeightExpandDistSq = FMath::Square((double)LatticeGrid.Dx().Max() * OuterWeightExpandCells);
const double SurfaceExpandDistSq = FMath::Square((double)LatticeGrid.Dx().Max() * SurfaceExpandCells);
TSet<int32> BonesSet(AllBonesForInfluence);
// Create sub-grid for the poisson solve.
UE::Geometry::FAxisAlignedBox3d SolidBBox = SolidSpatial.GetBoundingBox();
const Chaos::TVec3<int32> MinCornerCell = (LatticeGrid.Cell(SolidBBox.Min) - Chaos::TVec3<int32>(OuterWeightExpandCells)).ComponentwiseMax(Chaos::TVec3<int32>(0));
const Chaos::TVec3<int32> MaxCornerCell = (LatticeGrid.Cell(SolidBBox.Max) + Chaos::TVec3<int32>(OuterWeightExpandCells)).ComponentwiseMin(LatticeGrid.Counts() - Chaos::TVec3<int32>(1));
const Chaos::TUniformGrid<double, 3> SubGrid = LatticeGrid.SubGrid(MinCornerCell, MaxCornerCell);
// Get constrained nodes and weights for poisson solve
TArray<int32> ConstrainedNodes;
TArray<float> ConstrainedWeights;
const Chaos::TVec3<int32> NodeCounts = SubGrid.NodeCounts();
TSet<int32> OutsideWeighted;
for (int32 I = 0; I < NodeCounts.X; ++I)
{
for (int32 J = 0; J < NodeCounts.Y; ++J)
{
for (int32 K = 0; K < NodeCounts.Z; ++K)
{
const Chaos::TVector<int32, 3> Index(I, J, K);
const Chaos::TVector<double, 3> Location = SubGrid.Node(Index);
// Determine if close to surface of solid mesh.
double NearestDistSq = std::numeric_limits<double>::max();
const int32 NearestSolidTriangle = SolidSpatial.FindNearestTriangle(Location, NearestDistSq, QueryOptions);
if (NearestSolidTriangle == IndexConstants::InvalidID || NearestDistSq > SurfaceExpandCells)
{
// Not near surface.
// Determine if inside or outside
const bool IsInside = SolidFastWinding.IsInside(Location);
const bool IsGridBoundary = I == 0 || I == NodeCounts.X - 1 || J == 0 || J == NodeCounts.Y - 1 || K == 0 || K == NodeCounts.Z - 1;
if (!IsInside)
{
const bool IsOutsideOuterExpand = NearestSolidTriangle == IndexConstants::InvalidID || NearestDistSq > OuterWeightExpandDistSq;
if (IsOutsideOuterExpand)
{
// Outside the distance to diffuse this weight. Mark as dirichlet with weight = 0
ConstrainedNodes.Add(SubGrid.FlatIndex(Index, true));
ConstrainedWeights.Add(0.f);
continue;
}
else if (!IsGridBoundary)
{
OutsideWeighted.Add(SubGrid.FlatIndex(Index, true));
continue;
}
}
else if (!IsGridBoundary)
{
continue;
}
}
// Need to find closest triangle on original mesh to get weights. No distance limit since we might be in a hole that was filled
// by solidify.
const int32 NearestTriangle = Spatial.FindNearestTriangle(Location, NearestDistSq);
const UE::Geometry::FIndex3i& TriangleIndices = DynamicMesh.GetTriangle(NearestTriangle);
const FVector3f TriangleWeights(GetWeightForIndices(BonesSet, OrigIndices[TriangleIndices.A]), GetWeightForIndices(BonesSet, OrigIndices[TriangleIndices.B]), GetWeightForIndices(BonesSet, OrigIndices[TriangleIndices.C]));
UE::Geometry::FTriangle3d Triangle;
DynamicMesh.GetTriVertices(NearestTriangle, Triangle.V[0], Triangle.V[1], Triangle.V[2]);
const float Weight = CalculateTriangleWeight(Triangle, TriangleWeights, Location);
check(Weight != 0.f);
ConstrainedNodes.Add(SubGrid.FlatIndex(Index, true));
ConstrainedWeights.Add(Weight);
}
}
}
TArray<float> Weights;
Weights.SetNumUninitialized(SubGrid.GetNumNodes());
constexpr int32 MaxIter = 10000;
constexpr float Res = 1e-4;
constexpr bool bCheckResidual = true;
constexpr int32 MinParallelBatchSize = 10000;
Chaos::PoissonSolve<float, double, true>(ConstrainedNodes, ConstrainedWeights, SubGrid, MaxIter, Res, Weights, bCheckResidual, MinParallelBatchSize);
for (int32 Idx = 0; Idx < Weights.Num(); ++Idx)
{
if (Weights[Idx] > 0)
{
Chaos::TVec3<int32> SubIndex;
SubGrid.FlatToMultiIndex(Idx, SubIndex, true);
const int32 FullIndex = LatticeGrid.FlatIndex(SubIndex + MinCornerCell, true);
AddInfluence(FullIndex, PrimaryBoneIndex, Weights[Idx], OutsideWeighted.Contains(Idx));
}
}
}
FKSkinnedLevelSetElem FSkinnedLevelSetBuilder::CreateSkinnedLevelSetElem()
{
FinalizeInfluences([this](int32 BoneIndex)
{
return SkeletalMesh.GetRefSkeleton().GetBoneName(BoneIndex);
},
[this](int32 BoneIndex)
{
if (BoneIndex == INDEX_NONE)
{
return FTransform(FMatrix(SkeletalMesh.GetRefBasesInvMatrix()[RootBoneIndex]));
}
return FTransform(FMatrix(SkeletalMesh.GetRefBasesInvMatrix()[BoneIndex]));
}
);
TRefCountPtr<Chaos::TWeightedLatticeImplicitObject<Chaos::FLevelSet> > WeightedLevelSet = Generate(MoveTemp(LevelSet));
FKSkinnedLevelSetElem Ret;
Ret.SetWeightedLevelSet(MoveTemp(WeightedLevelSet));
return Ret;
}
float FSkinnedLevelSetBuilder::GetWeightForIndices(const TSet<int32>& BoneIndices, uint32 VertIndex) const
{
int32 SectionIndex;
int32 SoftVertIndex;
RenderData->LODRenderData[0].GetSectionFromVertexIndex(VertIndex, SectionIndex, SoftVertIndex);
const FSkelMeshSection& Section = StaticLODModel->LODModels[0].Sections[SectionIndex];
const FSoftSkinVertex& SoftVert = Section.SoftVertices[SoftVertIndex];
float Weight = 0.f;
for (int32 InfluenceIndex = 0; InfluenceIndex < MAX_TOTAL_INFLUENCES; ++InfluenceIndex)
{
const FBoneIndexType BoneMapIndex = SoftVert.InfluenceBones[InfluenceIndex];
const int32 ActualBoneIndex = Section.BoneMap[BoneMapIndex];
if (BoneIndices.Contains(ActualBoneIndex))
{
// Use max weight for all bones.
Weight = FMath::Max(Weight, (float)SoftVert.InfluenceWeights[InfluenceIndex] * UE::AnimationCore::InvMaxRawBoneWeightFloat);
}
}
return Weight;
}
void FSkinnedLevelSetBuilder::GetInfluencingBones(const TArray<uint32>& SkinnedVertexIndices, TSet<int32>& Bones)
{
for (uint32 SkinnedVertIndex : SkinnedVertexIndices)
{
int32 SectionIndex;
int32 SoftVertIndex;
RenderData->LODRenderData[0].GetSectionFromVertexIndex(SkinnedVertIndex, SectionIndex, SoftVertIndex);
const FSkelMeshSection& Section = StaticLODModel->LODModels[0].Sections[SectionIndex];
const FSoftSkinVertex& SoftVert = Section.SoftVertices[SoftVertIndex];
for (int32 InfluenceIndex = 0; InfluenceIndex < MAX_TOTAL_INFLUENCES; ++InfluenceIndex)
{
const uint16 InfluenceWeight = SoftVert.InfluenceWeights[InfluenceIndex];
if (InfluenceWeight >= 1)
{
const FBoneIndexType BoneMapIndex = SoftVert.InfluenceBones[InfluenceIndex];
const int32 ActualBoneIndex = Section.BoneMap[BoneMapIndex];
Bones.Add(ActualBoneIndex);
}
}
}
}