// Copyright Epic Games, Inc. All Rights Reserved. /*============================================================================= GeometryCollection.cpp: UGeometryCollection methods. =============================================================================*/ #include "GeometryCollection/GeometryCollectionObject.h" #include "GeometryCollection/GeometryCollection.h" #include "GeometryCollection/GeometryCollectionCache.h" #include "UObject/DestructionObjectVersion.h" #include "UObject/UE5MainStreamObjectVersion.h" #include "Serialization/ArchiveCountMem.h" #include "HAL/IConsoleManager.h" #include "Interfaces/ITargetPlatform.h" #include "UObject/Package.h" #include "Materials/MaterialInstance.h" #include "ProfilingDebugging/CookStats.h" #if WITH_EDITOR #include "GeometryCollection/DerivedDataGeometryCollectionCooker.h" #include "GeometryCollection/GeometryCollectionComponent.h" #include "DerivedDataCacheInterface.h" #include "Serialization/MemoryReader.h" #include "NaniteBuilder.h" #include "Rendering/NaniteResources.h" // TODO: Temp until new asset-agnostic builder API #include "StaticMeshResources.h" #endif #include "GeometryCollection/GeometryCollectionSimulationCoreTypes.h" #include "Chaos/ChaosArchive.h" #include "GeometryCollectionProxyData.h" DEFINE_LOG_CATEGORY_STATIC(UGeometryCollectionLogging, NoLogging, All); #if ENABLE_COOK_STATS namespace GeometryCollectionCookStats { static FCookStats::FDDCResourceUsageStats UsageStats; static FCookStatsManager::FAutoRegisterCallback RegisterCookStats([](FCookStatsManager::AddStatFuncRef AddStat) { UsageStats.LogStats(AddStat, TEXT("GeometryCollection.Usage"), TEXT("")); }); } #endif UGeometryCollection::UGeometryCollection(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) , EnableNanite(false) , CollisionType(ECollisionTypeEnum::Chaos_Surface_Volumetric) , ImplicitType(EImplicitTypeEnum::Chaos_Implicit_Box) , MinLevelSetResolution(10) , MaxLevelSetResolution(10) , MinClusterLevelSetResolution(50) , MaxClusterLevelSetResolution(50) , CollisionObjectReductionPercentage(0.0f) , bMassAsDensity(false) , Mass(1.0f) , MinimumMassClamp(0.1f) , CollisionParticlesFraction(1.0f) , MaximumCollisionParticles(60) , EnableRemovePiecesOnFracture(false) , GeometryCollection(new FGeometryCollection()) { PersistentGuid = FGuid::NewGuid(); InvalidateCollection(); } FGeometryCollectionSizeSpecificData::FGeometryCollectionSizeSpecificData() : MaxSize(0.0f) , CollisionType(ECollisionTypeEnum::Chaos_Surface_Volumetric) , ImplicitType(EImplicitTypeEnum::Chaos_Implicit_Box) , MinLevelSetResolution(5) , MaxLevelSetResolution(10) , MinClusterLevelSetResolution(25) , MaxClusterLevelSetResolution(50) , CollisionObjectReductionPercentage(0.0f) , CollisionParticlesFraction(1.0f) , MaximumCollisionParticles(60) { } void FillSharedSimulationSizeSpecificData(FSharedSimulationSizeSpecificData& ToData, const FGeometryCollectionSizeSpecificData& FromData) { ToData.CollisionType = FromData.CollisionType; ToData.ImplicitType = FromData.ImplicitType; ToData.MaxSize = FromData.MaxSize; ToData.MinLevelSetResolution = FromData.MinLevelSetResolution; ToData.MaxLevelSetResolution = FromData.MaxLevelSetResolution; ToData.MinClusterLevelSetResolution = FromData.MinClusterLevelSetResolution; ToData.MaxClusterLevelSetResolution = FromData.MaxClusterLevelSetResolution; ToData.CollisionObjectReductionPercentage = FromData.CollisionObjectReductionPercentage; ToData.CollisionParticlesFraction = FromData.CollisionParticlesFraction; ToData.MaximumCollisionParticles = FromData.MaximumCollisionParticles; } float KgCm3ToKgM3(float Density) { return Density * 1000000; } float KgM3ToKgCm3(float Density) { return Density / 1000000; } void UGeometryCollection::GetSharedSimulationParams(FSharedSimulationParameters& OutParams) const { OutParams.bMassAsDensity = bMassAsDensity; OutParams.Mass = bMassAsDensity ? KgM3ToKgCm3(Mass) : Mass; //todo(ocohen): we still have the solver working in old units. This is mainly to fix ui issues. Long term need to normalize units for best precision OutParams.MinimumMassClamp = MinimumMassClamp; OutParams.MaximumCollisionParticleCount = MaximumCollisionParticles; FGeometryCollectionSizeSpecificData InfSize; InfSize.MaxSize = FLT_MAX; InfSize.CollisionType = CollisionType; InfSize.ImplicitType = ImplicitType; InfSize.MinLevelSetResolution = MinLevelSetResolution; InfSize.MaxLevelSetResolution = MaxLevelSetResolution; InfSize.MinClusterLevelSetResolution = MinClusterLevelSetResolution; InfSize.MaxClusterLevelSetResolution = MaxClusterLevelSetResolution; InfSize.CollisionObjectReductionPercentage = CollisionObjectReductionPercentage; InfSize.CollisionParticlesFraction = CollisionParticlesFraction; InfSize.MaximumCollisionParticles = MaximumCollisionParticles; OutParams.SizeSpecificData.SetNum(SizeSpecificData.Num() + 1); FillSharedSimulationSizeSpecificData(OutParams.SizeSpecificData[0], InfSize); for (int32 Idx = 0; Idx < SizeSpecificData.Num(); ++Idx) { FillSharedSimulationSizeSpecificData(OutParams.SizeSpecificData[Idx+1], SizeSpecificData[Idx]); } if (EnableRemovePiecesOnFracture) { FixupRemoveOnFractureMaterials(OutParams); } OutParams.SizeSpecificData.Sort(); //can we do this at editor time on post edit change? } void UGeometryCollection::FixupRemoveOnFractureMaterials(FSharedSimulationParameters& SharedParms) const { // Match RemoveOnFracture materials with materials in model and record the material indices int32 NumMaterials = Materials.Num(); for (int32 MaterialIndex = 0; MaterialIndex < NumMaterials; MaterialIndex++) { UMaterialInterface* MaterialInfo = Materials[MaterialIndex]; for (int32 ROFMaterialIndex = 0; ROFMaterialIndex < RemoveOnFractureMaterials.Num(); ROFMaterialIndex++) { if (MaterialInfo == RemoveOnFractureMaterials[ROFMaterialIndex]) { SharedParms.RemoveOnFractureIndices.Add(MaterialIndex); break; } } } } /** AppendGeometry */ int32 UGeometryCollection::AppendGeometry(const UGeometryCollection & Element, bool ReindexAllMaterials, const FTransform& TransformRoot) { Modify(); InvalidateCollection(); // add all materials // if there are none, we assume all material assignments in Element are shared by this GeometryCollection // otherwise, we assume all assignments come from the contained materials int32 MaterialIDOffset = 0; if (Element.Materials.Num() > 0) { MaterialIDOffset = Materials.Num(); Materials.Append(Element.Materials); } return GeometryCollection->AppendGeometry(*Element.GetGeometryCollection(), MaterialIDOffset, ReindexAllMaterials, TransformRoot); } /** NumElements */ int32 UGeometryCollection::NumElements(const FName & Group) const { return GeometryCollection->NumElements(Group); } /** RemoveElements */ void UGeometryCollection::RemoveElements(const FName & Group, const TArray& SortedDeletionList) { Modify(); GeometryCollection->RemoveElements(Group, SortedDeletionList); InvalidateCollection(); } /** ReindexMaterialSections */ void UGeometryCollection::ReindexMaterialSections() { Modify(); GeometryCollection->ReindexMaterials(); InvalidateCollection(); } void UGeometryCollection::InitializeMaterials() { Modify(); // Consolidate materials // add all materials to a set TSet MaterialSet; for (UMaterialInterface* Curr : Materials) { MaterialSet.Add(Curr); } // create the final material array only containing unique materials // and one slot for each internal material TMap< UMaterialInterface *, int32> MaterialPtrToArrayIndex; TArray FinalMaterials; for (UMaterialInterface *Curr : MaterialSet) { // Add base material TTuple< UMaterialInterface *, int32> CurrTuple(Curr, FinalMaterials.Add(Curr)); MaterialPtrToArrayIndex.Add(CurrTuple); // Add interior material FinalMaterials.Add(Curr); } TManagedArray& MaterialID = GeometryCollection->MaterialID; // Reassign material ID for each face given the new consolidated array of materials for (int32 Material = 0; Material < MaterialID.Num(); ++Material) { if (MaterialID[Material] < Materials.Num()) { UMaterialInterface* OldMaterialPtr = Materials[MaterialID[Material]]; MaterialID[Material] = *MaterialPtrToArrayIndex.Find(OldMaterialPtr); } } // Set new material array on the collection Materials = FinalMaterials; // Last Material is the selection one UMaterialInterface* BoneSelectedMaterial = LoadObject(nullptr, GetSelectedMaterialPath(), nullptr, LOAD_None, nullptr); BoneSelectedMaterialIndex = Materials.Add(BoneSelectedMaterial); GeometryCollection->ReindexMaterials(); InvalidateCollection(); } /** Returns true if there is anything to render */ bool UGeometryCollection::HasVisibleGeometry() const { if(ensureMsgf(GeometryCollection.IsValid(), TEXT("Geometry Collection %s has an invalid internal collection"))) { return GeometryCollection->HasVisibleGeometry(); } return false; } /** Serialize */ void UGeometryCollection::Serialize(FArchive& Ar) { Ar.UsingCustomVersion(FDestructionObjectVersion::GUID); Ar.UsingCustomVersion(FUE5MainStreamObjectVersion::GUID); Chaos::FChaosArchive ChaosAr(Ar); #if WITH_EDITOR //Early versions did not have tagged properties serialize first if (Ar.CustomVer(FDestructionObjectVersion::GUID) < FDestructionObjectVersion::GeometryCollectionInDDC) { GeometryCollection->Serialize(ChaosAr); } if (Ar.CustomVer(FDestructionObjectVersion::GUID) < FDestructionObjectVersion::AddedTimestampedGeometryComponentCache) { if (Ar.IsLoading()) { // Strip old recorded cache data int32 DummyNumFrames; TArray> DummyTransforms; Ar << DummyNumFrames; DummyTransforms.SetNum(DummyNumFrames); for (int32 Index = 0; Index < DummyNumFrames; ++Index) { Ar << DummyTransforms[Index]; } } } else #endif { // Push up the chain to hit tagged properties too // This should have always been in here but because we have saved assets // from before this line was here it has to be gated Super::Serialize(Ar); } if (Ar.CustomVer(FDestructionObjectVersion::GUID) < FDestructionObjectVersion::DensityUnitsChanged) { if (bMassAsDensity) { Mass = KgCm3ToKgM3(Mass); } } bool bIsCookedOrCooking = Ar.IsCooking(); if (Ar.CustomVer(FDestructionObjectVersion::GUID) >= FDestructionObjectVersion::GeometryCollectionInDDC) { Ar << bIsCookedOrCooking; } //new versions serialize geometry collection after tagged properties if (Ar.CustomVer(FDestructionObjectVersion::GUID) >= FDestructionObjectVersion::GeometryCollectionInDDCAndAsset) { #if WITH_EDITOR if (Ar.IsSaving() && !Ar.IsTransacting()) { CreateSimulationDataImp(/*bCopyFromDDC=*/false); //make sure content is built before saving } #endif GeometryCollection->Serialize(ChaosAr); // Fix up the type change for implicits here, previously they were unique ptrs, now they're shared TManagedArray>* OldAttr = GeometryCollection->FindAttributeTyped>(FGeometryDynamicCollection::ImplicitsAttribute, FTransformCollection::TransformGroup); TManagedArray>* NewAttr = GeometryCollection->FindAttributeTyped>(FGeometryDynamicCollection::SharedImplicitsAttribute, FTransformCollection::TransformGroup); if(OldAttr) { if(!NewAttr) { NewAttr = &GeometryCollection->AddAttribute>(FGeometryDynamicCollection::SharedImplicitsAttribute, FTransformCollection::TransformGroup); const int32 NumElems = GeometryCollection->NumElements(FTransformCollection::TransformGroup); for(int32 Index = 0; Index < NumElems; ++Index) { (*NewAttr)[Index] = TSharedPtr((*OldAttr)[Index].Release()); } } GeometryCollection->RemoveAttribute(FGeometryDynamicCollection::ImplicitsAttribute, FTransformCollection::TransformGroup); } } if (Ar.CustomVer(FDestructionObjectVersion::GUID) < FDestructionObjectVersion::GroupAndAttributeNameRemapping) { GeometryCollection->UpdateOldAttributeNames(); InvalidateCollection(); #if WITH_EDITOR CreateSimulationData(); #endif } if (Ar.CustomVer(FUE5MainStreamObjectVersion::GUID) == FUE5MainStreamObjectVersion::GeometryCollectionNaniteData) { // This legacy version serialized structure information into archive, but the data is transient. // Just load it and throw away here, it will be rebuilt later and resaved past this point. int32 NumNaniteResources = 0; Ar << NumNaniteResources; TArray NaniteResources; NaniteResources.Reset(NumNaniteResources); NaniteResources.AddDefaulted(NumNaniteResources); for (int32 ResourceIndex = 0; ResourceIndex < NumNaniteResources; ++ResourceIndex) { NaniteResources[ResourceIndex].Serialize(ChaosAr, this); } } #if WITH_EDITOR //for all versions loaded, make sure sim data is up to date if (Ar.IsLoading()) { EnsureDataIsCooked(); //make sure loaded content is built } #endif } const TCHAR* UGeometryCollection::GetSelectedMaterialPath() const { return TEXT("/Engine/EditorMaterials/GeometryCollection/SelectedGeometryMaterial.SelectedGeometryMaterial"); } #if WITH_EDITOR void UGeometryCollection::CreateSimulationDataImp(bool bCopyFromDDC) { COOK_STAT(auto Timer = GeometryCollectionCookStats::UsageStats.TimeSyncWork()); // Skips the DDC fetch entirely for testing the builder without adding to the DDC const static bool bSkipDDC = false; //Use the DDC to build simulation data. If we are loading in the editor we then serialize this data into the geometry collection TArray DDCData; FDerivedDataGeometryCollectionCooker* GeometryCollectionCooker = new FDerivedDataGeometryCollectionCooker(*this); if (GeometryCollectionCooker->CanBuild()) { if (bSkipDDC) { GeometryCollectionCooker->Build(DDCData); COOK_STAT(Timer.AddMiss(DDCData.Num())); } else { bool bBuilt = false; const bool bSuccess = GetDerivedDataCacheRef().GetSynchronous(GeometryCollectionCooker, DDCData, &bBuilt); COOK_STAT(Timer.AddHitOrMiss(!bSuccess || bBuilt ? FCookStats::CallStats::EHitOrMiss::Miss : FCookStats::CallStats::EHitOrMiss::Hit, DDCData.Num())); } if (bCopyFromDDC) { FMemoryReader Ar(DDCData, true); // Must be persistent for BulkData to serialize Chaos::FChaosArchive ChaosAr(Ar); GeometryCollection->Serialize(ChaosAr); NaniteData = MakeUnique(); NaniteData->Serialize(ChaosAr, this); } } } void UGeometryCollection::CreateSimulationData() { CreateSimulationDataImp(/*bCopyFromDDC=*/false); } TUniquePtr UGeometryCollection::CreateNaniteData(FGeometryCollection* Collection) { TUniquePtr NaniteData; TRACE_CPUPROFILER_EVENT_SCOPE_TEXT(TEXT("UGeometryCollection::CreateNaniteData")); Nanite::IBuilderModule& NaniteBuilderModule = Nanite::IBuilderModule::Get(); NaniteData = MakeUnique(); // Transform Group const TManagedArray& TransformToGeometryIndexArray = Collection->TransformToGeometryIndex; const TManagedArray& SimulationTypeArray = Collection->SimulationType; const TManagedArray& StatusFlagsArray = Collection->StatusFlags; // Vertices Group const TManagedArray& VertexArray = Collection->Vertex; const TManagedArray& UVArray = Collection->UV; const TManagedArray& ColorArray = Collection->Color; const TManagedArray& TangentUArray = Collection->TangentU; const TManagedArray& TangentVArray = Collection->TangentV; const TManagedArray& NormalArray = Collection->Normal; const TManagedArray& BoneMapArray = Collection->BoneMap; // Faces Group const TManagedArray& IndicesArray = Collection->Indices; const TManagedArray& VisibleArray = Collection->Visible; const TManagedArray& MaterialIndexArray = Collection->MaterialIndex; const TManagedArray& MaterialIDArray = Collection->MaterialID; // Geometry Group const TManagedArray& TransformIndexArray = Collection->TransformIndex; const TManagedArray& BoundingBoxArray = Collection->BoundingBox; const TManagedArray& InnerRadiusArray = Collection->InnerRadius; const TManagedArray& OuterRadiusArray = Collection->OuterRadius; const TManagedArray& VertexStartArray = Collection->VertexStart; const TManagedArray& VertexCountArray = Collection->VertexCount; const TManagedArray& FaceStartArray = Collection->FaceStart; const TManagedArray& FaceCountArray = Collection->FaceCount; // Material Group const TManagedArray& Sections = Collection->Sections; int32 NumGeometry = Collection->NumElements(FGeometryCollection::GeometryGroup); NaniteData->Resources.AddDefaulted(NumGeometry); for (int32 GeometryGroupIndex = 0; GeometryGroupIndex < NumGeometry; GeometryGroupIndex++) { Nanite::FResources& NaniteResource = NaniteData->Resources[GeometryGroupIndex]; NaniteResource = {}; uint32 NumTexCoords = 1;// NumTextureCoord; bool bHasColors = ColorArray.Num() > 0; const int32 VertexStart = VertexStartArray[GeometryGroupIndex]; const int32 VertexCount = VertexCountArray[GeometryGroupIndex]; TArray BuildVertices; BuildVertices.Reserve(VertexCount); for (int32 VertexIndex = 0; VertexIndex < VertexCount; ++VertexIndex) { FStaticMeshBuildVertex& Vertex = BuildVertices.Emplace_GetRef(); Vertex.Position = VertexArray[VertexStart + VertexIndex]; Vertex.Color = bHasColors ? ColorArray[VertexStart + VertexIndex].ToFColor(false /* sRGB */) : FColor::White; Vertex.TangentX = FVector::ZeroVector; Vertex.TangentY = FVector::ZeroVector; Vertex.TangentZ = NormalArray[VertexStart + VertexIndex]; Vertex.UVs[0] = UVArray[VertexStart + VertexIndex]; if (Vertex.UVs[0].ContainsNaN()) { Vertex.UVs[0] = FVector2D::ZeroVector; } } const int32 FaceStart = FaceStartArray[GeometryGroupIndex]; const int32 FaceCount = FaceCountArray[GeometryGroupIndex]; // TODO: Respect multiple materials like in FGeometryCollectionConversion::AppendStaticMesh TArray MaterialIndices; MaterialIndices.Reserve(FaceCount); TArray BuildIndices; BuildIndices.Reserve(FaceCount * 3); for (int32 FaceIndex = 0; FaceIndex < FaceCount; ++FaceIndex) { if (!VisibleArray[FaceStart + FaceIndex]) // TODO: Always in range? { continue; } const FIntVector FaceIndices = IndicesArray[FaceStart + FaceIndex]; BuildIndices.Add(FaceIndices.X - VertexStart); BuildIndices.Add(FaceIndices.Y - VertexStart); BuildIndices.Add(FaceIndices.Z - VertexStart); const int32 MaterialIndex = MaterialIDArray[FaceStart + FaceIndex]; MaterialIndices.Add(MaterialIndex); } if (BuildIndices.Num() == 0) { // No visible faces of entire geometry, skip any building/rendering. continue; } FMeshNaniteSettings NaniteSettings = {}; NaniteSettings.bEnabled = true; NaniteSettings.PercentTriangles = 1.0f; // 100% - no reduction if (!NaniteBuilderModule.Build(NaniteResource, BuildVertices, BuildIndices, MaterialIndices, NumTexCoords, NaniteSettings)) { UE_LOG(LogStaticMesh, Error, TEXT("Failed to build Nanite for geometry collection. See previous line(s) for details.")); } } return NaniteData; } #endif void UGeometryCollection::InitResources() { if (NaniteData) { NaniteData->InitResources(this); } } void UGeometryCollection::ReleaseResources() { if (NaniteData) { NaniteData->ReleaseResources(); } } void UGeometryCollection::InvalidateCollection() { StateGuid = FGuid::NewGuid(); } FGuid UGeometryCollection::GetIdGuid() const { return PersistentGuid; } FGuid UGeometryCollection::GetStateGuid() const { return StateGuid; } #if WITH_EDITOR void UGeometryCollection::PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent) { if (PropertyChangedEvent.Property) { if (PropertyChangedEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED(UGeometryCollection, EnableNanite)) { InvalidateCollection(); EnsureDataIsCooked(); if (FApp::CanEverRender()) { InitResources(); } } else if (PropertyChangedEvent.Property->GetFName() != GET_MEMBER_NAME_CHECKED(UGeometryCollection, Materials)) { InvalidateCollection(); CreateSimulationData(); } } } bool UGeometryCollection::Modify(bool bAlwaysMarkDirty /*= true*/) { bool bSuperResult = Super::Modify(bAlwaysMarkDirty); UPackage* Package = GetOutermost(); if (Package->IsDirty()) { InvalidateCollection(); } return bSuperResult; } void UGeometryCollection::EnsureDataIsCooked() { if (StateGuid != LastBuiltGuid) { CreateSimulationDataImp(/*bCopyFromDDC=*/ true); LastBuiltGuid = StateGuid; } } #endif void UGeometryCollection::PostLoad() { Super::PostLoad(); // Initialize rendering resources. if (FApp::CanEverRender()) { InitResources(); } } void UGeometryCollection::BeginDestroy() { Super::BeginDestroy(); ReleaseResources(); } FGeometryCollectionNaniteData::FGeometryCollectionNaniteData() { } FGeometryCollectionNaniteData::~FGeometryCollectionNaniteData() { } void FGeometryCollectionNaniteData::Serialize(FArchive& Ar, UGeometryCollection* Owner) { if (Ar.IsSaving()) { if (Owner->EnableNanite) { // Nanite data is currently 1:1 with each geometry group in the collection. const int32 NumGeometryGroups = Owner->NumElements(FGeometryCollection::GeometryGroup); if (NumGeometryGroups != Resources.Num()) { Ar.SetError(); } } int32 NumNaniteResources = Resources.Num(); Ar << NumNaniteResources; for (int32 ResourceIndex = 0; ResourceIndex < NumNaniteResources; ++ResourceIndex) { #if WITH_EDITOR // HACK/TODO: Decompress data on platforms that already support LZ decompression in hardware. if (Ar.IsCooking() && Ar.CookingTarget()->SupportsFeature(ETargetPlatformFeatures::HardwareLZDecompression)) { Resources[ResourceIndex].DecompressPages(); } #endif Resources[ResourceIndex].Serialize(Ar, Owner); } } else if (Ar.IsLoading()) { int32 NumNaniteResources = 0; Ar << NumNaniteResources; Resources.Reset(NumNaniteResources); Resources.AddDefaulted(NumNaniteResources); for (int32 ResourceIndex = 0; ResourceIndex < NumNaniteResources; ++ResourceIndex) { Resources[ResourceIndex].Serialize(Ar, Owner); } if (!Owner->EnableNanite) { Resources.Reset(0); } } } void FGeometryCollectionNaniteData::InitResources(UGeometryCollection* Owner) { if (bIsInitialized) { ReleaseResources(); } for (Nanite::FResources& Resource : Resources) { Resource.InitResources(); } bIsInitialized = true; } void FGeometryCollectionNaniteData::ReleaseResources() { if (!bIsInitialized) { return; } for (Nanite::FResources& Resource : Resources) { Resource.ReleaseResources(); } bIsInitialized = false; }