Files
UnrealEngineUWP/Engine/Source/Editor/ClothingSystemEditor/Private/ClothingAssetFactory.cpp
halfdan ingvarsson e2210cb024 Support for 16-bit skin weights on the skelmesh.
The main change is that FSoftSkinVertex, used by FSkeletalMeshLODModel, in now stores weights as 16-bit after conversion from the import data. This increases the size of each FSoftSkinVertex from 144 bytes to 160 bytes (about 10% increase). By default render meshes still use 8-bit skin weights, with weights downshifted from the 16-bit modeling data, so no change in GPU memory consumption there. However, the vertex buffer will automatically return a 16-bit skin weights when requested from the GPU side (e.g. for CPU skinning and viewing tangents).

This change in the model data and vertex buffer CPU-side query, resulted in many changes throughout the codebase and will have an effect on licensees who are actively reading from and writing to these two storage locations.

The GPU skin cache shader has had one more permutation added when not using unlimited skin weights. The vertex factory is not affected.

#jira UE-164386
#rb alexis.matte, josie.yang
#preflight 632c0c5ab4515b7e22b4804d

[CL 22215219 by halfdan ingvarsson in ue5-main branch]
2022-09-27 19:48:05 -04:00

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