Files
UnrealEngine/Engine/Source/Developer/NaniteBuilder/Private/Encode/NaniteEncodeSkinning.cpp
Brandyn / Techy fcc1b09210 init
2026-04-04 15:40:51 -05:00

340 lines
10 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "NaniteEncodeSkinning.h"
#include "Math/UnrealMath.h"
#include "Cluster.h"
#include "NaniteDefinitions.h"
#include "Async/ParallelFor.h"
namespace Nanite
{
static_assert(sizeof(FClusterBoneInfluence) % 4 == 0, "sizeof(FClusterBoneInfluence) must be a multiple of 4"); // shader assumes multiple of 4
static_assert(sizeof(FPackedVoxelBoneInfluence) % 4 == 0, "sizeof(FPackedVoxelBoneInfluence) must be a multiple of 4");
// Carefully quantize a set of weights while making sure their sum hits an exact target.
// If the input weights are in non-ascending order, the output weights will also be in non-ascending order.
template<typename TGetWeight, typename TArrayType>
void QuantizeWeights(const uint32 N, const uint32 TargetTotalQuantizedWeight, TArrayType& QuantizedWeights, TGetWeight&& GetWeight)
{
float TotalWeight = 0.0f;
for (uint32 i = 0; i < N; i++)
{
TotalWeight += (float)GetWeight(i);
}
if (FMath::IsNearlyZero(TotalWeight))
{
// bail early on zero total weight
QuantizedWeights.SetNumZeroed(N);
return;
}
struct FHeapEntry
{
float Error;
uint32 Index;
};
TArray<FHeapEntry, TInlineAllocator<64>> ErrorHeap;
QuantizedWeights.SetNum(N);
uint32 TotalQuantizedWeight = 0;
for (uint32 i = 0; i < N; i++)
{
const float Weight = ((float)GetWeight(i) * (float)TargetTotalQuantizedWeight) / TotalWeight;
const uint32 QuantizedWeight = FMath::RoundToInt(Weight);
QuantizedWeights[i] = QuantizedWeight;
ErrorHeap.Emplace(FHeapEntry{ (float)QuantizedWeight - Weight, i });
TotalQuantizedWeight += QuantizedWeight;
}
if (TotalQuantizedWeight != TargetTotalQuantizedWeight)
{
// If the weights don't add up to TargetTotalQuantizedWeight exactly, iteratively increment/decrement the weight that introduces the smallest error.
const bool bTooSmall = (TotalQuantizedWeight < TargetTotalQuantizedWeight);
const int32 Diff = bTooSmall ? 1 : -1;
auto Predicate = [bTooSmall](const FHeapEntry& A, const FHeapEntry& B)
{
if (bTooSmall)
{
return (A.Error != B.Error) ? (A.Error < B.Error) : (A.Index < B.Index);
}
else
{
return (A.Error != B.Error) ? (A.Error > B.Error) : (A.Index > B.Index);
}
};
ErrorHeap.Heapify(Predicate);
while (TotalQuantizedWeight != TargetTotalQuantizedWeight)
{
check(ErrorHeap.Num() > 0);
FHeapEntry Entry;
ErrorHeap.HeapPop(Entry, Predicate, EAllowShrinking::No);
QuantizedWeights[Entry.Index] += Diff;
TotalQuantizedWeight += Diff;
}
}
#if DO_CHECK
uint32 WeightSum = 0;
for (uint32 i = 0; i < N; i++)
{
uint32 Weight = QuantizedWeights[i];
check(Weight <= TargetTotalQuantizedWeight);
WeightSum += Weight;
}
check(WeightSum == TargetTotalQuantizedWeight);
#endif
}
void CalculateInfluences(FBoneInfluenceInfo& InfluenceInfo, const FCluster& Cluster, int32 BoneWeightPrecision)
{
const uint32 NumClusterVerts = Cluster.Verts.Num();
const uint32 MaxBones = Cluster.Verts.Format.NumBoneInfluences;
if (MaxBones == 0)
return;
const bool bVoxel = (Cluster.NumTris == 0);
uint32 MaxVertexInfluences = 0;
uint32 MaxBoneIndex = 0;
bool bClusterBoneOverflow = false;
InfluenceInfo.ClusterBoneInfluences.Reserve(NANITE_MAX_CLUSTER_BONE_INFLUENCES);
TMap<uint32, float> TotalBoneWeightMap;
TArray<uint32, TInlineAllocator<NANITE_MAX_CLUSTER_BONE_INFLUENCES>> NumBoneInfluenceRefs;
NumBoneInfluenceRefs.SetNum(NANITE_MAX_CLUSTER_BONE_INFLUENCES);
for (uint32 i = 0; i < NumClusterVerts; i++)
{
const FVector3f LocalPosition = Cluster.Verts.GetPosition(i);
const FVector2f* BoneInfluences = Cluster.Verts.GetBoneInfluences(i);
uint32 NumVertexInfluences = 0;
for (uint32 j = 0; j < MaxBones; j++)
{
const uint32 BoneIndex = (uint32)BoneInfluences[j].X;
const float fBoneWeight = BoneInfluences[j].Y;
const uint32 BoneWeight = FMath::RoundToInt(fBoneWeight);
// Have we reached the end of weights?
if (BoneWeight == 0)
{
break;
}
if (bVoxel)
{
TotalBoneWeightMap.FindOrAdd(BoneIndex) += fBoneWeight;
}
if (!bClusterBoneOverflow)
{
// Have we seen this bone index already?
bool bFound = false;
for (uint32 InfluenceIndex = 0; InfluenceIndex < (uint32)InfluenceInfo.ClusterBoneInfluences.Num(); InfluenceIndex++)
{
FClusterBoneInfluence& ClusterBoneInfluence = InfluenceInfo.ClusterBoneInfluences[InfluenceIndex];
if (ClusterBoneInfluence.BoneIndex == BoneIndex)
{
NumBoneInfluenceRefs[InfluenceIndex]++;
bFound = true;
break;
}
}
if (!bFound)
{
if (InfluenceInfo.ClusterBoneInfluences.Num() < NANITE_MAX_CLUSTER_BONE_INFLUENCES)
{
NumBoneInfluenceRefs[InfluenceInfo.ClusterBoneInfluences.Num()]++;
FClusterBoneInfluence ClusterBoneInfluence;
ClusterBoneInfluence.BoneIndex = BoneIndex;
InfluenceInfo.ClusterBoneInfluences.Add(ClusterBoneInfluence);
}
else
{
// Bones don't fit. Don't bother storing any of them and just revert back to instance bounds
bClusterBoneOverflow = true;
InfluenceInfo.ClusterBoneInfluences.Empty();
}
}
}
if (!bVoxel)
{
MaxBoneIndex = FMath::Max(MaxBoneIndex, BoneIndex);
NumVertexInfluences++;
}
}
MaxVertexInfluences = FMath::Max(MaxVertexInfluences, NumVertexInfluences);
}
if (TotalBoneWeightMap.Num() > 0)
{
// Pick the bones with the largest total influence
struct FBoneInfluence
{
uint32 Bone;
float Weight;
};
TArray<FBoneInfluence, TInlineAllocator<64>> SortedInfluences;
SortedInfluences.Reserve(TotalBoneWeightMap.Num());
for (const auto& Pair : TotalBoneWeightMap)
{
SortedInfluences.Emplace(FBoneInfluence{ Pair.Key, Pair.Value });
}
SortedInfluences.Sort([](const FBoneInfluence& A, const FBoneInfluence& B)
{
return A.Weight > B.Weight;
});
const uint32 NumElements = (uint32)FMath::Min(SortedInfluences.Num(), NANITE_MAX_VOXEL_ANIMATION_BONE_INFLUENCES);
const uint32 TargetTotalQuantizedWeight = 255;
// Quantize weights to 8 bits
TArray<uint32, TInlineAllocator<64>> QuantizedWeights;
QuantizeWeights(NumElements, TargetTotalQuantizedWeight, QuantizedWeights,
[&SortedInfluences](uint32 Index)
{
return SortedInfluences[Index].Weight;
});
InfluenceInfo.VoxelBoneInfluences.Reserve(NumElements);
for (uint32 i = 0; i < NumElements; i++)
{
const uint32 Weight = QuantizedWeights[i];
if (Weight > 0)
{
const uint32 Weight_BoneIndex = Weight | (SortedInfluences[i].Bone << 8);
InfluenceInfo.VoxelBoneInfluences.Add(FPackedVoxelBoneInfluence{ Weight_BoneIndex });
}
}
}
// Pick dominant bone per brick
// TODO: Optimize me
const uint32 NumBricks = Cluster.Bricks.Num();
if(NumBricks > 0)
{
TMap<uint32, float> BoneWeightMap;
InfluenceInfo.BrickBoneIndices.SetNum(NumBricks);
for (uint32 BrickIndex = 0; BrickIndex < NumBricks; BrickIndex++)
{
const FCluster::FBrick& Brick = Cluster.Bricks[BrickIndex];
const uint32 NumVerts = FMath::CountBits(Brick.VoxelMask);
BoneWeightMap.Reset();
for (uint32 i = 0; i < NumVerts; i++)
{
const FVector2f* BoneInfluences = Cluster.Verts.GetBoneInfluences(Brick.VertOffset + i);
for (uint32 j = 0; j < MaxBones; j++)
{
const uint32 BoneIndex = (uint32)BoneInfluences[j].X;
const float fBoneWeight = BoneInfluences[j].Y;
const uint32 BoneWeight = FMath::RoundToInt(fBoneWeight);
BoneWeightMap.FindOrAdd(BoneIndex) += fBoneWeight;
}
}
float BestWeight = -MAX_flt;
uint32 BestBoneIndex = MAX_uint32;
for (const auto& Pair : BoneWeightMap)
{
if (Pair.Value > BestWeight)
{
BestWeight = Pair.Value;
BestBoneIndex = Pair.Key;
}
}
check(BestBoneIndex != MAX_uint32);
InfluenceInfo.BrickBoneIndices[BrickIndex] = BestBoneIndex;
}
}
InfluenceInfo.NumVertexBoneInfluences = MaxVertexInfluences;
InfluenceInfo.NumVertexBoneIndexBits = FMath::CeilLogTwo(MaxBoneIndex + 1u);
InfluenceInfo.NumVertexBoneWeightBits = MaxVertexInfluences > 1 ? BoneWeightPrecision : 0u; // Drop bone weights if only one bone is used
}
void PackBoneInfluenceHeader(FPackedBoneInfluenceHeader& PackedBoneInfluenceHeader, const FBoneInfluenceInfo& BoneInfluenceInfo)
{
PackedBoneInfluenceHeader = FPackedBoneInfluenceHeader();
PackedBoneInfluenceHeader.SetDataOffset(BoneInfluenceInfo.DataOffset);
PackedBoneInfluenceHeader.SetNumVertexInfluences(BoneInfluenceInfo.NumVertexBoneInfluences);
PackedBoneInfluenceHeader.SetNumVertexBoneIndexBits(BoneInfluenceInfo.NumVertexBoneIndexBits);
PackedBoneInfluenceHeader.SetNumVertexBoneWeightBits(BoneInfluenceInfo.NumVertexBoneWeightBits);
}
static void QuantizeBoneWeights(FCluster& Cluster, int32 BoneWeightPrecision)
{
const uint32 NumVerts = Cluster.Verts.Num();
const uint32 NumBoneInfluences = Cluster.Verts.Format.NumBoneInfluences;
const uint32 TargetTotalBoneWeight = BoneWeightPrecision ? ((1u << BoneWeightPrecision) - 1u) : 1u;
for (uint32 VertIndex = 0; VertIndex < NumVerts; VertIndex++)
{
FVector2f* BoneInfluences = Cluster.Verts.GetBoneInfluences(VertIndex);
QuantizeAndSortBoneInfluenceWeights(TArrayView<FVector2f>(BoneInfluences, NumBoneInfluences), TargetTotalBoneWeight);
}
}
void QuantizeAndSortBoneInfluenceWeights(TArrayView<FVector2f> BoneInfluences, uint32 TargetTotalQuantizedWeight)
{
const uint32 NumBoneInfluences = BoneInfluences.Num();
TArray<uint32, TInlineAllocator<64>> QuantizedWeights;
QuantizeWeights(NumBoneInfluences, TargetTotalQuantizedWeight, QuantizedWeights, [BoneInfluences](uint32 Index) -> float
{
return BoneInfluences[Index].Y;
});
for (uint32 i = 0; i < NumBoneInfluences; ++i)
{
BoneInfluences[i].Y = (float)QuantizedWeights[i];
if (QuantizedWeights[i] == 0)
{
BoneInfluences[i].X = 0.0f; // Clear index when weight is 0
}
}
// Sort just to be sure. Maybe the input was not in non-ascending order.
BoneInfluences.Sort([](const FVector2f& A, const FVector2f& B) { return A.Y > B.Y || (A.Y == B.Y && A.X > B.X); });
}
void QuantizeBoneWeights(TArray<FCluster>& Clusters, int32 BoneWeightPrecision)
{
ParallelFor(TEXT("NaniteEncode.QuantizeBoneWeights.PF"), Clusters.Num(), 256,
[&Clusters, BoneWeightPrecision](uint32 ClusterIndex)
{
QuantizeBoneWeights(Clusters[ClusterIndex], BoneWeightPrecision);
});
}
} // namespace Nanite