Files
UnrealEngine/Engine/Source/Runtime/AnimationCore/Public/BoneWeights.h
2025-05-18 13:04:45 +08:00

1439 lines
42 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "Algo/IsSorted.h"
#include "AnimationCore.h"
#include "BoneIndices.h"
#include "Containers/Array.h"
#include "Containers/ArrayView.h"
#include "Containers/ContainerAllocationPolicies.h"
#include "Containers/Set.h"
#include "Containers/UnrealString.h"
#include "CoreTypes.h"
#include "GPUSkinPublicDefs.h"
#include "HAL/PlatformCrt.h"
#include "Math/NumericLimits.h"
#include "Math/UnrealMathSSE.h"
#include "Misc/AssertionMacros.h"
#include "Serialization/Archive.h"
#include "Templates/TypeHash.h"
#include "Templates/UnrealTemplate.h"
#include <limits>
#include <type_traits>
namespace UE {
namespace AnimationCore {
/**
The maximum number of inline bone weights.
*/
static constexpr int32 MaxInlineBoneWeightCount = MAX_TOTAL_INFLUENCES;
/** The maximum raw weight value */
static constexpr uint16 MaxRawBoneWeight = std::numeric_limits<uint16>::max();
/** The maximum raw weight value as a float value */
static constexpr float MaxRawBoneWeightFloat = static_cast<float>(std::numeric_limits<uint16>::max());
/** The inverse of the maximum raw weight value as a float value. Used for scaling. */
static constexpr float InvMaxRawBoneWeightFloat = 1.0f / MaxRawBoneWeightFloat;
/** The threshold value at or above which a 0-1 normalized bone weight value will be stored as a non-zero value after
* scaling and quantizing to a 16-bit integer */
static constexpr float BoneWeightThreshold = InvMaxRawBoneWeightFloat;
class FBoneWeight
{
public:
/**
* Returns true if this object's bone weight values are equal to the other container's values.
* @note Only equality comparison are supported. Relational comparisons are meaningless.
*/
bool operator==(const FBoneWeight& InBoneWeight) const
{
return BoneIndex == InBoneWeight.BoneIndex && RawWeight == InBoneWeight.RawWeight;
}
/**
* Returns true if this object's bone weight values are not equal to the other container's values.
* @note Only equality comparison are supported. Relational comparisons are meaningless.
*/
bool operator!=(const FBoneWeight& InBoneWeight) const
{
return BoneIndex != InBoneWeight.BoneIndex || RawWeight != InBoneWeight.RawWeight;
}
/**
* The maximum raw weight value for a bone influence.
*/
static constexpr uint16 GetMaxRawWeight()
{
return TNumericLimits<uint16>::Max();
}
/** A standard predicate we use for sorting by weight, in a descending order of weights */
static bool DescSortByWeightPredicate(const FBoneWeight& A, const FBoneWeight& B)
{
return A.GetRawWeight() > B.GetRawWeight();
}
/** Default constructor. NOTE: The values are uninitialized. */
FBoneWeight() = default;
/**
* A constructor for old-style bone weights where the weight was stored as an unsigned eight
* bit integer.
*/
explicit FBoneWeight(FBoneIndexType InBoneIndex, uint8 InWeight)
: BoneIndex(InBoneIndex)
{
RawWeight = uint16( (InWeight << 8) | InWeight );
}
/**
* A constructor for bone weights where the weight is stored as an unsigned sixteen
* bit integer. This is the natural storage format for this container.
*/
explicit FBoneWeight(FBoneIndexType InBoneIndex, uint16 InRawWeight)
: BoneIndex(InBoneIndex), RawWeight(InRawWeight)
{
}
/**
* A constructor for bone weights which converts the weight from a float value in the
* [0-1] range. Values outside of that range are clamped to the range.
*/
explicit FBoneWeight(FBoneIndexType InBoneIndex, float InWeight)
: BoneIndex(InBoneIndex)
{
SetWeight(InWeight);
}
/**
* Set the bone stored index.
*/
void SetBoneIndex(FBoneIndexType InBoneIndex)
{
BoneIndex = InBoneIndex;
}
/**
* Returns the bone stored index.
*/
FBoneIndexType GetBoneIndex() const
{
return BoneIndex;
}
/**
* Set stored weight as a float. Values outside the [0-1] range are clamped to that range.
* Undefined float values will result in an undefined weight.
*/
void SetWeight(float InWeight)
{
InWeight = FMath::Clamp(InWeight, 0.0f, 1.0f);
RawWeight = static_cast<uint16>(InWeight * static_cast<float>(GetMaxRawWeight()) + 0.5f);
}
/**
* Returns the stored weight value as a float in the [0-1] range.
*/
float GetWeight() const
{
return RawWeight / static_cast<float>(GetMaxRawWeight());
}
/**
* Set the stored weight in the raw format. This avoids any floating point conversion.
*/
void SetRawWeight(const uint16 InRawWeight)
{
RawWeight = InRawWeight;
}
/**
* Returns the stored weight in the container's raw format, avoiding floating point
* conversion.
*/
uint16 GetRawWeight() const
{
return RawWeight;
}
void Serialize(FArchive& InArchive)
{
InArchive << BoneIndex;
InArchive << RawWeight;
}
/** Returns a hash value from the bone weight values */
uint32 GetTypeHash() const
{
return HashCombine(::GetTypeHash(static_cast<uint16>(BoneIndex)), ::GetTypeHash(RawWeight));
}
/**
* A helper function to return a human-readable version of the bone weight.
*/
FString ToString() const
{
return FString::Printf(TEXT("<%d, %g>"), BoneIndex, GetWeight());
}
/**
* Returns the contents of this object as an int32.
*/
int32 ToInt32() const
{
return static_cast<int32>(static_cast<uint16>(BoneIndex) << 16 | RawWeight);
}
/**
* Converts an int32 to an FBoneWeight object and returns it.
*/
static FBoneWeight FromInt32(int32 InBoneWeight)
{
FBoneWeight BW;
BW.RawWeight = static_cast<uint16>(static_cast<uint32>(InBoneWeight) & 0xFFFFU);
BW.BoneIndex = static_cast<FBoneIndexType>(static_cast<uint32>(InBoneWeight) >> 16);
return BW;
}
private:
FBoneIndexType BoneIndex;
uint16 RawWeight;
};
static_assert(sizeof(FBoneWeight) == sizeof(int32), "FBoneWeight must be 32-bits");
/** Specifies the method for which the bone weights are normalized after the bone weight
* list is modified.
*/
enum class EBoneWeightNormalizeType
{
/** No normalization is performed. The sum of the weight values can exceed 1.0 */
None,
/** Normalization is only performed if the sum of the weights exceeds 1.0 */
AboveOne,
/** Normalization is always performed such that the sum of the weights is always equal to 1.0 */
Always
};
class FBoneWeightsSettings
{
public:
FBoneWeightsSettings() = default;
/** Set the normalization type when manipulating the weight values in FBoneWeights */
FBoneWeightsSettings& SetNormalizeType(EBoneWeightNormalizeType InNormalizeType)
{
NormalizeType = InNormalizeType;
return *this;
}
/** Returns the current normalization type for these settings */
EBoneWeightNormalizeType GetNormalizeType() const { return NormalizeType; }
/** Sets the maximum number of weights that can be applied to a single FBoneWeights object.
* When weights are added, the smallest weights, past this limit, are dropped.
*/
FBoneWeightsSettings& SetMaxWeightCount(int32 InMaxWeightCount)
{
MaxWeightCount = FMath::Max(1, InMaxWeightCount);
return *this;
}
/** Returns the maximum number of weights for these settings */
int32 GetMaxWeightCount() const { return MaxWeightCount; }
/** Sets the minimum influence allowed. Any bone weight value below this threshold value is
* discarded. The threshold value is clamped to the half-closed interval (0, 1] since
* weight values of zero indicate no influence at all and are ignored completely.
* By default, all non-zero weights are allowed.
*/
FBoneWeightsSettings& SetWeightThreshold(float InWeightThreshold)
{
InWeightThreshold = FMath::Clamp(InWeightThreshold, 0.0f, 1.0f);
WeightThreshold = static_cast<uint16>(InWeightThreshold * FBoneWeight::GetMaxRawWeight() + 0.5f);
WeightThreshold = FMath::Max(WeightThreshold, static_cast<uint16>(1));
return *this;
}
/** Returns the weight threshold as a float value between (0, 1]. */
float GetWeightThreshold() const
{
return WeightThreshold / static_cast<float>(TNumericLimits<uint16>::Max());
}
/** Returns the raw weight threshold. This is the value used internally for weight
* computation.
*/
uint16 GetRawWeightThreshold() const
{
return WeightThreshold;
}
/** Set the default bone index to use if no weights were set. This can be used to ensure
that there's always a valid weight applied to a skinned vertex. */
void SetDefaultBoneIndex(FBoneIndexType InBoneIndex)
{
DefaultBoneIndex = InBoneIndex;
bHasDefaultBoneIndex = true;
}
/** Returns the current default bone index to set if no weights were set. If no default
bone index has been set, then this value is undefined. Call HasDefaultBoneIndex to
check */
FBoneIndexType GetDefaultBoneIndex() const
{
return DefaultBoneIndex;
}
/** Clears the default bone index. This allows bone weights arrays to be empty if no weights
are set. */
void ClearDefaultBoneIndex()
{
bHasDefaultBoneIndex = false;
}
/** Returns \c true if a default bone index should be applied in the absence of other
weights. */
bool HasDefaultBoneIndex() const
{
return bHasDefaultBoneIndex;
}
/** Set the way we treat the bones with zero influence during the blend. If true, we assume all
* bones with no influence have a zero weight (default) and participate in the blend calculation.
* If false, we ignore the bone during the blend.
*/
void SetBlendZeroInfluence(bool bInBlendZeroInfluence)
{
bBlendZeroInfluence = bInBlendZeroInfluence;
}
/** Return true if bones with no influence are considered to have a zero weight during the
* blend calculation. */
bool GetBlendZeroInfluence() const
{
return bBlendZeroInfluence;
}
private:
EBoneWeightNormalizeType NormalizeType = EBoneWeightNormalizeType::Always;
int32 MaxWeightCount = MaxInlineBoneWeightCount;
uint16 WeightThreshold = 1;
FBoneIndexType DefaultBoneIndex = static_cast<FBoneIndexType>(0);
bool bHasDefaultBoneIndex = false;
bool bBlendZeroInfluence = true;
};
/** A null adapter for a bone weight container to use with TBoneWeights. Use as a template to
create adapters for other types of containers. */
struct FBoneWeightNullAdapter
{
struct Empty {};
using ContainerType = Empty;
/** Set the number of elements to reserve in the container. The elements can be left
in an uninitialized state. The TBoneWeights algorithms will ensure that all elements
will be properly defined at the end of an operation. */
static void SetNum(ContainerType& InContainer, int32 InNum)
{
}
static int32 Num(const ContainerType& InContainer)
{
return INDEX_NONE;
}
static FBoneWeight Get(const ContainerType& InContainer, int32 InIndex)
{
return {};
}
static void Set(ContainerType& InContainer, int32 InIndex, FBoneWeight InBoneWeight)
{
}
static void Add(ContainerType& InContainer, FBoneWeight InBoneWeight)
{
}
static void Remove(ContainerType& InContainer, int32 InIndex)
{
}
template<typename Predicate>
static void Sort(ContainerType& InContainer, Predicate InPredicate)
{
}
template<typename Predicate>
static int32 IndexOf(const ContainerType& InContainer, Predicate InPredicate)
{
return INDEX_NONE;
}
};
/** A templated collection of bone weights algorithms. Requires an adapter to work with a
dynamically resizable container. */
template<typename ContainerAdapter>
class TBoneWeights
{
public:
using ContainerType = typename ContainerAdapter::ContainerType;
TBoneWeights(ContainerType& InContainer) :
Container(InContainer)
{ }
template<typename OtherContainerAdapter, typename CT = ContainerType>
inline typename std::enable_if<!std::is_const<CT>::value, void>::type
SetBoneWeights(
const TBoneWeights<OtherContainerAdapter> &InBoneWeights,
const FBoneWeightsSettings& InSettings = {});
template<typename CT = ContainerType>
inline typename std::enable_if<!std::is_const<CT>::value, void>::type
SetBoneWeights(
TArrayView<const FBoneWeight> BoneWeights,
const FBoneWeightsSettings& InSettings = {});
template<typename CT = ContainerType>
inline typename std::enable_if<!std::is_const<CT>::value, void>::type
SetBoneWeights(
const FBoneIndexType* InBones,
const float* InInfluences,
int32 NumEntries,
const FBoneWeightsSettings& InSettings = {}
);
template<typename CT = ContainerType>
inline typename std::enable_if<!std::is_const<CT>::value, void>::type
SetBoneWeights(
const FBoneIndexType InBones[MaxInlineBoneWeightCount],
const uint16 InInfluences[MaxInlineBoneWeightCount],
const FBoneWeightsSettings& InSettings = {});
template<typename CT = ContainerType>
inline typename std::enable_if<!std::is_const<CT>::value, bool>::type
AddBoneWeight(
FBoneWeight InBoneWeight,
const FBoneWeightsSettings& InSettings = {}
);
template<typename CT = ContainerType>
inline typename std::enable_if<!std::is_const<CT>::value, bool>::type
RemoveBoneWeight(
FBoneIndexType InBoneIndex,
const FBoneWeightsSettings& InSettings = {}
);
template<typename CT = ContainerType>
inline typename std::enable_if<!std::is_const<CT>::value, void>::type
Renormalize(
const FBoneWeightsSettings& InSettings = {}
);
/** Blend two bone weights together, making sure to add in every influence from both,
* using the given settings. The bias value should lie on the [0,1] interval. Values outside
* that range may give unwanted results.
* NOTE: The current container can also be used as an input.
*/
template<typename ContainerTypeA, typename ContainerTypeB, typename CT = ContainerType>
inline typename std::enable_if<!std::is_const<CT>::value, void>::type
Blend(
const TBoneWeights<ContainerTypeA>& InBoneWeightsA,
const TBoneWeights<ContainerTypeB>& InBoneWeightsB,
float InBias,
const FBoneWeightsSettings& InSettings = {});
int32 Num() const
{
return ContainerAdapter::Num(Container);
}
FBoneWeight Get(int32 Index) const
{
return ContainerAdapter::Get(Container, Index);
}
FBoneWeight operator[](int32 Index) const
{
return ContainerAdapter::Get(Container, Index);
}
int32 FindWeightIndexByBone(
FBoneIndexType InBoneIndex
) const
{
return Container.IndexOfByPredicate(
[InBoneIndex](const FBoneWeight& BW) { return InBoneIndex == BW.GetBoneIndex(); });
}
int32 GetTypeHash() const
{
uint32 Hash = ::GetTypeHash(Num());
for (int32 Index = 0; Index < Num(); Index++)
{
Hash = HashCombine(Hash, Get(Index).GetTypeHash());
}
return Hash;
}
FString ToString() const
{
FString Result(TEXT("["));
if (Num())
{
Result += Get(0).ToString();
for (int32 Index = 1; Index < Num(); Index++)
{
Result += TEXT(", ");
Result += Get(Index).ToString();
}
}
Result.Append(TEXT("]"));
return Result;
}
private:
inline void SetBoneWeightsInternal(
TArrayView<FBoneWeight> BoneWeights,
const FBoneWeightsSettings& InSettings = {});
inline void SortWeights();
inline bool CullWeights(const FBoneWeightsSettings& InSettings);
inline void NormalizeWeights(EBoneWeightNormalizeType InNormalizeType);
inline bool Verify() const;
// The externally owned container we're operating on.
ContainerType &Container;
};
/// A simple container for per-vertex influence of bones and their weights.
class FBoneWeights
{
using BoneWeightsTempAllocatorT = TInlineAllocator<MaxInlineBoneWeightCount>;
using BoneWeightArrayT = TArray<FBoneWeight, BoneWeightsTempAllocatorT>;
template<typename T>
struct TArrayAdapter
{
using ContainerType = T;
static void SetNum(ContainerType& InContainer, int32 InNum)
{
InContainer.SetNumUninitialized(InNum, EAllowShrinking::No);
}
static int32 Num(const ContainerType& InContainer)
{
return InContainer.Num();
}
static FBoneWeight Get(const ContainerType& InContainer, int32 InIndex)
{
return InContainer.GetData()[InIndex];
}
static void Set(ContainerType& InContainer, int32 InIndex, FBoneWeight InBoneWeight)
{
InContainer.GetData()[InIndex] = InBoneWeight;
}
static void Add(ContainerType& InContainer, FBoneWeight InBoneWeight)
{
InContainer.Add(InBoneWeight);
}
static void Remove(ContainerType& InContainer, int32 InIndex)
{
InContainer.RemoveAt(InIndex);
}
template<typename Predicate>
static void Sort(ContainerType& InContainer, Predicate InPredicate)
{
InContainer.Sort(InPredicate);
}
template<typename Predicate>
static int32 IndexOf(const ContainerType& InContainer, Predicate InPredicate)
{
return InContainer.IndexOfByPredicate(InPredicate);
}
};
using FArrayWrapper = TBoneWeights<TArrayAdapter<BoneWeightArrayT>>;
public:
FBoneWeights() = default;
/**
* Returns true if all of this container's values and count are equal to the other
* container's values and count.
* @note Only equality comparison are supported. Relational comparisons are meaningless.
*/
bool operator==(const FBoneWeights& InBoneWeight) const
{
return BoneWeights == InBoneWeight.BoneWeights;
}
/**
* Returns true if this container's values are equal to the other container's values.
@note Only equality comparison are supported. Relational comparisons are meaningless.
*/
bool operator!=(const FBoneWeights& InBoneWeight) const
{
return BoneWeights != InBoneWeight.BoneWeights;
}
/** Set a new bone weight. If an existing weight exists with the same bone index, it's
* weight value is replaced with the weight value of the given entry. Otherwise a
* new index is added. In both cases the new entry is subject to the given settings, which
* may influence what the entry's final weight value is and whether it gets thrown away
* for not passing the threshold value. If the weight was successfully incorporated, then
* this function returns true. Otherwise it returns false.
*/
inline bool SetBoneWeight(
FBoneWeight InBoneWeight,
const FBoneWeightsSettings& InSettings = {});
bool SetBoneWeight(
FBoneIndexType InBone,
float InWeight,
const FBoneWeightsSettings& InSettings = {})
{
return SetBoneWeight(FBoneWeight{InBone, InWeight}, InSettings);
}
/** Removes a specific bone from the list of weights, re-normalizing and pruning bones,
* if needed.
*/
inline bool RemoveBoneWeight(
FBoneIndexType InBone,
const FBoneWeightsSettings& InSettings = {});
/** Force normalization of weights. This is useful if a set of operations were performed
* with no normalization, for efficiency, and normalization is needed post-operation.
*/
inline void Renormalize(const FBoneWeightsSettings& InSettings = {});
/** A helper to create a FBoneWeights container from FSoftSkinVertex data structure. */
static inline FBoneWeights Create(
const FBoneIndexType InBones[MaxInlineBoneWeightCount],
const uint16 InWeights[MaxInlineBoneWeightCount],
const FBoneWeightsSettings& InSettings = {});
/** A helper to create a FBoneWeights container from separated bone index and weight arrays.
* The size of the two arrays *must* be the same -- otherwise the behavior is undefined.
*/
static inline FBoneWeights Create(
const FBoneIndexType* InBones,
const float* InWeights,
int32 NumEntries,
const FBoneWeightsSettings& InSettings = {});
/** A helper to create FBoneWeights container from a TArray of FBoneWeight objects. */
static inline FBoneWeights Create(
TArrayView<const FBoneWeight> BoneWeights,
const FBoneWeightsSettings& InSettings = {});
/** A helper to create FBoneWeights container from a TBoneWeights derivative object */
template<typename OtherContainerAdapter>
static FBoneWeights Create(
TBoneWeights<OtherContainerAdapter> InBoneWeights,
const FBoneWeightsSettings& InSettings = {})
{
FBoneWeights Result;
FArrayWrapper W(Result.BoneWeights);
W.SetBoneWeights(InBoneWeights, InSettings);
return Result;
}
/** Blend two bone weights together, making sure to add in every influence from both,
* using the given settings. The bias value should lie on the [0,1] interval. Values outside
* that range may give unwanted results.
*/
static inline FBoneWeights Blend(
const FBoneWeights& InA,
const FBoneWeights& InB,
float InBias,
const FBoneWeightsSettings& InSettings = {});
/** Blend three bone weights via barycentric coordinates using the given settings.
* Considers the influence from all of the bones. Coordinates should sum to one.
*/
static inline FBoneWeights Blend(
const FBoneWeights& InA,
const FBoneWeights& InB,
const FBoneWeights& InC,
float InBaryX,
float InBaryY,
float InBaryZ,
const FBoneWeightsSettings& InSettings = {});
// Ranged-based for loop compatibility -- but only the const version.
using RangedForConstIteratorType = BoneWeightArrayT::RangedForConstIteratorType;
RangedForConstIteratorType begin() const { return BoneWeights.begin(); }
RangedForConstIteratorType end() const { return BoneWeights.end(); }
/** Return the number of bone weights in this container.
* @return The number of bone weights.
*/
int32 Num() const { return BoneWeights.Num(); }
/** Return the weight at index Index. Using an index value less than zero -- or equal
* to or greater than the result of Num() -- is an undefined operation.
* @param Index The index of the desired bone weight value.
* @return The bone weight value for that index, or undefined if outside the valid range.
*/
const FBoneWeight& operator[](int32 Index) const { return BoneWeights.operator[](Index); }
/** Returns the FBoneWeight list as an array view */
TArrayView<const FBoneWeight> ToArrayView() const
{
return BoneWeights;
}
// -- Helper functions
/** Find the bone weight corresponding to the given bone index. If a bone weight with this
* bone index does not exist it, a value of INDEX_NONE is returned.
*/
int32 FindWeightIndexByBone(FBoneIndexType InBoneIndex) const
{
return BoneWeights.IndexOfByPredicate(
[InBoneIndex](const FBoneWeight& BW) { return InBoneIndex == BW.GetBoneIndex(); });
}
void Serialize(FArchive& InArchive)
{
InArchive << BoneWeights;
}
int32 GetTypeHash() const
{
uint32 Hash = ::GetTypeHash(BoneWeights.Num());
for (const FBoneWeight& BoneWeight : BoneWeights)
{
Hash = HashCombine(Hash, BoneWeight.GetTypeHash());
}
return Hash;
}
FString ToString() const
{
FString Result(TEXT("["));
Result.Append(FString::JoinBy(BoneWeights, TEXT(", "), [](const FBoneWeight& V) { return V.ToString(); }));
Result.Append(TEXT("]"));
return Result;
}
private:
friend uint32 GetTypeHash(const FBoneWeights& InBoneWeights);
/// List of bone weights, in order of descending weight.
BoneWeightArrayT BoneWeights;
};
/// TBoneWeights implementation
template<typename ContainerAdapter>
template<typename OtherContainerAdapter, typename CT>
typename std::enable_if<!std::is_const<CT>::value, void>::type
TBoneWeights<ContainerAdapter>::SetBoneWeights(
const TBoneWeights<OtherContainerAdapter>& InBoneWeights,
const FBoneWeightsSettings& InSettings /*= {}*/)
{
TArray<FBoneWeight, TInlineAllocator<MaxInlineBoneWeightCount>> StackBoneWeights;
StackBoneWeights.Reserve(InBoneWeights.Num());
for (int32 Index = 0; Index < InBoneWeights.Num(); Index++)
{
FBoneWeight BW = InBoneWeights.Get(Index);
if (BW.GetRawWeight() >= InSettings.GetRawWeightThreshold())
{
StackBoneWeights.Add(BW);
}
}
SetBoneWeightsInternal(StackBoneWeights, InSettings);
}
template<typename ContainerAdapter>
template<typename CT>
typename std::enable_if<!std::is_const<CT>::value, void>::type
TBoneWeights<ContainerAdapter>::SetBoneWeights(
TArrayView<const FBoneWeight> InBoneWeights,
const FBoneWeightsSettings& InSettings /*= {}*/)
{
TArray<FBoneWeight, TInlineAllocator<MaxInlineBoneWeightCount>> StackBoneWeights;
StackBoneWeights.Reserve(InBoneWeights.Num());
for (const FBoneWeight& BW : InBoneWeights)
{
if (BW.GetRawWeight() >= InSettings.GetRawWeightThreshold())
{
StackBoneWeights.Add(BW);
}
}
SetBoneWeightsInternal(StackBoneWeights, InSettings);
}
template<typename ContainerAdapter>
template<typename CT>
typename std::enable_if<!std::is_const<CT>::value, void>::type
TBoneWeights<ContainerAdapter>::SetBoneWeights(
const FBoneIndexType* InBones,
const float* InInfluences,
int32 NumEntries,
const FBoneWeightsSettings& InSettings /*= {}*/)
{
TArray<FBoneWeight, TInlineAllocator<MaxInlineBoneWeightCount>> StackBoneWeights;
StackBoneWeights.Reserve(NumEntries);
for (int32 Index = 0; Index < NumEntries; Index++)
{
FBoneWeight BW(InBones[Index], InInfluences[Index]);
if (BW.GetRawWeight() >= InSettings.GetRawWeightThreshold())
{
StackBoneWeights.Add(BW);
}
}
SetBoneWeightsInternal(StackBoneWeights, InSettings);
}
template<typename ContainerAdapter>
template<typename CT>
typename std::enable_if<!std::is_const<CT>::value, void>::type
TBoneWeights<ContainerAdapter>::SetBoneWeights(
const FBoneIndexType InBones[MaxInlineBoneWeightCount],
const uint16 InInfluences[MaxInlineBoneWeightCount],
const FBoneWeightsSettings& InSettings /*= {}*/)
{
// The weights are valid until the first zero influence.
int32 NumWeights = 0;
for (int32 Index = 0; Index < MaxInlineBoneWeightCount && InInfluences[Index]; Index++)
{
FBoneWeight BW(InBones[Index], InInfluences[Index]);
if (BW.GetRawWeight() >= InSettings.GetRawWeightThreshold())
{
NumWeights++;
}
}
ContainerAdapter::SetNum(Container, NumWeights);
int32 WeightIndex = 0;
for (int32 Index = 0; Index < MaxInlineBoneWeightCount && InInfluences[Index]; Index++)
{
FBoneWeight BW(InBones[Index], InInfluences[Index]);
if (BW.GetRawWeight() >= InSettings.GetRawWeightThreshold())
{
ContainerAdapter::Set(Container, WeightIndex++, BW);
}
}
// Sort the weights by descending weight value before we clip it.
SortWeights();
if (WeightIndex > InSettings.GetMaxWeightCount())
{
ContainerAdapter::SetNum(Container, InSettings.GetMaxWeightCount());
}
NormalizeWeights(InSettings.GetNormalizeType());
}
template<typename ContainerAdapter>
void TBoneWeights<ContainerAdapter>::SetBoneWeightsInternal(
TArrayView<FBoneWeight> BoneWeights,
const FBoneWeightsSettings& InSettings /*= {} */
)
{
BoneWeights.Sort(FBoneWeight::DescSortByWeightPredicate);
int32 NumEntries = FMath::Min(BoneWeights.Num(), InSettings.GetMaxWeightCount());
if (NumEntries == 0 && InSettings.HasDefaultBoneIndex())
{
ContainerAdapter::SetNum(Container, 1);
ContainerAdapter::Set(Container, 0, FBoneWeight(InSettings.GetDefaultBoneIndex(), FBoneWeight::GetMaxRawWeight()));
return;
}
ContainerAdapter::SetNum(Container, NumEntries);
for (int32 Index = 0; Index < NumEntries; Index++)
{
ContainerAdapter::Set(Container, Index, BoneWeights[Index]);
}
NormalizeWeights(InSettings.GetNormalizeType());
}
template<typename ContainerAdapter>
template<typename CT>
typename std::enable_if<!std::is_const<CT>::value, bool>::type
TBoneWeights<ContainerAdapter>::AddBoneWeight(
FBoneWeight InBoneWeight,
const FBoneWeightsSettings& InSettings /*= {}*/)
{
// Does this bone already exist?
int32 WeightIndex = FindWeightIndexByBone(InBoneWeight.GetBoneIndex());
// If the sum of weights could possibly exceed 1.0, we may need normalization based on
// the weight settings.
bool bMayNeedNormalization;
if (WeightIndex != INDEX_NONE)
{
FBoneWeight ExistingBoneWeight = ContainerAdapter::Get(Container, WeightIndex);
// New weight is below the threshold. Remove the current bone weight altogether.
if (InBoneWeight.GetRawWeight() < InSettings.GetRawWeightThreshold())
{
ContainerAdapter::Remove(Container, WeightIndex);
// If always normalizing, we need to re-normalize after removing this entry.
if (InSettings.GetNormalizeType() == EBoneWeightNormalizeType::Always)
{
NormalizeWeights(EBoneWeightNormalizeType::Always);
}
return false;
}
if (ExistingBoneWeight.GetRawWeight() == InBoneWeight.GetRawWeight())
{
return true;
}
bMayNeedNormalization = (ExistingBoneWeight.GetRawWeight() < InBoneWeight.GetRawWeight());
ExistingBoneWeight.SetRawWeight(InBoneWeight.GetRawWeight());
ContainerAdapter::Set(Container, WeightIndex, ExistingBoneWeight);
}
else
{
// If the new weight is below the threshold, reject and return.
if (InBoneWeight.GetRawWeight() < InSettings.GetRawWeightThreshold())
{
return false;
}
// Are we already at the limit of weights for this container?
const int32 NumWeights = ContainerAdapter::Num(Container);
if (NumWeights == InSettings.GetMaxWeightCount())
{
// If the weight is smaller than the smallest weight currently, then we reject.
if (InBoneWeight.GetRawWeight() < ContainerAdapter::Get(Container, NumWeights - 1).GetRawWeight())
{
return false;
}
// Overwrite the last one, we'll put it in its correct place when we sort.
ContainerAdapter::Set(Container, NumWeights - 1, InBoneWeight);
}
else
{
ContainerAdapter::Add(Container, InBoneWeight);
}
bMayNeedNormalization = true;
}
// If we got here, then we updated/added weights. We're contractually obligated to keep the
// weights sorted.
SortWeights();
if ((InSettings.GetNormalizeType() == EBoneWeightNormalizeType::Always) ||
(InSettings.GetNormalizeType() == EBoneWeightNormalizeType::AboveOne && bMayNeedNormalization))
{
Renormalize(InSettings);
}
return true;
}
template<typename ContainerAdapter>
template<typename CT>
typename std::enable_if<!std::is_const<CT>::value, bool>::type
TBoneWeights<ContainerAdapter>::RemoveBoneWeight(
FBoneIndexType InBoneIndex,
const FBoneWeightsSettings& InSettings /*= {}*/
)
{
int32 WeightIndex = FindWeightIndexByBone(InBoneIndex);
if (WeightIndex == INDEX_NONE)
{
return false;
}
ContainerAdapter::Remove(Container, WeightIndex);
// Cull all weights that exceed limits set by the settings.
CullWeights(InSettings);
// Removing weights will always cause the weight sum to decrease, so we only have to normalize
// if always asked to.
if (InSettings.GetNormalizeType() == EBoneWeightNormalizeType::Always)
{
NormalizeWeights(EBoneWeightNormalizeType::Always);
}
return true;
}
template<typename ContainerAdapter>
template<typename CT>
typename std::enable_if<!std::is_const<CT>::value, void>::type
TBoneWeights<ContainerAdapter>::Renormalize(
const FBoneWeightsSettings& InSettings /*= {}*/
)
{
NormalizeWeights(InSettings.GetNormalizeType());
// If entries are now below the threshold, remove them.
if (InSettings.GetNormalizeType() == EBoneWeightNormalizeType::Always && CullWeights(InSettings))
{
NormalizeWeights(EBoneWeightNormalizeType::Always);
}
}
template<typename ContainerAdapter>
template<typename ContainerAdapterA, typename ContainerAdapterB, typename CT>
typename std::enable_if<!std::is_const<CT>::value, void>::type
TBoneWeights<ContainerAdapter>::Blend(
const TBoneWeights<ContainerAdapterA>& InBoneWeightsA,
const TBoneWeights<ContainerAdapterB>& InBoneWeightsB,
const float InBias,
const FBoneWeightsSettings& InSettings /*= {}*/
)
{
checkSlow(InBoneWeightsA.Verify());
checkSlow(InBoneWeightsB.Verify());
// Both empty?
if (InBoneWeightsA.Num() == 0 && InBoneWeightsB.Num() == 0)
{
if (InSettings.HasDefaultBoneIndex())
{
ContainerAdapter::SetNum(Container, 1);
ContainerAdapter::Set(Container, 0, FBoneWeight{InSettings.GetDefaultBoneIndex(), FBoneWeight::GetMaxRawWeight()});
}
else
{
ContainerAdapter::SetNum(Container, 0);
}
return;
}
// FIXME: We can probably special-case a few more fast paths (one on either side, one each).
// But let's collect statistics first.
// To simplify lookup and iteration over the two bone weight arrays, we sort by bone index
// value, but indirectly, since we can't sort them directly, as that would violate the
// sorted-by-descending-weight contract. Instead we create an indirection array on the stack
// and use that to iterate
auto CreateIndirectIndex = [](const auto &InBoneWeights, TArrayView<int32> InIndexIndirect) {
for (int32 Index = 0; Index < InIndexIndirect.Num(); Index++)
{
InIndexIndirect[Index] = Index;
}
InIndexIndirect.Sort([InBoneWeights](int32 A, int32 B) {
return InBoneWeights[A].GetBoneIndex() < InBoneWeights[B].GetBoneIndex();
});
};
TArray<int32, TInlineAllocator<MaxInlineBoneWeightCount>> IndirectIndexA;
IndirectIndexA.SetNumUninitialized(InBoneWeightsA.Num());
CreateIndirectIndex(InBoneWeightsA, IndirectIndexA);
TArray<int32, TInlineAllocator<MaxInlineBoneWeightCount>> IndirectIndexB;
IndirectIndexB.SetNumUninitialized(InBoneWeightsB.Num());
CreateIndirectIndex(InBoneWeightsB, IndirectIndexB);
TArray<FBoneWeight, TInlineAllocator<MaxInlineBoneWeightCount * 2>> BoneWeights;
BoneWeights.Reserve(InBoneWeightsA.Num() + InBoneWeightsB.Num());
const int32 RawBiasB = static_cast<int32>(InBias * static_cast<float>(FBoneWeight::GetMaxRawWeight()));
const int32 RawBiasA = FBoneWeight::GetMaxRawWeight() - RawBiasB;
auto BlendWithZeroInfluenceBone = [&InSettings, &BoneWeights](const FBoneWeight& BW, const int32 RawBias)
{
if (InSettings.GetBlendZeroInfluence())
{
// Treat the missing bone as having a zero weight
uint16 RawWeight = uint16( (int32)BW.GetRawWeight() * RawBias / FBoneWeight::GetMaxRawWeight() );
BoneWeights.Emplace(BW.GetBoneIndex(), RawWeight);
}
else
{
// Ignore the missing bone
BoneWeights.Add(BW);
}
};
int32 IndexA = 0, IndexB = 0;
for (; IndexA < InBoneWeightsA.Num() && IndexB < InBoneWeightsB.Num(); /* */ )
{
const FBoneWeight& BWA = InBoneWeightsA[IndirectIndexA[IndexA]];
const FBoneWeight& BWB = InBoneWeightsB[IndirectIndexB[IndexB]];
// If both have the same bone index, we blend them using the bias given and advance
// both arrays. If the bone indices differ, we copy from the array with the lower bone
// index value, to ensure we can possibly catch up with the other array. We then
// advance until we hit the end of either array after which we blindly copy the remains.
if (BWA.GetBoneIndex() == BWB.GetBoneIndex())
{
uint16 RawWeight = uint16( ((int32)BWA.GetRawWeight() * RawBiasA + (int32)BWB.GetRawWeight() * RawBiasB) / FBoneWeight::GetMaxRawWeight() );
BoneWeights.Emplace(BWA.GetBoneIndex(), RawWeight);
IndexA++;
IndexB++;
}
else if (BWA.GetBoneIndex() < BWB.GetBoneIndex())
{
BlendWithZeroInfluenceBone(BWA, RawBiasA);
IndexA++;
}
else
{
BlendWithZeroInfluenceBone(BWB, RawBiasB);
IndexB++;
}
}
for (; IndexA < InBoneWeightsA.Num(); IndexA++)
{
BlendWithZeroInfluenceBone(InBoneWeightsA[IndirectIndexA[IndexA]], RawBiasA);
}
for (; IndexB < InBoneWeightsB.Num(); IndexB++)
{
BlendWithZeroInfluenceBone(InBoneWeightsB[IndirectIndexB[IndexB]], RawBiasB);
}
SetBoneWeightsInternal(BoneWeights, InSettings);
}
template<typename ContainerAdapter>
void TBoneWeights<ContainerAdapter>::SortWeights()
{
ContainerAdapter::Sort(Container, FBoneWeight::DescSortByWeightPredicate);
}
template<typename ContainerAdapter>
bool TBoneWeights<ContainerAdapter>::CullWeights(
const FBoneWeightsSettings& InSettings
)
{
bool bCulled = false;
int32 NumWeights = ContainerAdapter::Num(Container);
// If are are more entries in the container than the settings allow for, indiscriminately
// remove the excess entries.
if (NumWeights > InSettings.GetMaxWeightCount())
{
ContainerAdapter::SetNum(Container, InSettings.GetMaxWeightCount());
NumWeights = InSettings.GetMaxWeightCount();
bCulled = true;
}
// If any remaining entries are now below the threshold, remove them too.
while (NumWeights > 0 && ContainerAdapter::Get(Container, NumWeights - 1).GetRawWeight() < InSettings.GetRawWeightThreshold())
{
ContainerAdapter::SetNum(Container, --NumWeights);
bCulled = true;
}
return bCulled;
}
template<typename ContainerAdapter>
void TBoneWeights<ContainerAdapter>::NormalizeWeights(
EBoneWeightNormalizeType InNormalizeType
)
{
const int32 NumWeights = ContainerAdapter::Num(Container);
// Early checks
if (InNormalizeType == EBoneWeightNormalizeType::None || NumWeights == 0)
{
return;
}
// Common case.
if (NumWeights == 1)
{
if (InNormalizeType == EBoneWeightNormalizeType::Always)
{
// Set the weight to full for the sole entry if normalizing always.
FBoneWeight BoneWeight = ContainerAdapter::Get(Container, 0);
BoneWeight.SetRawWeight(FBoneWeight::GetMaxRawWeight());
ContainerAdapter::Set(Container, 0, BoneWeight);
}
return;
}
// We operate on int64, since we can easily end up with wraparound issues during one of the
// multiplications below when using int32. This would tank the division by WeightSum.
int64 WeightSum = 0;
for (int32 Index = 0; Index < NumWeights; Index++)
{
WeightSum += ContainerAdapter::Get(Container, Index).GetRawWeight();
}
if ((InNormalizeType == EBoneWeightNormalizeType::Always && ensure(WeightSum != 0)) ||
WeightSum > FBoneWeight::GetMaxRawWeight())
{
int64 Correction = 0;
// Here we treat the raw weight as a 16.16 fixed point value and ensure that the
// fraction, which would otherwise be lost through rounding, is carried over to the
// subsequent values to maintain a constant sum to the max weight value.
// We do this in descending weight order in an attempt to ensure that weight values
// aren't needlessly lost after scaling.
// It can happen, if all the weights are the same, or similar, that the last weight may
// now be greater in value than the others. In that case we re-sort to ensure that our
// descending weight order invariant holds.
bool bNeedResort = false;
uint16 LastWeight = std::numeric_limits<uint16>::max();
for (int32 Index = 0; Index < NumWeights; Index++)
{
FBoneWeight BW = ContainerAdapter::Get(Container, Index);
const int64 ScaledWeight = static_cast<int64>(BW.GetRawWeight()) * FBoneWeight::GetMaxRawWeight() + Correction;
const uint16 NewWeight = static_cast<uint16>(FMath::Min(ScaledWeight / WeightSum, static_cast<int64>(FBoneWeight::GetMaxRawWeight())));
BW.SetRawWeight(NewWeight);
Correction = ScaledWeight - BW.GetRawWeight() * WeightSum;
ContainerAdapter::Set(Container, Index, BW);
if (NewWeight > LastWeight)
{
bNeedResort = true;
}
LastWeight = NewWeight;
}
if (bNeedResort)
{
SortWeights();
}
}
}
template<typename ContainerAdapter>
bool UE::AnimationCore::TBoneWeights<ContainerAdapter>::Verify() const
{
const int32 NumEntries = Num();
if (NumEntries == 0)
{
return true;
}
// Check that bone indexes are unique
TSet<FBoneIndexType> BoneIndexes;
BoneIndexes.Reserve(NumEntries);
for (int32 Index = 0; Index < NumEntries; Index++)
{
FBoneWeight BW = Get(Index);
if (BoneIndexes.Find(BW.GetBoneIndex()) != nullptr)
{
// Commented out for now, to avoid linker errors, since this is a header-only file.
// UE_LOG(LogAnimationCore, Error, TEXT("Bone Index %d is duplicated"), static_cast<int>(BW.GetBoneIndex()));
return false;
}
BoneIndexes.Add(BW.GetBoneIndex());
}
// Check that all weights are ordered.
float LastWeight = Get(0).GetWeight();
for (int32 Index = 1; Index < NumEntries; Index++)
{
const float Weight = Get(Index).GetWeight();
if (Weight > LastWeight)
{
//UE_LOG(LogAnimationCore, Error, TEXT("Bone Weight at %d is greater than previous (%g > %g)"),
// Index, Weight, LastWeight);
return false;
}
LastWeight = Weight;
}
return true;
}
// FBoneWeights implementations
static auto WeightSortPredicate = [](const FBoneWeight& A, const FBoneWeight& B) {
return A.GetRawWeight() > B.GetRawWeight();
};
bool FBoneWeights::SetBoneWeight(
FBoneWeight InBoneWeight,
const FBoneWeightsSettings& InSettings /*= {} */
)
{
FArrayWrapper W(BoneWeights);
return W.AddBoneWeight(InBoneWeight, InSettings);
}
bool FBoneWeights::RemoveBoneWeight(
FBoneIndexType InBoneIndex,
const FBoneWeightsSettings& InSettings /*= {} */
)
{
FArrayWrapper W(BoneWeights);
return W.RemoveBoneWeight(InBoneIndex, InSettings);
}
void FBoneWeights::Renormalize(const FBoneWeightsSettings& InSettings /*= {}*/)
{
FArrayWrapper W(BoneWeights);
return W.Renormalize(InSettings);
}
FBoneWeights FBoneWeights::Create(
const FBoneIndexType InBones[MaxInlineBoneWeightCount],
const uint16 InInfluences[MaxInlineBoneWeightCount],
const FBoneWeightsSettings& InSettings /*= {} */
)
{
FBoneWeights Result;
FArrayWrapper W(Result.BoneWeights);
W.SetBoneWeights(InBones, InInfluences, InSettings);
return Result;
}
FBoneWeights FBoneWeights::Create(
const FBoneIndexType* InBones,
const float* InInfluences,
int32 NumEntries,
const FBoneWeightsSettings& InSettings /*= {} */
)
{
FBoneWeights Result;
FArrayWrapper W(Result.BoneWeights);
W.SetBoneWeights(InBones, InInfluences, NumEntries, InSettings);
return Result;
}
FBoneWeights FBoneWeights::Create(
TArrayView<const FBoneWeight> InBoneWeights,
const FBoneWeightsSettings& InSettings /*= {} */
)
{
FBoneWeights Result;
FArrayWrapper(Result.BoneWeights).SetBoneWeights(InBoneWeights, InSettings);
return Result;
}
FBoneWeights FBoneWeights::Blend(
const FBoneWeights& InA,
const FBoneWeights& InB,
float InBias,
const FBoneWeightsSettings& InSettings /*= {} */
)
{
FBoneWeights Result;
FArrayWrapper W(Result.BoneWeights);
FArrayWrapper A(const_cast<FBoneWeights&>(InA).BoneWeights);
FArrayWrapper B(const_cast<FBoneWeights&>(InB).BoneWeights);
W.Blend(A, B, InBias, InSettings);
return Result;
}
FBoneWeights FBoneWeights::Blend(
const FBoneWeights& InA,
const FBoneWeights& InB,
const FBoneWeights& InC,
float InBaryX,
float InBaryY,
float InBaryZ,
const FBoneWeightsSettings& InSettings)
{
if (FMath::IsNearlyZero(InBaryY + InBaryZ) == false)
{
const float BCW = InBaryZ / (InBaryY + InBaryZ);
// Use the default settings to make sure we don't normalize, don't prune bones if we exceed the default limit
// and treat the bones with zero influence as having a zero weight during the first blend
FBoneWeightsSettings FirstBlendSettings;
FirstBlendSettings.SetNormalizeType(UE::AnimationCore::EBoneWeightNormalizeType::None);
FirstBlendSettings.SetMaxWeightCount(InB.Num() + InC.Num());
const FBoneWeights BC = FBoneWeights::Blend(InB, InC, BCW, FirstBlendSettings);
const FBoneWeights BCA = FBoneWeights::Blend(BC, InA, InBaryX, InSettings);
return BCA;
}
else
{
return InA;
}
}
} // namespace AnimationCore
} // namespace UE
/**
* A hashing function to allow the FBoneWeight class to be used with hashing containers (e.g.
* TSet or TMap).
*/
static inline uint32 GetTypeHash(
const UE::AnimationCore::FBoneWeight& InBoneWeight
)
{
return InBoneWeight.GetTypeHash();
}
static inline FArchive& operator<<(
FArchive& InArchive,
UE::AnimationCore::FBoneWeight& InOutBoneWeight
)
{
InOutBoneWeight.Serialize(InArchive);
return InArchive;
}
static inline uint32 GetTypeHash(const UE::AnimationCore::FBoneWeights& InBoneWeights)
{
return InBoneWeights.GetTypeHash();
}
static inline FArchive& operator<<(
FArchive& InArchive,
UE::AnimationCore::FBoneWeights& InOutBoneWeights)
{
InOutBoneWeights.Serialize(InArchive);
return InArchive;
}