1553 lines
54 KiB
C++
1553 lines
54 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "ClothingAsset.h"
|
|
#include "ClothingAssetCustomVersion.h"
|
|
#include "ClothPhysicalMeshData.h"
|
|
#include "ClothConfig.h"
|
|
|
|
#include "Utils/ClothingMeshUtils.h"
|
|
#include "Features/IModularFeatures.h"
|
|
|
|
#include "Engine/SkeletalMesh.h"
|
|
|
|
#include "PhysicsEngine/PhysicsAsset.h"
|
|
|
|
#include "Widgets/Notifications/SNotificationList.h"
|
|
#include "Framework/Notifications/NotificationManager.h"
|
|
#include "Rendering/SkeletalMeshModel.h"
|
|
#include "Rendering/SkeletalMeshRenderData.h"
|
|
|
|
#include "Components/SkeletalMeshComponent.h"
|
|
|
|
#include "ClothingSimulationInteractor.h"
|
|
#include "ComponentReregisterContext.h"
|
|
#include "UObject/UObjectIterator.h"
|
|
#include "UObject/PhysicsObjectVersion.h"
|
|
#include "UObject/FortniteMainBranchObjectVersion.h"
|
|
#include "UObject/FortniteReleaseBranchCustomObjectVersion.h"
|
|
#include "UObject/UE5ReleaseStreamObjectVersion.h"
|
|
#include "UObject/UE5SpecialProjectStreamObjectVersion.h"
|
|
|
|
#include "GPUSkinPublicDefs.h"
|
|
#include "GPUSkinVertexFactory.h"
|
|
|
|
#include UE_INLINE_GENERATED_CPP_BY_NAME(ClothingAsset)
|
|
|
|
DEFINE_LOG_CATEGORY(LogClothingAsset)
|
|
#define LOCTEXT_NAMESPACE "ClothingAsset"
|
|
|
|
//==============================================================================
|
|
// ClothingAssetUtils
|
|
//==============================================================================
|
|
|
|
#if WITH_EDITOR
|
|
void ClothingAssetUtils::GetAllMeshClothingAssetBindings(const USkeletalMesh* SkeletalMesh, TArray<FClothingAssetMeshBinding>& OutBindings)
|
|
{
|
|
OutBindings.Empty();
|
|
if (!SkeletalMesh)
|
|
{
|
|
return;
|
|
}
|
|
if (const FSkeletalMeshModel* MeshModel = SkeletalMesh->GetImportedModel())
|
|
{
|
|
int32 LODNum = MeshModel->LODModels.Num();
|
|
for (int32 LODIndex = 0; LODIndex < LODNum; ++LODIndex)
|
|
{
|
|
const FSkeletalMeshLODModel& MeshLODModel = MeshModel->LODModels[LODIndex];
|
|
if (MeshLODModel.HasClothData())
|
|
{
|
|
TArray<FClothingAssetMeshBinding> LodBindings;
|
|
GetAllLodMeshClothingAssetBindings(SkeletalMesh, LodBindings, LODIndex);
|
|
OutBindings.Append(LodBindings);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ClothingAssetUtils::GetAllLodMeshClothingAssetBindings(const USkeletalMesh* SkeletalMesh, TArray<FClothingAssetMeshBinding>& OutBindings, int32 InLodIndex)
|
|
{
|
|
OutBindings.Empty();
|
|
if (!SkeletalMesh || !SkeletalMesh->GetImportedModel())
|
|
{
|
|
return;
|
|
}
|
|
const FSkeletalMeshModel* MeshModel = SkeletalMesh->GetImportedModel();
|
|
if (!MeshModel || !MeshModel->LODModels.IsValidIndex(InLodIndex))
|
|
{
|
|
return;
|
|
}
|
|
const FSkeletalMeshLODModel& MeshLODModel = MeshModel->LODModels[InLodIndex];
|
|
|
|
if (MeshLODModel.HasClothData())
|
|
{
|
|
TArray<FClothingAssetMeshBinding> LodBindings;
|
|
int32 SectionNum = MeshLODModel.Sections.Num();
|
|
for (int32 SectionIndex = 0; SectionIndex < SectionNum; ++SectionIndex)
|
|
{
|
|
const FSkelMeshSection& Section = MeshLODModel.Sections[SectionIndex];
|
|
if (Section.HasClothingData())
|
|
{
|
|
UClothingAssetBase* ClothingAsset = SkeletalMesh->GetClothingAsset(Section.ClothingData.AssetGuid);
|
|
FClothingAssetMeshBinding ClothBinding;
|
|
ClothBinding.Asset = Cast<UClothingAssetCommon>(ClothingAsset);
|
|
ClothBinding.AssetInternalLodIndex = Section.ClothingData.AssetLodIndex;// InSkelMesh->GetClothingAssetIndex(Section.ClothingData.AssetGuid);
|
|
check(ClothBinding.AssetInternalLodIndex == Section.ClothingData.AssetLodIndex);
|
|
ClothBinding.LODIndex = InLodIndex;
|
|
ClothBinding.SectionIndex = SectionIndex;
|
|
OutBindings.Add(ClothBinding);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ClothingAssetUtils::ClearSectionClothingData(FSkelMeshSection& InSection)
|
|
{
|
|
InSection.ClothingData.AssetGuid = FGuid();
|
|
InSection.ClothingData.AssetLodIndex = INDEX_NONE;
|
|
InSection.CorrespondClothAssetIndex = INDEX_NONE;
|
|
|
|
InSection.ClothMappingDataLODs.Empty();
|
|
}
|
|
#endif
|
|
|
|
//==============================================================================
|
|
// UClothingAssetCommon
|
|
//==============================================================================
|
|
|
|
UClothingAssetCommon::UClothingAssetCommon(const FObjectInitializer& ObjectInitializer)
|
|
: Super(ObjectInitializer)
|
|
, PhysicsAsset(nullptr)
|
|
#if WITH_EDITORONLY_DATA
|
|
, ClothSimConfig_DEPRECATED(nullptr)
|
|
, ChaosClothSimConfig_DEPRECATED(nullptr)
|
|
#endif
|
|
, ReferenceBoneIndex(0)
|
|
{
|
|
}
|
|
|
|
#if WITH_EDITOR
|
|
|
|
void Warn(const FText& Error)
|
|
{
|
|
FNotificationInfo* const NotificationInfo = new FNotificationInfo(Error);
|
|
NotificationInfo->ExpireDuration = 5.0f;
|
|
FSlateNotificationManager::Get().QueueNotification(NotificationInfo);
|
|
|
|
UE_LOG(LogClothingAsset, Warning, TEXT("%s"), *Error.ToString());
|
|
}
|
|
|
|
bool UClothingAssetCommon::BindToSkeletalMesh(
|
|
USkeletalMesh* InSkelMesh,
|
|
const int32 InMeshLodIndex,
|
|
const int32 InSectionIndex,
|
|
const int32 InAssetLodIndex)
|
|
{
|
|
// Make sure the legacy LOD are upgraded (BindToSkeletalMesh could be called before the Cloth Asset's PostLoad is completed)
|
|
for (UClothLODDataCommon_Legacy* LodDeprecated : ClothLodData_DEPRECATED)
|
|
{
|
|
if (LodDeprecated)
|
|
{
|
|
LodDeprecated->ConditionalPostLoad();
|
|
|
|
const int32 Idx = AddNewLod();
|
|
LodDeprecated->MigrateTo(LodData[Idx]);
|
|
}
|
|
}
|
|
ClothLodData_DEPRECATED.Empty();
|
|
|
|
// If we've been added to the wrong mesh
|
|
if(InSkelMesh != GetOuter())
|
|
{
|
|
FText Error = FText::Format(
|
|
LOCTEXT("Error_WrongMesh", "Failed to bind clothing asset {0} as the provided mesh is not the owner of this asset."),
|
|
FText::FromString(GetName()));
|
|
Warn(Error);
|
|
return false;
|
|
}
|
|
|
|
// If we don't have clothing data
|
|
if(!LodData.IsValidIndex(InAssetLodIndex))
|
|
{
|
|
FText Error = FText::Format(
|
|
LOCTEXT("Error_NoClothingLod", "Failed to bind clothing asset {0} LOD{1} as LOD{2} does not exist."),
|
|
FText::FromString(GetName()),
|
|
InAssetLodIndex,
|
|
InAssetLodIndex);
|
|
Warn(Error);
|
|
return false;
|
|
}
|
|
|
|
// If we don't have a mesh
|
|
if(!InSkelMesh)
|
|
{
|
|
FText Error = FText::Format(
|
|
LOCTEXT("Error_NoMesh", "Failed to bind clothing asset {0} as provided skel mesh does not exist."),
|
|
FText::FromString(GetName()));
|
|
Warn(Error);
|
|
return false;
|
|
}
|
|
|
|
// If the mesh LOD index is invalid
|
|
if(!InSkelMesh->GetImportedModel()->LODModels.IsValidIndex(InMeshLodIndex))
|
|
{
|
|
FText Error = FText::Format(
|
|
LOCTEXT("Error_InvalidMeshLOD", "Failed to bind clothing asset {0} as mesh LOD{1} does not exist."),
|
|
FText::FromString(GetName()),
|
|
InMeshLodIndex);
|
|
Warn(Error);
|
|
return false;
|
|
}
|
|
|
|
const int32 NumMapEntries = LodMap.Num();
|
|
for(int MapIndex = 0; MapIndex < NumMapEntries; ++MapIndex)
|
|
{
|
|
const int32& MappedLod = LodMap[MapIndex];
|
|
if(MappedLod == InAssetLodIndex)
|
|
{
|
|
FText Error = FText::Format(
|
|
LOCTEXT("Error_LodMapped", "Failed to bind clothing asset {0} LOD{1} as LOD{2} is already mapped to mesh LOD{3}."),
|
|
FText::FromString(GetName()),
|
|
InAssetLodIndex,
|
|
InAssetLodIndex,
|
|
MapIndex);
|
|
Warn(Error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if(LodMap.IsValidIndex(InMeshLodIndex) && LodMap[InMeshLodIndex] != INDEX_NONE)
|
|
{
|
|
// Already mapped
|
|
return false;
|
|
}
|
|
|
|
CalculateReferenceBoneIndex();
|
|
|
|
// Grab the clothing and skel lod data
|
|
FClothLODDataCommon& ClothLodData = LodData[InAssetLodIndex];
|
|
FSkeletalMeshLODModel& SkelLod = InSkelMesh->GetImportedModel()->LODModels[InMeshLodIndex];
|
|
|
|
FSkelMeshSection& OriginalSection = SkelLod.Sections[InSectionIndex];
|
|
|
|
// Data for mesh to mesh binding
|
|
TArray<FMeshToMeshVertData> MeshToMeshData;
|
|
TArray<FVector3f> RenderPositions;
|
|
TArray<FVector3f> RenderNormals;
|
|
TArray<FVector3f> RenderTangents;
|
|
|
|
RenderPositions.Reserve(OriginalSection.SoftVertices.Num());
|
|
RenderNormals.Reserve(OriginalSection.SoftVertices.Num());
|
|
RenderTangents.Reserve(OriginalSection.SoftVertices.Num());
|
|
|
|
// Original data to weight to the clothing simulation mesh
|
|
for(FSoftSkinVertex& UnrealVert : OriginalSection.SoftVertices)
|
|
{
|
|
RenderPositions.Add(UnrealVert.Position);
|
|
RenderNormals.Add(UnrealVert.TangentZ);
|
|
RenderTangents.Add(UnrealVert.TangentX);
|
|
}
|
|
|
|
TArrayView<uint32> IndexView(SkelLod.IndexBuffer);
|
|
IndexView = IndexView.Slice(OriginalSection.BaseIndex, OriginalSection.NumTriangles * 3);
|
|
|
|
TArray<uint32> RenderIndices;
|
|
RenderIndices.Reserve(OriginalSection.NumTriangles * 3);
|
|
for (uint32 OriginalIndex : IndexView)
|
|
{
|
|
const int32 TempIndex = (int32)OriginalIndex - (int32)OriginalSection.BaseVertexIndex;
|
|
if (ensure(RenderPositions.IsValidIndex(TempIndex)))
|
|
{
|
|
RenderIndices.Add(TempIndex);
|
|
}
|
|
}
|
|
|
|
const ClothingMeshUtils::ClothMeshDesc TargetMesh(RenderPositions, RenderNormals, RenderTangents, RenderIndices);
|
|
const ClothingMeshUtils::ClothMeshDesc SourceMesh(ClothLodData.PhysicalMeshData.Vertices, ClothLodData.PhysicalMeshData.Indices); // Calculate averaged normals
|
|
|
|
const FPointWeightMap* const MaxDistances = ClothLodData.PhysicalMeshData.FindWeightMap(EWeightMapTargetCommon::MaxDistance);
|
|
|
|
ClothingMeshUtils::GenerateMeshToMeshVertData(MeshToMeshData, TargetMesh, SourceMesh, MaxDistances,
|
|
ClothLodData.bSmoothTransition, ClothLodData.bUseMultipleInfluences, ClothLodData.SkinningKernelRadius);
|
|
|
|
// We have to copy the bone map to verify we don't exceed the maximum while adding the clothing bones
|
|
TArray<FBoneIndexType> TempBoneMap = OriginalSection.BoneMap;
|
|
for(FName& BoneName : UsedBoneNames)
|
|
{
|
|
const int32 BoneIndex = InSkelMesh->GetRefSkeleton().FindBoneIndex(BoneName);
|
|
if(BoneIndex != INDEX_NONE)
|
|
{
|
|
TempBoneMap.AddUnique(BoneIndex);
|
|
}
|
|
}
|
|
|
|
// Verify number of bones against current capabilities
|
|
if(TempBoneMap.Num() > FGPUBaseSkinVertexFactory::GetMaxGPUSkinBones())
|
|
{
|
|
// Failed to apply as we've exceeded the number of bones we can skin
|
|
FText Error = FText::Format(
|
|
LOCTEXT("Error_TooManyBones", "Failed to bind clothing asset {0} LOD{1} as this causes the section to require {2} bones. The maximum per section is currently {3}."),
|
|
FText::FromString(GetName()),
|
|
InAssetLodIndex,
|
|
TempBoneMap.Num(),
|
|
FGPUBaseSkinVertexFactory::GetMaxGPUSkinBones());
|
|
Warn(Error);
|
|
return false;
|
|
}
|
|
|
|
// After verifying copy the new bone map to the section
|
|
OriginalSection.BoneMap = TempBoneMap;
|
|
|
|
//Register the scope post edit change
|
|
FScopedSkeletalMeshPostEditChange SkeletalMeshPostEditChange(InSkelMesh);
|
|
|
|
// calculate LOD verts before adding our new section
|
|
uint32 NumLodVertices = 0;
|
|
for(const FSkelMeshSection& CurSection : SkelLod.Sections)
|
|
{
|
|
NumLodVertices += CurSection.GetNumVertices();
|
|
}
|
|
|
|
// Set the asset index, used during rendering to pick the correct sim mesh buffer
|
|
int32 AssetIndex = INDEX_NONE;
|
|
const bool bMeshClothingAssetsWasFound = InSkelMesh->GetMeshClothingAssets().Find(this, AssetIndex);
|
|
check(bMeshClothingAssetsWasFound);
|
|
OriginalSection.CorrespondClothAssetIndex = AssetIndex;
|
|
|
|
// sim properties
|
|
OriginalSection.ClothMappingDataLODs.SetNum(1);
|
|
OriginalSection.ClothMappingDataLODs[0] = MeshToMeshData;
|
|
OriginalSection.ClothingData.AssetGuid = AssetGuid;
|
|
OriginalSection.ClothingData.AssetLodIndex = InAssetLodIndex;
|
|
|
|
bool bRequireBoneChange = false;
|
|
for(FBoneIndexType& BoneIndex : OriginalSection.BoneMap)
|
|
{
|
|
if(!SkelLod.RequiredBones.Contains(BoneIndex))
|
|
{
|
|
bRequireBoneChange = true;
|
|
if(InSkelMesh->GetRefSkeleton().IsValidIndex(BoneIndex))
|
|
{
|
|
SkelLod.RequiredBones.Add(BoneIndex);
|
|
SkelLod.ActiveBoneIndices.AddUnique(BoneIndex);
|
|
}
|
|
}
|
|
}
|
|
if(bRequireBoneChange)
|
|
{
|
|
SkelLod.RequiredBones.Sort();
|
|
InSkelMesh->GetRefSkeleton().EnsureParentsExistAndSort(SkelLod.ActiveBoneIndices);
|
|
}
|
|
|
|
// Make sure the LOD map is always big enough for the asset to use.
|
|
// This shouldn't grow to an unwieldy size but maybe consider compacting later.
|
|
while(LodMap.Num() - 1 < InMeshLodIndex)
|
|
{
|
|
LodMap.Add(INDEX_NONE);
|
|
}
|
|
|
|
LodMap[InMeshLodIndex] = InAssetLodIndex;
|
|
|
|
// Update the extra cloth deformer mapping LOD bias using this cloth entry
|
|
if (InSkelMesh->GetSupportRayTracing())
|
|
{
|
|
check(LodMap[InMeshLodIndex] == InAssetLodIndex); // UpdateLODBiasMappings relies on the LodMap being up to date
|
|
UpdateLODBiasMappings(InSkelMesh, InMeshLodIndex, InSectionIndex);
|
|
}
|
|
|
|
return true;
|
|
|
|
// FScopedSkeletalMeshPostEditChange goes out of scope, causing postedit change and components to be re-registered
|
|
}
|
|
|
|
void UClothingAssetCommon::UpdateAllLODBiasMappings(USkeletalMesh* SkeletalMesh)
|
|
{
|
|
check(SkeletalMesh);
|
|
|
|
FSkeletalMeshModel* const SkeletalMeshModel = SkeletalMesh->GetImportedModel();
|
|
if (!SkeletalMeshModel)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Iterate through all source LODs with cloth that could lead to upper (raytraced) sections needing some additional mapping
|
|
TIndirectArray<FSkeletalMeshLODModel>& LODModels = SkeletalMesh->GetImportedModel()->LODModels;
|
|
|
|
for (int32 LODIndex = LODModels.Num() - 1; LODIndex >= 0; --LODIndex) // Iterate in reverse order to allow shrinking the mapping array first
|
|
{
|
|
// Go through all sections to find the one(s?) that uses this cloth asset and clear the existing bias mappings
|
|
TArray<FSkelMeshSection>& Sections = LODModels[LODIndex].Sections;
|
|
|
|
for (int32 SectionIndex = 0; SectionIndex < Sections.Num(); ++SectionIndex)
|
|
{
|
|
if (Sections[SectionIndex].ClothingData.AssetGuid == GetAssetGuid())
|
|
{
|
|
Sections[SectionIndex].ClothMappingDataLODs.SetNum(1); // Only keep ClothMappingDataLODs[0] that is the same LOD mapping to remove LOD bias mappings
|
|
|
|
if (SkeletalMesh->GetSupportRayTracing())
|
|
{
|
|
UpdateLODBiasMappings(SkeletalMesh, LODIndex, SectionIndex); // Updates the upper LODs mappings of the specified section from this LODIndex
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void UClothingAssetCommon::UpdateLODBiasMappings(const USkeletalMesh* SkeletalMesh, int32 UpdatedLODIndex, int32 SectionIndex)
|
|
{
|
|
check(SkeletalMesh);
|
|
check(UpdatedLODIndex >= 0);
|
|
|
|
FSkeletalMeshModel* const SkeletalMeshModel = SkeletalMesh->GetImportedModel();
|
|
check(SkeletalMeshModel);
|
|
|
|
const bool bAddMappingsToAnyLOD = (SkeletalMesh->GetClothLODBiasMode() == EClothLODBiasMode::MappingsToAnyLOD);
|
|
const bool bAddMappingsToMinLOD = (SkeletalMesh->GetClothLODBiasMode() == EClothLODBiasMode::MappingsToMinLOD && SkeletalMesh->GetRayTracingMinLOD() > 0);
|
|
|
|
TIndirectArray<FSkeletalMeshLODModel>& LODModels = SkeletalMeshModel->LODModels;
|
|
|
|
TArray<FVector3f> RenderPositions;
|
|
TArray<FVector3f> RenderNormals;
|
|
TArray<FVector3f> RenderTangents;
|
|
TArray<uint32> RenderIndices;
|
|
|
|
auto PrepareDeformerTargetDescriptor = [&LODModels, &RenderPositions, &RenderNormals, &RenderTangents, &RenderIndices](int32 LODIndex, int32 SectionIndex)
|
|
{
|
|
FSkeletalMeshLODModel& LODModel = LODModels[LODIndex];
|
|
FSkelMeshSection& Section = LODModel.Sections[SectionIndex];
|
|
|
|
RenderPositions.Reset(Section.SoftVertices.Num());
|
|
RenderNormals.Reset(Section.SoftVertices.Num());
|
|
RenderTangents.Reset(Section.SoftVertices.Num());
|
|
|
|
for (const FSoftSkinVertex& SoftSkinVertex : Section.SoftVertices)
|
|
{
|
|
RenderPositions.Add(SoftSkinVertex.Position);
|
|
RenderNormals.Add(SoftSkinVertex.TangentZ);
|
|
RenderTangents.Add(SoftSkinVertex.TangentX);
|
|
}
|
|
|
|
const TConstArrayView<uint32> Indices = TConstArrayView<uint32>(LODModel.IndexBuffer).Slice(Section.BaseIndex, Section.NumTriangles * 3);
|
|
|
|
RenderIndices.Reset(Section.NumTriangles * 3);
|
|
for (uint32 VertexIndex : Indices)
|
|
{
|
|
RenderIndices.Add(VertexIndex - Section.BaseVertexIndex);
|
|
}
|
|
|
|
return ClothingMeshUtils::ClothMeshDesc(RenderPositions, RenderNormals, RenderTangents, RenderIndices);
|
|
};
|
|
|
|
// Get the min and max LODs that might be affected by LOD bias and update other sections' bias with this LOD section
|
|
int32 MinLOD = 0;
|
|
int32 MaxLOD = 0;
|
|
|
|
if (bAddMappingsToAnyLOD)
|
|
{
|
|
MinLOD = UpdatedLODIndex + 1;
|
|
MaxLOD = LODModels.Num() - 1;
|
|
}
|
|
else if (bAddMappingsToMinLOD)
|
|
{
|
|
MinLOD = MaxLOD = SkeletalMesh->GetRayTracingMinLOD();
|
|
}
|
|
|
|
if (UpdatedLODIndex < MinLOD && MinLOD <= MaxLOD)
|
|
{
|
|
// Prepare the deformer source (simulation) mesh descriptor
|
|
const FClothLODDataCommon& ClothLodData = LodData[LodMap[UpdatedLODIndex]];
|
|
const ClothingMeshUtils::ClothMeshDesc SourceMesh(ClothLodData.PhysicalMeshData.Vertices, ClothLodData.PhysicalMeshData.Indices); // Calculate averaged normals
|
|
|
|
// Retrieve max distance mask
|
|
const FPointWeightMap* const MaxDistances = ClothLodData.PhysicalMeshData.FindWeightMap(EWeightMapTargetCommon::MaxDistance);
|
|
|
|
// Iterate through all deformer target (render) mesh sections
|
|
for (int32 LODIndex = MinLOD; LODIndex <= MaxLOD; ++LODIndex)
|
|
{
|
|
FSkeletalMeshLODModel& LODModel = LODModels[LODIndex]; // Note that the section LOD being updated here is the biased (raytraced) section LOD, not the UpdatedLODIndex which is done in the second loop below
|
|
|
|
if (!LODModel.Sections.IsValidIndex(SectionIndex))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
FSkelMeshSection& Section = LODModel.Sections[SectionIndex];
|
|
|
|
if (!Section.HasClothingData())
|
|
{
|
|
continue; // This LOD section won't render clothing and therefore can be skipped as it won't ever need mappings
|
|
}
|
|
|
|
// Update bias mapping to the newly updated LOD index
|
|
const int32 LODBias = LODIndex - UpdatedLODIndex;
|
|
check(LODBias > 0);
|
|
|
|
Section.ClothMappingDataLODs.SetNum(FMath::Max(LODBias + 1, Section.ClothMappingDataLODs.Num()));
|
|
|
|
TArray<FMeshToMeshVertData>& MeshToMeshData = Section.ClothMappingDataLODs[LODBias];
|
|
MeshToMeshData.Reset();
|
|
|
|
// Prepare the deformer target (render) mesh descriptor
|
|
const ClothingMeshUtils::ClothMeshDesc TargetMesh = PrepareDeformerTargetDescriptor(LODIndex, SectionIndex);
|
|
|
|
// Generate the missing bias mapping data
|
|
ClothingMeshUtils::GenerateMeshToMeshVertData(MeshToMeshData, TargetMesh, SourceMesh, MaxDistances,
|
|
ClothLodData.bSmoothTransition, ClothLodData.bUseMultipleInfluences, ClothLodData.SkinningKernelRadius);
|
|
|
|
UE_LOG(LogClothingAsset, Verbose, TEXT("Added deformer data for [%s/%s], section %d, LODBias = %d, render mesh LOD = %d, sim mesh LOD = %d"),
|
|
*SkeletalMesh->GetName(),
|
|
*GetName(),
|
|
SectionIndex,
|
|
LODBias,
|
|
LODIndex,
|
|
UpdatedLODIndex);
|
|
}
|
|
}
|
|
|
|
// Update this LOD section bias mappings that didn't get setup since there weren't no cloth attached to it yet
|
|
if (bAddMappingsToAnyLOD || (bAddMappingsToMinLOD && UpdatedLODIndex == SkeletalMesh->GetRayTracingMinLOD()))
|
|
{
|
|
FSkeletalMeshLODModel& LODModel = LODModels[UpdatedLODIndex]; // Note that the section LOD being updated here is the UpdatedLODIndex, not the biased (raytraced) section LOD which is done in the first loop above
|
|
FSkelMeshSection& Section = LODModel.Sections[SectionIndex];
|
|
check(Section.HasClothingData());
|
|
|
|
// Prepare the deformer target (render) mesh descriptor
|
|
const ClothingMeshUtils::ClothMeshDesc TargetMesh = PrepareDeformerTargetDescriptor(UpdatedLODIndex, SectionIndex);
|
|
|
|
// Iterate through all the LOD that could be rendered and need this source LOD to be raytraced
|
|
for (int32 LODIndex = 0; LODIndex < UpdatedLODIndex; ++LODIndex)
|
|
{
|
|
FSkeletalMeshLODModel& SourceLODModel = LODModels[LODIndex];
|
|
|
|
if (!SourceLODModel.Sections.IsValidIndex(SectionIndex))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
FSkelMeshSection& SourceSection = SourceLODModel.Sections[SectionIndex];
|
|
|
|
// Locate the deformer's source (simulation) cloth asset, since it could be a different cloth asset.
|
|
const UClothingAssetCommon* const ClothingAsset = Cast<UClothingAssetCommon>(SkeletalMesh->GetClothingAsset(SourceSection.ClothingData.AssetGuid));
|
|
|
|
if (!ClothingAsset)
|
|
{
|
|
const FText Error = FText::Format(
|
|
LOCTEXT("Error_DifferentAsset", "Incomplete raytracing cloth LOD bias mappings generation on [{0}/{1}], section {2}, LOD {3} due to missing cloth asset at LOD {4}."),
|
|
FText::FromString(SkeletalMesh->GetName()),
|
|
FText::FromString(GetName()),
|
|
SectionIndex,
|
|
UpdatedLODIndex,
|
|
LODIndex);
|
|
Warn(Error);
|
|
continue;
|
|
}
|
|
|
|
// Prepare the deformer source (simulation) mesh descriptor
|
|
const FClothLODDataCommon& ClothLodData = ClothingAsset->LodData[LodMap[LODIndex]];
|
|
const ClothingMeshUtils::ClothMeshDesc SourceMesh(ClothLodData.PhysicalMeshData.Vertices, ClothLodData.PhysicalMeshData.Indices); // Calculate averaged normals
|
|
|
|
// Retrieve max distance mask
|
|
const FPointWeightMap* const MaxDistances = ClothLodData.PhysicalMeshData.FindWeightMap(EWeightMapTargetCommon::MaxDistance);
|
|
|
|
// Update bias mapping for the updated LOD section
|
|
const int32 LODBias = UpdatedLODIndex - LODIndex;
|
|
check(LODBias > 0);
|
|
|
|
Section.ClothMappingDataLODs.SetNum(FMath::Max(LODBias + 1, Section.ClothMappingDataLODs.Num()));
|
|
TArray<FMeshToMeshVertData>& MeshToMeshData = Section.ClothMappingDataLODs[LODBias];
|
|
MeshToMeshData.Reset();
|
|
|
|
ClothingMeshUtils::GenerateMeshToMeshVertData(MeshToMeshData, TargetMesh, SourceMesh, MaxDistances,
|
|
ClothLodData.bSmoothTransition, ClothLodData.bUseMultipleInfluences, ClothLodData.SkinningKernelRadius);
|
|
|
|
UE_LOG(LogClothingAsset, Verbose, TEXT("Added deformer data for [%s/%s], section %d, LODBias = %d, render mesh LOD = %d, sim mesh LOD = %d"),
|
|
*SkeletalMesh->GetName(),
|
|
*GetName(),
|
|
SectionIndex,
|
|
LODBias,
|
|
UpdatedLODIndex,
|
|
LODIndex);
|
|
}
|
|
}
|
|
}
|
|
|
|
void UClothingAssetCommon::ClearLODBiasMappings(const USkeletalMesh* SkeletalMesh, int32 UpdatedLODIndex, int32 SectionIndex)
|
|
{
|
|
check(SkeletalMesh);
|
|
check(UpdatedLODIndex >= 0);
|
|
|
|
FSkeletalMeshModel* const SkeletalMeshModel = SkeletalMesh->GetImportedModel();
|
|
check(SkeletalMeshModel);
|
|
|
|
for (int32 LODIndex = UpdatedLODIndex + 1; LODIndex < SkeletalMeshModel->LODModels.Num(); ++LODIndex)
|
|
{
|
|
FSkeletalMeshLODModel& BiasLodModel = SkeletalMeshModel->LODModels[LODIndex];
|
|
if (BiasLodModel.Sections.IsValidIndex(SectionIndex))
|
|
{
|
|
FSkelMeshSection& BiasSection = BiasLodModel.Sections[SectionIndex];
|
|
const int32 LODBias = LODIndex - UpdatedLODIndex;
|
|
|
|
if (BiasSection.ClothMappingDataLODs.IsValidIndex(LODBias))
|
|
{
|
|
BiasSection.ClothMappingDataLODs[LODBias].Empty();
|
|
|
|
UE_LOG(LogClothingAsset, Verbose, TEXT("Removed deformer data for [%s/%s], section %d LODBias = %d, render mesh LOD = %d, sim mesh LOD = %d"),
|
|
*SkeletalMesh->GetName(),
|
|
*GetName(),
|
|
SectionIndex,
|
|
LODBias,
|
|
LODIndex,
|
|
UpdatedLODIndex);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void UClothingAssetCommon::UnbindFromSkeletalMesh(USkeletalMesh* InSkelMesh)
|
|
{
|
|
if(FSkeletalMeshModel* Mesh = InSkelMesh->GetImportedModel())
|
|
{
|
|
const int32 NumLods = Mesh->LODModels.Num();
|
|
for(int32 LodIndex = 0; LodIndex < NumLods; ++LodIndex)
|
|
{
|
|
UnbindFromSkeletalMesh(InSkelMesh, LodIndex);
|
|
}
|
|
}
|
|
}
|
|
|
|
void UClothingAssetCommon::UnbindFromSkeletalMesh(
|
|
USkeletalMesh* InSkelMesh,
|
|
const int32 InMeshLodIndex)
|
|
{
|
|
bool bChangedMesh = false;
|
|
|
|
// Find the chunk(s) we created
|
|
if(FSkeletalMeshModel* Mesh = InSkelMesh->GetImportedModel())
|
|
{
|
|
if(!Mesh->LODModels.IsValidIndex(InMeshLodIndex))
|
|
{
|
|
FText Error = FText::Format(
|
|
LOCTEXT("Error_UnbindNoMeshLod", "Failed to remove clothing asset {0} from mesh LOD{1} as that LOD doesn't exist."),
|
|
FText::FromString(GetName()),
|
|
InMeshLodIndex);
|
|
Warn(Error);
|
|
|
|
return;
|
|
}
|
|
|
|
FSkeletalMeshLODModel& LodModel = Mesh->LODModels[InMeshLodIndex];
|
|
|
|
for(int32 SectionIdx = LodModel.Sections.Num() - 1; SectionIdx >= 0; --SectionIdx)
|
|
{
|
|
FSkelMeshSection& Section = LodModel.Sections[SectionIdx];
|
|
if(Section.HasClothingData() && Section.ClothingData.AssetGuid == AssetGuid)
|
|
{
|
|
// Don't do this when in async task, this should have been done before initiating the build
|
|
if (IsInGameThread())
|
|
{
|
|
InSkelMesh->PreEditChange(nullptr);
|
|
}
|
|
|
|
// Log the LOD bias mapping data that will be removed through the call to ClearSectionClothingData
|
|
for (int32 LodIndex = 0; LodIndex < InMeshLodIndex; ++LodIndex)
|
|
{
|
|
const int32 LODBias = InMeshLodIndex - LodIndex;
|
|
|
|
UE_CLOG(Section.ClothMappingDataLODs.IsValidIndex(LODBias) && Section.ClothMappingDataLODs[LODBias].Num(),
|
|
LogClothingAsset, Verbose, TEXT("Removed deformer data for [%s/%s], section %d LODBias = %d, render mesh LOD = %d, sim mesh LOD = %d"),
|
|
*InSkelMesh->GetName(),
|
|
*GetName(),
|
|
SectionIdx,
|
|
LODBias,
|
|
InMeshLodIndex,
|
|
LodIndex);
|
|
}
|
|
|
|
ClothingAssetUtils::ClearSectionClothingData(Section);
|
|
if (FSkelMeshSourceSectionUserData* UserSectionData = LodModel.UserSectionsData.Find(Section.OriginalDataSectionIndex))
|
|
{
|
|
UserSectionData->CorrespondClothAssetIndex = INDEX_NONE;
|
|
UserSectionData->ClothingData.AssetLodIndex = INDEX_NONE;
|
|
UserSectionData->ClothingData.AssetGuid = FGuid();
|
|
}
|
|
|
|
// Clear all remaining LOD bias mapping data from other LODs that relied on this section
|
|
ClearLODBiasMappings(InSkelMesh, InMeshLodIndex, SectionIdx);
|
|
|
|
bChangedMesh = true;
|
|
}
|
|
}
|
|
|
|
// Clear the LOD map entry for this asset LOD, after a unbind we must be able to bind any asset
|
|
if (LodMap.IsValidIndex(InMeshLodIndex))
|
|
{
|
|
LodMap[InMeshLodIndex] = INDEX_NONE;
|
|
bChangedMesh = true;
|
|
}
|
|
}
|
|
|
|
// If the mesh changed we need to re-register any components that use it to reflect the changes
|
|
if(bChangedMesh)
|
|
{
|
|
//Register the scope post edit change
|
|
FScopedSkeletalMeshPostEditChange SkeletalMeshPostEditChange(InSkelMesh);
|
|
}
|
|
}
|
|
|
|
void UClothingAssetCommon::ReregisterComponentsUsingClothing()
|
|
{
|
|
if(USkeletalMesh* OwnerMesh = Cast<USkeletalMesh>(GetOuter()))
|
|
{
|
|
for(TObjectIterator<USkeletalMeshComponent> It; It; ++It)
|
|
{
|
|
if(USkeletalMeshComponent* Component = *It)
|
|
{
|
|
if(Component->GetSkeletalMeshAsset() == OwnerMesh)
|
|
{
|
|
FComponentReregisterContext Context(Component);
|
|
// Context goes out of scope, causing Component to be re-registered
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void UClothingAssetCommon::ForEachInteractorUsingClothing(TFunction<void(UClothingSimulationInteractor*)> Func)
|
|
{
|
|
if(USkeletalMesh* OwnerMesh = Cast<USkeletalMesh>(GetOuter()))
|
|
{
|
|
for(TObjectIterator<USkeletalMeshComponent> It; It; ++It)
|
|
{
|
|
if(USkeletalMeshComponent* Component = *It)
|
|
{
|
|
if(Component->GetSkeletalMeshAsset() == OwnerMesh)
|
|
{
|
|
if(UClothingSimulationInteractor* CurInteractor = Component->GetClothingSimulationInteractor())
|
|
{
|
|
Func(CurInteractor);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void UClothingAssetCommon::ApplyParameterMasks(bool bUpdateFixedVertData, bool bInvalidateDerivedDataCache)
|
|
{
|
|
for(FClothLODDataCommon& Lod : LodData)
|
|
{
|
|
Lod.PushWeightsToMesh();
|
|
}
|
|
// Invalidate all cached data that depend on masks
|
|
InvalidateFlaggedCachedData(EClothingCachedDataFlagsCommon::InverseMasses | EClothingCachedDataFlagsCommon::Tethers);
|
|
|
|
// Recompute weights if needed
|
|
USkeletalMesh* const SkeletalMesh = Cast<USkeletalMesh>(GetOuter());
|
|
|
|
if (bUpdateFixedVertData && SkeletalMesh)
|
|
{
|
|
FSkeletalMeshModel* Resource = SkeletalMesh->GetImportedModel();
|
|
FScopedSkeletalMeshPostEditChange ScopedSkeletalMeshPostEditChange(SkeletalMesh);
|
|
|
|
SkeletalMesh->PreEditChange(nullptr);
|
|
|
|
for (FSkeletalMeshLODModel& LodModel : Resource->LODModels)
|
|
{
|
|
for (FSkelMeshSection& Section : LodModel.Sections)
|
|
{
|
|
if (!Section.HasClothingData() || Cast<UClothingAssetCommon>(SkeletalMesh->GetClothingAsset(Section.ClothingData.AssetGuid)) != this)
|
|
{
|
|
continue;
|
|
}
|
|
const FClothLODDataCommon& LodDatum = LodData[Section.ClothingData.AssetLodIndex];
|
|
const FPointWeightMap* const MaxDistances = LodDatum.PhysicalMeshData.FindWeightMap(EWeightMapTargetCommon::MaxDistance);
|
|
|
|
for (TArray<FMeshToMeshVertData>& ClothMappingData : Section.ClothMappingDataLODs)
|
|
{
|
|
ClothingMeshUtils::ComputeVertexContributions(ClothMappingData, MaxDistances, LodDatum.bSmoothTransition, LodDatum.bUseMultipleInfluences);
|
|
}
|
|
}
|
|
}
|
|
if (bInvalidateDerivedDataCache) // We must always dirty the DDC key unless previewing
|
|
{
|
|
SkeletalMesh->InvalidateDeriveDataCacheGUID();
|
|
}
|
|
}
|
|
}
|
|
|
|
void UClothingAssetCommon::BuildLodTransitionData()
|
|
{
|
|
const int32 NumLods = GetNumLods();
|
|
for(int32 LodIndex = 0; LodIndex < NumLods; ++LodIndex)
|
|
{
|
|
const bool bHasPrevLod = LodIndex > 0;
|
|
const bool bHasNextLod = LodIndex < NumLods - 1;
|
|
|
|
FClothLODDataCommon& CurrentLod = LodData[LodIndex];
|
|
const FClothPhysicalMeshData& CurrentPhysMesh = CurrentLod.PhysicalMeshData;
|
|
|
|
FClothLODDataCommon* const PrevLod = bHasPrevLod ? &LodData[LodIndex - 1] : nullptr;
|
|
FClothLODDataCommon* const NextLod = bHasNextLod ? &LodData[LodIndex + 1] : nullptr;
|
|
|
|
const int32 CurrentLodNumVerts = CurrentPhysMesh.Vertices.Num();
|
|
|
|
ClothingMeshUtils::ClothMeshDesc CurrentMeshDesc(CurrentPhysMesh.Vertices, CurrentPhysMesh.Normals, CurrentPhysMesh.Indices);
|
|
|
|
const FPointWeightMap* MaxDistances = nullptr; // No need to update the vertex contribution on the transition maps
|
|
constexpr bool bUseSmoothTransitions = false; // Smooth transitions are only used at rendering for now and not during LOD transitions
|
|
constexpr bool bUseMultipleInfluences = false; // Multiple influences must not be used for LOD transitions
|
|
constexpr float SkinningKernelRadius = 0.f; // KernelRadius is only required when using multiple influences
|
|
|
|
if(PrevLod)
|
|
{
|
|
FClothPhysicalMeshData& PrevPhysMesh = PrevLod->PhysicalMeshData;
|
|
CurrentLod.TransitionUpSkinData.Empty(CurrentLodNumVerts);
|
|
ClothingMeshUtils::ClothMeshDesc PrevMeshDesc(PrevPhysMesh.Vertices, PrevPhysMesh.Indices); // Will calculate averaged normals
|
|
|
|
ClothingMeshUtils::GenerateMeshToMeshVertData(CurrentLod.TransitionUpSkinData, CurrentMeshDesc, PrevMeshDesc,
|
|
MaxDistances, bUseSmoothTransitions, bUseMultipleInfluences, SkinningKernelRadius);
|
|
}
|
|
if(NextLod)
|
|
{
|
|
FClothPhysicalMeshData& NextPhysMesh = NextLod->PhysicalMeshData;
|
|
CurrentLod.TransitionDownSkinData.Empty(CurrentLodNumVerts);
|
|
ClothingMeshUtils::ClothMeshDesc NextMeshDesc(NextPhysMesh.Vertices, NextPhysMesh.Indices); // Will calculate averaged normals
|
|
ClothingMeshUtils::GenerateMeshToMeshVertData(CurrentLod.TransitionDownSkinData, CurrentMeshDesc, NextMeshDesc,
|
|
MaxDistances, bUseSmoothTransitions, bUseMultipleInfluences, SkinningKernelRadius);
|
|
}
|
|
}
|
|
}
|
|
|
|
void UClothingAssetCommon::PreEditUndo()
|
|
{
|
|
Super::PreEditUndo();
|
|
|
|
// Stop the simulation
|
|
if (const USkeletalMesh* const OwnerMesh = Cast<USkeletalMesh>(GetOuter()))
|
|
{
|
|
for (TObjectIterator<USkeletalMeshComponent> It; It; ++It)
|
|
{
|
|
if (USkeletalMeshComponent* const Component = *It)
|
|
{
|
|
if (Component->GetSkeletalMeshAsset() == OwnerMesh)
|
|
{
|
|
Component->ReleaseAllClothingResources();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void UClothingAssetCommon::PostEditUndo()
|
|
{
|
|
Super::PostEditUndo();
|
|
|
|
// Resume the simulation
|
|
if (const USkeletalMesh* const OwnerMesh = Cast<USkeletalMesh>(GetOuter()))
|
|
{
|
|
for (TObjectIterator<USkeletalMeshComponent> It; It; ++It)
|
|
{
|
|
if (USkeletalMeshComponent* const Component = *It)
|
|
{
|
|
if (Component->GetSkeletalMeshAsset() == OwnerMesh)
|
|
{
|
|
Component->RecreateClothingActors();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endif // WITH_EDITOR
|
|
|
|
void UClothingAssetCommon::RefreshBoneMapping(USkeletalMesh* InSkelMesh)
|
|
{
|
|
// No mesh, can't remap
|
|
if(!InSkelMesh)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if(UsedBoneNames.Num() != UsedBoneIndices.Num())
|
|
{
|
|
UsedBoneIndices.Reset();
|
|
UsedBoneIndices.AddDefaulted(UsedBoneNames.Num());
|
|
}
|
|
|
|
// Repopulate the used indices.
|
|
for(int32 BoneNameIndex = 0; BoneNameIndex < UsedBoneNames.Num(); ++BoneNameIndex)
|
|
{
|
|
const int32 BoneIndex = InSkelMesh->GetRefSkeleton().FindBoneIndex(UsedBoneNames[BoneNameIndex]);
|
|
UsedBoneIndices[BoneNameIndex] = (BoneIndex != INDEX_NONE) ? BoneIndex : 0; // Remap to the root bone if the two skeletons differ
|
|
}
|
|
}
|
|
|
|
void UClothingAssetCommon::CalculateReferenceBoneIndex()
|
|
{
|
|
// Starts at root
|
|
ReferenceBoneIndex = 0;
|
|
|
|
// Find the root bone for this clothing asset (common bone for all used bones)
|
|
typedef TArray<int32> BoneIndexArray;
|
|
|
|
// List of valid paths to the root bone from each weighted bone
|
|
TArray<BoneIndexArray> PathsToRoot;
|
|
|
|
const USkeletalMesh* OwnerMesh = Cast<USkeletalMesh>(GetOuter());
|
|
|
|
if(OwnerMesh)
|
|
{
|
|
// First build a list per used bone for it's path to root
|
|
TArray<int32> WeightedBones; // List of actually weighted (not just used) bones
|
|
|
|
for(FClothLODDataCommon& CurLod : LodData)
|
|
{
|
|
const FClothPhysicalMeshData& MeshData = CurLod.PhysicalMeshData;
|
|
for(const FClothVertBoneData& VertBoneData : MeshData.BoneData)
|
|
{
|
|
for(int32 InfluenceIndex = 0; InfluenceIndex < MAX_TOTAL_INFLUENCES; ++InfluenceIndex)
|
|
{
|
|
if(VertBoneData.BoneWeights[InfluenceIndex] > SMALL_NUMBER)
|
|
{
|
|
const int32 UnmappedBoneIndex = VertBoneData.BoneIndices[InfluenceIndex];
|
|
check(UsedBoneIndices.IsValidIndex(UnmappedBoneIndex));
|
|
WeightedBones.AddUnique(UsedBoneIndices[UnmappedBoneIndex]);
|
|
}
|
|
else
|
|
{
|
|
// Hit the last weight (they're sorted)
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const int32 NumWeightedBones = WeightedBones.Num();
|
|
PathsToRoot.Reserve(NumWeightedBones);
|
|
|
|
// Compute paths to the root bone
|
|
const FReferenceSkeleton& RefSkel = OwnerMesh->GetRefSkeleton();
|
|
for(int32 WeightedBoneIndex = 0; WeightedBoneIndex < NumWeightedBones; ++WeightedBoneIndex)
|
|
{
|
|
PathsToRoot.AddDefaulted();
|
|
BoneIndexArray& Path = PathsToRoot.Last();
|
|
|
|
int32 CurrentBone = WeightedBones[WeightedBoneIndex];
|
|
Path.Add(CurrentBone);
|
|
|
|
while(CurrentBone != 0 && CurrentBone != INDEX_NONE)
|
|
{
|
|
CurrentBone = RefSkel.GetParentIndex(CurrentBone);
|
|
Path.Add(CurrentBone);
|
|
}
|
|
}
|
|
|
|
// Paths are from leaf->root, we want the other way
|
|
for(BoneIndexArray& Path : PathsToRoot)
|
|
{
|
|
Algo::Reverse(Path);
|
|
}
|
|
|
|
// Verify the last common bone in all paths as the root of the sim space
|
|
const int32 NumPaths = PathsToRoot.Num();
|
|
if(NumPaths > 0)
|
|
{
|
|
BoneIndexArray& FirstPath = PathsToRoot[0];
|
|
|
|
const int32 FirstPathSize = FirstPath.Num();
|
|
for(int32 PathEntryIndex = 0; PathEntryIndex < FirstPathSize; ++PathEntryIndex)
|
|
{
|
|
const int32 CurrentQueryIndex = FirstPath[PathEntryIndex];
|
|
bool bValidRoot = true;
|
|
|
|
for(int32 PathIndex = 1; PathIndex < NumPaths; ++PathIndex)
|
|
{
|
|
if(!PathsToRoot[PathIndex].Contains(CurrentQueryIndex))
|
|
{
|
|
bValidRoot = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(bValidRoot)
|
|
{
|
|
ReferenceBoneIndex = CurrentQueryIndex;
|
|
}
|
|
else
|
|
{
|
|
// Once we fail to find a valid root we're done.
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Just use root
|
|
ReferenceBoneIndex = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool UClothingAssetCommon::IsValidLod(int32 InLodIndex) const
|
|
{
|
|
return LodData.IsValidIndex(InLodIndex);
|
|
}
|
|
|
|
int32 UClothingAssetCommon::GetNumLods() const
|
|
{
|
|
return LodData.Num();
|
|
}
|
|
|
|
void UClothingAssetCommon::PostLoad()
|
|
{
|
|
Super::PostLoad();
|
|
|
|
#if WITH_EDITORONLY_DATA
|
|
// Migrate the deprecated UObject based lod class to the non-UObject lod structure to prevent PostLoad dependency issues
|
|
// TODO: Remove all UObject PostLoad dependencies.
|
|
// Even with these ConditionalPostLoad calls, the UObject PostLoads' order of execution cannot be guaranteed.
|
|
for (UClothLODDataCommon_Legacy* LodDeprecated : ClothLodData_DEPRECATED)
|
|
{
|
|
if (LodDeprecated)
|
|
{
|
|
LodDeprecated->ConditionalPostLoad();
|
|
|
|
const int32 Idx = AddNewLod();
|
|
LodDeprecated->MigrateTo(LodData[Idx]);
|
|
}
|
|
}
|
|
ClothLodData_DEPRECATED.Empty();
|
|
|
|
const int32 AnimPhysCustomVersion = GetLinkerCustomVersion(FAnimPhysObjectVersion::GUID);
|
|
if (AnimPhysCustomVersion < FAnimPhysObjectVersion::AddedClothingMaskWorkflow)
|
|
{
|
|
// Convert current parameters to masks
|
|
for (FClothLODDataCommon& Lod : LodData)
|
|
{
|
|
const FClothPhysicalMeshData& PhysMesh = Lod.PhysicalMeshData;
|
|
|
|
// Didn't do anything previously - clear out in case there's something in there
|
|
// so we can use it correctly now.
|
|
Lod.PointWeightMaps.Reset(3);
|
|
|
|
// Max distances
|
|
const FPointWeightMap* const MaxDistances = PhysMesh.FindWeightMap(EWeightMapTargetCommon::MaxDistance);
|
|
if (MaxDistances)
|
|
{
|
|
Lod.PointWeightMaps.AddDefaulted();
|
|
FPointWeightMap& MaxDistanceMask = Lod.PointWeightMaps.Last();
|
|
MaxDistanceMask.Initialize(*MaxDistances, EWeightMapTargetCommon::MaxDistance);
|
|
}
|
|
|
|
// Following params are only added if necessary, if we don't have any backstop
|
|
// radii then there's no backstops.
|
|
const FPointWeightMap* const BackstopRadiuses = PhysMesh.FindWeightMap(EWeightMapTargetCommon::BackstopRadius);
|
|
if (BackstopRadiuses && !BackstopRadiuses->IsZeroed())
|
|
{
|
|
// Backstop radii
|
|
Lod.PointWeightMaps.AddDefaulted();
|
|
FPointWeightMap& BackstopRadiusMask = Lod.PointWeightMaps.Last();
|
|
BackstopRadiusMask.Initialize(*BackstopRadiuses, EWeightMapTargetCommon::BackstopRadius);
|
|
|
|
// Backstop distances
|
|
Lod.PointWeightMaps.AddDefaulted();
|
|
FPointWeightMap& BackstopDistanceMask = Lod.PointWeightMaps.Last();
|
|
const FPointWeightMap& BackstopDistances = PhysMesh.GetWeightMap(EWeightMapTargetCommon::BackstopDistance);
|
|
BackstopDistanceMask.Initialize(BackstopDistances, EWeightMapTargetCommon::BackstopDistance);
|
|
}
|
|
}
|
|
|
|
// Make sure we're transactional
|
|
SetFlags(RF_Transactional);
|
|
}
|
|
|
|
const int32 ClothingCustomVersion = GetLinkerCustomVersion(FClothingAssetCustomVersion::GUID);
|
|
// Fix content imported before we kept vertex colors
|
|
if (ClothingCustomVersion < FClothingAssetCustomVersion::AddVertexColorsToPhysicalMesh)
|
|
{
|
|
for (FClothLODDataCommon& Lod : LodData)
|
|
{
|
|
const int32 NumVerts = Lod.PhysicalMeshData.Vertices.Num(); // number of verts
|
|
|
|
Lod.PhysicalMeshData.VertexColors.Reset();
|
|
Lod.PhysicalMeshData.VertexColors.AddUninitialized(NumVerts);
|
|
for (int32 VertIdx = 0; VertIdx < NumVerts; VertIdx++)
|
|
{
|
|
Lod.PhysicalMeshData.VertexColors[VertIdx] = FColor::White;
|
|
}
|
|
}
|
|
}
|
|
|
|
EClothingCachedDataFlagsCommon ClothingCachedDataFlags = EClothingCachedDataFlagsCommon::None;
|
|
if (AnimPhysCustomVersion < FAnimPhysObjectVersion::CacheClothMeshInfluences)
|
|
{
|
|
ClothingCachedDataFlags |= EClothingCachedDataFlagsCommon::NumInfluences;
|
|
}
|
|
|
|
// Add any missing configs for the available cloth factories, and try to migrate them from any existing one
|
|
// TODO: Remove all UObject PostLoad dependencies.
|
|
// Even with these ConditionalPostLoad calls, the UObject PostLoads' order of execution cannot be guaranteed.
|
|
for (auto& ClothConfig : ClothConfigs)
|
|
{
|
|
if (ClothConfig.Value)
|
|
{
|
|
ClothConfig.Value->ConditionalPostLoad(); // PostLoad configs before adding new ones
|
|
}
|
|
}
|
|
|
|
if (ClothSimConfig_DEPRECATED)
|
|
{
|
|
ClothSimConfig_DEPRECATED->ConditionalPostLoad(); // PostLoad old configs before replacing them
|
|
ClothSimConfig_DEPRECATED->Rename(nullptr, nullptr, REN_DoNotDirty | REN_DontCreateRedirectors | REN_NonTransactional); // Rename the config so that the name doesn't collide with the new config map name
|
|
}
|
|
if (ChaosClothSimConfig_DEPRECATED)
|
|
{
|
|
ChaosClothSimConfig_DEPRECATED->ConditionalPostLoad(); // PostLoad old configs before replacing them
|
|
ChaosClothSimConfig_DEPRECATED->Rename(nullptr, nullptr, REN_DoNotDirty | REN_DontCreateRedirectors | REN_NonTransactional); // Rename the config so that the name doesn't collide with the new config map name
|
|
}
|
|
if (ClothSharedSimConfig_DEPRECATED)
|
|
{
|
|
ClothSharedSimConfig_DEPRECATED->ConditionalPostLoad(); // PostLoad old configs before replacing them
|
|
ClothSharedSimConfig_DEPRECATED->Rename(nullptr, nullptr, REN_DoNotDirty | REN_DontCreateRedirectors | REN_NonTransactional); // Rename the config so that the name doesn't collide with the new config map name
|
|
}
|
|
|
|
if (AddClothConfigs())
|
|
{
|
|
// With a new config added best to recache everything
|
|
ClothingCachedDataFlags |= EClothingCachedDataFlagsCommon::All;
|
|
}
|
|
|
|
// Migrate configs
|
|
bool bMigrateSharedConfigToConfig = true; // Shared config to config migration can be disabled to avoid overriding the newly migrated values
|
|
|
|
if (ClothingCustomVersion < FClothingAssetCustomVersion::MovePropertiesToCommonBaseClasses)
|
|
{
|
|
// Remap legacy struct FClothConfig to new config objects
|
|
for (auto& ClothConfig : ClothConfigs)
|
|
{
|
|
if (UClothConfigCommon* const ClothConfigCommon = Cast<UClothConfigCommon>(ClothConfig.Value))
|
|
{
|
|
ClothConfigCommon->ConditionalPostLoad();
|
|
ClothConfigCommon->MigrateFrom(ClothConfig_DEPRECATED);
|
|
}
|
|
}
|
|
bMigrateSharedConfigToConfig = false;
|
|
}
|
|
else
|
|
{
|
|
// Migrate simulation dependent config parameters to the new config map
|
|
if (ClothSimConfig_DEPRECATED)
|
|
{
|
|
// Try a remap to the new config objects through the legacy structure
|
|
if (const UClothConfigCommon* const ClothSimConfigCommon = Cast<UClothConfigCommon>(ClothSimConfig_DEPRECATED))
|
|
{
|
|
FClothConfig_Legacy ClothConfigLegacy;
|
|
if (ClothSimConfigCommon->MigrateTo(ClothConfigLegacy))
|
|
{
|
|
for (auto& ClothConfig : ClothConfigs)
|
|
{
|
|
if (UClothConfigCommon* const ClothConfigCommon = Cast<UClothConfigCommon>(ClothConfig.Value))
|
|
{
|
|
ClothConfigCommon->ConditionalPostLoad();
|
|
ClothConfigCommon->MigrateFrom(ClothConfigLegacy);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// And keep the old config too
|
|
SetClothConfig(ToRawPtr(ClothSimConfig_DEPRECATED));
|
|
ClothSimConfig_DEPRECATED = nullptr;
|
|
bMigrateSharedConfigToConfig = false;
|
|
}
|
|
if (ChaosClothSimConfig_DEPRECATED)
|
|
{
|
|
SetClothConfig(ToRawPtr(ChaosClothSimConfig_DEPRECATED));
|
|
ChaosClothSimConfig_DEPRECATED = nullptr;
|
|
bMigrateSharedConfigToConfig = false;
|
|
}
|
|
if (ClothSharedSimConfig_DEPRECATED)
|
|
{
|
|
SetClothConfig(ToRawPtr(ClothSharedSimConfig_DEPRECATED));
|
|
ClothSharedSimConfig_DEPRECATED = nullptr;
|
|
bMigrateSharedConfigToConfig = false;
|
|
}
|
|
}
|
|
|
|
// Propagate shared configs between cloth assets
|
|
PropagateSharedConfigs(bMigrateSharedConfigToConfig);
|
|
|
|
// Cache tethers and reference bone index should already have been calculated
|
|
const int32 FortniteReleaseBranchCustomObjectVersion = GetLinkerCustomVersion(FFortniteReleaseBranchCustomObjectVersion::GUID);
|
|
const int32 SpecialProjectBranchObjectVersion = GetLinkerCustomVersion(FUE5SpecialProjectStreamObjectVersion::GUID);
|
|
if (SpecialProjectBranchObjectVersion < FUE5SpecialProjectStreamObjectVersion::ChaosClothRemoveKinematicTethers)
|
|
{
|
|
ClothingCachedDataFlags |= EClothingCachedDataFlagsCommon::Tethers;
|
|
}
|
|
|
|
if (FortniteReleaseBranchCustomObjectVersion < FFortniteReleaseBranchCustomObjectVersion::ChaosClothAddTethersToCachedData &&
|
|
SpecialProjectBranchObjectVersion < FUE5SpecialProjectStreamObjectVersion::ChaosClothAddTethersToCachedData)
|
|
{
|
|
ClothingCachedDataFlags |= EClothingCachedDataFlagsCommon::Tethers;
|
|
|
|
// ReferenceBoneIndex is only required when rebinding the cloth.
|
|
USkeletalMesh* const OwnerMesh = CastChecked<USkeletalMesh>(GetOuter());
|
|
RefreshBoneMapping(OwnerMesh);
|
|
CalculateReferenceBoneIndex();
|
|
}
|
|
|
|
// After fixing the content, we are ready to call functions that rely on it
|
|
if (ClothingCachedDataFlags != EClothingCachedDataFlagsCommon::None)
|
|
{
|
|
// Rebuild data cache
|
|
InvalidateFlaggedCachedData(ClothingCachedDataFlags);
|
|
}
|
|
|
|
const int32 PhysicsObjectVersion = GetLinkerCustomVersion(FPhysicsObjectVersion::GUID);
|
|
const int32 FortniteMainBranchObjectVersion = GetLinkerCustomVersion(FFortniteMainBranchObjectVersion::GUID);
|
|
if (PhysicsObjectVersion < FPhysicsObjectVersion::ChaosClothFixLODTransitionMaps ||
|
|
FortniteMainBranchObjectVersion < FFortniteMainBranchObjectVersion::ChaosClothFixLODTransitionMaps)
|
|
{
|
|
BuildLodTransitionData();
|
|
}
|
|
#endif // #if WITH_EDITORONLY_DATA
|
|
}
|
|
|
|
#if WITH_EDITORONLY_DATA
|
|
void UClothingAssetCommon::DeclareConstructClasses(TArray<FTopLevelAssetPath>& OutConstructClasses, const UClass* SpecificSubclass)
|
|
{
|
|
Super::DeclareConstructClasses(OutConstructClasses, SpecificSubclass);
|
|
const TArray<IClothingSimulationFactoryClassProvider*> ClassProviders =
|
|
IModularFeatures::Get().GetModularFeatureImplementations<IClothingSimulationFactoryClassProvider>(IClothingSimulationFactoryClassProvider::FeatureName);
|
|
for (IClothingSimulationFactoryClassProvider* Provider : ClassProviders)
|
|
{
|
|
check(Provider);
|
|
if (UClass* const ClothingSimulationFactoryClass = *TSubclassOf<class UClothingSimulationFactory>(Provider->GetClothingSimulationFactoryClass()))
|
|
{
|
|
const UClothingSimulationFactory* const ClothingSimulationFactory = ClothingSimulationFactoryClass->GetDefaultObject<UClothingSimulationFactory>();
|
|
for (TSubclassOf<UClothConfigBase> ClothConfigClass : ClothingSimulationFactory->GetClothConfigClasses())
|
|
{
|
|
OutConstructClasses.Add(FTopLevelAssetPath(ClothConfigClass));
|
|
}
|
|
}
|
|
}
|
|
// OutConstructClasses.Add(FTopLevelAssetPath(UChaosClothSharedSimConfig::StaticClass()));
|
|
//OutConstructClasses.Add(FTopLevelAssetPath(UChaosClothConfig::StaticClass()));
|
|
}
|
|
#endif
|
|
|
|
void UClothingAssetCommon::Serialize(FArchive& Ar)
|
|
{
|
|
Super::Serialize(Ar);
|
|
Ar.UsingCustomVersion(FAnimPhysObjectVersion::GUID);
|
|
Ar.UsingCustomVersion(FClothingAssetCustomVersion::GUID);
|
|
Ar.UsingCustomVersion(FFortniteMainBranchObjectVersion::GUID);
|
|
Ar.UsingCustomVersion(FPhysicsObjectVersion::GUID);
|
|
Ar.UsingCustomVersion(FUE5SpecialProjectStreamObjectVersion::GUID);
|
|
}
|
|
|
|
bool UClothingAssetCommon::AddClothConfigs()
|
|
{
|
|
bool bNewConfigAdded = false;
|
|
|
|
const TArray<IClothingSimulationFactoryClassProvider*> ClassProviders =
|
|
IModularFeatures::Get().GetModularFeatureImplementations<IClothingSimulationFactoryClassProvider>(IClothingSimulationFactoryClassProvider::FeatureName);
|
|
|
|
for (IClothingSimulationFactoryClassProvider* Provider : ClassProviders)
|
|
{
|
|
check(Provider);
|
|
if (UClass* const ClothingSimulationFactoryClass = *TSubclassOf<class UClothingSimulationFactory>(Provider->GetClothingSimulationFactoryClass()))
|
|
{
|
|
const UClothingSimulationFactory* const ClothingSimulationFactory = ClothingSimulationFactoryClass->GetDefaultObject<UClothingSimulationFactory>();
|
|
for (TSubclassOf<UClothConfigBase> ClothConfigClass : ClothingSimulationFactory->GetClothConfigClasses())
|
|
{
|
|
const FName ClothConfigName = ClothConfigClass->GetFName();
|
|
|
|
TObjectPtr<UClothConfigBase>* const ClothConfigPtr = ClothConfigs.Find(ClothConfigName);
|
|
if (!ClothConfigPtr || !*ClothConfigPtr)
|
|
{
|
|
// Create new config object
|
|
checkf(!StaticFindObject(ClothConfigClass, this, *ClothConfigClass->GetName(), true), TEXT("Found an existing instance of ClothConfigClass in %s. Class:%s"), *GetPathNameSafe(this), *ClothConfigClass->GetName());
|
|
UClothConfigBase* const ClothConfig = NewObject<UClothConfigBase>(this, ClothConfigClass, ClothConfigClass->GetFName(), RF_Transactional);
|
|
|
|
// Use the legacy config struct to try find a common config as an acceptable migration source
|
|
// This code could be removed once the legacy code is removed, although this will then prevent
|
|
// migration from compatible config sources
|
|
if (UClothConfigCommon* const ClothConfigCommon = Cast<UClothConfigCommon>(ClothConfig))
|
|
{
|
|
for (auto ClothConfigPair : ClothConfigs)
|
|
{
|
|
if (const UClothConfigCommon* SourceConfig = Cast<UClothConfigCommon>(ClothConfigPair.Value))
|
|
{
|
|
FClothConfig_Legacy LegacyConfig;
|
|
if (SourceConfig->MigrateTo(LegacyConfig))
|
|
{
|
|
ClothConfigCommon->MigrateFrom(LegacyConfig);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set the new config
|
|
check(ClothConfig);
|
|
if (ClothConfigPtr)
|
|
{
|
|
*ClothConfigPtr = ClothConfig;
|
|
}
|
|
else
|
|
{
|
|
ClothConfigs.Emplace(ClothConfigName, ClothConfig);
|
|
}
|
|
bNewConfigAdded = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return bNewConfigAdded;
|
|
}
|
|
|
|
void UClothingAssetCommon::PropagateSharedConfigs(bool bMigrateSharedConfigToConfig)
|
|
{
|
|
// Update this asset's shared config when the asset belongs to a skeletal mesh
|
|
if (USkeletalMesh* const SkeletalMesh = Cast<USkeletalMesh>(GetOuter()))
|
|
{
|
|
const TArray<UClothingAssetBase*>& ClothingAssets = SkeletalMesh->GetMeshClothingAssets();
|
|
|
|
// Collect all shared configs found in the other assets
|
|
decltype(ClothConfigs) ClothSharedConfigs;
|
|
|
|
for (const UClothingAssetBase* ClothingAssetBase : ClothingAssets)
|
|
{
|
|
if (ClothingAssetBase == static_cast<UClothingAssetBase* >(this))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Only common assets have shared configs
|
|
if (const UClothingAssetCommon* const ClothingAsset = Cast<UClothingAssetCommon>(ClothingAssetBase))
|
|
{
|
|
// Reserve space in the map, use the total number of configs in case they're unlikely all shared configs
|
|
const int32 Max = ClothSharedConfigs.Num() + ClothingAsset->ClothConfigs.Num();
|
|
ClothSharedConfigs.Reserve(Max);
|
|
|
|
// Iterate through all configs, and find the shared ones
|
|
for (const auto& ClothSharedConfigItem : ClothingAsset->ClothConfigs)
|
|
{
|
|
if (Cast<UClothSharedConfigCommon>(ClothSharedConfigItem.Value) && // Only needs shared configs
|
|
!ClothSharedConfigs.Find(ClothSharedConfigItem.Key)) // Only needs a single shared config per type
|
|
{
|
|
ClothSharedConfigs.Add(ClothSharedConfigItem);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Propagate the found shared configs to this asset
|
|
for (const auto& ClothSharedConfigItem : ClothSharedConfigs)
|
|
{
|
|
// Set share config
|
|
if (TObjectPtr<UClothConfigBase>* const ClothConfigBase = ClothConfigs.Find(ClothSharedConfigItem.Key))
|
|
{
|
|
// Reset this shared config
|
|
*ClothConfigBase = ClothSharedConfigItem.Value;
|
|
}
|
|
else
|
|
{
|
|
// Add new map entry
|
|
ClothConfigs.Add(ClothSharedConfigItem);
|
|
}
|
|
}
|
|
|
|
// Migrate the common shared configs' deprecated parameters to all per cloth configs, and fix the shared config ownership
|
|
for (TPair<FName, TObjectPtr<UClothConfigBase>>& ClothSharedConfigItem : ClothConfigs)
|
|
{
|
|
if (UClothSharedConfigCommon* const ClothSharedConfig = Cast<UClothSharedConfigCommon>(ClothSharedConfigItem.Value))
|
|
{
|
|
// Migrate from this shared config to non shared configs if needed
|
|
if (bMigrateSharedConfigToConfig)
|
|
{
|
|
for (const TPair<FName, TObjectPtr<UClothConfigBase>>& ClothConfigItem : ClothConfigs)
|
|
{
|
|
if (Cast<UClothSharedConfigCommon>(ClothConfigItem.Value))
|
|
{
|
|
continue; // Don't migrate shared configs to another shared configs (or itself)
|
|
}
|
|
if (UClothConfigCommon* const ClothConfig = Cast<UClothConfigCommon>(ClothConfigItem.Value))
|
|
{
|
|
ClothConfig->MigrateFrom(ClothSharedConfig);
|
|
}
|
|
}
|
|
}
|
|
// Fix the shared config outer if it is still a UClothingAssetCommon (the config must belong to the skeletal mesh, as it is shared between assets)
|
|
if (Cast<UClothingAssetCommon>(ClothSharedConfig->GetOuter()))
|
|
{
|
|
ClothSharedConfig->Rename(nullptr, SkeletalMesh, REN_DontCreateRedirectors | REN_NonTransactional);
|
|
}
|
|
|
|
// Fix the shared config ownership, asset might have been copied and the shared config could still point to a different skeletal mesh
|
|
if (ClothSharedConfig->GetOuter() != SkeletalMesh)
|
|
{
|
|
ClothSharedConfigItem.Value = DuplicateObject<UClothSharedConfigCommon>(ClothSharedConfig, SkeletalMesh, ClothSharedConfig->GetFName());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void UClothingAssetCommon::PostUpdateAllAssets()
|
|
{
|
|
// Add any missing configs for the available cloth factories, and try to migrate them from any existing one
|
|
const bool bInvalidateCachedData = AddClothConfigs();
|
|
|
|
// Propagate shared configs
|
|
PropagateSharedConfigs();
|
|
|
|
#if WITH_EDITORONLY_DATA
|
|
// Invalidate cached data if the configs have changed
|
|
if (bInvalidateCachedData)
|
|
{
|
|
InvalidateAllCachedData();
|
|
}
|
|
#endif
|
|
}
|
|
|
|
|
|
#if WITH_EDITORONLY_DATA
|
|
void UClothingAssetCommon::InvalidateFlaggedCachedData(EClothingCachedDataFlagsCommon Flags)
|
|
{
|
|
const bool bNeedsInverseMasses = EnumHasAllFlags((EClothingCachedDataFlagsCommon)Flags, EClothingCachedDataFlagsCommon::InverseMasses) && AnyOfClothConfigs([](UClothConfigBase& Config) { return Config.NeedsInverseMasses(); });
|
|
const bool bNeedsNumInfluences = EnumHasAllFlags((EClothingCachedDataFlagsCommon)Flags, EClothingCachedDataFlagsCommon::NumInfluences) && AnyOfClothConfigs([](UClothConfigBase& Config) { return Config.NeedsNumInfluences(); });
|
|
const bool bNeedsSelfCollisionData = EnumHasAllFlags((EClothingCachedDataFlagsCommon)Flags, EClothingCachedDataFlagsCommon::SelfCollisionData) && AnyOfClothConfigs([](UClothConfigBase& Config) { return Config.NeedsSelfCollisionData(); });
|
|
const bool bNeedsTethers = EnumHasAllFlags((EClothingCachedDataFlagsCommon)Flags, EClothingCachedDataFlagsCommon::Tethers) && AnyOfClothConfigs([](UClothConfigBase& Config) { return Config.NeedsTethers(); });
|
|
|
|
float SelfCollisionRadius = 0.f;
|
|
if (bNeedsSelfCollisionData)
|
|
{
|
|
// Note: Only PhysX based NvCloth needs to build the SelfCollisionIndices at the moment
|
|
for (const TPair<FName, TObjectPtr<UClothConfigBase>>& ClothConfig : ClothConfigs)
|
|
{
|
|
SelfCollisionRadius = FMath::Max(SelfCollisionRadius, ClothConfig.Value->GetSelfCollisionRadius());
|
|
}
|
|
}
|
|
|
|
bool bThethersUseEuclideanDistance = false;
|
|
bool bThethersUseGeodesicDistance = false;
|
|
if (bNeedsTethers)
|
|
{
|
|
for (const TPair<FName, TObjectPtr<UClothConfigBase>>& ClothConfig : ClothConfigs)
|
|
{
|
|
if (ClothConfig.Value->NeedsTethers())
|
|
{
|
|
if (ClothConfig.Value->TethersUseGeodesicDistance())
|
|
{
|
|
bThethersUseGeodesicDistance = true;
|
|
}
|
|
else
|
|
{
|
|
bThethersUseEuclideanDistance = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Recalculate cached data
|
|
bool bHasClothChanged = false;
|
|
for (FClothLODDataCommon& CurrentLodData : LodData)
|
|
{
|
|
FClothPhysicalMeshData& PhysMesh = CurrentLodData.PhysicalMeshData;
|
|
|
|
if (bNeedsInverseMasses)
|
|
{
|
|
PhysMesh.CalculateInverseMasses();
|
|
bHasClothChanged = true;
|
|
}
|
|
|
|
if (bNeedsNumInfluences)
|
|
{
|
|
PhysMesh.CalculateNumInfluences();
|
|
bHasClothChanged = true;
|
|
}
|
|
|
|
if (bNeedsSelfCollisionData)
|
|
{
|
|
PhysMesh.BuildSelfCollisionData(SelfCollisionRadius);
|
|
bHasClothChanged = true;
|
|
}
|
|
|
|
if (bNeedsTethers)
|
|
{
|
|
PhysMesh.CalculateTethers(bThethersUseEuclideanDistance, bThethersUseGeodesicDistance);
|
|
bHasClothChanged = true;
|
|
}
|
|
}
|
|
|
|
// Inform the simulations that the cloth has changed
|
|
if (bHasClothChanged)
|
|
{
|
|
ForEachInteractorUsingClothing([](UClothingSimulationInteractor* InInteractor)
|
|
{
|
|
if (InInteractor)
|
|
{
|
|
InInteractor->ClothConfigUpdated();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
#endif // #if WITH_EDITORONLY_DATA
|
|
|
|
#if WITH_EDITOR
|
|
int32 UClothingAssetCommon::AddNewLod()
|
|
{
|
|
return LodData.AddDefaulted();
|
|
}
|
|
|
|
void UClothingAssetCommon::PostEditChangeChainProperty(FPropertyChangedChainEvent& ChainEvent)
|
|
{
|
|
Super::PostEditChangeChainProperty(ChainEvent);
|
|
|
|
bool bReregisterComponents = false;
|
|
|
|
if (ChainEvent.ChangeType != EPropertyChangeType::Interactive)
|
|
{
|
|
const FName& PropertyName = ChainEvent.PropertyChain.GetActiveMemberNode() && ChainEvent.PropertyChain.GetActiveMemberNode()->GetNextNode() ?
|
|
ChainEvent.PropertyChain.GetActiveMemberNode()->GetNextNode()->GetValue()->GetFName() : NAME_None;
|
|
|
|
if (PropertyName == FName("bUseSelfCollisionSpheres") ||
|
|
PropertyName == FName("SelfCollisionSphereRadius") ||
|
|
PropertyName == FName("SelfCollisionSphereRadiusCullMultiplier"))
|
|
{
|
|
InvalidateFlaggedCachedData(EClothingCachedDataFlagsCommon::SelfCollisionData);
|
|
bReregisterComponents = true;
|
|
}
|
|
else if (PropertyName == FName("bUseGeodesicDistance"))
|
|
{
|
|
InvalidateFlaggedCachedData(EClothingCachedDataFlagsCommon::Tethers);
|
|
bReregisterComponents = true;
|
|
}
|
|
else if(ChainEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED(UClothingAssetCommon, PhysicsAsset))
|
|
{
|
|
bReregisterComponents = true;
|
|
}
|
|
else
|
|
{
|
|
// Other properties just require a config refresh
|
|
ForEachInteractorUsingClothing([](UClothingSimulationInteractor* InInteractor)
|
|
{
|
|
if (InInteractor)
|
|
{
|
|
InInteractor->ClothConfigUpdated();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
if (bReregisterComponents)
|
|
{
|
|
ReregisterComponentsUsingClothing();
|
|
}
|
|
}
|
|
|
|
#endif // WITH_EDITOR
|
|
|
|
|
|
#undef LOCTEXT_NAMESPACE
|
|
|