Files
2025-05-18 13:04:45 +08:00

553 lines
19 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "ChaosClothAsset/TerminalNode.h"
#include "ChaosClothAsset/ClothAsset.h"
#include "ChaosClothAsset/ClothCollectionGroup.h"
#include "ChaosClothAsset/ClothDataflowTools.h"
#include "ChaosClothAsset/ClothGeometryTools.h"
#include "ChaosClothAsset/ClothLodTransitionDataCache.h"
#include "ChaosClothAsset/CollectionClothFacade.h"
#include "ChaosClothAsset/CollectionClothSelectionFacade.h"
#include "Animation/Skeleton.h"
#include "Chaos/CollectionEmbeddedSpringConstraintFacade.h"
#include "Chaos/CollectionPropertyFacade.h"
#include "Dataflow/DataflowInputOutput.h"
#include "Engine/SkinnedAssetCommon.h"
#include "Materials/Material.h"
#include "PhysicsEngine/PhysicsAsset.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(TerminalNode)
#define LOCTEXT_NAMESPACE "ChaosClothAssetTerminalNode"
namespace UE::Chaos::ClothAsset::Private
{
uint32 CalculateClothChecksum(const TArray<TSharedRef<const FManagedArrayCollection>>& InClothCollections)
{
uint32 Checksum = 0;
for (const TSharedRef<const FManagedArrayCollection>& ClothCollection : InClothCollections)
{
constexpr bool bIncludeWeightMapsTrue = true; // Currently, editing weight maps is destructive
FCollectionClothConstFacade Cloth(ClothCollection);
if (Cloth.HasValidRenderData()) // The cloth collection must at least have a render mesh
{
Checksum = Cloth.CalculateTypeHash(bIncludeWeightMapsTrue, Checksum);
const TArray<FName> GroupNames = ClothCollection->GroupNames();
for (const FName& GroupName : GroupNames)
{
Checksum = Cloth.CalculateUserDefinedAttributesTypeHash<int32>(GroupName, Checksum);
Checksum = Cloth.CalculateUserDefinedAttributesTypeHash<float>(GroupName, Checksum);
Checksum = Cloth.CalculateUserDefinedAttributesTypeHash<FVector3f>(GroupName, Checksum);
}
}
FCollectionClothSelectionConstFacade Selection(ClothCollection);
if (Selection.IsValid())
{
// Just checksum the sets that are SimVertex3D and SimFace sets since those are the only ones we care about right now
const TArray<FName> SelectionNames = Selection.GetNames();
for (const FName& SelectionName : SelectionNames)
{
const FName SelectionGroup = Selection.GetSelectionGroup(SelectionName);
if (SelectionGroup == ClothCollectionGroup::SimVertices3D ||
SelectionGroup == ClothCollectionGroup::SimFaces)
{
const TArray<int32> SelectionAsArray = Selection.GetSelectionSet(SelectionName).Array();
Checksum = HashCombineFast(Checksum, GetTypeHash(SelectionName));
Checksum = GetArrayHash(SelectionAsArray.GetData(), SelectionAsArray.Num(), Checksum);
}
}
}
const ::Chaos::Softs::FEmbeddedSpringFacade SpringFacade(const_cast<const FManagedArrayCollection&>(ClothCollection.Get()), ClothCollectionGroup::SimVertices3D);
if (SpringFacade.IsValid())
{
Checksum = SpringFacade.CalculateTypeHash(Checksum);
}
}
return Checksum;
}
bool PropertyKeysAndSolverTypesMatch(const TArray<TSharedRef<const FManagedArrayCollection>>& Collections0, const TArray<TSharedRef<const FManagedArrayCollection>>& Collections1)
{
if (Collections0.Num() != Collections1.Num())
{
return false;
}
for (int32 LODIndex = 0; LODIndex < Collections0.Num(); ++LODIndex)
{
::Chaos::Softs::FCollectionPropertyConstFacade Property0(Collections0[LODIndex].ToSharedPtr());
::Chaos::Softs::FCollectionPropertyConstFacade Property1(Collections1[LODIndex].ToSharedPtr());
if (Property0.Num() != Property1.Num())
{
return false;
}
for (int32 PropertyIndex = 0; PropertyIndex < Property0.Num(); ++PropertyIndex)
{
if (Property0.GetKey(PropertyIndex) != Property1.GetKey(PropertyIndex))
{
return false;
}
}
}
return true;
}
}
FChaosClothAssetTerminalNode_v2::FChaosClothAssetTerminalNode_v2(const UE::Dataflow::FNodeParameters& InParam, FGuid InGuid)
: FDataflowTerminalNode(InParam, InGuid)
, Refresh(FDataflowFunctionProperty::FDelegate::CreateLambda([this](UE::Dataflow::FContext& /*Context*/) { bClothCollectionChecksumValid = false; }))
{
// Start with Lod0
for (int32 Index = 0; Index < NumInitialCollectionLods; ++Index)
{
AddPins();
}
check(GetNumInputs() == NumRequiredInputs + NumInitialCollectionLods); // Update NumRequiredInputs if you add more Inputs. This is used by Serialize.
}
void FChaosClothAssetTerminalNode_v2::SetAssetValue(TObjectPtr<UObject> Asset, UE::Dataflow::FContext& Context) const
{
if (UChaosClothAsset* ClothAsset = Cast<UChaosClothAsset>(Asset.Get()))
{
using namespace UE::Chaos::ClothAsset;
TArray<TSharedRef<const FManagedArrayCollection>> InClothCollections = GetCleanedCollectionLodValues(Context);
TArray<TSharedRef<const FManagedArrayCollection>>& ClothCollections = ClothAsset->GetClothCollections();
const uint32 PreviousChecksum = ClothColllectionChecksum;
const bool bPreviousChecksumsValid = bClothCollectionChecksumValid;
ClothColllectionChecksum = Private::CalculateClothChecksum(InClothCollections);
bClothCollectionChecksumValid = InClothCollections.Num() > 0;
if (bPreviousChecksumsValid && PreviousChecksum == ClothColllectionChecksum && Private::PropertyKeysAndSolverTypesMatch(InClothCollections, ClothCollections))
{
// Cloth and property keys match. Just update property values.
check(InClothCollections.Num() == ClothCollections.Num());
check(ClothCollections.Num() > 0);
for (int32 LODIndex = 0; LODIndex < InClothCollections.Num(); ++LODIndex)
{
TSharedRef<FManagedArrayCollection> ClothCollection = MakeShared<FManagedArrayCollection>(*ClothCollections[LODIndex]);
Chaos::Softs::FCollectionPropertyFacade Properties(ClothCollection);
Properties.UpdateProperties(InClothCollections[LODIndex].ToSharedPtr());
ClothCollections[LODIndex] = MoveTemp(ClothCollection);
}
// Asset must be resaved
ClothAsset->MarkPackageDirty();
return;
}
FText ErrorText;
FText VerboseText;
ClothAsset->Build(InClothCollections, &LODTransitionDataCache, &ErrorText, &VerboseText);
if (!ErrorText.IsEmpty())
{
FClothDataflowTools::LogAndToastWarning(*this, ErrorText, VerboseText);
}
// Asset must be resaved
ClothAsset->MarkPackageDirty();
}
}
TArray<UE::Dataflow::FPin> FChaosClothAssetTerminalNode_v2::AddPins()
{
const int32 Index = CollectionLods.AddDefaulted();
const FDataflowInput& Input = RegisterInputArrayConnection(GetConnectionReference(Index));
return { { UE::Dataflow::FPin::EDirection::INPUT, Input.GetType(), Input.GetName() } };
}
TArray<UE::Dataflow::FPin> FChaosClothAssetTerminalNode_v2::GetPinsToRemove() const
{
const int32 Index = CollectionLods.Num() - 1;
check(CollectionLods.IsValidIndex(Index));
if (const FDataflowInput* const Input = FindInput(GetConnectionReference(Index)))
{
return { { UE::Dataflow::FPin::EDirection::INPUT, Input->GetType(), Input->GetName() } };
}
return Super::GetPinsToRemove();
}
void FChaosClothAssetTerminalNode_v2::OnPinRemoved(const UE::Dataflow::FPin& Pin)
{
const int32 Index = CollectionLods.Num() - 1;
check(CollectionLods.IsValidIndex(Index));
#if DO_CHECK
const FDataflowInput* const Input = FindInput(GetConnectionReference(Index));
check(Input);
check(Input->GetName() == Pin.Name);
check(Input->GetType() == Pin.Type);
#endif
CollectionLods.SetNum(Index);
return Super::OnPinRemoved(Pin);
}
TArray<TSharedRef<const FManagedArrayCollection>> FChaosClothAssetTerminalNode_v2::GetCleanedCollectionLodValues(UE::Dataflow::FContext& Context) const
{
using namespace UE::Chaos::ClothAsset;
TArray<TSharedRef<const FManagedArrayCollection>> CollectionLodValues;
CollectionLodValues.Reserve(CollectionLods.Num());
int32 LastValidLodIndex = INDEX_NONE;
for (int32 LodIndex = 0; LodIndex < CollectionLods.Num(); ++LodIndex)
{
TSharedRef<FManagedArrayCollection> CollectionLodValue = MakeShared<FManagedArrayCollection>(GetValue<FManagedArrayCollection>(Context, GetConnectionReference(LodIndex)));
FCollectionClothFacade ClothFacade(CollectionLodValue);
if (ClothFacade.HasValidRenderData()) // The cloth collection must at least have a render mesh
{
FClothGeometryTools::CleanupAndCompactMesh(CollectionLodValue);
LastValidLodIndex = LodIndex;
}
else if (LastValidLodIndex >= 0)
{
ClothFacade.DefineSchema();
ClothFacade.Initialize(FCollectionClothConstFacade(CollectionLodValues[LastValidLodIndex]));
FClothDataflowTools::LogAndToastWarning(*this,
LOCTEXT("InvalidInputLodNHeadline", "Invalid input LOD."),
FText::Format(
LOCTEXT("InvalidInputLodNDetails",
"Invalid or empty input LOD for LOD {0}.\n"
"Using the previous valid LOD {1} instead."),
LodIndex,
LastValidLodIndex));
}
else
{
FClothDataflowTools::LogAndToastWarning(*this,
LOCTEXT("InvalidInputLod0Headline", "Invalid input LOD 0."),
LOCTEXT("InvalidInputLod0Details",
"Invalid or empty input LOD for LOD 0.\n"
"LOD 0 cannot be empty in order to construct a valid Cloth Asset."));
break;
}
CollectionLodValues.Emplace(MoveTemp(CollectionLodValue));
}
return CollectionLodValues;
}
UE::Dataflow::TConnectionReference<FManagedArrayCollection> FChaosClothAssetTerminalNode_v2::GetConnectionReference(int32 Index) const
{
return { &CollectionLods[Index], Index, &CollectionLods };
}
void FChaosClothAssetTerminalNode_v2::PostSerialize(const FArchive& Ar)
{
// because we add pins we need to make sure we restore them when loading
// to make sure they can get properly reconnected
if (Ar.IsLoading())
{
if (CollectionLods.Num() < NumInitialCollectionLods)
{
CollectionLods.SetNum(NumInitialCollectionLods); // In case the FManagedArrayCollection wasn't serialized with the node (pre the WithSerializer trait)
}
for (int32 Index = 0; Index < NumInitialCollectionLods; ++Index)
{
check(FindInput(GetConnectionReference(Index)));
}
for (int32 Index = NumInitialCollectionLods; Index < CollectionLods.Num(); ++Index)
{
FindOrRegisterInputArrayConnection(GetConnectionReference(Index));
}
if (Ar.IsTransacting())
{
const int32 OrigNumRegisteredInputs = GetNumInputs();
check(OrigNumRegisteredInputs >= NumRequiredInputs + NumInitialCollectionLods);
const int32 OrigNumCollections = CollectionLods.Num();
const int32 OrigNumRegisteredCollections = (OrigNumRegisteredInputs - NumRequiredInputs);
if (OrigNumRegisteredCollections > OrigNumCollections)
{
// Inputs have been removed.
// Temporarily expand Collections so we can get connection references.
CollectionLods.SetNum(OrigNumRegisteredCollections);
for (int32 Index = OrigNumCollections; Index < CollectionLods.Num(); ++Index)
{
UnregisterInputConnection(GetConnectionReference(Index));
}
CollectionLods.SetNum(OrigNumCollections);
}
}
else
{
ensureAlways(CollectionLods.Num() + NumRequiredInputs == GetNumInputs());
}
}
}
FChaosClothAssetTerminalNode::FChaosClothAssetTerminalNode(const UE::Dataflow::FNodeParameters& InParam, FGuid InGuid)
: FDataflowTerminalNode(InParam, InGuid)
{
RegisterInputConnection(&CollectionLod0);
check(NumInitialCollectionLods + NumRequiredInputs == GetNumInputs()); // Update NumRequiredInputs if you add more Inputs. This is used by Serialize.
}
void FChaosClothAssetTerminalNode::SetAssetValue(TObjectPtr<UObject> Asset, UE::Dataflow::FContext& Context) const
{
if (UChaosClothAsset* ClothAsset = Cast<UChaosClothAsset>(Asset.Get()))
{
using namespace UE::Chaos::ClothAsset;
PRAGMA_DISABLE_DEPRECATION_WARNINGS
if (RefreshAsset.bRefreshAsset)
{
bClothCollectionChecksumValid = false;
RefreshAsset.bRefreshAsset = false;
}
PRAGMA_ENABLE_DEPRECATION_WARNINGS
TArray<TSharedRef<const FManagedArrayCollection>> InClothCollections = GetCleanedCollectionLodValues(Context);
TArray<TSharedRef<const FManagedArrayCollection>>& ClothCollections = ClothAsset->GetClothCollections();
const uint32 PreviousChecksum = ClothColllectionChecksum;
const bool bPreviousChecksumsValid = bClothCollectionChecksumValid;
ClothColllectionChecksum = Private::CalculateClothChecksum(InClothCollections);
bClothCollectionChecksumValid = InClothCollections.Num() > 0;
if (bPreviousChecksumsValid && PreviousChecksum == ClothColllectionChecksum && Private::PropertyKeysAndSolverTypesMatch(InClothCollections, ClothCollections))
{
// Cloth and property keys match. Just update property values.
check(InClothCollections.Num() == ClothCollections.Num());
check(ClothCollections.Num() > 0);
for (int32 LODIndex = 0; LODIndex < InClothCollections.Num(); ++LODIndex)
{
TSharedRef<FManagedArrayCollection> ClothCollection = MakeShared<FManagedArrayCollection>(*ClothCollections[LODIndex]);
Chaos::Softs::FCollectionPropertyFacade Properties(ClothCollection);
Properties.UpdateProperties(InClothCollections[LODIndex].ToSharedPtr());
ClothCollections[LODIndex] = MoveTemp(ClothCollection);
}
// Asset must be resaved
ClothAsset->MarkPackageDirty();
return;
}
FText ErrorText;
FText VerboseText;
ClothAsset->Build(InClothCollections, &LODTransitionDataCache, &ErrorText, &VerboseText);
if (!ErrorText.IsEmpty())
{
FClothDataflowTools::LogAndToastWarning(*this, ErrorText, VerboseText);
}
// Asset must be resaved
ClothAsset->MarkPackageDirty();
}
}
TArray<UE::Dataflow::FPin> FChaosClothAssetTerminalNode::AddPins()
{
auto AddInput = [this](const FManagedArrayCollection* Collection) -> TArray<UE::Dataflow::FPin>
{
RegisterInputConnection(Collection);
const FDataflowInput* const Input = FindInput(Collection);
return { { UE::Dataflow::FPin::EDirection::INPUT, Input->GetType(), Input->GetName() } };
};
switch (NumLods)
{
case 1: ++NumLods; return AddInput(&CollectionLod1);
case 2: ++NumLods; return AddInput(&CollectionLod2);
case 3: ++NumLods; return AddInput(&CollectionLod3);
case 4: ++NumLods; return AddInput(&CollectionLod4);
case 5: ++NumLods; return AddInput(&CollectionLod5);
default: break;
}
return Super::AddPins();
}
TArray<UE::Dataflow::FPin> FChaosClothAssetTerminalNode::GetPinsToRemove() const
{
auto PinToRemove = [this](const FManagedArrayCollection* Collection) -> TArray<UE::Dataflow::FPin>
{
const FDataflowInput* const Input = FindInput(Collection);
check(Input);
return { { UE::Dataflow::FPin::EDirection::INPUT, Input->GetType(), Input->GetName() } };
};
switch (NumLods - 1)
{
case 1: return PinToRemove(&CollectionLod1);
case 2: return PinToRemove(&CollectionLod2);
case 3: return PinToRemove(&CollectionLod3);
case 4: return PinToRemove(&CollectionLod4);
case 5: return PinToRemove(&CollectionLod5);
default: break;
}
return Super::GetPinsToRemove();
}
void FChaosClothAssetTerminalNode::OnPinRemoved(const UE::Dataflow::FPin& Pin)
{
auto CheckPinRemoved = [this, &Pin](const FManagedArrayCollection* Collection)
{
check(Pin.Direction == UE::Dataflow::FPin::EDirection::INPUT);
#if DO_CHECK
const FDataflowInput* const Input = FindInput(Collection);
check(Input);
check(Input->GetName() == Pin.Name);
check(Input->GetType() == Pin.Type);
#endif
};
switch (NumLods - 1)
{
case 1:
CheckPinRemoved(&CollectionLod1);
--NumLods;
break;
case 2:
CheckPinRemoved(&CollectionLod2);
--NumLods;
break;
case 3:
CheckPinRemoved(&CollectionLod3);
--NumLods;
break;
case 4:
CheckPinRemoved(&CollectionLod4);
--NumLods;
break;
case 5:
CheckPinRemoved(&CollectionLod5);
--NumLods;
break;
default:
checkNoEntry();
break;
}
return Super::OnPinRemoved(Pin);
}
TArray<const FManagedArrayCollection*> FChaosClothAssetTerminalNode::GetCollectionLods() const
{
TArray<const FManagedArrayCollection*> CollectionLods;
CollectionLods.SetNumUninitialized(NumLods);
for (int32 LodIndex = 0; LodIndex < NumLods; ++LodIndex)
{
switch (LodIndex)
{
case 0: CollectionLods[LodIndex] = &CollectionLod0; break;
case 1: CollectionLods[LodIndex] = &CollectionLod1; break;
case 2: CollectionLods[LodIndex] = &CollectionLod2; break;
case 3: CollectionLods[LodIndex] = &CollectionLod3; break;
case 4: CollectionLods[LodIndex] = &CollectionLod4; break;
case 5: CollectionLods[LodIndex] = &CollectionLod5; break;
default: check(false); break;
}
}
return CollectionLods;
}
PRAGMA_DISABLE_DEPRECATION_WARNINGS // Unexpected deprecation message on some platforms otherwise
const FManagedArrayCollection* FChaosClothAssetTerminalNode::GetCollectionLod(int32 LodIndex) const
PRAGMA_ENABLE_DEPRECATION_WARNINGS
{
switch (LodIndex)
{
case 0: return &CollectionLod0;
case 1: return &CollectionLod1;
case 2: return &CollectionLod2;
case 3: return &CollectionLod3;
case 4: return &CollectionLod4;
case 5: return &CollectionLod5;
default: check(false); return nullptr;
}
}
TArray<TSharedRef<const FManagedArrayCollection>> FChaosClothAssetTerminalNode::GetCleanedCollectionLodValues(UE::Dataflow::FContext& Context) const
{
using namespace UE::Chaos::ClothAsset;
const TArray<const FManagedArrayCollection*> CollectionLods = GetCollectionLods();
TArray<TSharedRef<const FManagedArrayCollection>> CollectionLodValues;
CollectionLodValues.Reserve(CollectionLods.Num());
int32 LastValidLodIndex = INDEX_NONE;
for (int32 LodIndex = 0; LodIndex < CollectionLods.Num(); ++LodIndex)
{
TSharedRef<FManagedArrayCollection> CollectionLodValue = MakeShared<FManagedArrayCollection>(GetValue<FManagedArrayCollection>(Context, CollectionLods[LodIndex]));
FCollectionClothFacade ClothFacade(CollectionLodValue);
if (ClothFacade.HasValidRenderData()) // The cloth collection must at least have a render mesh
{
FClothGeometryTools::CleanupAndCompactMesh(CollectionLodValue);
LastValidLodIndex = LodIndex;
}
else if (LastValidLodIndex >= 0)
{
ClothFacade.DefineSchema();
ClothFacade.Initialize(FCollectionClothConstFacade(CollectionLodValues[LastValidLodIndex]));
FClothDataflowTools::LogAndToastWarning(*this,
LOCTEXT("InvalidInputLodNHeadline", "Invalid input LOD."),
FText::Format(
LOCTEXT("InvalidInputLodNDetails",
"Invalid or empty input LOD for LOD {0}.\n"
"Using the previous valid LOD {1} instead."),
LodIndex,
LastValidLodIndex));
}
else
{
FClothDataflowTools::LogAndToastWarning(*this,
LOCTEXT("InvalidInputLod0Headline", "Invalid input LOD 0."),
LOCTEXT("InvalidInputLod0Details",
"Invalid or empty input LOD for LOD 0.\n"
"LOD 0 cannot be empty in order to construct a valid Cloth Asset."));
break;
}
CollectionLodValues.Emplace(MoveTemp(CollectionLodValue));
}
return CollectionLodValues;
}
void FChaosClothAssetTerminalNode::PostSerialize(const FArchive& Ar)
{
// because we add pins we need to make sure we restore them when loading
// to make sure they can get properly reconnected
if (Ar.IsLoading())
{
const int32 OrigNumRegisteredInputs = GetNumInputs();
check(OrigNumRegisteredInputs >= NumRequiredInputs + NumInitialCollectionLods);
const int32 OrigNumLods = NumLods;
const int32 OrigNumRegisteredLods = OrigNumRegisteredInputs - NumRequiredInputs;
const int32 NumLodToAdd = (OrigNumLods - OrigNumRegisteredLods);
check(Ar.IsTransacting() || OrigNumRegisteredLods == NumInitialCollectionLods);
if (NumLodToAdd > 0)
{
NumLods = OrigNumRegisteredLods; // AddPin will increment it again
for (int32 LodIndex = 0; LodIndex < NumLodToAdd; ++LodIndex)
{
AddPins();
}
}
else if (NumLodToAdd < 0)
{
check(Ar.IsTransacting());
for (int32 Index = NumLods; Index < OrigNumRegisteredLods; ++Index)
{
UnregisterInputConnection(GetCollectionLod(Index));
}
}
check(NumLods + NumRequiredInputs == GetNumInputs());
}
}
#undef LOCTEXT_NAMESPACE