Files
UnrealEngine/Engine/Source/Runtime/SkeletalMeshDescription/Private/SkeletalMeshOperations.cpp
2025-05-18 13:04:45 +08:00

1927 lines
74 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "SkeletalMeshOperations.h"
#include "BoneWeights.h"
#include "MeshDescriptionAdapter.h"
#include "Math/GenericOctree.h"
#include "SkeletalMeshAttributes.h"
#include "Spatial/MeshAABBTree3.h"
DEFINE_LOG_CATEGORY(LogSkeletalMeshOperations);
#define LOCTEXT_NAMESPACE "SkeletalMeshOperations"
namespace UE::Private
{
template<typename T>
struct FCreateAndCopyAttributeValues
{
FCreateAndCopyAttributeValues(
const FMeshDescription& InSourceMesh,
FMeshDescription& InTargetMesh,
TArray<FName>& InTargetCustomAttributeNames,
int32 InTargetVertexIndexOffset)
: SourceMesh(InSourceMesh)
, TargetMesh(InTargetMesh)
, TargetCustomAttributeNames(InTargetCustomAttributeNames)
, TargetVertexIndexOffset(InTargetVertexIndexOffset)
{}
void operator()(const FName InAttributeName, TVertexAttributesConstRef<T> InSrcAttribute)
{
// Ignore attributes with reserved names.
if (FSkeletalMeshAttributes::IsReservedAttributeName(InAttributeName))
{
return;
}
TAttributesSet<FVertexID>& VertexAttributes = TargetMesh.VertexAttributes();
const bool bAppend = TargetCustomAttributeNames.Contains(InAttributeName);
if (!bAppend)
{
VertexAttributes.RegisterAttribute<T>(InAttributeName, InSrcAttribute.GetNumChannels(), InSrcAttribute.GetDefaultValue(), InSrcAttribute.GetFlags());
TargetCustomAttributeNames.Add(InAttributeName);
}
//Copy the data
TVertexAttributesRef<T> TargetVertexAttributes = VertexAttributes.GetAttributesRef<T>(InAttributeName);
for (const FVertexID SourceVertexID : SourceMesh.Vertices().GetElementIDs())
{
const FVertexID TargetVertexID = FVertexID(TargetVertexIndexOffset + SourceVertexID.GetValue());
TargetVertexAttributes.Set(TargetVertexID, InSrcAttribute.Get(SourceVertexID));
}
}
// Unhandled sub-types.
void operator()(const FName, TVertexAttributesConstRef<TArrayAttribute<T>>) { }
void operator()(const FName, TVertexAttributesConstRef<TArrayView<T>>) { }
private:
const FMeshDescription& SourceMesh;
FMeshDescription& TargetMesh;
TArray<FName>& TargetCustomAttributeNames;
int32 TargetVertexIndexOffset = 0;
};
} // ns UE::Private
//Add specific skeletal mesh descriptions implementation here
void FSkeletalMeshOperations::AppendSkinWeight(const FMeshDescription& SourceMesh, FMeshDescription& TargetMesh, FSkeletalMeshAppendSettings& AppendSettings)
{
TRACE_CPUPROFILER_EVENT_SCOPE_STR("FSkeletalMeshOperations::AppendSkinWeight");
FSkeletalMeshConstAttributes SourceSkeletalMeshAttributes(SourceMesh);
FSkeletalMeshAttributes TargetSkeletalMeshAttributes(TargetMesh);
constexpr bool bKeepExistingAttribute = true;
TargetSkeletalMeshAttributes.Register(bKeepExistingAttribute);
FSkinWeightsVertexAttributesConstRef SourceVertexSkinWeights = SourceSkeletalMeshAttributes.GetVertexSkinWeights();
FSkinWeightsVertexAttributesRef TargetVertexSkinWeights = TargetSkeletalMeshAttributes.GetVertexSkinWeights();
TargetMesh.SuspendVertexIndexing();
//Append Custom VertexAttribute
if(AppendSettings.bAppendVertexAttributes)
{
TArray<FName> TargetCustomAttributeNames;
TargetMesh.VertexAttributes().GetAttributeNames(TargetCustomAttributeNames);
int32 TargetVertexIndexOffset = FMath::Max(TargetMesh.Vertices().Num() - SourceMesh.Vertices().Num(), 0);
SourceMesh.VertexAttributes().ForEachByType<float>(UE::Private::FCreateAndCopyAttributeValues<float>(SourceMesh, TargetMesh, TargetCustomAttributeNames, AppendSettings.SourceVertexIDOffset));
SourceMesh.VertexAttributes().ForEachByType<FVector2f>(UE::Private::FCreateAndCopyAttributeValues<FVector2f>(SourceMesh, TargetMesh, TargetCustomAttributeNames, AppendSettings.SourceVertexIDOffset));
SourceMesh.VertexAttributes().ForEachByType<FVector3f>(UE::Private::FCreateAndCopyAttributeValues<FVector3f>(SourceMesh, TargetMesh, TargetCustomAttributeNames, AppendSettings.SourceVertexIDOffset));
SourceMesh.VertexAttributes().ForEachByType<FVector4f>(UE::Private::FCreateAndCopyAttributeValues<FVector4f>(SourceMesh, TargetMesh, TargetCustomAttributeNames, AppendSettings.SourceVertexIDOffset));
}
for (const FVertexID SourceVertexID : SourceMesh.Vertices().GetElementIDs())
{
const FVertexID TargetVertexID = FVertexID(AppendSettings.SourceVertexIDOffset + SourceVertexID.GetValue());
FVertexBoneWeightsConst SourceBoneWeights = SourceVertexSkinWeights.Get(SourceVertexID);
TArray<UE::AnimationCore::FBoneWeight> TargetBoneWeights;
const int32 InfluenceCount = SourceBoneWeights.Num();
for (int32 InfluenceIndex = 0; InfluenceIndex < InfluenceCount; ++InfluenceIndex)
{
const FBoneIndexType SourceBoneIndex = SourceBoneWeights[InfluenceIndex].GetBoneIndex();
if(AppendSettings.SourceRemapBoneIndex.IsValidIndex(SourceBoneIndex))
{
UE::AnimationCore::FBoneWeight& TargetBoneWeight = TargetBoneWeights.AddDefaulted_GetRef();
TargetBoneWeight.SetBoneIndex(AppendSettings.SourceRemapBoneIndex[SourceBoneIndex]);
TargetBoneWeight.SetRawWeight(SourceBoneWeights[InfluenceIndex].GetRawWeight());
}
}
TargetVertexSkinWeights.Set(TargetVertexID, TargetBoneWeights);
}
TargetMesh.ResumeVertexIndexing();
}
bool FSkeletalMeshOperations::CopySkinWeightAttributeFromMesh(
const FMeshDescription& InSourceMesh,
FMeshDescription& InTargetMesh,
const FName InSourceProfile,
const FName InTargetProfile,
const TMap<int32, int32>* SourceBoneIndexToTargetBoneIndexMap
)
{
// This is effectively a slower and dumber version of FTransferBoneWeights.
using namespace UE::AnimationCore;
using namespace UE::Geometry;
FSkeletalMeshConstAttributes SourceAttributes(InSourceMesh);
FSkeletalMeshAttributes TargetAttributes(InTargetMesh);
FSkinWeightsVertexAttributesConstRef SourceWeights = SourceAttributes.GetVertexSkinWeights(InSourceProfile);
FSkinWeightsVertexAttributesRef TargetWeights = TargetAttributes.GetVertexSkinWeights(InTargetProfile);
TVertexAttributesConstRef<FVector3f> TargetPositions = TargetAttributes.GetVertexPositions();
if (!SourceWeights.IsValid() || !TargetWeights.IsValid())
{
return false;
}
FMeshDescriptionTriangleMeshAdapter MeshAdapter(&InSourceMesh);
TMeshAABBTree3<FMeshDescriptionTriangleMeshAdapter> BVH(&MeshAdapter);
auto RemapBoneWeights = [SourceBoneIndexToTargetBoneIndexMap](const FVertexBoneWeightsConst& InWeights) -> FBoneWeights
{
TArray<FBoneWeight, TInlineAllocator<MaxInlineBoneWeightCount>> Weights;
if (SourceBoneIndexToTargetBoneIndexMap)
{
for (FBoneWeight OriginalWeight: InWeights)
{
if (const int32* BoneIndexPtr = SourceBoneIndexToTargetBoneIndexMap->Find(OriginalWeight.GetBoneIndex()))
{
FBoneWeight NewWeight(static_cast<FBoneIndexType>(*BoneIndexPtr), OriginalWeight.GetRawWeight());
Weights.Add(NewWeight);
}
}
if (Weights.IsEmpty())
{
const FBoneWeight RootBoneWeight(0, 1.0f);
Weights.Add(RootBoneWeight);
}
}
else
{
for (FBoneWeight Weight: InWeights)
{
Weights.Add(Weight);
}
}
return FBoneWeights::Create(Weights);
};
auto InterpolateWeights = [&MeshAdapter, &SourceWeights, &RemapBoneWeights](int32 InTriangleIndex, const FVector3d& InTargetPoint) -> FBoneWeights
{
const FDistPoint3Triangle3d Query = TMeshQueries<FMeshDescriptionTriangleMeshAdapter>::TriangleDistance(MeshAdapter, InTriangleIndex, InTargetPoint);
const FIndex3i TriangleVertexes = MeshAdapter.GetTriangle(InTriangleIndex);
const FVector3f BaryCoords(VectorUtil::BarycentricCoords(Query.ClosestTrianglePoint, MeshAdapter.GetVertex(TriangleVertexes.A), MeshAdapter.GetVertex(TriangleVertexes.B), MeshAdapter.GetVertex(TriangleVertexes.C)));
const FBoneWeights WeightsA = RemapBoneWeights(SourceWeights.Get(TriangleVertexes.A));
const FBoneWeights WeightsB = RemapBoneWeights(SourceWeights.Get(TriangleVertexes.B));
const FBoneWeights WeightsC = RemapBoneWeights(SourceWeights.Get(TriangleVertexes.C));
FBoneWeights BoneWeights = FBoneWeights::Blend(WeightsA, WeightsB, WeightsC, BaryCoords.X, BaryCoords.Y, BaryCoords.Z);
// Blending can leave us with zero weights. Let's strip them out here.
BoneWeights.Renormalize();
return BoneWeights;
};
TArray<FBoneWeights> TargetBoneWeights;
TargetBoneWeights.SetNum(InTargetMesh.Vertices().GetArraySize());
ParallelFor(InTargetMesh.Vertices().GetArraySize(), [&BVH, &InTargetMesh, &TargetPositions, &TargetBoneWeights, &InterpolateWeights](int32 InVertexIndex)
{
const FVertexID VertexID(InVertexIndex);
if (!InTargetMesh.Vertices().IsValid(VertexID))
{
return;
}
const FVector3d TargetPoint(TargetPositions.Get(VertexID));
const IMeshSpatial::FQueryOptions Options;
double NearestDistanceSquared;
const int32 NearestTriangleIndex = BVH.FindNearestTriangle(TargetPoint, NearestDistanceSquared, Options);
if (!ensure(NearestTriangleIndex != IndexConstants::InvalidID))
{
return;
}
TargetBoneWeights[InVertexIndex] = InterpolateWeights(NearestTriangleIndex, TargetPoint);
});
// Transfer the computed bone weights to the target mesh.
for (FVertexID TargetVertexID: InTargetMesh.Vertices().GetElementIDs())
{
FBoneWeights& BoneWeights = TargetBoneWeights[TargetVertexID];
if (BoneWeights.Num() == 0)
{
// Bind to root so that we have something.
BoneWeights.SetBoneWeight(FBoneIndexType{0}, 1.0);
}
TargetWeights.Set(TargetVertexID, BoneWeights);
}
return true;
}
bool FSkeletalMeshOperations::RemapBoneIndicesOnSkinWeightAttribute(FMeshDescription& InMesh, TConstArrayView<int32> InBoneIndexMapping)
{
using namespace UE::AnimationCore;
FSkeletalMeshAttributes MeshAttributes(InMesh);
// Don't renormalize, since we are not changing the weights or order.
FBoneWeightsSettings Settings;
Settings.SetNormalizeType(EBoneWeightNormalizeType::None);
TArray<FBoneWeight> NewBoneWeights;
for (const FName AttributeName: MeshAttributes.GetSkinWeightProfileNames())
{
FSkinWeightsVertexAttributesRef SkinWeights(MeshAttributes.GetVertexSkinWeights(AttributeName));
for (FVertexID VertexID: InMesh.Vertices().GetElementIDs())
{
FVertexBoneWeights OldBoneWeights = SkinWeights.Get(VertexID);
NewBoneWeights.Reset(OldBoneWeights.Num());
for (FBoneWeight BoneWeight: OldBoneWeights)
{
if (!ensure(InBoneIndexMapping.IsValidIndex(BoneWeight.GetBoneIndex())))
{
return false;
}
BoneWeight.SetBoneIndex(InBoneIndexMapping[BoneWeight.GetBoneIndex()]);
NewBoneWeights.Add(BoneWeight);
}
SkinWeights.Set(VertexID, FBoneWeights::Create(NewBoneWeights, Settings));
}
}
return true;
}
namespace UE::Impl
{
static void PoseMesh(
FMeshDescription& InOutTargetMesh,
TConstArrayView<FMatrix44f> InRefToUserTransforms,
const FName InSkinWeightProfile,
const TMap<FName, float>& InMorphTargetWeights,
bool bInSkipRecomputeNormalsTangents = false
)
{
struct FMorphInfo
{
TVertexAttributesRef<FVector3f> PositionDelta;
TVertexInstanceAttributesRef<FVector3f> NormalDelta;
float Weight = 0.0f;
};
FSkeletalMeshAttributes Attributes(InOutTargetMesh);
// We need the mesh to be compact for the parallel for to work.
if (InOutTargetMesh.NeedsCompact())
{
FElementIDRemappings Remappings;
InOutTargetMesh.Compact(Remappings);
}
else
{
// Make sure indexers are built before entering parallel work
InOutTargetMesh.BuildVertexIndexers();
}
TVertexAttributesRef<FVector3f> PositionAttribute = Attributes.GetVertexPositions();
TVertexInstanceAttributesRef<FVector3f> NormalAttribute = Attributes.GetVertexInstanceNormals();
TVertexInstanceAttributesRef<FVector3f> TangentAttribute = Attributes.GetVertexInstanceTangents();
TVertexInstanceAttributesRef<float> BinormalSignsAttribute = Attributes.GetVertexInstanceBinormalSigns();
// See which morph target attributes we can peel out. If the normal attributes are not all valid, then
// we have to automatically compute the normal from the positions. Otherwise, we only use the normal deltas.
TArray<FMorphInfo> MorphInfos;
bool bAllMorphNormalsValid = true;
for (const TPair<FName, float>& Item: InMorphTargetWeights)
{
const FName MorphName{Item.Key};
const float MorphWeight{Item.Value};
TVertexAttributesRef<FVector3f> PositionDelta = Attributes.GetVertexMorphPositionDelta(MorphName);
// Q: Should we use the value of `r.MorphTarget.WeightThreshold` instead? The following condition is
// identical to the default setting of that value.
if (PositionDelta.IsValid() && !FMath::IsNearlyZero(MorphWeight))
{
TVertexInstanceAttributesRef<FVector3f> NormalDelta = Attributes.GetVertexInstanceMorphNormalDelta(MorphName);
if (!NormalDelta.IsValid())
{
bAllMorphNormalsValid = false;
}
FMorphInfo MorphInfo;
MorphInfo.PositionDelta = PositionDelta;
MorphInfo.NormalDelta = NormalDelta;
MorphInfo.Weight = MorphWeight;
MorphInfos.Add(MorphInfo);
}
}
// First we apply the morph info on the positions and normals.
if (!MorphInfos.IsEmpty())
{
struct FMorphProcessContext
{
TSet<FVertexInstanceID> DirtyVertexInstances;
TArray<FVertexID> Neighbors;
};
TArray<FMorphProcessContext> Contexts;
ParallelForWithTaskContext(Contexts, InOutTargetMesh.Vertices().Num(),
[&Mesh=InOutTargetMesh, &PositionAttribute, &MorphInfos, bAllMorphNormalsValid, bInSkipRecomputeNormalsTangents](FMorphProcessContext& Context, int32 Index)
{
const FVertexID VertexID{Index};
FVector3f Position = PositionAttribute.Get(VertexID);
bool bMoved = false;
for (const FMorphInfo& MorphInfo: MorphInfos)
{
const FVector3f PositionDelta = MorphInfo.PositionDelta.Get(VertexID) * MorphInfo.Weight;
if (!PositionDelta.IsNearlyZero())
{
Position += PositionDelta;
bMoved = true;
}
}
// If we need to re-generate the normals, store which vertices got moved _and_ their neighbors, since the whole
// triangle moved, which affects neighboring vertices of the moved vertex.
if (bMoved)
{
PositionAttribute.Set(VertexID, Position);
if (!bAllMorphNormalsValid && !bInSkipRecomputeNormalsTangents)
{
Context.DirtyVertexInstances.Append(Mesh.GetVertexVertexInstanceIDs(VertexID));
Mesh.GetVertexAdjacentVertices(VertexID, Context.Neighbors);
for (const FVertexID NeighborVertexID: Context.Neighbors)
{
Context.DirtyVertexInstances.Append(Mesh.GetVertexVertexInstanceIDs(NeighborVertexID));
}
}
}
});
if (bAllMorphNormalsValid)
{
ParallelForWithTaskContext(Contexts, InOutTargetMesh.VertexInstances().Num(),
[&MorphInfos, &NormalAttribute, &TangentAttribute, &BinormalSignsAttribute, bInSkipRecomputeNormalsTangents](FMorphProcessContext& Context, int32 Index)
{
FVertexInstanceID VertexInstanceID{Index};
FVector3f Normal = NormalAttribute.Get(VertexInstanceID);
FVector3f Tangent = TangentAttribute.Get(VertexInstanceID);
FVector3f Binormal = FVector3f::CrossProduct(Normal, Tangent) * BinormalSignsAttribute.Get(VertexInstanceID);
bool bMoved = false;
for (const FMorphInfo& MorphInfo: MorphInfos)
{
const FVector3f NormalDelta = MorphInfo.NormalDelta.Get(VertexInstanceID) * MorphInfo.Weight;
if (!NormalDelta.IsNearlyZero())
{
Normal += NormalDelta;
bMoved = true;
}
}
if (bMoved)
{
if (Normal.Normalize())
{
// Badly named function. This orthonormalizes X & Y using Z as the control.
FVector3f::CreateOrthonormalBasis(Tangent, Binormal, Normal);
NormalAttribute.Set(VertexInstanceID, Normal);
TangentAttribute.Set(VertexInstanceID, Tangent);
const float BinormalSign = FMatrix44f(Tangent, Binormal, Normal, FVector3f::ZeroVector).Determinant() < 0 ? -1.0f : +1.0f;
BinormalSignsAttribute.Set(VertexInstanceID, BinormalSign);
}
else
{
if (!bInSkipRecomputeNormalsTangents)
{
// Something went wrong. Reconstruct the normal from the tangent and binormal.
Context.DirtyVertexInstances.Add(VertexInstanceID);
}
}
}
});
}
if (!bInSkipRecomputeNormalsTangents)
{
// Clear out any normals that were affected by the point move, or ended up being degenerate during normal offsetting.
TSet<FVertexInstanceID> DirtyVertexInstances;
for (const FMorphProcessContext& ProcessContext: Contexts)
{
DirtyVertexInstances.Append(ProcessContext.DirtyVertexInstances);
}
if (!DirtyVertexInstances.IsEmpty())
{
// Mark any vector as zero that we want to regenerate from triangle + neighbors + tangents.
for (const FVertexInstanceID VertexInstanceID: DirtyVertexInstances)
{
NormalAttribute.Set(VertexInstanceID, FVector3f::ZeroVector);
}
FSkeletalMeshOperations::ComputeTriangleTangentsAndNormals(InOutTargetMesh, UE_SMALL_NUMBER, nullptr);
// Compute the normals on the dirty vertices, and adjust the tangents to match.
FSkeletalMeshOperations::ComputeTangentsAndNormals(InOutTargetMesh, EComputeNTBsFlags::WeightedNTBs);
// We don't need the triangle tangents and normals anymore.
InOutTargetMesh.TriangleAttributes().UnregisterAttribute(MeshAttribute::Triangle::Normal);
InOutTargetMesh.TriangleAttributes().UnregisterAttribute(MeshAttribute::Triangle::Tangent);
InOutTargetMesh.TriangleAttributes().UnregisterAttribute(MeshAttribute::Triangle::Binormal);
}
}
}
using namespace UE::AnimationCore;
// The normal needs to be transformed using the inverse transpose of the transform matrices to ensure that
// scaling works correctly.
TArray<FMatrix44f> RefToUserTransformsNormal;
RefToUserTransformsNormal.Reserve(InRefToUserTransforms.Num());
for (const FMatrix44f& Mat: InRefToUserTransforms)
{
RefToUserTransformsNormal.Add(Mat.Inverse().GetTransposed());
}
FSkinWeightsVertexAttributesRef SkinWeightAttribute = Attributes.GetVertexSkinWeights(InSkinWeightProfile);
ParallelFor(InOutTargetMesh.Vertices().Num(),
[&Mesh=InOutTargetMesh, &PositionAttribute, &NormalAttribute, &TangentAttribute, &SkinWeightAttribute, &RefToUserTransforms=InRefToUserTransforms, &RefToUserTransformsNormal](int32 Index)
{
const FVertexID VertexID(Index);
const FVertexBoneWeights BoneWeights = SkinWeightAttribute.Get(VertexID);
const FVector3f Position = PositionAttribute.Get(VertexID);
FVector3f SkinnedPosition = FVector3f::ZeroVector;
for (const FBoneWeight BW: BoneWeights)
{
SkinnedPosition += RefToUserTransforms[BW.GetBoneIndex()].TransformPosition(Position) * BW.GetWeight();
}
PositionAttribute.Set(VertexID, SkinnedPosition);
for (const FVertexInstanceID VertexInstanceID: Mesh.GetVertexVertexInstanceIDs(VertexID))
{
const FVector3f Normal = NormalAttribute.Get(VertexInstanceID);
const FVector3f Tangent = TangentAttribute.Get(VertexInstanceID);
FVector3f SkinnedNormal = FVector3f::ZeroVector;
FVector3f SkinnedTangent = FVector3f::ZeroVector;
for (const FBoneWeight BW: BoneWeights)
{
SkinnedNormal += RefToUserTransformsNormal[BW.GetBoneIndex()].TransformVector(Normal) * BW.GetWeight();
SkinnedTangent += RefToUserTransforms[BW.GetBoneIndex()].TransformVector(Tangent) * BW.GetWeight();
}
SkinnedNormal.Normalize();
SkinnedTangent.Normalize();
NormalAttribute.Set(VertexInstanceID, SkinnedNormal);
TangentAttribute.Set(VertexInstanceID, SkinnedTangent);
}
});
}
static void UnposeMesh(
FMeshDescription& InOutTargetMesh,
const FMeshDescription& InRefMesh,
TConstArrayView<FMatrix44f> InRefToUserTransforms,
const FName InSkinWeightProfile,
const TMap<FName, float>& InMorphTargetWeights
)
{
struct FMorphInfo
{
TVertexAttributesConstRef<FVector3f> PositionDelta;
TVertexInstanceAttributesConstRef<FVector3f> NormalDelta;
float Weight = 0.0f;
};
FSkeletalMeshAttributes Attributes(InOutTargetMesh);
FSkeletalMeshConstAttributes RefAttributes(InRefMesh);
// We need the mesh to be compact for the parallel for to work.
if (InOutTargetMesh.NeedsCompact())
{
FElementIDRemappings Remappings;
InOutTargetMesh.Compact(Remappings);
}
else
{
// Make sure indexers are built before entering parallel work
InOutTargetMesh.BuildVertexIndexers();
}
TVertexAttributesRef<FVector3f> PositionAttribute = Attributes.GetVertexPositions();
TVertexInstanceAttributesRef<FVector3f> NormalAttribute = Attributes.GetVertexInstanceNormals();
TVertexInstanceAttributesRef<FVector3f> TangentAttribute = Attributes.GetVertexInstanceTangents();
TVertexInstanceAttributesRef<float> BinormalSignsAttribute = Attributes.GetVertexInstanceBinormalSigns();
// Invert skinning first
using namespace UE::AnimationCore;
// The normal needs to be transformed using the inverse transpose of the transform matrices to ensure that
// scaling works correctly.
TArray<FMatrix44f> RefToUserTransformsNormal;
RefToUserTransformsNormal.Reserve(InRefToUserTransforms.Num());
for (const FMatrix44f& Mat : InRefToUserTransforms)
{
RefToUserTransformsNormal.Add(Mat.Inverse().GetTransposed());
}
FSkinWeightsVertexAttributesRef SkinWeightAttribute = Attributes.GetVertexSkinWeights(InSkinWeightProfile);
ParallelFor(InOutTargetMesh.Vertices().Num(),
[&Mesh = InOutTargetMesh, &PositionAttribute, &NormalAttribute, &TangentAttribute, &SkinWeightAttribute, &RefToUserTransforms = InRefToUserTransforms, &RefToUserTransformsNormal](int32 Index)
{
const FVertexID VertexID(Index);
const FVertexBoneWeights BoneWeights = SkinWeightAttribute.Get(VertexID);
const FVector3f Position = PositionAttribute.Get(VertexID);
FVector3f SkinnedPosition = FVector3f::ZeroVector;
FMatrix44f SkinMatrix = FMatrix44f(FVector3f::ZeroVector, FVector3f::ZeroVector, FVector3f::ZeroVector, FVector3f::ZeroVector);
SkinMatrix.M[3][3] = 0.0f;
for (const FBoneWeight BW : BoneWeights)
{
SkinMatrix += RefToUserTransforms[BW.GetBoneIndex()] * BW.GetWeight();
}
SkinnedPosition = SkinMatrix.Inverse().TransformPosition(Position);
PositionAttribute.Set(VertexID, SkinnedPosition);
for (const FVertexInstanceID VertexInstanceID : Mesh.GetVertexVertexInstanceIDs(VertexID))
{
const FVector3f Normal = NormalAttribute.Get(VertexInstanceID);
const FVector3f Tangent = TangentAttribute.Get(VertexInstanceID);
FVector3f SkinnedNormal = FVector3f::ZeroVector;
FVector3f SkinnedTangent = FVector3f::ZeroVector;
for (const FBoneWeight BW : BoneWeights)
{
SkinnedNormal += RefToUserTransformsNormal[BW.GetBoneIndex()].TransformVector(Normal) * BW.GetWeight();
SkinnedTangent += RefToUserTransforms[BW.GetBoneIndex()].TransformVector(Tangent) * BW.GetWeight();
}
SkinnedNormal.Normalize();
SkinnedTangent.Normalize();
NormalAttribute.Set(VertexInstanceID, SkinnedNormal);
TangentAttribute.Set(VertexInstanceID, SkinnedTangent);
}
});
// See which morph target attributes we can peel out. If the normal attributes are not all valid, then
// we have to automatically compute the normal from the positions. Otherwise, we only use the normal deltas.
TArray<FMorphInfo> MorphInfos;
bool bAllMorphNormalsValid = true;
for (const TPair<FName, float>& Item : InMorphTargetWeights)
{
const FName MorphName{ Item.Key };
const float MorphWeight{ Item.Value };
TVertexAttributesConstRef<FVector3f> PositionDelta = RefAttributes.GetVertexMorphPositionDelta(MorphName);
// Q: Should we use the value of `r.MorphTarget.WeightThreshold` instead? The following condition is
// identical to the default setting of that value.
if (PositionDelta.IsValid() && !FMath::IsNearlyZero(MorphWeight))
{
TVertexInstanceAttributesConstRef<FVector3f> NormalDelta = RefAttributes.GetVertexInstanceMorphNormalDelta(MorphName);
if (!NormalDelta.IsValid())
{
bAllMorphNormalsValid = false;
}
FMorphInfo MorphInfo;
MorphInfo.PositionDelta = PositionDelta;
MorphInfo.NormalDelta = NormalDelta;
MorphInfo.Weight = MorphWeight;
MorphInfos.Add(MorphInfo);
}
}
// Inverse morph deltas
if (!MorphInfos.IsEmpty())
{
struct FMorphProcessContext
{
TSet<FVertexInstanceID> DirtyVertexInstances;
TArray<FVertexID> Neighbors;
};
TArray<FMorphProcessContext> Contexts;
ParallelForWithTaskContext(Contexts, InOutTargetMesh.Vertices().Num(),
[&Mesh = InOutTargetMesh, &PositionAttribute, &MorphInfos, bAllMorphNormalsValid](FMorphProcessContext& Context, int32 Index)
{
const FVertexID VertexID{ Index };
FVector3f Position = PositionAttribute.Get(VertexID);
bool bMoved = false;
for (const FMorphInfo& MorphInfo : MorphInfos)
{
const FVector3f PositionDelta = MorphInfo.PositionDelta.Get(VertexID) * MorphInfo.Weight * -1.0;
if (!PositionDelta.IsNearlyZero())
{
Position += PositionDelta;
bMoved = true;
}
}
// If we need to re-generate the normals, store which vertices got moved _and_ their neighbors, since the whole
// triangle moved, which affects neighboring vertices of the moved vertex.
if (bMoved)
{
PositionAttribute.Set(VertexID, Position);
if (!bAllMorphNormalsValid)
{
Context.DirtyVertexInstances.Append(Mesh.GetVertexVertexInstanceIDs(VertexID));
Mesh.GetVertexAdjacentVertices(VertexID, Context.Neighbors);
for (const FVertexID NeighborVertexID : Context.Neighbors)
{
Context.DirtyVertexInstances.Append(Mesh.GetVertexVertexInstanceIDs(NeighborVertexID));
}
}
}
});
if (bAllMorphNormalsValid)
{
ParallelForWithTaskContext(Contexts, InOutTargetMesh.VertexInstances().Num(),
[&MorphInfos, &NormalAttribute, &TangentAttribute, &BinormalSignsAttribute](FMorphProcessContext& Context, int32 Index)
{
FVertexInstanceID VertexInstanceID{Index};
FVector3f Normal = NormalAttribute.Get(VertexInstanceID);
FVector3f Tangent = TangentAttribute.Get(VertexInstanceID);
FVector3f Binormal = FVector3f::CrossProduct(Normal, Tangent) * BinormalSignsAttribute.Get(VertexInstanceID);
bool bMoved = false;
for (const FMorphInfo& MorphInfo: MorphInfos)
{
const FVector3f NormalDelta = MorphInfo.NormalDelta.Get(VertexInstanceID) * MorphInfo.Weight * -1.0f;
if (!NormalDelta.IsNearlyZero())
{
Normal += NormalDelta;
bMoved = true;
}
}
if (bMoved)
{
if (Normal.Normalize())
{
// Badly named function. This orthonormalizes X & Y using Z as the control.
FVector3f::CreateOrthonormalBasis(Tangent, Binormal, Normal);
NormalAttribute.Set(VertexInstanceID, Normal);
TangentAttribute.Set(VertexInstanceID, Tangent);
const float BinormalSign = FMatrix44f(Tangent, Binormal, Normal, FVector3f::ZeroVector).Determinant() < 0 ? -1.0f : +1.0f;
BinormalSignsAttribute.Set(VertexInstanceID, BinormalSign);
}
else
{
// Something went wrong. Reconstruct the normal from the tangent and binormal.
Context.DirtyVertexInstances.Add(VertexInstanceID);
}
}
});
}
// Clear out any normals that were affected by the point move, or ended up being degenerate during normal offsetting.
TSet<FVertexInstanceID> DirtyVertexInstances;
for (const FMorphProcessContext& ProcessContext: Contexts)
{
DirtyVertexInstances.Append(ProcessContext.DirtyVertexInstances);
}
if (!DirtyVertexInstances.IsEmpty())
{
// Mark any vector as zero that we want to regenerate from triangle + neighbors + tangents.
for (const FVertexInstanceID VertexInstanceID: DirtyVertexInstances)
{
NormalAttribute.Set(VertexInstanceID, FVector3f::ZeroVector);
}
FSkeletalMeshOperations::ComputeTriangleTangentsAndNormals(InOutTargetMesh, UE_SMALL_NUMBER, nullptr);
// Compute the normals on the dirty vertices, and adjust the tangents to match.
FSkeletalMeshOperations::ComputeTangentsAndNormals(InOutTargetMesh, EComputeNTBsFlags::WeightedNTBs);
// We don't need the triangle tangents and normals anymore.
InOutTargetMesh.TriangleAttributes().UnregisterAttribute(MeshAttribute::Triangle::Normal);
InOutTargetMesh.TriangleAttributes().UnregisterAttribute(MeshAttribute::Triangle::Tangent);
InOutTargetMesh.TriangleAttributes().UnregisterAttribute(MeshAttribute::Triangle::Binormal);
}
}
}
} // namespace Impl
bool FSkeletalMeshOperations::GetPosedMesh(
const FMeshDescription& InSourceMesh,
FMeshDescription& OutTargetMesh,
TConstArrayView<FTransform> InComponentSpaceTransforms,
const FName InSkinWeightProfile,
const TMap<FName, float>& InMorphTargetWeights
)
{
// Verify that the mesh is valid.
FSkeletalMeshConstAttributes Attributes(InSourceMesh);
if (!Attributes.HasBonePoseAttribute() || !Attributes.HasBoneParentIndexAttribute())
{
return false;
}
if (!Attributes.GetVertexSkinWeights(InSkinWeightProfile).IsValid())
{
return false;
}
if (Attributes.GetNumBones() != InComponentSpaceTransforms.Num())
{
return false;
}
// Convert the component-space transforms into a set of matrices that transform from the reference pose to
// the user pose. These are then used to nudge the vertices from the reference pose to the wanted
// user pose by weighing the influence of each bone on a given vertex. If the user pose and the reference pose
// are identical, these are all identity matrices.
TArray<FMatrix44f> RefToUserTransforms;
TArray<FMatrix44f> RefPoseTransforms;
FSkeletalMeshAttributes::FBonePoseAttributesConstRef BonePoseAttribute = Attributes.GetBonePoses();
FSkeletalMeshAttributes::FBoneParentIndexAttributesConstRef ParentBoneIndexAttribute = Attributes.GetBoneParentIndices();
RefToUserTransforms.SetNumUninitialized(Attributes.GetNumBones());
RefPoseTransforms.SetNumUninitialized(Attributes.GetNumBones());
for (int32 BoneIndex = 0; BoneIndex < Attributes.GetNumBones(); BoneIndex++)
{
const int32 ParentBoneIndex = ParentBoneIndexAttribute.Get(BoneIndex);
RefPoseTransforms[BoneIndex] = FMatrix44f{BonePoseAttribute.Get(BoneIndex).ToMatrixWithScale()};
if (ParentBoneIndex != INDEX_NONE)
{
RefPoseTransforms[BoneIndex] = RefPoseTransforms[BoneIndex] * RefPoseTransforms[ParentBoneIndex];
}
RefToUserTransforms[BoneIndex] = RefPoseTransforms[BoneIndex].Inverse() * FMatrix44f{InComponentSpaceTransforms[BoneIndex].ToMatrixWithScale()};
}
// Start with a fresh duplicate and then pose the target mesh in-place.
OutTargetMesh = InSourceMesh;
UE::Impl::PoseMesh(OutTargetMesh, RefToUserTransforms, InSkinWeightProfile, InMorphTargetWeights);
// Write out the current ref pose (in bone-space) to the mesh.
FSkeletalMeshAttributes WriteAttributes(OutTargetMesh);
FSkeletalMeshAttributes::FBonePoseAttributesRef WriteBonePoseAttribute = WriteAttributes.GetBonePoses();
for (int32 BoneIndex = 0; BoneIndex < Attributes.GetNumBones(); BoneIndex++)
{
const int32 ParentBoneIndex = ParentBoneIndexAttribute.Get(BoneIndex);
FTransform RefPoseTransform = InComponentSpaceTransforms[BoneIndex];
if (ParentBoneIndex != INDEX_NONE)
{
RefPoseTransform = RefPoseTransform.GetRelativeTransform(InComponentSpaceTransforms[ParentBoneIndex]);
}
WriteBonePoseAttribute.Set(BoneIndex, RefPoseTransform);
}
return true;
}
bool FSkeletalMeshOperations::GetPosedMeshInPlace(FMeshDescription& InOutTargetMesh, TConstArrayView<FTransform> InComponentSpaceTransforms,
const FName InSkinWeightProfile, const TMap<FName, float>& InMorphTargetWeights, bool bInSkipRecomputeNormalsTangents, bool bInWriteBonePose)
{
// Verify that the mesh is valid.
FSkeletalMeshConstAttributes Attributes(InOutTargetMesh);
if (!Attributes.HasBonePoseAttribute() || !Attributes.HasBoneParentIndexAttribute())
{
return false;
}
if (!Attributes.GetVertexSkinWeights(InSkinWeightProfile).IsValid())
{
return false;
}
if (Attributes.GetNumBones() != InComponentSpaceTransforms.Num())
{
return false;
}
// Convert the component-space transforms into a set of matrices that transform from the reference pose to
// the user pose. These are then used to nudge the vertices from the reference pose to the wanted
// user pose by weighing the influence of each bone on a given vertex. If the user pose and the reference pose
// are identical, these are all identity matrices.
TArray<FMatrix44f> RefToUserTransforms;
TArray<FMatrix44f> RefPoseTransforms;
FSkeletalMeshAttributes::FBonePoseAttributesConstRef BonePoseAttribute = Attributes.GetBonePoses();
FSkeletalMeshAttributes::FBoneParentIndexAttributesConstRef ParentBoneIndexAttribute = Attributes.GetBoneParentIndices();
RefToUserTransforms.SetNumUninitialized(Attributes.GetNumBones());
RefPoseTransforms.SetNumUninitialized(Attributes.GetNumBones());
for (int32 BoneIndex = 0; BoneIndex < Attributes.GetNumBones(); BoneIndex++)
{
const int32 ParentBoneIndex = ParentBoneIndexAttribute.Get(BoneIndex);
RefPoseTransforms[BoneIndex] = FMatrix44f{BonePoseAttribute.Get(BoneIndex).ToMatrixWithScale()};
if (ParentBoneIndex != INDEX_NONE)
{
RefPoseTransforms[BoneIndex] = RefPoseTransforms[BoneIndex] * RefPoseTransforms[ParentBoneIndex];
}
RefToUserTransforms[BoneIndex] = RefPoseTransforms[BoneIndex].Inverse() * FMatrix44f{InComponentSpaceTransforms[BoneIndex].ToMatrixWithScale()};
}
// Pose the target mesh in-place.
UE::Impl::PoseMesh(InOutTargetMesh, RefToUserTransforms, InSkinWeightProfile, InMorphTargetWeights, bInSkipRecomputeNormalsTangents);
if (bInWriteBonePose)
{
// Write out the current ref pose (in bone-space) to the mesh.
FSkeletalMeshAttributes WriteAttributes(InOutTargetMesh);
FSkeletalMeshAttributes::FBonePoseAttributesRef WriteBonePoseAttribute = WriteAttributes.GetBonePoses();
for (int32 BoneIndex = 0; BoneIndex < Attributes.GetNumBones(); BoneIndex++)
{
const int32 ParentBoneIndex = ParentBoneIndexAttribute.Get(BoneIndex);
FTransform RefPoseTransform = InComponentSpaceTransforms[BoneIndex];
if (ParentBoneIndex != INDEX_NONE)
{
RefPoseTransform = RefPoseTransform.GetRelativeTransform(InComponentSpaceTransforms[ParentBoneIndex]);
}
WriteBonePoseAttribute.Set(BoneIndex, RefPoseTransform);
}
}
return true;
}
bool FSkeletalMeshOperations::GetPosedMesh(
const FMeshDescription& InSourceMesh,
FMeshDescription& OutTargetMesh,
const TMap<FName, FTransform>& InBoneSpaceTransforms,
const FName InSkinWeightProfile,
const TMap<FName, float>& InMorphTargetWeights
)
{
// Verify that the mesh is valid.
FSkeletalMeshConstAttributes Attributes(InSourceMesh);
if (!Attributes.HasBoneNameAttribute() || !Attributes.HasBonePoseAttribute() || !Attributes.HasBoneParentIndexAttribute())
{
return false;
}
if (!Attributes.GetVertexSkinWeights(InSkinWeightProfile).IsValid())
{
return false;
}
TArray<FMatrix44f> RefToUserTransforms;
TArray<FMatrix44f> RefPoseTransforms;
TArray<FMatrix44f> UserPoseTransforms;
FSkeletalMeshAttributes::FBoneNameAttributesConstRef BoneNameAttribute = Attributes.GetBoneNames();
FSkeletalMeshAttributes::FBonePoseAttributesConstRef BonePoseAttribute = Attributes.GetBonePoses();
FSkeletalMeshAttributes::FBoneParentIndexAttributesConstRef ParentBoneIndexAttribute = Attributes.GetBoneParentIndices();
RefToUserTransforms.SetNumUninitialized(Attributes.GetNumBones());
RefPoseTransforms.SetNumUninitialized(Attributes.GetNumBones());
UserPoseTransforms.SetNumUninitialized(Attributes.GetNumBones());
for (int32 BoneIndex = 0; BoneIndex < Attributes.GetNumBones(); BoneIndex++)
{
const FName BoneName = BoneNameAttribute.Get(BoneIndex);
const int32 ParentBoneIndex = ParentBoneIndexAttribute.Get(BoneIndex);
RefPoseTransforms[BoneIndex] = FMatrix44f{BonePoseAttribute.Get(BoneIndex).ToMatrixWithScale()};
if (const FTransform* UserTransform = InBoneSpaceTransforms.Find(BoneName))
{
UserPoseTransforms[BoneIndex] = FMatrix44f{UserTransform->ToMatrixWithScale()};
// Update the pose on the mesh to match the user pose.
}
else
{
UserPoseTransforms[BoneIndex] = RefPoseTransforms[BoneIndex];
}
if (ParentBoneIndex != INDEX_NONE)
{
RefPoseTransforms[BoneIndex] = RefPoseTransforms[BoneIndex] * RefPoseTransforms[ParentBoneIndex];
UserPoseTransforms[BoneIndex] = UserPoseTransforms[BoneIndex] * UserPoseTransforms[ParentBoneIndex];
}
RefToUserTransforms[BoneIndex] = RefPoseTransforms[BoneIndex].Inverse() * UserPoseTransforms[BoneIndex];
}
// Start with a fresh duplicate and then pose the target mesh in-place.
OutTargetMesh = InSourceMesh;
UE::Impl::PoseMesh(OutTargetMesh, RefToUserTransforms, InSkinWeightProfile, InMorphTargetWeights);
FSkeletalMeshAttributes WriteAttributes(OutTargetMesh);
FSkeletalMeshAttributes::FBonePoseAttributesRef WriteBonePoseAttribute = WriteAttributes.GetBonePoses();
for (int32 BoneIndex = 0; BoneIndex < Attributes.GetNumBones(); BoneIndex++)
{
const FName BoneName = BoneNameAttribute.Get(BoneIndex);
if (const FTransform* UserTransform = InBoneSpaceTransforms.Find(BoneName))
{
WriteBonePoseAttribute.Set(BoneIndex, *UserTransform);
}
}
return true;
}
bool FSkeletalMeshOperations::GetUnposedMesh(
const FMeshDescription& InPosedMesh,
const FMeshDescription& InRefMesh,
TArray<FTransform>& RefBoneTransforms,
FMeshDescription& OutUnposedMesh,
TConstArrayView<FTransform> InComponentSpaceTransforms,
const FName InSkinWeightProfile,
const TMap<FName, float>& InMorphTargetWeights
)
{
// Verify that the mesh is valid.
FSkeletalMeshConstAttributes Attributes(InPosedMesh);
if (!Attributes.HasBonePoseAttribute() || !Attributes.HasBoneParentIndexAttribute())
{
return false;
}
if (!Attributes.GetVertexSkinWeights(InSkinWeightProfile).IsValid())
{
return false;
}
if (Attributes.GetNumBones() != InComponentSpaceTransforms.Num())
{
return false;
}
// Convert the component-space transforms into a set of matrices that transform from the reference pose to
// the user pose. These are then used to nudge the vertices from the reference pose to the wanted
// user pose by weighing the influence of each bone on a given vertex. If the user pose and the reference pose
// are identical, these are all identity matrices.
TArray<FMatrix44f> RefToUserTransforms;
TArray<FMatrix44f> RefPoseTransforms;
FSkeletalMeshAttributes::FBonePoseAttributesConstRef BonePoseAttribute = Attributes.GetBonePoses();
FSkeletalMeshAttributes::FBoneParentIndexAttributesConstRef ParentBoneIndexAttribute = Attributes.GetBoneParentIndices();
RefToUserTransforms.SetNumUninitialized(Attributes.GetNumBones());
RefPoseTransforms.SetNumUninitialized(Attributes.GetNumBones());
for (int32 BoneIndex = 0; BoneIndex < Attributes.GetNumBones(); BoneIndex++)
{
const int32 ParentBoneIndex = ParentBoneIndexAttribute.Get(BoneIndex);
FTransform ReferenceBoneTransform = RefBoneTransforms[BoneIndex];
RefPoseTransforms[BoneIndex] = FMatrix44f{ ReferenceBoneTransform.ToMatrixWithScale() };
RefToUserTransforms[BoneIndex] = RefPoseTransforms[BoneIndex].Inverse() * FMatrix44f { InComponentSpaceTransforms[BoneIndex].ToMatrixWithScale() };
}
// Start with a fresh duplicate and then pose the target mesh in-place.
OutUnposedMesh = InPosedMesh;
UE::Impl::UnposeMesh(OutUnposedMesh, InRefMesh, RefToUserTransforms, InSkinWeightProfile, InMorphTargetWeights);
// Write out the current ref pose (in bone-space) to the mesh.
FSkeletalMeshAttributes WriteAttributes(OutUnposedMesh);
FSkeletalMeshAttributes::FBonePoseAttributesRef WriteBonePoseAttribute = WriteAttributes.GetBonePoses();
for (int32 BoneIndex = 0; BoneIndex < Attributes.GetNumBones(); BoneIndex++)
{
const int32 ParentBoneIndex = ParentBoneIndexAttribute.Get(BoneIndex);
FTransform RefPoseTransform = InComponentSpaceTransforms[BoneIndex];
if (ParentBoneIndex != INDEX_NONE)
{
RefPoseTransform = RefPoseTransform.GetRelativeTransform(InComponentSpaceTransforms[ParentBoneIndex]);
}
WriteBonePoseAttribute.Set(BoneIndex, RefPoseTransform);
}
return true;
}
bool FSkeletalMeshOperations::GetUnposedMeshInPlace(FMeshDescription& InOutTargetMesh, const FMeshDescription& InRefMesh,
TArray<FTransform>& RefBoneTransforms, TConstArrayView<FTransform> InComponentSpaceTransforms, const FName InSkinWeightProfile,
const TMap<FName, float>& InMorphTargetWeights, bool bInWriteBonePose)
{
// Verify that the mesh is valid.
FSkeletalMeshConstAttributes Attributes(InOutTargetMesh);
if (!Attributes.HasBonePoseAttribute() || !Attributes.HasBoneParentIndexAttribute())
{
return false;
}
if (!Attributes.GetVertexSkinWeights(InSkinWeightProfile).IsValid())
{
return false;
}
if (Attributes.GetNumBones() != InComponentSpaceTransforms.Num())
{
return false;
}
// Convert the component-space transforms into a set of matrices that transform from the reference pose to
// the user pose. These are then used to nudge the vertices from the reference pose to the wanted
// user pose by weighing the influence of each bone on a given vertex. If the user pose and the reference pose
// are identical, these are all identity matrices.
TArray<FMatrix44f> RefToUserTransforms;
TArray<FMatrix44f> RefPoseTransforms;
FSkeletalMeshAttributes::FBonePoseAttributesConstRef BonePoseAttribute = Attributes.GetBonePoses();
FSkeletalMeshAttributes::FBoneParentIndexAttributesConstRef ParentBoneIndexAttribute = Attributes.GetBoneParentIndices();
RefToUserTransforms.SetNumUninitialized(Attributes.GetNumBones());
RefPoseTransforms.SetNumUninitialized(Attributes.GetNumBones());
for (int32 BoneIndex = 0; BoneIndex < Attributes.GetNumBones(); BoneIndex++)
{
const int32 ParentBoneIndex = ParentBoneIndexAttribute.Get(BoneIndex);
FTransform ReferenceBoneTransform = RefBoneTransforms[BoneIndex];
RefPoseTransforms[BoneIndex] = FMatrix44f{ ReferenceBoneTransform.ToMatrixWithScale() };
RefToUserTransforms[BoneIndex] = RefPoseTransforms[BoneIndex].Inverse() * FMatrix44f { InComponentSpaceTransforms[BoneIndex].ToMatrixWithScale() };
}
UE::Impl::UnposeMesh(InOutTargetMesh, InRefMesh, RefToUserTransforms, InSkinWeightProfile, InMorphTargetWeights);
if (bInWriteBonePose)
{
// Write out the current ref pose (in bone-space) to the mesh.
FSkeletalMeshAttributes WriteAttributes(InOutTargetMesh);
FSkeletalMeshAttributes::FBonePoseAttributesRef WriteBonePoseAttribute = WriteAttributes.GetBonePoses();
for (int32 BoneIndex = 0; BoneIndex < Attributes.GetNumBones(); BoneIndex++)
{
const int32 ParentBoneIndex = ParentBoneIndexAttribute.Get(BoneIndex);
FTransform RefPoseTransform = InComponentSpaceTransforms[BoneIndex];
if (ParentBoneIndex != INDEX_NONE)
{
RefPoseTransform = RefPoseTransform.GetRelativeTransform(InComponentSpaceTransforms[ParentBoneIndex]);
}
WriteBonePoseAttribute.Set(BoneIndex, RefPoseTransform);
}
}
return true;
}
void FSkeletalMeshOperations::ConvertHardEdgesToSmoothMasks(
const FMeshDescription& InMeshDescription,
TArray<uint32>& OutSmoothMasks
)
{
OutSmoothMasks.SetNumZeroed(InMeshDescription.Triangles().Num());
TSet<FTriangleID> ProcessedTriangles;
TArray<FTriangleID> TriangleQueue;
uint32 CurrentSmoothMask = 1;
const TEdgeAttributesConstRef<bool> IsEdgeHard = InMeshDescription.EdgeAttributes().GetAttributesRef<bool>(MeshAttribute::Edge::IsHard);
for (FTriangleID SeedTriangleID : InMeshDescription.Triangles().GetElementIDs())
{
if (ProcessedTriangles.Contains(SeedTriangleID))
{
continue;
}
TriangleQueue.Push(SeedTriangleID);
while (!TriangleQueue.IsEmpty())
{
const FTriangleID TriangleID = TriangleQueue.Pop(EAllowShrinking::No);
TArrayView<const FEdgeID> TriangleEdges = InMeshDescription.GetTriangleEdges(TriangleID);
OutSmoothMasks[TriangleID.GetValue()] = CurrentSmoothMask;
ProcessedTriangles.Add(TriangleID);
for (const FEdgeID EdgeID : TriangleEdges)
{
if (!IsEdgeHard.Get(EdgeID))
{
TArrayView<const FTriangleID> ConnectedTriangles = InMeshDescription.GetEdgeConnectedTriangleIDs(EdgeID);
for (const FTriangleID NeighborTriangleID : ConnectedTriangles)
{
if (!ProcessedTriangles.Contains(NeighborTriangleID))
{
TriangleQueue.Push(NeighborTriangleID);
}
}
}
}
}
CurrentSmoothMask <<= 1;
if (CurrentSmoothMask == 0)
{
// If we exhausted all available bits, then thunk to the more complete algorithm. For reasons unknown at this time, it doesn't generate
// nice smooth groups for some simpler test objects. For more complex input products it does a decent job though.
OutSmoothMasks.SetNumZeroed(InMeshDescription.Triangles().Num());
FStaticMeshOperations::ConvertHardEdgesToSmoothGroup(InMeshDescription, OutSmoothMasks);
break;
}
}
}
void FSkeletalMeshOperations::FixVertexInstanceStructure(FMeshDescription& SourceMeshDescription, FMeshDescription& TargetMeshDescription /*Expected to be empty*/, const TArray<uint32>& SourceSmoothingMasks, TArray<uint32>& TargetFaceSmoothingMasks)
{
TRACE_CPUPROFILER_EVENT_SCOPE(FixVertexInstanceStructure)
if (!ensure(TargetMeshDescription.IsEmpty()))
{
return;
}
struct FMeshAttributesHelper
{
FSkeletalMeshAttributes MeshAttributes;
//For read/write
TPolygonGroupAttributesRef<FName> PolygonGroupMaterialSlotNames;
//For read/write Vertex attributes:
TVertexAttributesRef<FVector3f> Positions;
FSkinWeightsVertexAttributesRef SkinWeights;
//For read/write VertexInstance attributes
TVertexInstanceAttributesRef<FVector3f> Normals;
TVertexInstanceAttributesRef<FVector3f> Tangents;
TVertexInstanceAttributesRef<float> BinormalSigns;
TVertexInstanceAttributesRef<FVector4f> Colors;
TVertexInstanceAttributesRef<FVector2f> UVs;
//GeometryParts
FSkeletalMeshAttributes::FSourceGeometryPartNameRef GeometryPartNames;
FSkeletalMeshAttributes::FSourceGeometryPartVertexOffsetAndCountRef GeometryPartVertexOffsetAndCounts;
//For read only
int32 NumUVChannels;
int32 NumberOfMorphTargets;
TArray<FName> MorphTargetNames; //Primarily important for the Source (or do we just move it outside of the helper?)
TArray<TVertexAttributesRef<FVector3f>> MorphPositionDeltas; //Vertex, SourceMeshMorphPosDeltas[0] belongs to -> SourceMorphTargetNames[0] and so on..
TArray<TVertexInstanceAttributesRef<FVector3f>> MorphNormals; //VertexInstance, SourceMeshMorphPosDeltas[0] belongs to -> SourceMorphTargetNames[0] and so on..
FMeshAttributesHelper(FMeshDescription& SourceMeshDescription, bool bRegisterAttributes = false)
: MeshAttributes(SourceMeshDescription)
{
if (bRegisterAttributes)
{
MeshAttributes.Register();
MeshAttributes.RegisterSourceGeometryPartsAttributes();
}
PolygonGroupMaterialSlotNames = MeshAttributes.GetPolygonGroupMaterialSlotNames();
Positions = MeshAttributes.GetVertexPositions();
SkinWeights = MeshAttributes.GetVertexSkinWeights();
Normals = MeshAttributes.GetVertexInstanceNormals();
Tangents = MeshAttributes.GetVertexInstanceTangents();
BinormalSigns = MeshAttributes.GetVertexInstanceBinormalSigns();
Colors = MeshAttributes.GetVertexInstanceColors();
UVs = MeshAttributes.GetVertexInstanceUVs();
GeometryPartNames = MeshAttributes.GetSourceGeometryPartNames();
GeometryPartVertexOffsetAndCounts = MeshAttributes.GetSourceGeometryPartVertexOffsetAndCounts();
NumUVChannels = UVs.GetNumChannels();
MorphTargetNames = MeshAttributes.GetMorphTargetNames();
NumberOfMorphTargets = MorphTargetNames.Num();
MorphPositionDeltas.Reserve(NumberOfMorphTargets);
MorphNormals.Reserve(NumberOfMorphTargets);
for (const FName& MorphTargetName : MorphTargetNames)
{
MorphPositionDeltas.Add(MeshAttributes.GetVertexMorphPositionDelta(MorphTargetName));
MorphNormals.Add(MeshAttributes.GetVertexInstanceMorphNormalDelta(MorphTargetName));
}
}
};
FMeshAttributesHelper Source(SourceMeshDescription);
FMeshAttributesHelper Target(TargetMeshDescription, true);
const int32 TriangleCount = SourceMeshDescription.Triangles().Num();
const int32 VertexInstanceCount = TriangleCount * 3;
const int32 VertexCount = SourceMeshDescription.Vertices().Num();
TargetMeshDescription.ReserveNewPolygonGroups(Source.PolygonGroupMaterialSlotNames.GetNumElements());
TargetMeshDescription.ReserveNewPolygons(TriangleCount);
TargetMeshDescription.ReserveNewTriangles(TriangleCount);
TargetMeshDescription.ReserveNewVertexInstances(VertexInstanceCount);
TargetMeshDescription.ReserveNewVertices(VertexCount);
Target.UVs.SetNumChannels(Source.NumUVChannels);
TMap<FVertexID, FVertexID> SourceToTargetVertexIDMap;
SourceToTargetVertexIDMap.Reserve(VertexCount);
//Copy PolygonGroups and MaterialSlots:
for (size_t PolygonGroupIndex = 0; PolygonGroupIndex < Source.PolygonGroupMaterialSlotNames.GetNumElements(); PolygonGroupIndex++)
{
FPolygonGroupID PolygonGroupID = TargetMeshDescription.CreatePolygonGroup();
Target.PolygonGroupMaterialSlotNames[PolygonGroupIndex] = Source.PolygonGroupMaterialSlotNames[PolygonGroupIndex];
}
//Copy vertices (aka Position and SkinWeights)
for (FVertexID SourceVertexID : SourceMeshDescription.Vertices().GetElementIDs())
{
FVertexID TargetVertexID = TargetMeshDescription.CreateVertex();
//Position:
Target.Positions[TargetVertexID] = Source.Positions[SourceVertexID];
//SkinWeights:
FVertexBoneWeights VertexBoneWeights = Source.SkinWeights.Get(SourceVertexID);
TArray<UE::AnimationCore::FBoneWeight> BoneWeights;
BoneWeights.Reserve(VertexBoneWeights.Num());
for (const UE::AnimationCore::FBoneWeight BoneWeight : VertexBoneWeights)
{
BoneWeights.Add(BoneWeight);
}
Target.SkinWeights.Set(TargetVertexID, BoneWeights);
SourceToTargetVertexIDMap.Add(SourceVertexID, TargetVertexID);
}
auto CopyVertexInstance = [&](FVertexInstanceID SourceVertexInstanceID, FVertexInstanceID TargetVertexInstanceID)
{
Target.Normals[TargetVertexInstanceID] = Source.Normals[SourceVertexInstanceID];
Target.Tangents[TargetVertexInstanceID] = Source.Tangents[SourceVertexInstanceID];
Target.BinormalSigns[TargetVertexInstanceID] = Source.BinormalSigns[SourceVertexInstanceID];
Target.Colors[TargetVertexInstanceID] = Source.Colors[SourceVertexInstanceID];
for (int32 UVIndex = 0; UVIndex < Source.NumUVChannels; ++UVIndex)
{
Target.UVs.Set(TargetVertexInstanceID, UVIndex, Source.UVs.Get(SourceVertexInstanceID, UVIndex));
}
};
TargetFaceSmoothingMasks.SetNumZeroed(SourceMeshDescription.Triangles().Num());
//Vertex Instances are created to be in complete order. so we don't copy over the existing VertexInstances prior to Triangle traverser.
TArray<TPair<int32, int32>> SourceToTargetVertexInstanceIDs;
SourceToTargetVertexInstanceIDs.Reserve(SourceMeshDescription.Triangles().Num() * 3);
for (FTriangleID TriangleID : SourceMeshDescription.Triangles().GetElementIDs())
{
FPolygonID PolygonID = SourceMeshDescription.GetTrianglePolygon(TriangleID);
FPolygonGroupID PolygonGroupID = SourceMeshDescription.GetTrianglePolygonGroup(TriangleID);
TArray<FVertexInstanceID> SourceVertexInstanceIDs = SourceMeshDescription.GetPolygonVertexInstances(PolygonID);
TArray<FVertexInstanceID> TargetVertexInstanceIDs;
TargetVertexInstanceIDs.SetNum(SourceVertexInstanceIDs.Num());
for (int32 Corner = 0; Corner < SourceVertexInstanceIDs.Num(); ++Corner)
{
FVertexInstanceID SourceVertexInstanceID = SourceVertexInstanceIDs[Corner];
FVertexID SourceParentVertexID = SourceMeshDescription.GetVertexInstanceVertex(SourceVertexInstanceID);
FVertexID TargetParentVertexID = SourceToTargetVertexIDMap[SourceParentVertexID];
FVertexInstanceID TargetVertexInstanceID = TargetMeshDescription.CreateVertexInstance(TargetParentVertexID);
//Copy VertexInstance Values:
CopyVertexInstance(SourceVertexInstanceID, TargetVertexInstanceID);
TargetVertexInstanceIDs[Corner] = TargetVertexInstanceID;
SourceToTargetVertexInstanceIDs.Add(TPair<int32, int32>(SourceVertexInstanceID, TargetVertexInstanceID)); //to be used for MorphTargetNormals copying
}
FPolygonID TargetPolyongId = TargetMeshDescription.CreatePolygon(PolygonGroupID, TargetVertexInstanceIDs);
//Smooth mask:
if (SourceSmoothingMasks.IsValidIndex(TriangleID.GetValue()))
{
if (ensure(TargetFaceSmoothingMasks.IsValidIndex(TargetPolyongId.GetValue())))
{
TargetFaceSmoothingMasks[TargetPolyongId.GetValue()] = SourceSmoothingMasks[TriangleID.GetValue()];
}
}
}
//Create MorphTarget Attributes:
TArray<bool> UseMorphTarget;
UseMorphTarget.SetNumUninitialized(Source.NumberOfMorphTargets);
TArray<bool> UseMorphTargetNormals;
UseMorphTargetNormals.SetNumUninitialized(Source.NumberOfMorphTargets);
for (const FName& SourceMorphTargetName : Source.MorphTargetNames)
{
Target.MeshAttributes.RegisterMorphTargetAttribute(SourceMorphTargetName, Source.MeshAttributes.HasMorphTargetNormalsAttribute(SourceMorphTargetName));
Target.MorphPositionDeltas.Add(Target.MeshAttributes.GetVertexMorphPositionDelta(SourceMorphTargetName));
Target.MorphNormals.Add(Target.MeshAttributes.GetVertexInstanceMorphNormalDelta(SourceMorphTargetName));
//Validations and Normal checks:
int32 MorphTargetIndex = Target.MorphPositionDeltas.Num() - 1;
UseMorphTarget[MorphTargetIndex] = Target.MorphPositionDeltas[MorphTargetIndex].IsValid() && Source.MorphPositionDeltas[MorphTargetIndex].IsValid();
UseMorphTargetNormals[MorphTargetIndex] = Target.MorphNormals[MorphTargetIndex].IsValid() && Source.MorphNormals[MorphTargetIndex].IsValid();
}
//Morph Target Positions:
for (size_t MorphTargetIndex = 0; MorphTargetIndex < Source.NumberOfMorphTargets; MorphTargetIndex++)
{
if (!UseMorphTarget[MorphTargetIndex])
{
continue;
}
for (const TPair<FVertexID, FVertexID>& Entry : SourceToTargetVertexIDMap)
{
Target.MorphPositionDeltas[MorphTargetIndex][Entry.Value] = Source.MorphPositionDeltas[MorphTargetIndex][Entry.Key];
}
}
//Copy the potential MorphTargetNormals:
for (size_t MorphTargetIndex = 0; MorphTargetIndex < Source.NumberOfMorphTargets; MorphTargetIndex++)
{
if (UseMorphTarget[MorphTargetIndex] && UseMorphTargetNormals[MorphTargetIndex])
{
for (const TPair<int32, int32>& Entry : SourceToTargetVertexInstanceIDs)
{
Target.MorphNormals[MorphTargetIndex][Entry.Value] = Source.MorphNormals[MorphTargetIndex][Entry.Key];
}
}
}
//Copy GeometryParts:
for (const FSourceGeometryPartID GeometryPartID : Source.MeshAttributes.SourceGeometryParts().GetElementIDs())
{
FName Name = Source.GeometryPartNames.Get(GeometryPartID);
TArrayView<const int32> OffsetAndCount = Source.GeometryPartVertexOffsetAndCounts.Get(GeometryPartID);
FSourceGeometryPartID PartID = Target.MeshAttributes.CreateSourceGeometryPart();
Target.GeometryPartNames.Set(PartID, Name);
Target.GeometryPartVertexOffsetAndCounts.Set(PartID, { OffsetAndCount[0], OffsetAndCount[1]});
}
};
void FSkeletalMeshOperations::ValidateFixComputeMeshDescriptionData(FMeshDescription& MeshDescription, const TArray<uint32>& FaceSmoothingMasks, int32 LODIndex, const bool bComputeWeightedNormals, const FString& SkeletalMeshPath)
{
//As FSkeletalMeshImportData::GetMeshDescription(...) (which we replaced with direct FMeshDescription usage) is doing the following calls:
// - ConvertSmoothGroupToHardEdges
// - ValidateAndFixData
// - HasInvalidVertexInstanceNormalsOrTangents -> ComputeTriangleTangentsAndNormals
// - (Re)BuildIndexers
//we are implementing the same mechanism here directly:
{
//TArray<uint32> FaceSmoothingMasks;
//ConvertHardEdgesToSmoothMasks(MeshDescription, FaceSmoothingMasks);
FSkeletalMeshOperations::ConvertSmoothGroupToHardEdges(FaceSmoothingMasks, MeshDescription);
// Check if we have any broken data, including UVs and normals/tangents.
FSkeletalMeshOperations::ValidateAndFixData(MeshDescription, SkeletalMeshPath);
bool bHasInvalidNormals, bHasInvalidTangents;
FStaticMeshOperations::HasInvalidVertexInstanceNormalsOrTangents(MeshDescription, bHasInvalidNormals, bHasInvalidTangents);
if (bHasInvalidNormals || bHasInvalidTangents)
{
// This is required by FSkeletalMeshOperations::ComputeTangentsAndNormals to function correctly.
FSkeletalMeshOperations::ComputeTriangleTangentsAndNormals(MeshDescription, UE_SMALL_NUMBER, !SkeletalMeshPath.IsEmpty() ? *SkeletalMeshPath : nullptr);
EComputeNTBsFlags ComputeNTBsOptions = EComputeNTBsFlags::None;
if (bComputeWeightedNormals)
{
ComputeNTBsOptions |= EComputeNTBsFlags::WeightedNTBs;
}
// This only recomputes broken normals/tangents. The ValidateAndFixData function above will have turned all non-finite normals and tangents into
// zero vectors.
FSkeletalMeshOperations::ComputeTangentsAndNormals(MeshDescription, ComputeNTBsOptions);
// We don't need the triangle tangents and normals anymore.
MeshDescription.TriangleAttributes().UnregisterAttribute(MeshAttribute::Triangle::Normal);
MeshDescription.TriangleAttributes().UnregisterAttribute(MeshAttribute::Triangle::Tangent);
MeshDescription.TriangleAttributes().UnregisterAttribute(MeshAttribute::Triangle::Binormal);
}
MeshDescription.RebuildIndexers();
}
}
void FSkeletalMeshOperations::ValidateAndFixInfluences(FMeshDescription& MeshDescription, bool& bOutInfluenceCountLimitHit)
{
//MeshDescription influences data validation and fixing:
// - Sort influences by weight and BoneIndex.
// - Normalize influence weights.
// - Warn about too many influences? (MAX_TOTAL_INFLUENCES)
// - Make sure all verts have influences set (if none exist bone 0 with weight 1)
FSkeletalMeshAttributes MeshAttributes(MeshDescription);
FSkinWeightsVertexAttributesRef VertexSkinWeights = MeshAttributes.GetVertexSkinWeights();
bOutInfluenceCountLimitHit = false;
if (!VertexSkinWeights.IsValid())
{
return;
}
for (FVertexID VertexID : MeshDescription.Vertices().GetElementIDs())
{
FVertexBoneWeights BoneWeights = VertexSkinWeights.Get(VertexID);
const int32 InfluenceCount = BoneWeights.Num();
if (InfluenceCount == 0)
{
VertexSkinWeights.Set(VertexID, { UE::AnimationCore::FBoneWeight(0, 1.f) });
continue;
}
TArray<UE::AnimationCore::FBoneWeight> BoneWeightArray;
BoneWeightArray.Reserve(InfluenceCount);
float TotalWeight = 0.0f;
for (int32 InfluenceIndex = 0; InfluenceIndex < InfluenceCount; ++InfluenceIndex)
{
BoneWeightArray.Add(BoneWeights[InfluenceIndex]);
TotalWeight += BoneWeights[InfluenceIndex].GetWeight();
}
if (InfluenceCount && (TotalWeight != 1.0f))
{
float OneOverTotalWeight = 1.f / TotalWeight;
for (int32 InfluenceIndex = 0; InfluenceIndex < InfluenceCount; ++InfluenceIndex)
{
BoneWeightArray[InfluenceIndex].SetWeight(BoneWeights[InfluenceIndex].GetWeight() * OneOverTotalWeight);
}
}
if (InfluenceCount > MAX_TOTAL_INFLUENCES)
{
bOutInfluenceCountLimitHit = true;
}
BoneWeightArray.Sort([](const UE::AnimationCore::FBoneWeight& A, const UE::AnimationCore::FBoneWeight& B)
{
if (A.GetWeight() < B.GetWeight()) return false;
else if (A.GetWeight() > B.GetWeight()) return true;
else if (A.GetBoneIndex() > B.GetBoneIndex()) return false;
else if (A.GetBoneIndex() < B.GetBoneIndex()) return true;
else return false;
});
for (int32 InfluenceIndex = 0; InfluenceIndex < InfluenceCount; ++InfluenceIndex)
{
BoneWeights[InfluenceIndex].SetBoneIndex(BoneWeightArray[InfluenceIndex].GetBoneIndex());
BoneWeights[InfluenceIndex].SetRawWeight(BoneWeightArray[InfluenceIndex].GetRawWeight());
}
}
}
namespace RigApplicationHelpers
{
struct FVertexInfo
{
FVector3f Position;
FVertexID VertexID;
FVertexInfo(const FVector3f& InPosition, const FVertexID InVertexID)
: Position(InPosition)
, VertexID(InVertexID)
{
}
};
/** Helper struct for the mesh component vert position octree */
struct FVertexInfoOctreeSemantics
{
enum { MaxElementsPerLeaf = 16 };
enum { MinInclusiveElementsPerNode = 7 };
enum { MaxNodeDepth = 12 };
typedef TInlineAllocator<MaxElementsPerLeaf> ElementAllocator;
/**
* Get the bounding box of the provided octree element. In this case, the box
* is merely the point specified by the element.
*
* @param Element Octree element to get the bounding box for
*
* @return Bounding box of the provided octree element
*/
FORCEINLINE static FBoxCenterAndExtent GetBoundingBox(const FVertexInfo& Element)
{
return FBoxCenterAndExtent(FVector(Element.Position), FVector::ZeroVector);
}
/**
* Determine if two octree elements are equal
*
* @param A First octree element to check
* @param B Second octree element to check
*
* @return true if both octree elements are equal, false if they are not
*/
FORCEINLINE static bool AreElementsEqual(const FVertexInfo& A, const FVertexInfo& B)
{
return (A.Position == B.Position && A.VertexID == B.VertexID);
}
/** Ignored for this implementation */
FORCEINLINE static void SetElementId(const FVertexInfo& Element, FOctreeElementId2 Id)
{
}
};
typedef TOctree2<FVertexInfo, FVertexInfoOctreeSemantics> TVertexInfoPosOctree;
/** Helper struct for building acceleration structures. */
struct FIndexAndZ
{
float Z;
int32 Index;
/** Initialization constructor. */
FIndexAndZ(int32 InIndex, const FVector3f& V)
{
Z = 0.30f * V.X + 0.33f * V.Y + 0.37f * V.Z;
Index = InIndex;
}
};
/** Sorting function for vertex Z/index pairs. */
struct FCompareIndexAndZ
{
FORCEINLINE bool operator()(FIndexAndZ const& A, FIndexAndZ const& B) const { return A.Z < B.Z; }
};
inline bool PointsEqual(const FVector3f& V1, const FVector3f& V2, float ComparisonThreshold)
{
if (FMath::Abs(V1.X - V2.X) > ComparisonThreshold
|| FMath::Abs(V1.Y - V2.Y) > ComparisonThreshold
|| FMath::Abs(V1.Z - V2.Z) > ComparisonThreshold)
{
return false;
}
return true;
}
void FindMatchingPositionVertexIndexes(const FVector3f& Position,
const TArray<FIndexAndZ>& SortedPositions, TVertexAttributesRef<const FVector3f> RigVertexPositions,
float ComparisonThreshold,
TArray<int32>& OutResults)
{
int32 SortedPositionNumber = SortedPositions.Num();
OutResults.Empty();
if (SortedPositionNumber == 0)
{
//No possible match
return;
}
FIndexAndZ PositionIndexAndZ(INDEX_NONE, Position);
int32 SortedIndex = SortedPositions.Num() / 2;
int32 StartIndex = 0;
int32 LastTopIndex = SortedPositions.Num();
int32 LastBottomIndex = 0;
int32 SearchIterationCount = 0;
{
double Increments = ((double)SortedPositions[SortedPositionNumber - 1].Z - (double)SortedPositions[0].Z) / (double)SortedPositionNumber;
//Optimize the iteration count when a value is not in the middle
SortedIndex = FMath::RoundToInt(((double)PositionIndexAndZ.Z - (double)SortedPositions[0].Z) / Increments);
}
for (SearchIterationCount = 0; SortedPositions.IsValidIndex(SortedIndex); ++SearchIterationCount)
{
if (LastTopIndex - LastBottomIndex < 5)
{
break;
}
if (FMath::Abs(PositionIndexAndZ.Z - SortedPositions[SortedIndex].Z) < ComparisonThreshold)
{
//Continue since we want the lowest start
LastTopIndex = SortedIndex;
SortedIndex = LastBottomIndex + ((LastTopIndex - LastBottomIndex) / 2);
if (SortedIndex <= LastBottomIndex)
{
break;
}
}
else if (PositionIndexAndZ.Z > SortedPositions[SortedIndex].Z + ComparisonThreshold)
{
LastBottomIndex = SortedIndex;
SortedIndex = SortedIndex + FMath::Max(((LastTopIndex - SortedIndex) / 2), 1);
}
else
{
LastTopIndex = SortedIndex;
SortedIndex = SortedIndex - FMath::Max(((SortedIndex - LastBottomIndex) / 2), 1);
}
}
//////////////////////////////////////////////////////////////////////////
//Closest point data (!bExactMatch)
float MinDistance = UE_MAX_FLT;
int32 ClosestIndex = LastBottomIndex;
for (int32 i = LastBottomIndex; i < SortedPositionNumber; i++)
{
//Get fast to the close position
if (PositionIndexAndZ.Z > SortedPositions[i].Z + ComparisonThreshold)
{
continue;
}
//break when we pass point close to the position
if (SortedPositions[i].Z > PositionIndexAndZ.Z + ComparisonThreshold)
break; // can't be any more dups
//Point is close to the position, verify it
const FVector3f& PositionA = RigVertexPositions[SortedPositions[i].Index];
if (PointsEqual(PositionA, Position, ComparisonThreshold))
{
OutResults.Add(SortedPositions[i].Index);
}
}
}
float GetSmallestDeltaBetweenTriangleLists(TArray<const FTriangleID>* RigTriangles, TArray<const FTriangleID>* GeoTriangles,
TTriangleAttributesRef<TArrayView<const FVertexID>> RigTriangleVertices, TTriangleAttributesRef<TArrayView<const FVertexID>> GeoTriangleVertices,
TVertexAttributesRef<const FVector3f> RigVertexPositions, TVertexAttributesRef<const FVector3f> GeoVertexPositions)
{
float SmallestTriangleDeltaSum = FLT_MAX;
for (const FTriangleID& RigTriangle : *RigTriangles)
{
const FVector3f RigPointA = RigVertexPositions[RigTriangleVertices[RigTriangle][0]];
const FVector3f RigPointB = RigVertexPositions[RigTriangleVertices[RigTriangle][1]];
const FVector3f RigPointC = RigVertexPositions[RigTriangleVertices[RigTriangle][2]];
for (const FTriangleID& GeoTriangle : *GeoTriangles)
{
const FVector3f GeoPointA = GeoVertexPositions[GeoTriangleVertices[GeoTriangle][0]];
const FVector3f GeoPointB = GeoVertexPositions[GeoTriangleVertices[GeoTriangle][1]];
const FVector3f GeoPointC = GeoVertexPositions[GeoTriangleVertices[GeoTriangle][2]];
float TriangleDeltaSum =
(GeoPointA - RigPointA).Size() +
(GeoPointB - RigPointB).Size() +
(GeoPointC - RigPointC).Size();
if (SmallestTriangleDeltaSum > TriangleDeltaSum)
{
SmallestTriangleDeltaSum = TriangleDeltaSum;
}
}
}
return SmallestTriangleDeltaSum;
}
void FindNearestVertexIndices(TVertexInfoPosOctree& VertexInfoPosOctree, const FVector3f& SearchPosition, TArray<FVertexInfo>& OutNearestVertices)
{
OutNearestVertices.Empty();
const float OctreeExtent = VertexInfoPosOctree.GetRootBounds().Extent.Size3();
//Use the max between 1e-4 cm and 1% of the bounding box extend
FVector Extend(FMath::Max(UE_KINDA_SMALL_NUMBER, OctreeExtent * 0.005f));
//Pass Extent size % of the Octree bounding box extent
//PassIndex 0 -> 0.5%
//PassIndex n -> 0.05*n
//PassIndex 1 -> 5%
//PassIndex 2 -> 10%
//...
for (int32 PassIndex = 0; PassIndex < 5; ++PassIndex)
{
// Query the octree to find the vertices close(inside the extend) to the SearchPosition
VertexInfoPosOctree.FindElementsWithBoundsTest(FBoxCenterAndExtent((FVector)SearchPosition, Extend), [&OutNearestVertices](const FVertexInfo& VertexInfo)
{
// Add all of the elements in the current node to the list of points to consider for closest point calculations
OutNearestVertices.Add(VertexInfo);
});
if (OutNearestVertices.Num() == 0)
{
float ExtentPercent = 0.05f * ((float)PassIndex + 1.0f);
Extend = FVector(FMath::Max(UE_KINDA_SMALL_NUMBER, OctreeExtent * ExtentPercent));
}
else
{
break;
}
}
}
}// namespace RigApplicationHelpers
void FSkeletalMeshOperations::ApplyRigToGeo(FMeshDescription& RigMeshDescription /*Base/From/'Other'*/, FMeshDescription& GeoMeshDescription /*Target/To*/)
{
//Primarily used for "bApplyGeometryOnly" and "bApplySkinningOnly"
//ExistingMeshDescription vs NewMeshDescription
//both consists of "Geometry" and "Skinning"
//IF bApplyGeometryOnly is active:
// then we want to apply the Rig (aka skinning) from the ExistingMeshDescription to the NewMeshDescription and hence the created 'new' NewMeshDescription is going be the result.
//IF bApplySkinningOnly is active:
// then we want to apply the Rig (aka skinning) from the NewMeshDescription to the ExistingMeshDescription and hence the created 'new' ExistingMeshDescription is going to be the result.
using namespace RigApplicationHelpers;
FSkeletalMeshAttributes RigAttributes(RigMeshDescription);
FSkeletalMeshAttributes GeoAttributes(GeoMeshDescription);
TMap<const FVertexID, TArray<const FTriangleID>> RigVertexToTriangleIDs;
TMap<const FVertexID, TArray<const FTriangleID>> GeoVertexToTriangleIDs;
TTriangleAttributesRef<TArrayView<const FVertexID>> RigTriangleVertices = RigAttributes.GetTriangleVertexIndices();
TTriangleAttributesRef<TArrayView<const FVertexID>> GeoTriangleVertices = GeoAttributes.GetTriangleVertexIndices();
TVertexAttributesRef<const FVector3f> RigVertexPositions = RigAttributes.GetVertexPositions();
TVertexAttributesRef<const FVector3f> GeoVertexPositions = GeoAttributes.GetVertexPositions();
//Build look up table from vertexid to triangles:
for (FTriangleID TriangleID : RigMeshDescription.Triangles().GetElementIDs())
{
TArrayView<const FVertexID> VertexIndices = RigTriangleVertices[TriangleID];
for (const FVertexID& VertexID : VertexIndices)
{
TArray<const FTriangleID>& Triangles = RigVertexToTriangleIDs.FindOrAdd(VertexID);
Triangles.Add(TriangleID);
}
}
for (FTriangleID TriangleID : GeoMeshDescription.Triangles().GetElementIDs())
{
TArrayView<const FVertexID> VertexIndices = GeoTriangleVertices[TriangleID];
for (const FVertexID& VertexID : VertexIndices)
{
TArray<const FTriangleID>& Triangles = GeoVertexToTriangleIDs.FindOrAdd(VertexID);
Triangles.Add(TriangleID);
}
}
// Find the extents formed by the cached vertex positions in order to optimize the octree used later
FBox3f Bounds(ForceInitToZero);
for (const FVertexID VertexID : RigMeshDescription.Vertices().GetElementIDs())
{
const FVector3f& Position = RigVertexPositions[VertexID];
Bounds += Position;
}
for (const FVertexID VertexID : GeoMeshDescription.Vertices().GetElementIDs())
{
const FVector3f& Position = GeoVertexPositions[VertexID];
Bounds += Position;
}
//Init Octree and SortedPositions.
TVertexInfoPosOctree RigVertPosOctree(FVector(Bounds.GetCenter()), Bounds.GetExtent().GetMax());
TArray<FIndexAndZ> SortedPositions;
//Adding the rig's geometry to the oct tree.
//As we are going to look for Geometry's positions in the Rig's Geometry in order to find the closes match for the influence to be applied:
for (const FVertexID VertexID : RigMeshDescription.Vertices().GetElementIDs())
{
const FVector3f& Position = RigVertexPositions[VertexID];
RigVertPosOctree.AddElement(FVertexInfo(Position, VertexID));
SortedPositions.Add(FIndexAndZ(VertexID.GetValue(), Position));
}
SortedPositions.Sort(FCompareIndexAndZ());
//Start finding influences from Rig for Geo:
TMap<int32, int32> GeoToRigMap;
GeoToRigMap.Reserve(RigMeshDescription.Vertices().Num());
for (const FVertexID VertexID : GeoMeshDescription.Vertices().GetElementIDs())
{
TArray<const FTriangleID>* GeoTriangles = GeoVertexToTriangleIDs.Find(VertexID);
if (!GeoTriangles)
{
continue;
}
const FVector3f& SearchPosition = GeoVertexPositions[VertexID]; //Position to match.
//Important Note: FSkeletalMeshImportData::ApplyRigToGeo seem to work based on VertexInstances, it also checks the VertexCandidate Normal and UVs and only finds the candidate legitimate if they match between Rig and Geo)
// As Influences (BoneIndex and BoneWeights) are Vertex (NOT VertexInstance) dependent.
// Whilst original implementation in FSkeletalMeshImportData::ApplyRigToGeo was checking and validating against normals and UVs for NearestWedges,
// with current implementation we try the NearestVertices with the same principle as the FindMatchingPositionVertexIndexes. (aka based on GetSmallestDeltaBetweenTriangleLists)
//First we look for identical matches: (aka FWedgePosition::FindMatchingPositionWegdeIndexes)
TArray<int32> VertexCandidates; //Vertex IDs from Rig.
FindMatchingPositionVertexIndexes(SearchPosition,
SortedPositions, RigVertexPositions,
UE_THRESH_POINTS_ARE_SAME,
VertexCandidates);
bool bFoundMatch = false;
if (VertexCandidates.Num() > 0)
{
int32 BestVertexIndexCandidate = INDEX_NONE; //From Rig
float LowestTriangleDeltaSum = 0;
for (int32 VertexCandidate : VertexCandidates)
{
TArray<const FTriangleID>* RigTriangles = RigVertexToTriangleIDs.Find(VertexCandidate);
if (!RigTriangles)
{
continue;
}
float CandidateSmallestTriangleDelta = GetSmallestDeltaBetweenTriangleLists(RigTriangles, GeoTriangles, RigTriangleVertices, GeoTriangleVertices, RigVertexPositions, GeoVertexPositions);
if (BestVertexIndexCandidate == INDEX_NONE || LowestTriangleDeltaSum > CandidateSmallestTriangleDelta)
{
BestVertexIndexCandidate = VertexCandidate;
LowestTriangleDeltaSum = CandidateSmallestTriangleDelta;
}
}
if (BestVertexIndexCandidate != INDEX_NONE)
{
GeoToRigMap.Add(VertexID, BestVertexIndexCandidate); //Aka mapping the geometry vertex id to the rig vertex id, as to which righ vertex id is the best one to use for influence.
bFoundMatch = true;
}
}
if (!bFoundMatch)
{
//In case exact matching didn't produce a result,
//then we do FindNearestWedgeIndexes.
int32 BestVertexIndexCandidate = INDEX_NONE; //From Rig
float LowestTriangleDeltaSum = 0;
TArray<FVertexInfo> NearestVertices;
FindNearestVertexIndices(RigVertPosOctree, SearchPosition, NearestVertices);
for (const FVertexInfo& VertexInfoCandidate : NearestVertices)
{
int32 VertexCandidate = VertexInfoCandidate.VertexID;
TArray<const FTriangleID>* RigTriangles = RigVertexToTriangleIDs.Find(VertexCandidate);
if (!RigTriangles)
{
continue;
}
float CandidateSmallestTriangleDelta = GetSmallestDeltaBetweenTriangleLists(RigTriangles, GeoTriangles, RigTriangleVertices, GeoTriangleVertices, RigVertexPositions, GeoVertexPositions);
if (BestVertexIndexCandidate == INDEX_NONE || LowestTriangleDeltaSum > CandidateSmallestTriangleDelta)
{
BestVertexIndexCandidate = VertexCandidate;
LowestTriangleDeltaSum = CandidateSmallestTriangleDelta;
}
}
if (BestVertexIndexCandidate != INDEX_NONE)
{
GeoToRigMap.Add(VertexID, BestVertexIndexCandidate); //Aka mapping the geometry vertex id to the rig vertex id, as to which righ vertex id is the best one to use for influence.
}
}
}
FSkinWeightsVertexAttributesRef RigVertexSkinWeights = RigAttributes.GetVertexSkinWeights();
FSkinWeightsVertexAttributesRef GeoVertexSkinWeights = GeoAttributes.GetVertexSkinWeights();
for (const FVertexID VertexID : GeoMeshDescription.Vertices().GetElementIDs())
{
const int32* RigVertexID = GeoToRigMap.Find(VertexID);
if (RigVertexID)
{
FVertexBoneWeights VertexBoneWeights = RigVertexSkinWeights.Get(*RigVertexID);
TArray<UE::AnimationCore::FBoneWeight> BoneWeights;
for (const UE::AnimationCore::FBoneWeight BoneWeight : VertexBoneWeights)
{
BoneWeights.Add(BoneWeight);
}
GeoVertexSkinWeights.Set(VertexID, BoneWeights);
}
else
{
//if the VertexID does not have a mapping, then set boneIndex 0 with weight 1.
GeoVertexSkinWeights.Set(VertexID, { UE::AnimationCore::FBoneWeight(0, 1.f) });
}
}
}
#undef LOCTEXT_NAMESPACE