You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
#rb kriss.gossart #preflight 62aafc9ada0af39a4783930a [CL 20686007 by Josie Yang in ue5-main branch]
1077 lines
30 KiB
C++
1077 lines
30 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "SkinWeightsBindingTool.h"
|
|
|
|
#include "BoneWeights.h"
|
|
#include "DynamicMeshToMeshDescription.h"
|
|
#include "DynamicMesh/DynamicVertexSkinWeightsAttribute.h"
|
|
#include "InteractiveToolManager.h"
|
|
#include "MeshDescriptionToDynamicMesh.h"
|
|
#include "MeshOpPreviewHelpers.h"
|
|
#include "SkeletalMeshAttributes.h"
|
|
#include "ToolSetupUtil.h"
|
|
#include "ToolTargetManager.h"
|
|
#include "Components/SkeletalMeshComponent.h"
|
|
#include "Engine/SkeletalMesh.h"
|
|
#include "Spatial/FastWinding.h"
|
|
#include "Spatial/MeshWindingNumberGrid.h"
|
|
|
|
#include "TargetInterfaces/PrimitiveComponentBackedTarget.h"
|
|
#include "ModelingToolTargetUtil.h"
|
|
|
|
|
|
// #pragma optimize( "", off )
|
|
|
|
#define LOCTEXT_NAMESPACE "USkinWeightsBindingTool"
|
|
|
|
// TODO: Move to a helper function.
|
|
|
|
// A simple FIFO queue. Maintains a set of blocks, rather than allocate for each element.
|
|
template<typename T, int32 BlockSize=256>
|
|
class TFIFOQueue
|
|
{
|
|
public:
|
|
TFIFOQueue() = default;
|
|
|
|
void Push(T InElem)
|
|
{
|
|
// Container Empty?
|
|
if (Blocks.IsEmpty())
|
|
{
|
|
Blocks.SetNum(1);
|
|
Blocks[0].bIsFree = false;
|
|
PushIndex = -1;
|
|
}
|
|
// Or are we at the end of current block?
|
|
else if (PushIndex == (BlockSize - 1))
|
|
{
|
|
PushBlock = AllocateNewPushBlock();
|
|
PushIndex = -1;
|
|
}
|
|
|
|
PushIndex++;
|
|
|
|
Blocks[PushBlock].Data[PushIndex] = InElem;
|
|
}
|
|
|
|
T Pop()
|
|
{
|
|
check(!IsEmpty());
|
|
|
|
if (PopIndex == (BlockSize - 1))
|
|
{
|
|
// Reached the end. Free this block and move onto the next one.
|
|
PopBlock = FreeCurrentPopBlock();
|
|
PopIndex = -1;
|
|
}
|
|
|
|
PopIndex++;
|
|
return Blocks[PopBlock].Data[PopIndex];
|
|
}
|
|
|
|
bool TryPop(T &OutValue)
|
|
{
|
|
if (IsEmpty())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
OutValue = Pop();
|
|
return true;
|
|
}
|
|
|
|
void Reset()
|
|
{
|
|
Blocks.Reset();
|
|
PushBlock = 0;
|
|
PushIndex = -1;
|
|
PopBlock = 0;
|
|
PopIndex = -1;
|
|
}
|
|
|
|
|
|
bool IsEmpty() const
|
|
{
|
|
return PushBlock == PopBlock && PushIndex == PopIndex;
|
|
}
|
|
|
|
private:
|
|
int32 AllocateNewPushBlock()
|
|
{
|
|
checkSlow(PushBlock != -1);
|
|
|
|
int NewBlockIndex = INDEX_NONE;
|
|
|
|
if (FreeCount > 0)
|
|
{
|
|
// Find a free block
|
|
for (int32 Index = 0; Index < Blocks.Num(); Index++)
|
|
{
|
|
if (Blocks[Index].bIsFree)
|
|
{
|
|
NewBlockIndex = Index;
|
|
FreeCount--;
|
|
break;
|
|
}
|
|
}
|
|
checkfSlow(NewBlockIndex != INDEX_NONE, TEXT("We should have found a free block."));
|
|
}
|
|
else
|
|
{
|
|
NewBlockIndex = Blocks.Num();
|
|
Blocks.AddDefaulted();
|
|
}
|
|
|
|
Blocks[PushBlock].NextBlock = NewBlockIndex;
|
|
Blocks[NewBlockIndex].bIsFree = false;
|
|
Blocks[NewBlockIndex].NextBlock = INDEX_NONE;
|
|
return NewBlockIndex;
|
|
}
|
|
|
|
int32 FreeCurrentPopBlock()
|
|
{
|
|
const int32 NextBlock = Blocks[PopBlock].NextBlock;
|
|
checkSlow(NextBlock != -1);
|
|
Blocks[PopBlock].bIsFree = true;
|
|
FreeCount++;
|
|
|
|
return NextBlock;
|
|
}
|
|
|
|
|
|
int32 PushBlock = 0;
|
|
int32 PushIndex = -1;
|
|
|
|
int32 PopBlock = 0;
|
|
int32 PopIndex = -1;
|
|
|
|
int32 FreeCount = 0;
|
|
|
|
struct FBlock
|
|
{
|
|
FBlock()
|
|
{
|
|
Data = new T[BlockSize];
|
|
}
|
|
~FBlock()
|
|
{
|
|
delete [] Data;
|
|
}
|
|
|
|
FBlock(FBlock &&InOther) noexcept
|
|
{
|
|
Data = InOther.Data;
|
|
InOther.Data = nullptr;
|
|
}
|
|
|
|
bool bIsFree = true;
|
|
int32 NextBlock = INDEX_NONE;
|
|
T *Data;
|
|
};
|
|
|
|
TArray<FBlock> Blocks;
|
|
};
|
|
|
|
|
|
static float DistanceToLineSegment(const FVector& P, const FVector& A, const FVector& B)
|
|
{
|
|
const FVector M = B - A;
|
|
const FVector T = P - A;
|
|
|
|
const float C1 = FVector::DotProduct(M, T);
|
|
if (C1 <= 0.0f)
|
|
{
|
|
return FVector::Dist(P, A);
|
|
}
|
|
|
|
const float C2 = FVector::DotProduct(M, M);
|
|
if (C2 <= C1)
|
|
{
|
|
return FVector::Dist(P, B);
|
|
}
|
|
|
|
// Project the point onto the line and get the distance between them.
|
|
const FVector PT = A + M * (C1 / C2);
|
|
return FVector::Dist(P, PT);
|
|
}
|
|
|
|
|
|
// List of bones as used by the binding class. In this case for each bone transform, we want to
|
|
// store a list of line segments going from the bone transform to all the child bone transforms.
|
|
struct FTransformHierarchyQuery
|
|
{
|
|
explicit FTransformHierarchyQuery(const TArray<TPair<FTransform, int32>>& InTransformHierarchy)
|
|
{
|
|
for (int Index = 0; Index < InTransformHierarchy.Num(); Index++)
|
|
{
|
|
FTransform Xform = InTransformHierarchy[Index].Key;
|
|
int32 ParentIndex = InTransformHierarchy[Index].Value;
|
|
|
|
while (ParentIndex != INDEX_NONE)
|
|
{
|
|
Xform = Xform * InTransformHierarchy[ParentIndex].Key;
|
|
ParentIndex = InTransformHierarchy[ParentIndex].Value;
|
|
}
|
|
|
|
BoneFans.Add({ Xform.GetLocation() });
|
|
}
|
|
|
|
// Fill in the fan tips, as needed.
|
|
for (int Index = 0; Index < InTransformHierarchy.Num(); Index++)
|
|
{
|
|
const int32 ParentIndex = InTransformHierarchy[Index].Value;
|
|
if (ParentIndex != INDEX_NONE)
|
|
{
|
|
BoneFans[ParentIndex].TipsPos.Add(BoneFans[Index].RootPos);
|
|
}
|
|
}
|
|
}
|
|
|
|
float GetDistanceToBoneFan(const int32 InBoneIndex, const FVector& InPoint) const
|
|
{
|
|
return BoneFans[InBoneIndex].GetDistance(InPoint);
|
|
}
|
|
|
|
FBox GetBoneFanBBox(const int32 InBoneIndex) const
|
|
{
|
|
return BoneFans[InBoneIndex].GetBBox();
|
|
}
|
|
|
|
bool GetBoneFanIntersectsBox(const int32 InBoneIndex, const FBox &InBox) const
|
|
{
|
|
return BoneFans[InBoneIndex].IntersectsBox(InBox);
|
|
}
|
|
|
|
private:
|
|
struct FBoneFan
|
|
{
|
|
FVector RootPos;
|
|
TArray<FVector> TipsPos;
|
|
|
|
float GetDistance(const FVector& InPoint) const
|
|
{
|
|
if (TipsPos.IsEmpty())
|
|
{
|
|
return FVector::Distance(RootPos, InPoint);
|
|
}
|
|
else
|
|
{
|
|
float Distance = std::numeric_limits<float>::max();
|
|
for (const FVector& TipPos: TipsPos)
|
|
{
|
|
Distance = FMath::Min(Distance, DistanceToLineSegment(InPoint, RootPos, TipPos));
|
|
}
|
|
return Distance;
|
|
}
|
|
}
|
|
|
|
FBox GetBBox() const
|
|
{
|
|
FBox Box(RootPos, RootPos);
|
|
for (const FVector& TipPos: TipsPos)
|
|
{
|
|
Box += TipPos;
|
|
}
|
|
|
|
return Box;
|
|
}
|
|
|
|
bool IntersectsBox(const FBox &InBox) const
|
|
{
|
|
if (TipsPos.IsEmpty())
|
|
{
|
|
return FMath::PointBoxIntersection(RootPos, InBox);
|
|
}
|
|
|
|
if (GetBBox().Intersect(InBox))
|
|
{
|
|
for (const FVector& TipPos: TipsPos)
|
|
{
|
|
if (FMath::LineBoxIntersection(InBox, RootPos, TipPos, TipPos - RootPos))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
};
|
|
|
|
TArray<FBoneFan> BoneFans;
|
|
};
|
|
|
|
|
|
struct FOccupancyGrid
|
|
{
|
|
enum EDomain : int32
|
|
{
|
|
Exterior,
|
|
Boundary,
|
|
Interior
|
|
};
|
|
|
|
UE::Geometry::FDenseGrid3i Occupancy;
|
|
float CellSize;
|
|
FVector3f GridOrigin;
|
|
FVector3f CellMidPoint;
|
|
|
|
FOccupancyGrid(
|
|
const FDynamicMesh3& InMesh,
|
|
int32 InVoxelResolution
|
|
)
|
|
{
|
|
using namespace UE::Geometry;
|
|
|
|
// Compute a voxel grid
|
|
FDynamicMeshAABBTree3 Spatial(&InMesh);
|
|
TFastWindingTree FastWinding(&Spatial);
|
|
FAxisAlignedBox3d Bounds = Spatial.GetBoundingBox();
|
|
CellSize = Bounds.MaxDim() / InVoxelResolution;
|
|
CellMidPoint = FVector3f(CellSize / 2.0f, CellSize / 2.0f, CellSize / 2.0f);
|
|
|
|
TMeshWindingNumberGrid WindingGrid(&InMesh, &FastWinding, CellSize);
|
|
|
|
WindingGrid.Compute();
|
|
|
|
// Our occupancy grid is computed on the winding number grid's cell centers.
|
|
const FVector3i WindingDims = WindingGrid.Dimensions();
|
|
Occupancy = FDenseGrid3i(WindingDims.X - 1, WindingDims.Y - 1, WindingDims.Z - 1, Exterior);
|
|
|
|
GridOrigin = WindingGrid.GridOrigin + FVector3f(CellSize / 2.0f, CellSize / 2.0f, CellSize / 2.0f);
|
|
|
|
static const FVector3i CornerOffsets[] = {
|
|
FVector3i(0, 0, 0),
|
|
FVector3i(0, 0, 1),
|
|
FVector3i(0, 1, 0),
|
|
FVector3i(0, 1, 1),
|
|
FVector3i(1, 0, 0),
|
|
FVector3i(1, 0, 1),
|
|
FVector3i(1, 1, 0),
|
|
FVector3i(1, 1, 1),
|
|
};
|
|
|
|
// TODO: Parallel-for
|
|
for (int32 OccupancyId = 0; OccupancyId < Occupancy.Size(); OccupancyId++)
|
|
{
|
|
const FVector3i OccupancyIndex(Occupancy.ToIndex(OccupancyId));
|
|
int32 Count = 0;
|
|
for (int32 CornerId = 0; CornerId < 8; CornerId++)
|
|
{
|
|
const FVector3i CornerIndex(OccupancyIndex + CornerOffsets[CornerId]);
|
|
if (WindingGrid.GetValue(CornerIndex) >= WindingGrid.WindingIsoValue)
|
|
{
|
|
Count++;
|
|
}
|
|
}
|
|
|
|
if (Count == 8)
|
|
{
|
|
Occupancy[OccupancyIndex] = Interior;
|
|
}
|
|
else if (Count > 0)
|
|
{
|
|
Occupancy[OccupancyIndex] = Boundary;
|
|
}
|
|
}
|
|
|
|
// Make sure we include all the vertices of the mesh as a part of the boundary, if
|
|
// the vertex areas are marked as being exterior.
|
|
for (int32 VertexIdx = 0; VertexIdx < InMesh.VertexCount(); VertexIdx++)
|
|
{
|
|
const FVector3d& Pos = InMesh.GetVertex(VertexIdx);
|
|
const FVector3i OccupancyIndex = GetCellIndexFromPoint(FVector(Pos));
|
|
if (Occupancy[OccupancyIndex] == Exterior)
|
|
{
|
|
Occupancy[OccupancyIndex] = Boundary;
|
|
}
|
|
}
|
|
}
|
|
|
|
UE::Geometry::FVector3i GetCellIndexFromPoint(const FVector &InPoint) const
|
|
{
|
|
FVector3f PP(InPoint);
|
|
PP -= GridOrigin;
|
|
PP += CellMidPoint;
|
|
|
|
return { FMath::FloorToInt(PP.X / CellSize),
|
|
FMath::FloorToInt(PP.Y / CellSize),
|
|
FMath::FloorToInt(PP.Z / CellSize) };
|
|
}
|
|
|
|
FVector3d GetCellCenterFromIndex(const UE::Geometry::FVector3i &Index) const
|
|
{
|
|
const float CS = CellSize;
|
|
return {Index.X * CS + GridOrigin.X, Index.Y * CS + GridOrigin.Y, Index.Z * CS + GridOrigin.Z};
|
|
}
|
|
|
|
FBox GetCellBoxFromIndex(const UE::Geometry::FVector3i &Index) const
|
|
{
|
|
const FVector3f P = (FVector3f)GetCellCenterFromIndex(Index);
|
|
return {P - CellMidPoint, P + CellMidPoint};
|
|
}
|
|
};
|
|
|
|
|
|
namespace
|
|
{
|
|
struct FCreateSkinWeights_Closest_WorkData final :
|
|
TThreadSingleton<FCreateSkinWeights_Closest_WorkData>
|
|
{
|
|
TArray<TPair<FBoneIndexType, float>> RawBoneWeights;
|
|
TArray<UE::AnimationCore::FBoneWeight> BoneWeights;
|
|
};
|
|
|
|
float ComputeWeightStiffness(const float InWeight, const float InStiffness)
|
|
{
|
|
return (1.0f - InStiffness) * InWeight + InStiffness * InWeight * InWeight;
|
|
}
|
|
|
|
}
|
|
|
|
|
|
class FComputeSkinWeightsBindingOp : public UE::Geometry::FDynamicMeshOperator
|
|
{
|
|
public:
|
|
virtual ~FComputeSkinWeightsBindingOp() override {}
|
|
|
|
// The transform hierarchy to bind to. Listed in the same order as the bones in the
|
|
// reference skeleton that this skelmesh is tied to.
|
|
TSharedPtr<FDynamicMesh3, ESPMode::ThreadSafe> OriginalMesh;
|
|
TArray<TPair<FTransform, int32>> TransformHierarchy;
|
|
|
|
FName ProfileName = FSkeletalMeshAttributes::DefaultSkinWeightProfileName;
|
|
|
|
ESkinWeightsBindType BindType = ESkinWeightsBindType::DirectDistance;
|
|
float Stiffness = 0.2f;
|
|
int32 MaxInfluences = 5;
|
|
int32 VoxelResolution = 256;
|
|
|
|
void CalculateResult(FProgressCancel* InProgress) override
|
|
{
|
|
if (InProgress && InProgress->Cancelled())
|
|
{
|
|
return;
|
|
}
|
|
|
|
ResultMesh->Copy(*OriginalMesh, true, true, true, true);
|
|
|
|
if (InProgress && InProgress->Cancelled())
|
|
{
|
|
return;
|
|
}
|
|
|
|
const float ClampedStiffness = FMath::Clamp(Stiffness, 0.0f, 1.0f);
|
|
|
|
UE::AnimationCore::FBoneWeightsSettings Settings;
|
|
Settings.SetMaxWeightCount(MaxInfluences);
|
|
|
|
switch(BindType)
|
|
{
|
|
case ESkinWeightsBindType::DirectDistance:
|
|
CreateSkinWeights_DirectDistance(*ResultMesh, ClampedStiffness, Settings);
|
|
break;
|
|
|
|
case ESkinWeightsBindType::GeodesicVoxel:
|
|
CreateSkinWeights_GeodesicVoxel(*ResultMesh, ClampedStiffness, Settings);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private:
|
|
static UE::Geometry::FDynamicMeshVertexSkinWeightsAttribute *GetOrCreateSkinWeightsAttribute(
|
|
FDynamicMesh3& InMesh,
|
|
FName InProfileName
|
|
)
|
|
{
|
|
using namespace UE::Geometry;
|
|
|
|
FDynamicMeshVertexSkinWeightsAttribute *Attribute = InMesh.Attributes()->GetSkinWeightsAttribute(InProfileName);
|
|
if (!Attribute)
|
|
{
|
|
Attribute = new FDynamicMeshVertexSkinWeightsAttribute(&InMesh);
|
|
InMesh.Attributes()->AttachSkinWeightsAttribute(InProfileName, Attribute);
|
|
}
|
|
return Attribute;
|
|
}
|
|
|
|
void CreateSkinWeights_DirectDistance(
|
|
FDynamicMesh3& InMesh,
|
|
float InStiffness,
|
|
const UE::AnimationCore::FBoneWeightsSettings& InSettings
|
|
)
|
|
{
|
|
using namespace UE::AnimationCore;
|
|
using namespace UE::Geometry;
|
|
|
|
FDynamicMeshVertexSkinWeightsAttribute *SkinWeights = InMesh.Attributes()->GetSkinWeightsAttribute(ProfileName);
|
|
|
|
const int32 NumVertices = InMesh.VertexCount();
|
|
|
|
// Use the diagonal size of the bbox to make the bone distance falloff scale invariant.
|
|
const float DiagBounds = InMesh.GetBounds(true).DiagonalLength();
|
|
|
|
const FTransformHierarchyQuery Skeleton(TransformHierarchy);
|
|
|
|
ParallelFor(NumVertices, [&](const int32 VertexIdx)
|
|
{
|
|
const FVector3d& Pos = InMesh.GetVertex(VertexIdx);
|
|
|
|
FCreateSkinWeights_Closest_WorkData &WorkData = FCreateSkinWeights_Closest_WorkData::Get();
|
|
|
|
WorkData.RawBoneWeights.Reset(TransformHierarchy.Num());
|
|
|
|
float TotalWeight = 0.0f;
|
|
for (int32 BoneIndex = 0; BoneIndex < TransformHierarchy.Num(); BoneIndex++)
|
|
{
|
|
// Normalize the distance by the diagonal size of the bbox to maintain scale invariance.
|
|
float Weight = Skeleton.GetDistanceToBoneFan(BoneIndex, Pos) / DiagBounds;
|
|
|
|
// Avoid div-by-zero but allow for the possibility that multiple bones may
|
|
// touch this vertex.
|
|
Weight = FMath::Max(Weight, KINDA_SMALL_NUMBER);
|
|
|
|
// Compute the actual weight, factoring in the stiffness value. W = (1/S(D))^2
|
|
// Where S(x) is the stiffness function.
|
|
Weight = FMath::Square(1.0f / ComputeWeightStiffness(Weight, InStiffness));
|
|
TotalWeight += Weight;
|
|
WorkData.RawBoneWeights.Add(MakeTuple(static_cast<FBoneIndexType>(BoneIndex), Weight));
|
|
}
|
|
|
|
// Normalize
|
|
for (TPair<FBoneIndexType, float> &BoneWeight: WorkData.RawBoneWeights)
|
|
{
|
|
BoneWeight.Value /= TotalWeight;
|
|
}
|
|
|
|
WorkData.RawBoneWeights.Sort([](const TPair<FBoneIndexType, float> &A, const TPair<FBoneIndexType, float> &B)
|
|
{
|
|
return A.Value > B.Value;
|
|
});
|
|
|
|
WorkData.BoneWeights.Reset(InSettings.GetMaxWeightCount());
|
|
for (int32 BoneIndex = 0; BoneIndex < FMath::Min(InSettings.GetMaxWeightCount(), WorkData.RawBoneWeights.Num()); BoneIndex++)
|
|
{
|
|
const TPair<FBoneIndexType, float>& BoneWeight = WorkData.RawBoneWeights[BoneIndex];
|
|
WorkData.BoneWeights.Add(FBoneWeight(BoneWeight.Key, BoneWeight.Value));
|
|
}
|
|
|
|
SkinWeights->SetValue(VertexIdx, FBoneWeights::Create(WorkData.BoneWeights, InSettings));
|
|
});
|
|
}
|
|
|
|
void CreateSkinWeights_GeodesicVoxel(
|
|
FDynamicMesh3& InMesh,
|
|
float InStiffness,
|
|
const UE::AnimationCore::FBoneWeightsSettings& InSettings
|
|
)
|
|
{
|
|
using namespace UE::AnimationCore;
|
|
using namespace UE::Geometry;
|
|
|
|
FDynamicMeshVertexSkinWeightsAttribute *SkinWeights = InMesh.Attributes()->GetSkinWeightsAttribute(ProfileName);
|
|
|
|
const int32 NumVertices = InMesh.VertexCount();
|
|
|
|
// Use the diagonal size of the bbox to make the bone distance falloff scale invariant.
|
|
const float DiagBounds = InMesh.GetBounds(true).DiagonalLength();
|
|
|
|
const FTransformHierarchyQuery Skeleton(TransformHierarchy);
|
|
|
|
// This is grossly inefficient but tricky to do otherwise, since each bone distance
|
|
// computation is done per-thread. We could possibly solve this by chunking instead
|
|
// and accumulating partial results.
|
|
TArray<float> Weights;
|
|
Weights.SetNumUninitialized(NumVertices * TransformHierarchy.Num());
|
|
|
|
FOccupancyGrid Occupancy(InMesh, VoxelResolution);
|
|
|
|
const FVector3i Dimensions = Occupancy.Occupancy.GetDimensions();
|
|
|
|
ParallelFor(TransformHierarchy.Num(), [&](int32 BoneIndex) {
|
|
TFIFOQueue<FVector3i> WorkingSet;
|
|
FDenseGrid3f BoneDistance(Dimensions.X, Dimensions.Y, Dimensions.Z, DiagBounds);
|
|
|
|
// Mark all the cells that the bone intersects with distance of 0 and put them
|
|
// on the work queue.
|
|
const FBox BoneBox = Skeleton.GetBoneFanBBox(BoneIndex);
|
|
const FVector3i BoneMin = Occupancy.GetCellIndexFromPoint(BoneBox.Min);
|
|
const FVector3i BoneMax = Occupancy.GetCellIndexFromPoint(BoneBox.Max);
|
|
|
|
for (int32 I = BoneMin.X; I <= BoneMax.X; I++)
|
|
{
|
|
for (int32 J = BoneMin.Y; J <= BoneMax.Y; J++)
|
|
{
|
|
for (int32 K = BoneMin.Z; K <= BoneMax.Z; K++)
|
|
{
|
|
const FVector3i Candidate(I, J, K);
|
|
const FBox CellBox = Occupancy.GetCellBoxFromIndex(Candidate);
|
|
if (Skeleton.GetBoneFanIntersectsBox(BoneIndex, CellBox))
|
|
{
|
|
WorkingSet.Push(Candidate);
|
|
BoneDistance[Candidate] = 0.0f;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Iterate over all the voxels until we have constructed shortest distance paths
|
|
// throughout the level set.
|
|
while (!WorkingSet.IsEmpty())
|
|
{
|
|
const FVector3i WorkItem = WorkingSet.Pop();
|
|
|
|
// Loop through each of the neighbours (6 face neighbours, 12 edge neighbors,
|
|
// and 8 corner neighbours) and see if any of them are closer to the bone
|
|
// than their current marked distance.
|
|
float CurrentDistance = BoneDistance[WorkItem];
|
|
for (int32 N = 0; N < 26; N++)
|
|
{
|
|
FVector3i Offset(IndexUtil::GridOffsets26[N]);
|
|
FVector3i Candidate(WorkItem + Offset);
|
|
|
|
if (!BoneDistance.IsValidIndex(Candidate))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Ensure this entry is either a part of the interior or boundary domain.
|
|
if (Occupancy.Occupancy[Candidate] == FOccupancyGrid::Exterior)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
const float CellDistance = (FVector3f(Offset) * Occupancy.CellSize).Length();
|
|
const float CandidateDistance = CurrentDistance + CellDistance;
|
|
const float OldDistance = BoneDistance[Candidate];
|
|
|
|
if (OldDistance > CandidateDistance)
|
|
{
|
|
WorkingSet.Push(Candidate);
|
|
BoneDistance[Candidate] = CandidateDistance;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Loop through all the vertices, find the voxel each belongs to, and compute
|
|
// the distance from the voxel to the vertex (assuming the distance stored in the
|
|
// voxel is based on traversing from voxel center to voxel center).
|
|
for (int32 VertexIdx = 0; VertexIdx < NumVertices; VertexIdx++)
|
|
{
|
|
const FVector3d& Pos = InMesh.GetVertex(VertexIdx);
|
|
const FVector3i CellIndex = Occupancy.GetCellIndexFromPoint(Pos);
|
|
const FVector3d CellCenter = Occupancy.GetCellCenterFromIndex(CellIndex);
|
|
|
|
float Distance = BoneDistance[CellIndex];
|
|
const FOccupancyGrid::EDomain Domain = static_cast<FOccupancyGrid::EDomain>(Occupancy.Occupancy[CellIndex]);
|
|
// check(Distance != std::numeric_limits<float>::max());
|
|
|
|
Distance += FVector3d::Distance(CellCenter, Pos);
|
|
|
|
// Normalize the distance by the diagonal size of the bbox to maintain scale invariance.
|
|
float Weight = Distance / DiagBounds;
|
|
|
|
// Avoid div-by-zero but allow for the possibility that multiple bones may
|
|
// touch this vertex.
|
|
Weight = FMath::Max(Weight, KINDA_SMALL_NUMBER);
|
|
|
|
// Compute the actual weight, factoring in the stiffness value. W = (1/S(D))^2
|
|
// Where S(x) is the stiffness function.
|
|
Weight = FMath::Square(1.0f / ComputeWeightStiffness(Weight, InStiffness));
|
|
|
|
Weights[VertexIdx * TransformHierarchy.Num() + BoneIndex] = Weight;
|
|
}
|
|
});
|
|
|
|
ParallelFor(NumVertices, [&](const int32 VertexIdx)
|
|
{
|
|
FCreateSkinWeights_Closest_WorkData &WorkData = FCreateSkinWeights_Closest_WorkData::Get();
|
|
|
|
WorkData.RawBoneWeights.Reset(TransformHierarchy.Num());
|
|
|
|
float TotalWeight = 0.0f;
|
|
for (int32 BoneIndex = 0; BoneIndex < TransformHierarchy.Num(); BoneIndex++)
|
|
{
|
|
const float Weight = Weights[VertexIdx * TransformHierarchy.Num() + BoneIndex];
|
|
TotalWeight += Weight;
|
|
WorkData.RawBoneWeights.Add(MakeTuple(static_cast<FBoneIndexType>(BoneIndex), Weight));
|
|
}
|
|
|
|
// Normalize
|
|
for (TPair<FBoneIndexType, float> &BoneWeight: WorkData.RawBoneWeights)
|
|
{
|
|
BoneWeight.Value /= TotalWeight;
|
|
}
|
|
|
|
WorkData.RawBoneWeights.Sort([](const TPair<FBoneIndexType, float> &A, const TPair<FBoneIndexType, float> &B)
|
|
{
|
|
return A.Value > B.Value;
|
|
});
|
|
|
|
WorkData.BoneWeights.Reset(InSettings.GetMaxWeightCount());
|
|
for (int32 BoneIndex = 0; BoneIndex < FMath::Min(InSettings.GetMaxWeightCount(), WorkData.RawBoneWeights.Num()); BoneIndex++)
|
|
{
|
|
const TPair<FBoneIndexType, float>& BoneWeight = WorkData.RawBoneWeights[BoneIndex];
|
|
WorkData.BoneWeights.Add(FBoneWeight(BoneWeight.Key, BoneWeight.Value));
|
|
}
|
|
|
|
SkinWeights->SetValue(VertexIdx, FBoneWeights::Create(WorkData.BoneWeights, InSettings));
|
|
});
|
|
}
|
|
};
|
|
|
|
|
|
|
|
|
|
bool USkinWeightsBindingToolBuilder::CanBuildTool(const FToolBuilderState& SceneState) const
|
|
{
|
|
return SceneState.TargetManager->CountSelectedAndTargetable(SceneState, FToolTargetTypeRequirements()) == 1;
|
|
}
|
|
|
|
|
|
UMultiSelectionMeshEditingTool* USkinWeightsBindingToolBuilder::CreateNewTool(const FToolBuilderState& SceneState) const
|
|
{
|
|
return NewObject<USkinWeightsBindingTool>(SceneState.ToolManager);
|
|
}
|
|
|
|
|
|
USkeleton* USkinWeightsBindingToolProperties::GetSkeleton(bool& bInvalidSkeletonIsError, const IPropertyHandle* PropertyHandle)
|
|
{
|
|
bInvalidSkeletonIsError = false;
|
|
return SkeletalMesh ? SkeletalMesh->GetSkeleton() : nullptr;
|
|
}
|
|
|
|
|
|
USkinWeightsBindingTool::USkinWeightsBindingTool()
|
|
{
|
|
Properties = CreateDefaultSubobject<USkinWeightsBindingToolProperties>(TEXT("SkinWeightsBindingProperties"));
|
|
// CreateDefaultSubobject automatically sets RF_Transactional flag, we need to clear it so that undo/redo doesn't affect tool properties
|
|
Properties->ClearFlags(RF_Transactional);
|
|
}
|
|
|
|
|
|
USkinWeightsBindingTool::~USkinWeightsBindingTool()
|
|
{
|
|
}
|
|
|
|
|
|
void USkinWeightsBindingTool::Setup()
|
|
{
|
|
Super::Setup();
|
|
|
|
if (ensure(Properties))
|
|
{
|
|
Properties->RestoreProperties(this);
|
|
}
|
|
|
|
if (!ensure(Targets.Num() > 0) || !ensure(Targets[0]))
|
|
{
|
|
return;
|
|
}
|
|
|
|
const USkeletalMeshComponent* SkelMeshComponent = Cast<USkeletalMeshComponent>(UE::ToolTarget::GetTargetComponent(Targets[0]));
|
|
|
|
if (SkelMeshComponent && SkelMeshComponent->GetSkeletalMesh())
|
|
{
|
|
USkeletalMesh* SkeletalMesh = SkelMeshComponent->GetSkeletalMesh();
|
|
|
|
// Initialize the bone browser
|
|
FCurveEvaluationOption CurveEvalOption(
|
|
SkelMeshComponent->GetAllowedAnimCurveEvaluate(),
|
|
&SkelMeshComponent->GetDisallowedAnimCurvesEvaluation(),
|
|
0 /* Always use the highest LOD */
|
|
);
|
|
BoneContainer.InitializeTo(SkelMeshComponent->RequiredBones, CurveEvalOption, *SkeletalMesh);
|
|
|
|
Properties->SkeletalMesh = SkeletalMesh;
|
|
Properties->CurrentBone.Initialize(BoneContainer);
|
|
|
|
const FReferenceSkeleton& RefSkeleton = SkeletalMesh->GetRefSkeleton();
|
|
for (int32 Index = 0; Index < RefSkeleton.GetRawBoneNum(); Index++)
|
|
{
|
|
BoneToIndex.Add(RefSkeleton.GetRawRefBoneInfo()[Index].Name, Index);
|
|
}
|
|
|
|
// Pick the first root bone as the initial selection.
|
|
Properties->CurrentBone.BoneName = SkeletalMesh->GetRefSkeleton().GetBoneName(0);
|
|
|
|
const TArray<FMeshBoneInfo>& BoneInfo = RefSkeleton.GetRawRefBoneInfo();
|
|
const TArray<FTransform>& BonePose = RefSkeleton.GetRawRefBonePose();
|
|
|
|
TransformHierarchy.Reserve(BoneInfo.Num());
|
|
|
|
for (int32 Index = 0; Index < BoneInfo.Num(); Index++)
|
|
{
|
|
TransformHierarchy.Add(MakeTuple(BonePose[Index], BoneInfo[Index].ParentIndex));
|
|
}
|
|
}
|
|
|
|
UE::ToolTarget::HideSourceObject(Targets[0]);
|
|
Preview = NewObject<UMeshOpPreviewWithBackgroundCompute>(this, "Preview");
|
|
Preview->Setup(GetTargetWorld(), this);
|
|
Preview->SetIsMeshTopologyConstant(true, EMeshRenderAttributeFlags::VertexColors);
|
|
Preview->OnMeshUpdated.AddLambda([this](UMeshOpPreviewWithBackgroundCompute* Compute)
|
|
{
|
|
UpdateVisualization();
|
|
});
|
|
|
|
FComponentMaterialSet MaterialSet = UE::ToolTarget::GetMaterialSet(Targets[0]);
|
|
|
|
UMaterialInterface* VtxColorMaterial = GetToolManager()->GetContextQueriesAPI()->GetStandardMaterial(EStandardToolContextMaterials::VertexColorMaterial);
|
|
if (VtxColorMaterial != nullptr)
|
|
{
|
|
for (UMaterialInterface*& Material: MaterialSet.Materials)
|
|
{
|
|
Material = VtxColorMaterial;
|
|
}
|
|
}
|
|
|
|
Preview->ConfigureMaterials( MaterialSet.Materials,
|
|
ToolSetupUtil::GetDefaultWorkingMaterial(GetToolManager())
|
|
);
|
|
|
|
Properties->WatchProperty(Properties->CurrentBone.BoneName,
|
|
[this](FName) { UpdateVisualization();});
|
|
|
|
OriginalMesh = MakeShared<FDynamicMesh3, ESPMode::ThreadSafe>();
|
|
FMeshDescriptionToDynamicMesh Converter;
|
|
Converter.Convert(UE::ToolTarget::GetMeshDescription(Targets[0]), *OriginalMesh);
|
|
|
|
// Enable or override vertex colors on the original mesh.
|
|
OriginalMesh->EnableAttributes();
|
|
OriginalMesh->Attributes()->DisablePrimaryColors();
|
|
OriginalMesh->Attributes()->EnablePrimaryColors();
|
|
// Create an overlay that has no split elements, init with zero value.
|
|
OriginalMesh->Attributes()->PrimaryColors()->CreateFromPredicate([](int ParentVID, int TriIDA, int TriIDB){return true;}, 0.f);
|
|
|
|
Preview->PreviewMesh->SetTransform((FTransform) UE::ToolTarget::GetLocalToWorldTransform(Targets[0]));
|
|
Preview->PreviewMesh->SetTangentsMode(EDynamicMeshComponentTangentsMode::AutoCalculated);
|
|
Preview->PreviewMesh->SetShadowsEnabled(false);
|
|
Preview->PreviewMesh->UpdatePreview(OriginalMesh.Get());
|
|
|
|
Occupancy = MakeShared<FOccupancyGrid>(*OriginalMesh, Properties->VoxelResolution);
|
|
|
|
UpdateVisualization(/*bForce=*/true);
|
|
|
|
// add properties to GUI
|
|
AddToolPropertySource(Properties);
|
|
|
|
Preview->InvalidateResult();
|
|
|
|
SetToolDisplayName(LOCTEXT("ToolName", "Bind Skin"));
|
|
GetToolManager()->DisplayMessage(
|
|
LOCTEXT("OnStartTool", "Creates a rigid binding for the skin weights."),
|
|
EToolMessageLevel::UserNotification);
|
|
}
|
|
|
|
|
|
void USkinWeightsBindingTool::OnShutdown(EToolShutdownType ShutdownType)
|
|
{
|
|
Properties->SaveProperties(this);
|
|
|
|
UE::ToolTarget::ShowSourceObject(Targets[0]);
|
|
|
|
FDynamicMeshOpResult Result = Preview->Shutdown();
|
|
if (ShutdownType == EToolShutdownType::Accept)
|
|
{
|
|
GenerateAsset(Result);
|
|
}
|
|
}
|
|
|
|
|
|
void USkinWeightsBindingTool::OnTick(float DeltaTime)
|
|
{
|
|
Preview->Tick(DeltaTime);
|
|
}
|
|
|
|
static void DrawBox(IToolsContextRenderAPI* RenderAPI, const FTransform& Transform, const FBox &Box, const FLinearColor &Color, float LineThickness)
|
|
{
|
|
FPrimitiveDrawInterface* PDI = RenderAPI->GetPrimitiveDrawInterface();
|
|
const float PDIScale = RenderAPI->GetCameraState().GetPDIScalingFactor();
|
|
|
|
FVector Corners[2] = {
|
|
Transform.TransformPosition(Box.Min),
|
|
Transform.TransformPosition(Box.Max)
|
|
};
|
|
|
|
static const UE::Geometry::FVector3i Offsets[12][2] =
|
|
{
|
|
// Bottom
|
|
{{0, 0, 0}, {1, 0, 0}},
|
|
{{1, 0, 0}, {1, 1, 0}},
|
|
{{1, 1, 0}, {0, 1, 0}},
|
|
{{0, 1, 0}, {0, 0, 0}},
|
|
|
|
// Top
|
|
{{0, 0, 1}, {1, 0, 1}},
|
|
{{1, 0, 1}, {1, 1, 1}},
|
|
{{1, 1, 1}, {0, 1, 1}},
|
|
{{0, 1, 1}, {0, 0, 1}},
|
|
|
|
// Sides
|
|
{{0, 0, 0}, {0, 0, 1}},
|
|
{{1, 0, 0}, {1, 0, 1}},
|
|
{{1, 1, 0}, {1, 1, 1}},
|
|
{{0, 1, 0}, {0, 1, 1}},
|
|
};
|
|
|
|
for (int32 Index = 0; Index < 12; Index++)
|
|
{
|
|
const UE::Geometry::FVector3i* LineOffsets = Offsets[Index];
|
|
FVector A(Corners[LineOffsets[0].X].X, Corners[LineOffsets[0].Y].Y, Corners[LineOffsets[0].Z].Z);
|
|
FVector B(Corners[LineOffsets[1].X].X, Corners[LineOffsets[1].Y].Y, Corners[LineOffsets[1].Z].Z);
|
|
|
|
PDI->DrawTranslucentLine(A, B, Color, 1, LineThickness * PDIScale);
|
|
}
|
|
}
|
|
|
|
|
|
void USkinWeightsBindingTool::Render(IToolsContextRenderAPI* RenderAPI)
|
|
{
|
|
/**/
|
|
|
|
if (Occupancy && Properties->bDebugDraw)
|
|
{
|
|
bool bShowInterior = false;
|
|
bool bShowBoundary = true;
|
|
|
|
FPrimitiveDrawInterface* PDI = RenderAPI->GetPrimitiveDrawInterface();
|
|
const FTransform Transform = (FTransform) UE::ToolTarget::GetLocalToWorldTransform(Targets[0]);
|
|
float PDIScale = RenderAPI->GetCameraState().GetPDIScalingFactor();
|
|
|
|
for (int32 I = 0; I < Occupancy->Occupancy.Size(); I++)
|
|
{
|
|
UE::Geometry::FVector3i G(Occupancy->Occupancy.ToIndex(I));
|
|
|
|
FOccupancyGrid::EDomain Domain = static_cast<FOccupancyGrid::EDomain>(Occupancy->Occupancy[G]);
|
|
if (bShowBoundary && Domain == FOccupancyGrid::Boundary)
|
|
{
|
|
FBox Box = Occupancy->GetCellBoxFromIndex(G);
|
|
DrawBox(RenderAPI, Transform, Box, FLinearColor(1.0, 1.0, 0.0, 0.5), 0.5f);
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
|
|
bool USkinWeightsBindingTool::CanAccept() const
|
|
{
|
|
return Super::CanAccept() && Preview->HaveValidResult();
|
|
}
|
|
|
|
|
|
void USkinWeightsBindingTool::OnPropertyModified(UObject* PropertySet, FProperty* Property)
|
|
{
|
|
if ( Property )
|
|
{
|
|
if (Property->GetFName() == GET_MEMBER_NAME_CHECKED(FBoneReference, BoneName) /* ||
|
|
Property->GetFName() == GET_MEMBER_NAME_CHECKED(USkinWeightsBindingToolProperties, bDebugDraw) */ )
|
|
{
|
|
// Handled by the property watcher.
|
|
}
|
|
else
|
|
{
|
|
Occupancy = MakeShared<FOccupancyGrid>(*OriginalMesh, Properties->VoxelResolution);
|
|
|
|
Preview->InvalidateResult();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
TUniquePtr<UE::Geometry::FDynamicMeshOperator> USkinWeightsBindingTool::MakeNewOperator()
|
|
{
|
|
TUniquePtr<FComputeSkinWeightsBindingOp> Op = MakeUnique<FComputeSkinWeightsBindingOp>();
|
|
|
|
Op->ProfileName = FSkeletalMeshAttributes::DefaultSkinWeightProfileName;
|
|
Op->BindType = Properties->BindingType;
|
|
Op->Stiffness = Properties->Stiffness;
|
|
Op->MaxInfluences = Properties->MaxInfluences;
|
|
Op->VoxelResolution = Properties->VoxelResolution;
|
|
|
|
Op->OriginalMesh = OriginalMesh;
|
|
Op->TransformHierarchy = TransformHierarchy;
|
|
|
|
const FTransform3d LocalToWorld = UE::ToolTarget::GetLocalToWorldTransform(Targets[0]);
|
|
Op->SetResultTransform(LocalToWorld);
|
|
|
|
return Op;
|
|
}
|
|
|
|
|
|
void USkinWeightsBindingTool::GenerateAsset(const FDynamicMeshOpResult& Result)
|
|
{
|
|
// TODO: Update FDynamicMeshToMeshDescription to allow update the skin weights only.
|
|
GetToolManager()->BeginUndoTransaction(LOCTEXT("SkinWeightsBindingToolTransactionName", "Create Rigid Binding"));
|
|
|
|
check(Result.Mesh.Get() != nullptr);
|
|
UE::ToolTarget::CommitMeshDescriptionUpdateViaDynamicMesh(Targets[0], *Result.Mesh.Get(), true);
|
|
|
|
GetToolManager()->EndUndoTransaction();
|
|
}
|
|
|
|
FVector4f USkinWeightsBindingTool::WeightToColor(float Value)
|
|
{
|
|
Value = FMath::Clamp(Value, 0.0f, 1.0f);
|
|
|
|
{
|
|
// A close approximation of the skeletal mesh editor's bone weight ramp.
|
|
const FLinearColor HSV((1.0f - Value) * 285.0f, 100.0f, 85.0f);
|
|
return UE::Geometry::ToVector4<float>(HSV.HSVToLinearRGB());
|
|
}
|
|
}
|
|
|
|
void USkinWeightsBindingTool::UpdateVisualization(bool bInForce)
|
|
{
|
|
using namespace UE::AnimationCore;
|
|
using namespace UE::Geometry;
|
|
|
|
if ((bInForce || Preview->HaveValidNonEmptyResult()) && BoneToIndex.Contains(Properties->CurrentBone.BoneName))
|
|
{
|
|
const FBoneIndexType BoneIndex = BoneToIndex[Properties->CurrentBone.BoneName];
|
|
|
|
// update mesh with new value colors
|
|
Preview->PreviewMesh->EditMesh([&](FDynamicMesh3& InMesh)
|
|
{
|
|
FDynamicMeshVertexSkinWeightsAttribute *SkinWeights = InMesh.Attributes()->GetSkinWeightsAttribute(FSkeletalMeshAttributes::DefaultSkinWeightProfileName);
|
|
FDynamicMeshColorOverlay* ColorOverlay = InMesh.Attributes()->PrimaryColors();
|
|
|
|
if (!ColorOverlay)
|
|
{
|
|
InMesh.EnableAttributes();
|
|
InMesh.Attributes()->EnablePrimaryColors();
|
|
// Create an overlay that has no split elements, init with zero value.
|
|
ColorOverlay = InMesh.Attributes()->PrimaryColors();
|
|
ColorOverlay->CreateFromPredicate([](int ParentVID, int TriIDA, int TriIDB){return true;}, 0.f);
|
|
}
|
|
|
|
FBoneWeights BoneWeights;
|
|
|
|
for (int32 ElementId : ColorOverlay->ElementIndicesItr())
|
|
{
|
|
const int32 VertexId = ColorOverlay->GetParentVertex(ElementId);
|
|
SkinWeights->GetValue(VertexId, BoneWeights);
|
|
|
|
float Weight = 0.0f;
|
|
for (FBoneWeight BW: BoneWeights)
|
|
{
|
|
if (BW.GetBoneIndex() == BoneIndex)
|
|
{
|
|
Weight = BW.GetWeight();
|
|
break;
|
|
}
|
|
}
|
|
|
|
const FVector4f Color(WeightToColor(Weight));
|
|
ColorOverlay->SetElement(ElementId, Color);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
#undef LOCTEXT_NAMESPACE
|
|
|
|
// #pragma optimize( "", on )
|