520 lines
17 KiB
C++
520 lines
17 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "ClothingAssetFactory.h"
|
|
|
|
#include "BoneIndices.h"
|
|
#include "BoneWeights.h"
|
|
#include "ClothLODData.h"
|
|
#include "ClothPhysicalMeshData.h"
|
|
#include "ClothVertBoneData.h"
|
|
#include "ClothingAsset.h"
|
|
#include "ClothingAssetBase.h"
|
|
#include "Containers/Array.h"
|
|
#include "Containers/ContainersFwd.h"
|
|
#include "Containers/IndirectArray.h"
|
|
#include "CoreGlobals.h"
|
|
#include "Engine/SkeletalMesh.h"
|
|
#include "Framework/Notifications/NotificationManager.h"
|
|
#include "GPUSkinPublicDefs.h"
|
|
#include "HAL/PlatformCrt.h"
|
|
#include "Internationalization/Internationalization.h"
|
|
#include "Internationalization/Text.h"
|
|
#include "Logging/LogCategory.h"
|
|
#include "Math/UnrealMathSSE.h"
|
|
#include "Math/Vector.h"
|
|
#include "Math/Vector4.h"
|
|
#include "Misc/AssertionMacros.h"
|
|
#include "Misc/ConfigCacheIni.h"
|
|
#include "Misc/Guid.h"
|
|
#include "ObjectTools.h"
|
|
#include "PhysicsEngine/PhysicsAsset.h"
|
|
#include "PointWeightMap.h"
|
|
#include "ReferenceSkeleton.h"
|
|
#include "Rendering/SkeletalMeshLODModel.h"
|
|
#include "Rendering/SkeletalMeshModel.h"
|
|
#include "Serialization/StructuredArchiveAdapters.h"
|
|
#include "SkeletalMeshTypes.h"
|
|
#include "Templates/Casts.h"
|
|
#include "Templates/UnrealTemplate.h"
|
|
#include "Trace/Detail/Channel.h"
|
|
#include "UObject/ObjectPtr.h"
|
|
#include "UObject/SoftObjectPtr.h"
|
|
#include "UObject/WeakObjectPtrTemplates.h"
|
|
#include "Utils/ClothingMeshUtils.h"
|
|
#include "Widgets/Notifications/SNotificationList.h"
|
|
|
|
#define LOCTEXT_NAMESPACE "ClothingAssetFactory"
|
|
DEFINE_LOG_CATEGORY(LogClothingAssetFactory)
|
|
|
|
using namespace nvidia::apex;
|
|
|
|
namespace ClothingFactoryConstants
|
|
{
|
|
// For verifying the file
|
|
static const char ClothingAssetClass[] = "ClothingAssetParameters";
|
|
|
|
// Import transformation params
|
|
static const char ParamName_BoneActors[] = "boneActors";
|
|
static const char ParamName_BoneSpheres[] = "boneSpheres";
|
|
static const char ParamName_GravityDirection[] = "simulation.gravityDirection";
|
|
static const char ParamName_UvOrigin[] = "textureUVOrigin";
|
|
|
|
// UV flip params
|
|
static const char ParamName_SubmeshArray[] = "submeshes";
|
|
static const char ParamName_SubmeshBufferFormats[] = "vertexBuffer.vertexFormat.bufferFormats";
|
|
static const char ParamName_VertexBuffers[] = "vertexBuffer.buffers";
|
|
static const char ParamName_Semantic[] = "semantic";
|
|
static const char ParamName_BufferData[] = "data";
|
|
|
|
static const char ParamName_GLOD_Platforms[] = "platforms";
|
|
static const char ParamName_GLOD_LOD[] = "lod";
|
|
static const char ParamName_GLOD_PhysMeshID[] = "physicalMeshId";
|
|
static const char ParamName_GLOD_RenderMeshAsset[] = "renderMeshAsset";
|
|
static const char ParamName_GLOD_ImmediateClothMap[] = "immediateClothMap";
|
|
static const char ParamName_GLOD_SkinClothMapB[] = "SkinClothMapB";
|
|
static const char ParamName_GLOD_SkinClothMap[] = "SkinClothMap";
|
|
static const char ParamName_GLOD_SkinClothMapThickness[] = "skinClothMapThickness";
|
|
static const char ParamName_GLOD_SkinClothMapOffset[] = "skinClothMapOffset";
|
|
static const char ParamName_GLOD_TetraMap[] = "tetraMap";
|
|
static const char ParamName_GLOD_RenderMeshAssetSorting[] = "renderMeshAssetSorting";
|
|
static const char ParamName_GLOD_PhysicsMeshPartitioning[] = "physicsMeshPartitioning";
|
|
|
|
static const char ParamName_Partition_GraphicalSubmesh[] = "graphicalSubmesh";
|
|
static const char ParamName_Partition_NumSimVerts[] = "numSimulatedVertices";
|
|
static const char ParamName_Partition_NumSimVertsAdditional[] = "numSimulatedVerticesAdditional";
|
|
static const char ParamName_Partition_NumSimIndices[] = "numSimulatedIndices";
|
|
}
|
|
|
|
void LogAndToastWarning(const FText& Error)
|
|
{
|
|
FNotificationInfo Info(Error);
|
|
Info.ExpireDuration = 5.0f;
|
|
FSlateNotificationManager::Get().AddNotification(Info);
|
|
|
|
UE_LOG(LogClothingAssetFactory, Warning, TEXT("%s"), *Error.ToString());
|
|
}
|
|
|
|
UClothingAssetFactory::UClothingAssetFactory(const FObjectInitializer& ObjectInitializer)
|
|
: Super(ObjectInitializer)
|
|
{
|
|
}
|
|
|
|
UClothingAssetBase* UClothingAssetFactory::Import
|
|
(
|
|
const FString& Filename,
|
|
USkeletalMesh* TargetMesh,
|
|
FName InName
|
|
)
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
UClothingAssetBase* UClothingAssetFactory::Reimport(const FString& Filename, USkeletalMesh* TargetMesh, UClothingAssetBase* OriginalAsset)
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
UClothingAssetBase* UClothingAssetFactory::CreateFromSkeletalMesh(USkeletalMesh* TargetMesh, FSkeletalMeshClothBuildParams& Params)
|
|
{
|
|
// Need a valid skel mesh
|
|
if(!TargetMesh)
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
FSkeletalMeshModel* Mesh = TargetMesh->GetImportedModel();
|
|
|
|
// Need a valid resource
|
|
if(!Mesh)
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
// Need a valid LOD model
|
|
if(!Mesh->LODModels.IsValidIndex(Params.LodIndex))
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
FSkeletalMeshLODModel& LodModel = Mesh->LODModels[Params.LodIndex];
|
|
|
|
// Need a valid section
|
|
if(!LodModel.Sections.IsValidIndex(Params.SourceSection))
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
// Ok, we have a valid mesh and section, we can now extract it as a sim mesh
|
|
FSkelMeshSection& SourceSection = LodModel.Sections[Params.SourceSection];
|
|
|
|
// Can't convert to a clothing asset if bound to clothing
|
|
if(SourceSection.HasClothingData())
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
FString SanitizedName = ObjectTools::SanitizeObjectName(Params.AssetName);
|
|
FName ObjectName = MakeUniqueObjectName(TargetMesh, UClothingAssetCommon::StaticClass(), FName(*SanitizedName));
|
|
|
|
UClothingAssetCommon* NewAsset = NewObject<UClothingAssetCommon>(TargetMesh, ObjectName);
|
|
NewAsset->SetFlags(RF_Transactional);
|
|
|
|
// Adding a new LOD from this skeletal mesh
|
|
NewAsset->AddNewLod();
|
|
FClothLODDataCommon& LodData = NewAsset->LodData.Last();
|
|
|
|
if(ImportToLodInternal(TargetMesh, Params.LodIndex, Params.SourceSection, NewAsset, LodData, Params.LodIndex)) // Use the same LOD index as both source and destination index
|
|
{
|
|
FScopedSkeletalMeshPostEditChange ScopedSkeletalMeshPostEditChange(TargetMesh);
|
|
if(Params.bRemoveFromMesh)
|
|
{
|
|
// User doesn't want the section anymore as a renderable, get rid of it
|
|
TargetMesh->RemoveMeshSection(Params.LodIndex, Params.SourceSection); // Note: this is now taken care of ahead of the call to this function in order to get the correct used bone array, left here to not break the API behavior
|
|
}
|
|
|
|
// Set asset guid
|
|
NewAsset->AssetGuid = FGuid::NewGuid();
|
|
|
|
// Set physics asset, will be used when building actors for cloth collisions
|
|
NewAsset->PhysicsAsset = Params.PhysicsAsset.LoadSynchronous();
|
|
|
|
// Build the final bone map
|
|
NewAsset->RefreshBoneMapping(TargetMesh);
|
|
|
|
// Invalidate cached data as the mesh has changed
|
|
NewAsset->InvalidateAllCachedData();
|
|
|
|
return NewAsset;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
UClothingAssetBase* UClothingAssetFactory::CreateFromExistingCloth(USkeletalMesh* TargetMesh, USkeletalMesh* SourceMesh, UClothingAssetBase* SourceAsset)
|
|
{
|
|
UClothingAssetCommon* SourceClothingAsset = Cast<UClothingAssetCommon>(SourceAsset);
|
|
|
|
if (!SourceClothingAsset)
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
//Duplicating the clothing asset using the existing asset as a template
|
|
UClothingAssetCommon* NewAsset = DuplicateObject<UClothingAssetCommon>(SourceClothingAsset, TargetMesh, SourceClothingAsset->GetFName());
|
|
|
|
NewAsset->AssetGuid = FGuid::NewGuid();
|
|
//Need to empty LODMap to remove previous mappings from cloth LOD to SkelMesh LOD
|
|
NewAsset->LodMap.Empty();
|
|
NewAsset->RefreshBoneMapping(TargetMesh);
|
|
NewAsset->InvalidateAllCachedData();
|
|
|
|
return NewAsset;
|
|
}
|
|
|
|
UClothingAssetBase* UClothingAssetFactory::ImportLodToClothing(USkeletalMesh* TargetMesh, FSkeletalMeshClothBuildParams& Params)
|
|
{
|
|
if(!TargetMesh)
|
|
{
|
|
// Invalid target - can't continue.
|
|
LogAndToastWarning(LOCTEXT("Warning_InvalidLodMesh", "Failed to import clothing LOD, invalid target mesh specified"));
|
|
return nullptr;
|
|
}
|
|
|
|
if(!Params.TargetAsset.IsValid())
|
|
{
|
|
// Invalid target - can't continue.
|
|
LogAndToastWarning(LOCTEXT("Warning_InvalidClothTarget", "Failed to import clothing LOD, invalid target clothing object"));
|
|
return nullptr;
|
|
}
|
|
|
|
FSkeletalMeshModel* MeshResource = TargetMesh->GetImportedModel();
|
|
check(MeshResource);
|
|
|
|
const int32 NumMeshLods = MeshResource->LODModels.Num();
|
|
|
|
if(UClothingAssetBase* TargetClothing = Params.TargetAsset.Get())
|
|
{
|
|
// Find the clothing asset in the mesh to verify the params are correct
|
|
int32 MeshAssetIndex = INDEX_NONE;
|
|
if(TargetMesh->GetMeshClothingAssets().Find(TargetClothing, MeshAssetIndex))
|
|
{
|
|
// Everything looks good, continue to actual import
|
|
UClothingAssetCommon* ConcreteTarget = CastChecked<UClothingAssetCommon>(TargetClothing);
|
|
|
|
const FClothLODDataCommon* RemapSource = nullptr;
|
|
|
|
if(Params.bRemapParameters)
|
|
{
|
|
if(Params.TargetLod == ConcreteTarget->GetNumLods())
|
|
{
|
|
// New LOD, remap from previous
|
|
RemapSource = &ConcreteTarget->LodData.Last();
|
|
}
|
|
else
|
|
{
|
|
// This is a replacement, remap from current LOD
|
|
check(ConcreteTarget->LodData.IsValidIndex(Params.TargetLod));
|
|
RemapSource = &ConcreteTarget->LodData[Params.TargetLod];
|
|
}
|
|
}
|
|
|
|
if(Params.TargetLod == ConcreteTarget->GetNumLods())
|
|
{
|
|
ConcreteTarget->AddNewLod();
|
|
}
|
|
else if(!ConcreteTarget->LodData.IsValidIndex(Params.TargetLod))
|
|
{
|
|
LogAndToastWarning(LOCTEXT("Warning_InvalidLodTarget", "Failed to import clothing LOD, invalid target LOD."));
|
|
return nullptr;
|
|
}
|
|
|
|
FClothLODDataCommon& NewLod = ConcreteTarget->LodData[Params.TargetLod];
|
|
|
|
if(Params.TargetLod > 0 && Params.bRemapParameters)
|
|
{
|
|
RemapSource = &ConcreteTarget->LodData[Params.TargetLod - 1];
|
|
}
|
|
|
|
if(ImportToLodInternal(TargetMesh, Params.LodIndex, Params.SourceSection, ConcreteTarget, NewLod, Params.TargetLod, RemapSource))
|
|
{
|
|
// Rebuild the final bone map
|
|
ConcreteTarget->RefreshBoneMapping(TargetMesh);
|
|
|
|
// Build Lod skinning map for smooth transitions
|
|
ConcreteTarget->BuildLodTransitionData();
|
|
|
|
// Invalidate cached data as the mesh has changed
|
|
ConcreteTarget->InvalidateAllCachedData();
|
|
|
|
return ConcreteTarget;
|
|
}
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
UClothingAssetBase* UClothingAssetFactory::CreateFromApexAsset(nvidia::apex::ClothingAsset* InApexAsset, USkeletalMesh* TargetMesh, FName InName)
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
bool UClothingAssetFactory::CanImport(const FString& Filename)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool UClothingAssetFactory::ImportToLodInternal(
|
|
USkeletalMesh* SourceMesh,
|
|
int32 SourceLodIndex,
|
|
int32 SourceSectionIndex,
|
|
UClothingAssetCommon* DestAsset,
|
|
FClothLODDataCommon& DestLod,
|
|
int32 DestLodIndex,
|
|
const FClothLODDataCommon* InParameterRemapSource)
|
|
{
|
|
if(!SourceMesh || !SourceMesh->GetImportedModel())
|
|
{
|
|
// Invalid mesh
|
|
return false;
|
|
}
|
|
|
|
FSkeletalMeshModel* SkeletalResource = SourceMesh->GetImportedModel();
|
|
|
|
if(!SkeletalResource->LODModels.IsValidIndex(SourceLodIndex))
|
|
{
|
|
// Invalid LOD
|
|
return false;
|
|
}
|
|
|
|
FSkeletalMeshLODModel& SourceLod = SkeletalResource->LODModels[SourceLodIndex];
|
|
|
|
if(!SourceLod.Sections.IsValidIndex(SourceSectionIndex))
|
|
{
|
|
// Invalid Section
|
|
return false;
|
|
}
|
|
|
|
FSkelMeshSection& SourceSection = SourceLod.Sections[SourceSectionIndex];
|
|
|
|
const int32 NumVerts = SourceSection.SoftVertices.Num();
|
|
const int32 NumIndices = SourceSection.NumTriangles * 3;
|
|
const int32 BaseIndex = SourceSection.BaseIndex;
|
|
const int32 BaseVertexIndex = SourceSection.BaseVertexIndex;
|
|
|
|
// We need to weld the mesh verts to get rid of duplicates (happens for smoothing groups)
|
|
TArray<FVector> UniqueVerts;
|
|
TArray<uint32> OriginalIndexes;
|
|
|
|
TArray<uint32> IndexRemap;
|
|
IndexRemap.AddDefaulted(NumVerts);
|
|
{
|
|
static const float ThreshSq = SMALL_NUMBER * SMALL_NUMBER;
|
|
for(int32 VertIndex = 0; VertIndex < NumVerts; ++VertIndex)
|
|
{
|
|
const FSoftSkinVertex& SourceVert = SourceSection.SoftVertices[VertIndex];
|
|
|
|
bool bUnique = true;
|
|
int32 RemapIndex = INDEX_NONE;
|
|
|
|
const int32 NumUniqueVerts = UniqueVerts.Num();
|
|
for(int32 UniqueVertIndex = 0; UniqueVertIndex < NumUniqueVerts; ++UniqueVertIndex)
|
|
{
|
|
FVector& UniqueVert = UniqueVerts[UniqueVertIndex];
|
|
|
|
if((UniqueVert - (FVector)SourceVert.Position).SizeSquared() <= ThreshSq)
|
|
{
|
|
// Not unique
|
|
bUnique = false;
|
|
RemapIndex = UniqueVertIndex;
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(bUnique)
|
|
{
|
|
// Unique
|
|
UniqueVerts.Add((FVector)SourceVert.Position);
|
|
OriginalIndexes.Add(VertIndex);
|
|
IndexRemap[VertIndex] = UniqueVerts.Num() - 1;
|
|
}
|
|
else
|
|
{
|
|
IndexRemap[VertIndex] = RemapIndex;
|
|
}
|
|
}
|
|
}
|
|
|
|
const int32 NumUniqueVerts = UniqueVerts.Num();
|
|
|
|
// If we're going to remap the parameters we need to cache the remap source
|
|
// data. We copy it here incase the destination and remap source
|
|
// lod models are aliased (as in a reimport)
|
|
TArray<FVector3f> CachedPositions;
|
|
TArray<FVector3f> CachedNormals;
|
|
TArray<uint32> CachedIndices;
|
|
int32 NumSourceMasks = 0;
|
|
TArray<FPointWeightMap> SourceMaskCopy;
|
|
|
|
bool bPerformParamterRemap = false;
|
|
|
|
if(InParameterRemapSource)
|
|
{
|
|
const FClothPhysicalMeshData& RemapPhysMesh = InParameterRemapSource->PhysicalMeshData;
|
|
CachedPositions = RemapPhysMesh.Vertices;
|
|
CachedNormals = RemapPhysMesh.Normals;
|
|
CachedIndices = RemapPhysMesh.Indices;
|
|
SourceMaskCopy = InParameterRemapSource->PointWeightMaps;
|
|
NumSourceMasks = SourceMaskCopy.Num();
|
|
|
|
bPerformParamterRemap = true;
|
|
}
|
|
|
|
FClothPhysicalMeshData& PhysMesh = DestLod.PhysicalMeshData;
|
|
PhysMesh.Reset(NumUniqueVerts, NumIndices);
|
|
|
|
const FSkeletalMeshLODModel* const DestLodModel =
|
|
SkeletalResource->LODModels.IsValidIndex(DestLodIndex) ? // The dest section LOD level might not exist yet, that shouldn't prevent a cloth asset LOD creation
|
|
&SkeletalResource->LODModels[DestLodIndex] : nullptr;
|
|
|
|
for(int32 VertexIndex = 0; VertexIndex < NumUniqueVerts; ++VertexIndex)
|
|
{
|
|
const FSoftSkinVertex& SourceVert = SourceSection.SoftVertices[OriginalIndexes[VertexIndex]];
|
|
|
|
PhysMesh.Vertices[VertexIndex] = SourceVert.Position;
|
|
PhysMesh.Normals[VertexIndex] = SourceVert.TangentZ;
|
|
PhysMesh.VertexColors[VertexIndex] = SourceVert.Color;
|
|
|
|
FClothVertBoneData& BoneData = PhysMesh.BoneData[VertexIndex];
|
|
for(int32 InfluenceIndex = 0; InfluenceIndex < MAX_TOTAL_INFLUENCES; ++InfluenceIndex)
|
|
{
|
|
uint16 SourceIndex = SourceSection.BoneMap[SourceVert.InfluenceBones[InfluenceIndex]];
|
|
|
|
// If the current bone is not active in the destination LOD, then remap to the first ancestor bone that is
|
|
while (DestLodModel && !DestLodModel->ActiveBoneIndices.Contains(SourceIndex))
|
|
{
|
|
SourceIndex = SourceMesh->GetRefSkeleton().GetParentIndex(SourceIndex);
|
|
}
|
|
|
|
if(SourceIndex != INDEX_NONE)
|
|
{
|
|
FName BoneName = SourceMesh->GetRefSkeleton().GetBoneName(SourceIndex);
|
|
BoneData.BoneIndices[InfluenceIndex] = DestAsset->UsedBoneNames.AddUnique(BoneName);
|
|
BoneData.BoneWeights[InfluenceIndex] = (float)SourceVert.InfluenceWeights[InfluenceIndex] / UE::AnimationCore::MaxRawBoneWeightFloat;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add a max distance parameter mask to the physics mesh
|
|
FPointWeightMap& PhysMeshMaxDistances = PhysMesh.AddWeightMap(EWeightMapTargetCommon::MaxDistance);
|
|
PhysMeshMaxDistances.Initialize(PhysMesh.Vertices.Num());
|
|
|
|
// Add a max distance parameter mask to the LOD
|
|
DestLod.PointWeightMaps.AddDefaulted();
|
|
FPointWeightMap& LodMaxDistances = DestLod.PointWeightMaps.Last();
|
|
LodMaxDistances.Initialize(PhysMeshMaxDistances, EWeightMapTargetCommon::MaxDistance);
|
|
|
|
PhysMesh.MaxBoneWeights = SourceSection.MaxBoneInfluences;
|
|
|
|
for(int32 IndexIndex = 0; IndexIndex < NumIndices; ++IndexIndex)
|
|
{
|
|
PhysMesh.Indices[IndexIndex] = SourceLod.IndexBuffer[BaseIndex + IndexIndex] - BaseVertexIndex;
|
|
PhysMesh.Indices[IndexIndex] = IndexRemap[PhysMesh.Indices[IndexIndex]];
|
|
}
|
|
|
|
// Validate the generated triangles. If the source mesh has colinear triangles then clothing simulation will fail
|
|
const int32 NumTriangles = PhysMesh.Indices.Num() / 3;
|
|
for(int32 TriIndex = 0; TriIndex < NumTriangles; ++TriIndex)
|
|
{
|
|
FVector A = (FVector)PhysMesh.Vertices[PhysMesh.Indices[TriIndex * 3 + 0]];
|
|
FVector B = (FVector)PhysMesh.Vertices[PhysMesh.Indices[TriIndex * 3 + 1]];
|
|
FVector C = (FVector)PhysMesh.Vertices[PhysMesh.Indices[TriIndex * 3 + 2]];
|
|
|
|
FVector TriNormal = (B - A) ^ (C - A);
|
|
if(TriNormal.SizeSquared() <= SMALL_NUMBER)
|
|
{
|
|
// This triangle is colinear
|
|
LogAndToastWarning(FText::Format(LOCTEXT("Colinear_Error", "Failed to generate clothing sim mesh due to degenerate triangle, found conincident vertices in triangle A={0} B={1} C={2}"), FText::FromString(A.ToString()), FText::FromString(B.ToString()), FText::FromString(C.ToString())));
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if(bPerformParamterRemap)
|
|
{
|
|
ClothingMeshUtils::FVertexParameterMapper ParameterRemapper(PhysMesh.Vertices, PhysMesh.Normals, CachedPositions, CachedNormals, CachedIndices);
|
|
|
|
DestLod.PointWeightMaps.Reset(NumSourceMasks);
|
|
|
|
for(int32 MaskIndex = 0; MaskIndex < NumSourceMasks; ++MaskIndex)
|
|
{
|
|
const FPointWeightMap& SourceMask = SourceMaskCopy[MaskIndex];
|
|
|
|
DestLod.PointWeightMaps.AddDefaulted();
|
|
FPointWeightMap& DestMask = DestLod.PointWeightMaps.Last();
|
|
|
|
DestMask.Initialize(PhysMesh.Vertices.Num());
|
|
DestMask.CurrentTarget = SourceMask.CurrentTarget;
|
|
DestMask.bEnabled = SourceMask.bEnabled;
|
|
|
|
ParameterRemapper.Map(SourceMask.Values, DestMask.Values);
|
|
}
|
|
|
|
DestAsset->ApplyParameterMasks();
|
|
}
|
|
|
|
int32 LODVertexBudget;
|
|
if (GConfig->GetInt(TEXT("ClothSettings"), TEXT("LODVertexBudget"), LODVertexBudget, GEditorIni) && LODVertexBudget > 0 && NumUniqueVerts > LODVertexBudget)
|
|
{
|
|
LogAndToastWarning(FText::Format(LOCTEXT("LODVertexBudgetWarning", "This cloth LOD has {0} more vertices than what is budgeted on this project (current={1}, budget={2})"),
|
|
NumUniqueVerts - LODVertexBudget,
|
|
NumUniqueVerts,
|
|
LODVertexBudget));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
#undef LOCTEXT_NAMESPACE
|