// Copyright Epic Games, Inc. All Rights Reserved. #include "ChaosCache/FleshComponentCacheAdapter.h" #include "Chaos/ChaosCache.h" #include "Chaos/CacheManagerActor.h" #include "Chaos/PBDEvolution.h" #include "ChaosFlesh/ChaosDeformableSolverComponent.h" #include "ChaosFlesh/FleshComponent.h" #if USE_USD_SDK && DO_USD_CACHING #include "HAL/PlatformFile.h" #include "HAL/PlatformFileManager.h" #include "Misc/Paths.h" #include "ChaosCachingUSD/Operations.h" #include "USDConversionUtils.h" // for GetPrimPathForObject() #include "USDMemory.h" #endif // USE_USD_SDK && DO_USD_CACHING DEFINE_LOG_CATEGORY(LogChaosFleshCache) namespace Chaos { FFleshCacheAdapterCVarParams CVarParams; #if USE_USD_SDK && DO_USD_CACHING FAutoConsoleVariableRef CVarDeformableFleshCacheWriteBinaryUSD( TEXT("p.Chaos.Caching.USD.WriteBinary"), CVarParams.bWriteBinary, TEXT("Write binary (usdc) cache files. [def: true]")); FAutoConsoleVariableRef CVarDeformableFleshCacheNoClobberUSD( TEXT("p.Chaos.Caching.USD.NoClobber"), CVarParams.bNoClobber, TEXT("Rename rather than over write existing cach files. [def: true]")); FAutoConsoleVariableRef CVarDeformableFleshCacheSaveFrequency( TEXT("p.Chaos.Caching.USD.SaveFrequency"), CVarParams.SaveFrequency, TEXT("Interval in frames to flush USD data to disk. 2 saves every other frame, 1 saves every frame, 0 caches in memory until complete. [def: 10]")); #endif // USE_USD_SDK && DO_USD_CACHING FFleshCacheAdapter::~FFleshCacheAdapter() { #if USE_USD_SDK && DO_USD_CACHING for(TPair& PrimitiveStage : PrimitiveStages) { if(PrimitiveStage.Value.MonolithStage) { UE::ChaosCachingUSD::CloseStage(PrimitiveStage.Value.MonolithStage); } } #endif // USE_USD_SDK && DO_USD_CACHING } FComponentCacheAdapter::SupportType FFleshCacheAdapter::SupportsComponentClass(UClass* InComponentClass) const { const UClass* Desired = GetDesiredClass(); if(InComponentClass == Desired) { return FComponentCacheAdapter::SupportType::Direct; } else if(InComponentClass->IsChildOf(Desired)) { return FComponentCacheAdapter::SupportType::Derived; } return FComponentCacheAdapter::SupportType::None; } UClass* FFleshCacheAdapter::GetDesiredClass() const { return UFleshComponent::StaticClass(); } uint8 FFleshCacheAdapter::GetPriority() const { return EngineAdapterPriorityBegin; } void FFleshCacheAdapter::Record_PostSolve(UPrimitiveComponent* InComponent, const FTransform& InRootTransform, FPendingFrameWrite& OutFrame, Chaos::FReal InTime) const { if( FDeformableSolver* Solver = GetDeformableSolver(InComponent)) { FDeformableSolver::FPhysicsThreadAccess PhysicsThreadAccess(Solver, Chaos::Softs::FPhysicsThreadAccessor()); if (FEvolution* Evolution = PhysicsThreadAccess.GetEvolution()) { const FParticles& Particles = Evolution->Particles(); const uint32 NumParticles = Particles.Size(); //We always write a cache even if there are no particles { #if USE_USD_SDK && DO_USD_CACHING FScopedUsdAllocs UsdAllocs; // Use USD memory allocator FPrimitiveStage& PrimitiveStage = PrimitiveStages.FindOrAdd(InComponent); const Chaos::FRange ParticleRange = GetParticleRange(InComponent, NumParticles); // Update time range. PrimitiveStage.MinTime = FMath::Min(InTime, PrimitiveStage.MinTime); PrimitiveStage.MaxTime = FMath::Max(InTime, PrimitiveStage.MaxTime); if (PrimitiveStage.MonolithStage) { if (!UE::ChaosCachingUSD::WritePoints(PrimitiveStage.MonolithStage, PrimitiveStage.PrimPath, InTime, Particles.XArray(), Particles.GetV(), ParticleRange)) { UE_LOG(LogChaosFleshCache, Error, TEXT("Failed to write points '%s' at time %g to file: '%s'"), *PrimitiveStage.PrimPath, InTime, *PrimitiveStage.MonolithStage.GetRootLayer().GetDisplayName()); return; } // Write muscle activation const TArray ParticleMuscleActivation = PhysicsThreadAccess.GetParticleMuscleActivation(); if (!UE::ChaosCachingUSD::WriteActivations(PrimitiveStage.MonolithStage, PrimitiveStage.PrimPath, InTime, ParticleMuscleActivation, ParticleRange)) { UE_LOG(LogChaosFleshCache, Error, TEXT("Failed to write activations '%s' at time %g to file: '%s'"), *PrimitiveStage.PrimPath, InTime, *PrimitiveStage.MonolithStage.GetRootLayer().GetDisplayName()); return; } uint64 NumTimeSamples = UE::ChaosCachingUSD::GetNumTimeSamples(PrimitiveStage.MonolithStage, PrimitiveStage.PrimPath, UE::ChaosCachingUSD::GetPointsAttrName()); if (CVarParams.SaveFrequency >= 1 && NumTimeSamples % CVarParams.SaveFrequency == 0) { if (!UE::ChaosCachingUSD::SaveStage(PrimitiveStage.MonolithStage, PrimitiveStage.MinTime, PrimitiveStage.MaxTime)) { UE_LOG(LogChaosFleshCache, Error, TEXT("Failed to save file: '%s'"), *PrimitiveStage.MonolithStage.GetRootLayer().GetDisplayName()); } } } #else // USE_USD_SDK && DO_USD_CACHING const Softs::FSolverVec3* ParticleXs = &Particles.X(0); const Softs::FSolverVec3* ParticleVs = &Particles.V(0); TArray PendingVX, PendingVY, PendingVZ, PendingPX, PendingPY, PendingPZ; TArray& PendingID = OutFrame.PendingChannelsIndices; PendingID.Reserve(NumParticles); PendingVX.Reserve(NumParticles); PendingVY.Reserve(NumParticles); PendingVZ.Reserve(NumParticles); PendingPX.Reserve(NumParticles); PendingPY.Reserve(NumParticles); PendingPZ.Reserve(NumParticles); for (uint32 ParticleIndex = 0; ParticleIndex < NumParticles; ++ParticleIndex) { const Softs::FSolverVec3& ParticleV = ParticleVs[ParticleIndex]; const Softs::FSolverVec3& ParticleX = ParticleXs[ParticleIndex]; // Adding the vertices relative position to the particle write datas PendingID.Add(ParticleIndex); PendingVX.Add(ParticleV.X); PendingVY.Add(ParticleV.Y); PendingVZ.Add(ParticleV.Z); PendingPX.Add(ParticleX.X); PendingPY.Add(ParticleX.Y); PendingPZ.Add(ParticleX.Z); } OutFrame.PendingChannelsData.Add(VelocityXName, PendingVX); OutFrame.PendingChannelsData.Add(VelocityYName, PendingVY); OutFrame.PendingChannelsData.Add(VelocityZName, PendingVZ); OutFrame.PendingChannelsData.Add(PositionXName, PendingPX); OutFrame.PendingChannelsData.Add(PositionYName, PendingPY); OutFrame.PendingChannelsData.Add(PositionZName, PendingPZ); #endif // USE_USD_SDK && DO_USD_CACHING } } } } void FFleshCacheAdapter::LoadCacheAtTime(UPrimitiveComponent* PrimitiveComponent, Chaos::FReal TargetTime, const int32 NumParticles, const bool bNeedsRange, const TFunction& LoadFunction) const { #if USE_USD_SDK && DO_USD_CACHING const FPrimitiveStage& PrimitiveStage = PrimitiveStages.FindOrAdd(PrimitiveComponent); if (PrimitiveStage.MonolithStage) { // GetParticleRange only called in play/record mode (not load), so PT only const Chaos::FRange ParticleRange = bNeedsRange ? GetParticleRange(PrimitiveComponent, NumParticles) : Chaos::FRange(0,NumParticles); const int32 NumComponentParticles = ParticleRange.Count; FScopedUsdAllocs UEAllocs; // Use USD memory allocator pxr::VtArray Points0, Points1; pxr::VtArray Vels0, Vels1; pxr::VtArray Activations0, Activations1; double Prev = -TNumericLimits::Max(); double Next = -TNumericLimits::Max(); double PrevV = -TNumericLimits::Max(); double NextV = -TNumericLimits::Max(); if (!UE::ChaosCachingUSD::GetBracketingTimeSamples( PrimitiveStage.MonolithStage, PrimitiveStage.PrimPath, UE::ChaosCachingUSD::GetPointsAttrName(), TargetTime, &Prev, &Next) || !UE::ChaosCachingUSD::GetBracketingTimeSamples( PrimitiveStage.MonolithStage, PrimitiveStage.PrimPath, UE::ChaosCachingUSD::GetVelocityAttrName(), TargetTime, &PrevV, &NextV) || Prev != PrevV || Next != NextV) { UE_LOG(LogChaosFleshCache, Error, TEXT("Inconsistent bracketing time samples for attributes '%s' and '%s' at frame %g from file: '%s'"), *UE::ChaosCachingUSD::GetPointsAttrName(), *UE::ChaosCachingUSD::GetVelocityAttrName(), TargetTime, *PrimitiveStage.MonolithStage.GetRootLayer().GetDisplayName()); return; } if (!UE::ChaosCachingUSD::ReadPoints(PrimitiveStage.MonolithStage, PrimitiveStage.PrimPath, Prev, Points0, Vels0) || Points0.size() != Vels0.size()) { UE_LOG(LogChaosFleshCache, Error, TEXT("Failed to read points '%s' at time %g from file: '%s'"), *PrimitiveStage.PrimPath, Prev, *PrimitiveStage.MonolithStage.GetRootLayer().GetDisplayName()); return; } // load muscle activation if (!UE::ChaosCachingUSD::ReadMuscleActivation(PrimitiveStage.MonolithStage, PrimitiveStage.PrimPath, Prev, Activations0) || Points0.size() != Activations0.size()) { Activations0.assign(Points0.size(), -1.f); } int32 NumCachedParticles = static_cast(Points0.size()); if (NumCachedParticles > NumComponentParticles) { // Cached particles doesn't match solver particles. Truncate. NumCachedParticles = NumComponentParticles; } // < time range start, > time range end, or exact hit if (FMath::IsNearlyEqual(Prev, Next)) { // Directly set the result of the cache into the solver particles for (int32 CachedIndex = 0; CachedIndex < NumCachedParticles; ++CachedIndex) { // Note that VtArray::operator[] is non-const access and will cause trigger // the copy-on-write memcopy! VtArray::cdata() avoids that. const pxr::GfVec3f& Pos = Points0.cdata()[CachedIndex]; const pxr::GfVec3f& Vel = Vels0.cdata()[CachedIndex]; LoadFunction(CachedIndex, ParticleRange.Start, FVector3f(Pos[0], Pos[1], Pos[2]), FVector3f(Vel[0], Vel[1], Vel[2]), Activations0.cdata()[CachedIndex]); } return; } if (!UE::ChaosCachingUSD::ReadPoints(PrimitiveStage.MonolithStage, PrimitiveStage.PrimPath, Next, Points1, Vels1) || Points1.size() != Vels1.size() || Points0.size() != Points1.size()) { UE_LOG(LogChaosFleshCache, Error, TEXT("Failed to read points '%s' at time %g from file: '%s'"), *PrimitiveStage.PrimPath, Next, *PrimitiveStage.MonolithStage.GetRootLayer().GetDisplayName()); return; } // load muscle activation if (!UE::ChaosCachingUSD::ReadMuscleActivation(PrimitiveStage.MonolithStage, PrimitiveStage.PrimPath, Prev, Activations1) || Points1.size() != Activations1.size()) { Activations1.assign(Points1.size(), -1.f); } double Duration = Next - Prev; double Alpha = Duration > UE_SMALL_NUMBER ? (TargetTime - Prev) / Duration : 0.5; for (int32 CachedIndex = 0; CachedIndex < NumCachedParticles; ++CachedIndex) { // Note that VtArray::operator[] is non-const access and will cause trigger // the copy-on-write memcopy! VtArray::cdata() avoids that. const pxr::GfVec3f& P0 = Points0.cdata()[CachedIndex]; const pxr::GfVec3f& P1 = Points1.cdata()[CachedIndex]; const pxr::GfVec3f& V0 = Vels0.cdata()[CachedIndex]; const pxr::GfVec3f& V1 = Vels1.cdata()[CachedIndex]; pxr::GfVec3f Pos = (1.0 - Alpha) * P0 + Alpha * P1; pxr::GfVec3f Vel = (1.0 - Alpha) * V0 + Alpha * V1; float Activation = (1.0 - Alpha) * Activations0.cdata()[CachedIndex] + Alpha * Activations1.cdata()[CachedIndex]; LoadFunction(CachedIndex, ParticleRange.Start, FVector3f(Pos[0], Pos[1], Pos[2]), FVector3f(Vel[0], Vel[1], Vel[2]), Activation); } } #endif } void FFleshCacheAdapter::Playback_PreSolve(UPrimitiveComponent* InComponent, UChaosCache* InCache, Chaos::FReal InTime, FPlaybackTickRecord& TickRecord, TArray*>& OutUpdatedRigids) const { if (FDeformableSolver* Solver = GetDeformableSolver(InComponent)) { FDeformableSolver::FPhysicsThreadAccess PhysicsThreadAccess(Solver, Softs::FPhysicsThreadAccessor()); if (FEvolution* Evolution = PhysicsThreadAccess.GetEvolution()) { #if USE_USD_SDK && DO_USD_CACHING FParticles& Particles = Evolution->Particles(); const int32 NumParticles = Particles.Size(); Softs::FSolverVec3* ParticleXs = &Particles.X(0); Softs::FSolverVec3* ParticleVs = &Particles.V(0); Softs::FSolverReal* ParticleInvMs = &Particles.InvM(0); Softs::FPAndInvM* ParticlePAndInvMs = &Particles.PAndInvM(0); LoadCacheAtTime(InComponent, InTime, NumParticles, true, [&ParticleXs, &ParticleVs, &ParticleInvMs, &ParticlePAndInvMs]( const int32 ParticleIndex, const int32 ParticleOffset, const FVector3f& ParticlePosition, const FVector3f& ParticleVelocity, const float MuscleActivation) { const int32 GlobalIndex = ParticleIndex + ParticleOffset; ParticleXs[GlobalIndex].Set(ParticlePosition[0], ParticlePosition[1], ParticlePosition[2]); ParticleVs[GlobalIndex].Set(ParticleVelocity[0], ParticleVelocity[1], ParticleVelocity[2]); ParticleInvMs[GlobalIndex] = Softs::FSolverReal(0); ParticlePAndInvMs[GlobalIndex].InvM = Softs::FSolverReal(0); ParticlePAndInvMs[GlobalIndex].P = ParticleXs[GlobalIndex]; }); if(UFleshComponent* FleshComp = CastChecked(InComponent) ) { if (const UDeformablePhysicsComponent::FThreadingProxy* PhysicsProxy = FleshComp->GetPhysicsProxy()) { if (const Chaos::Softs::FFleshThreadingProxy* Proxy = PhysicsProxy->As()) { Chaos::FRange ParticleRange = Proxy->GetSolverParticleRange(); Evolution->ActivateParticleRange(ParticleRange.Start, false); } } } #else // USE_USD_SDK && DO_USD_CACHING FCacheEvaluationContext Context(TickRecord); Context.bEvaluateTransform = false; Context.bEvaluateCurves = false; Context.bEvaluateEvents = false; Context.bEvaluateChannels = true; // The evaluated result are already in world space since we are passing the tickrecord.spacetransform in the context FCacheEvaluationResult EvaluatedResult = InCache->Evaluate(Context, nullptr); const TArray* PendingVX = EvaluatedResult.Channels.Find(VelocityXName); const TArray* PendingVY = EvaluatedResult.Channels.Find(VelocityYName); const TArray* PendingVZ = EvaluatedResult.Channels.Find(VelocityZName); const TArray* PendingPX = EvaluatedResult.Channels.Find(PositionXName); const TArray* PendingPY = EvaluatedResult.Channels.Find(PositionYName); const TArray* PendingPZ = EvaluatedResult.Channels.Find(PositionZName); const int32 NumCachedParticles = EvaluatedResult.ParticleIndices.Num(); FParticles& Particles = Evolution->Particles(); const int32 NumParticles = Particles.Size(); if (NumCachedParticles > 0) { // Directly set the result of the cache into the solver particles Softs::FSolverVec3* ParticleXs = &Particles.X(0); Softs::FSolverVec3* ParticleVs = &Particles.V(0); for (int32 CachedIndex = 0; CachedIndex < NumCachedParticles; ++CachedIndex) { const int32 ParticleIndex = EvaluatedResult.ParticleIndices[CachedIndex]; if (ensure(ParticleIndex < NumParticles)) { Softs::FSolverVec3& ParticleV = ParticleVs[ParticleIndex]; Softs::FSolverVec3& ParticleX = ParticleXs[ParticleIndex]; ParticleV.X = (*PendingVX)[CachedIndex]; ParticleV.Y = (*PendingVY)[CachedIndex]; ParticleV.Z = (*PendingVZ)[CachedIndex]; ParticleX.X = (*PendingPX)[CachedIndex]; ParticleX.Y = (*PendingPY)[CachedIndex]; ParticleX.Z = (*PendingPZ)[CachedIndex]; } } } #endif // USE_USD_SDK && DO_USD_CACHING } } } FGuid FFleshCacheAdapter::GetGuid() const { FGuid NewGuid; checkSlow(FGuid::Parse(TEXT("2C054706CB7441B582377B0EDACD12EE"), NewGuid)); return NewGuid; } bool FFleshCacheAdapter::ValidForPlayback(UPrimitiveComponent* InComponent, UChaosCache* InCache) const { // If we have a flesh mesh we can play back any cache as long as it has one or more tracks const UFleshComponent* FleshComp = CastChecked(InComponent); #if USE_USD_SDK && DO_USD_CACHING return FleshComp != nullptr; #else return FleshComp && InCache->ChannelCurveToParticle.Num() > 0; #endif // USE_USD_SDK && DO_USD_CACHING } Chaos::Softs::FDeformableSolver* FFleshCacheAdapter::GetDeformableSolver(UPrimitiveComponent* InComponent) { if(InComponent) { if(UFleshComponent* FleshComp = CastChecked(InComponent) ) { if(FleshComp->GetDeformableSolver()) { return FleshComp->GetDeformableSolver()->FleshSolverProxy.Solver.Get(); } } } return nullptr; } Chaos::FRange FFleshCacheAdapter::GetParticleRange(UPrimitiveComponent* InComponent, const int32 NumParticles) { Chaos::FRange ParticleRange(0, NumParticles); if(InComponent) { if(UFleshComponent* FleshComp = CastChecked(InComponent)) { if(FleshComp->GetPhysicsProxy()) { if (const Chaos::Softs::FFleshThreadingProxy* Proxy = FleshComp->GetPhysicsProxy()->As()) { ParticleRange = Proxy->GetSolverParticleRange(); } } } } return ParticleRange; } Chaos::FPhysicsSolver* FFleshCacheAdapter::GetComponentSolver(UPrimitiveComponent* InComponent) const { return nullptr; } Chaos::FPhysicsSolverEvents* FFleshCacheAdapter::BuildEventsSolver(UPrimitiveComponent* InComponent) const { if(UFleshComponent* FleshComp = CastChecked(InComponent)) { // Initialize the physics solver at the beginning of the play/record FleshComp->RecreatePhysicsState(); return GetDeformableSolver(InComponent); } return nullptr; } void FFleshCacheAdapter::InitializeForLoad(UPrimitiveComponent* InComponent, FObservedComponent& InObserved) { #if USE_USD_SDK && DO_USD_CACHING if(const UFleshComponent* FleshComp = CastChecked(InComponent)) { FPrimitiveStage& PrimitiveStage = PrimitiveStages.FindOrAdd(InComponent); PrimitiveStage.PrimPath = UsdUtils::GetPrimPathForObject(FleshComp); PrimitiveStage.FilePath = GetUSDCacheFilePathRO(InObserved, FleshComp); if (!PrimitiveStage.MonolithStage) { FPlatformFileManager& FileManager = FPlatformFileManager::Get(); IPlatformFile& PlatformFile = FileManager.GetPlatformFile(); if (PlatformFile.FileExists(*PrimitiveStage.FilePath)) { UE::ChaosCachingUSD::OpenStage(PrimitiveStage.FilePath, PrimitiveStage.MonolithStage); } else { UE_LOG(LogChaosFleshCache, Warning, TEXT("Read Failure: USD File Path = %s"), *PrimitiveStage.FilePath); } } } #endif // USE_USD_SDK && DO_USD_CACHING } void FFleshCacheAdapter::SetRestState(UPrimitiveComponent* InComponent, UChaosCache* InCache, const FTransform& InRootTransform, Chaos::FReal InTime) const { #if !USE_USD_SDK && DO_USD_CACHING if (!InCache || InCache->GetDuration() == 0.0f) { return; } #endif // USE_USD_SDK && DO_USD_CACHING if (UFleshComponent* FleshComp = CastChecked(InComponent)) { #if USE_USD_SDK && DO_USD_CACHING FleshComp->ResetDynamicCollection(); if (UFleshDynamicAsset* DynamicCollection = FleshComp->GetDynamicCollection()) { TManagedArray& DynamicVertex = DynamicCollection->GetPositions(); TManagedArray& DynamicVertexActivation = DynamicCollection->GetActivations(); const int32 NumDynamicVertex = DynamicVertex.Num(); auto UEVertd = [](Chaos::FVec3 V) { return FVector3d(V.X, V.Y, V.Z); }; auto UEVertf = [](FVector3d V) { return FVector3f((float)V.X, (float)V.Y, (float)V.Z); }; LoadCacheAtTime(InComponent, InTime, NumDynamicVertex, false, [&DynamicVertex, &DynamicVertexActivation, FleshComp, UEVertd, UEVertf]( const int32 ParticleIndex, const int32 ParticleOffset, const FVector3f& ParticlePosition, const FVector3f&, const float& MuscleActivation) { const int32 GlobalIndex = ParticleIndex + ParticleOffset; DynamicVertex[GlobalIndex].Set(ParticlePosition[0], ParticlePosition[1], ParticlePosition[2]); DynamicVertex[GlobalIndex] = UEVertf(FleshComp->GetComponentTransform().InverseTransformPosition( UEVertd(DynamicVertex[GlobalIndex]))); DynamicVertexActivation[GlobalIndex] = MuscleActivation; }); } #else // USE_USD_SDK && DO_USD_CACHING FPlaybackTickRecord TickRecord; TickRecord.SetLastTime(InTime); FCacheEvaluationContext Context(TickRecord); Context.bEvaluateTransform = false; Context.bEvaluateCurves = false; Context.bEvaluateEvents = false; Context.bEvaluateChannels = true; FCacheEvaluationResult EvaluatedResult = InCache->Evaluate(Context, nullptr); const TArray* PendingVX = EvaluatedResult.Channels.Find(VelocityXName); const TArray* PendingVY = EvaluatedResult.Channels.Find(VelocityYName); const TArray* PendingVZ = EvaluatedResult.Channels.Find(VelocityZName); const TArray* PendingPX = EvaluatedResult.Channels.Find(PositionXName); const TArray* PendingPY = EvaluatedResult.Channels.Find(PositionYName); const TArray* PendingPZ = EvaluatedResult.Channels.Find(PositionZName); const int32 NumCachedParticles = EvaluatedResult.ParticleIndices.Num(); const bool bHasPositions = PendingPX && PendingPY && PendingPZ; const bool bHasVelocities = PendingVX && PendingVY && PendingVZ; FleshComp->ResetDynamicCollection(); if (bHasPositions && NumCachedParticles > 0) { if (UFleshDynamicAsset* DynamicCollection = FleshComp->GetDynamicCollection()) { TManagedArray& DynamicVertex = DynamicCollection->GetPositions(); //TManagedArray& DynamicVertex = DynamicCollection->GetVelocities(); const int32 NumDynamicVertex = DynamicVertex.Num(); if (NumDynamicVertex == NumCachedParticles) { for (int32 CachedIndex = 0; CachedIndex < NumCachedParticles; ++CachedIndex) { const int32 ParticleIndex = EvaluatedResult.ParticleIndices[CachedIndex]; if (ensure(ParticleIndex < NumDynamicVertex)) { DynamicVertex[ParticleIndex].X = (*PendingPX)[CachedIndex]; DynamicVertex[ParticleIndex].Y = (*PendingPY)[CachedIndex]; DynamicVertex[ParticleIndex].Z = (*PendingPZ)[CachedIndex]; auto UEVertd = [](Chaos::FVec3 V) { return FVector3d(V.X, V.Y, V.Z); }; auto UEVertf = [](FVector3d V) { return FVector3f((float)V.X, (float)V.Y, (float)V.Z); }; DynamicVertex[ParticleIndex] = UEVertf(FleshComp->GetComponentTransform().InverseTransformPosition(UEVertd(DynamicVertex[ParticleIndex]))); } } } } } #endif // USE_USD_SDK && DO_USD_CACHING } } #if USE_USD_SDK && DO_USD_CACHING FString FFleshCacheAdapter::GetUSDCacheDirectory(const FObservedComponent& InObserved) { // USDCacheDirectory is relative to the content dir, but with "/Game" rather than just "/" or some relative path. FString CacheDir = InObserved.USDCacheDirectory.Path; if (CacheDir.IsEmpty()) { CacheDir = FPaths::Combine(FPaths::ProjectSavedDir(), FString(TEXT("SimCache"))); } FPaths::NormalizeDirectoryName(CacheDir); if (CacheDir.StartsWith(FString(TEXT("/Game")))) { CacheDir = FPaths::Combine(FPaths::ProjectContentDir(), CacheDir.RightChop(5)); } return CacheDir; } FString FFleshCacheAdapter::GetUSDCacheFileName(const UDeformablePhysicsComponent* FleshComp) { const UObject* CurrObject = FleshComp; const AActor* Actor = nullptr; do { CurrObject = CurrObject->GetOuter(); Actor = Cast(CurrObject); } while (!Actor); const UObject* ActorParent = Actor ? Actor->GetOuter() : nullptr; FString CompName; if (ActorParent) { CompName = FleshComp->GetPathName(ActorParent); } else { FleshComp->GetName(CompName); } if (!CompName.Len()) { CompName = FString(TEXT("FleshCache")); } return CompName; } FString FFleshCacheAdapter::GetUSDCacheFilePathRO( const FObservedComponent& InObserved, const UDeformablePhysicsComponent* FleshComp) { FString CacheDir = GetUSDCacheDirectory(InObserved); FString CompName = GetUSDCacheFileName(FleshComp); FString PrimPath = UsdUtils::GetPrimPathForObject(FleshComp); // Look for an existing file of a supported format. TArray SupportedFormats = UnrealUSDWrapper::GetNativeFileFormats(); for (const FString& Ext : SupportedFormats) { FString FileName = FString::Printf(TEXT("%s.%s"), *CompName, *Ext); FString FilePath = FPaths::Combine(CacheDir, FileName); if (FPaths::FileExists(FilePath)) { return FilePath; } } // Fall back on the format we're currently set to write. FString Ext = CVarParams.bWriteBinary ? FString(TEXT("usd")) : FString(TEXT("usda")); FString FileName = FString::Printf(TEXT("%s.%s"), *CompName, *Ext); FString FilePath = FPaths::Combine(CacheDir, FileName); return FilePath; } FString FFleshCacheAdapter::GetUSDCacheFilePathRW( const FObservedComponent& InObserved, const UDeformablePhysicsComponent* FleshComp) { FString CacheDir = GetUSDCacheDirectory(InObserved); FString CompName = GetUSDCacheFileName(FleshComp); FString PrimPath = UsdUtils::GetPrimPathForObject(FleshComp); FString Ext = CVarParams.bWriteBinary ? FString(TEXT("usd")) : FString(TEXT("usda")); FString FileName = FString::Printf(TEXT("%s.%s"), *CompName, *Ext); FString FilePath = FPaths::Combine(CacheDir, FileName); return FilePath; } #endif // USE_USD_SDK && DO_USD_CACHING bool FFleshCacheAdapter::InitializeForRecord(UPrimitiveComponent* InComponent, FObservedComponent& InObserved) { if( FDeformableSolver* Solver = GetDeformableSolver(InComponent)) { FDeformableSolver::FGameThreadAccess GameThreadAccess(Solver, Softs::FGameThreadAccessor()); GameThreadAccess.SetEnableSolver(true); if (UFleshComponent* FleshComp = CastChecked(InComponent)) { FleshComp->ResetDynamicCollection(); #if USE_USD_SDK && DO_USD_CACHING // // USD caching // FString CompName = GetUSDCacheFileName(FleshComp); FString CacheDir = GetUSDCacheDirectory(InObserved); FPlatformFileManager& FileManager = FPlatformFileManager::Get(); IPlatformFile& PlatformFile = FileManager.GetPlatformFile(); if (!PlatformFile.DirectoryExists(*CacheDir)) { if (!PlatformFile.CreateDirectoryTree(*CacheDir)) { UE_LOG(LogChaosFleshCache, Error, TEXT("Failed to create output directory: '%s'"), *CacheDir); return false; } } const UFleshAsset* RestCollectionAsset = FleshComp->GetRestCollection(); TSharedPtr RestCollection = RestCollectionAsset ? RestCollectionAsset->GetFleshCollection() : nullptr; if (!RestCollection) { UE_LOG(LogChaosFleshCache, Error, TEXT("Failed to get rest collection from flesh component: '%s'"), *FleshComp->GetName()); return false; } FPrimitiveStage& PrimitiveStage = PrimitiveStages.FindOrAdd(InComponent); PrimitiveStage.PrimPath = UsdUtils::GetPrimPathForObject(FleshComp); if (bUseMonolith) { bReadOnly = false; FString Ext = CVarParams.bWriteBinary ? FString(TEXT("usd")) : FString(TEXT("usda")); FString FileName = FString::Printf(TEXT("%s.%s"), *CompName, *Ext); PrimitiveStage.FilePath = FPaths::Combine(CacheDir, FileName); if (CVarParams.bNoClobber) { if (PlatformFile.FileExists(*PrimitiveStage.FilePath)) { // Rename the file to 'path/to/file.usd' to 'path/to/file_#.usd', where '#' // is a unique version number. FString UniqueFilePath; int32 i = 1; do { FString UniqueCompName = FString::Printf(TEXT("%s_%d.%s"), *CompName, i++, *Ext); UniqueFilePath = FPaths::Combine(CacheDir, UniqueCompName); } while (PlatformFile.FileExists(*UniqueFilePath)); if (bRestartSimulation) { if (!PlatformFile.CopyFile(*UniqueFilePath, *PrimitiveStage.FilePath)) { UE_LOG(LogChaosFleshCache, Error, TEXT("Failed to copy file from '%s' to '%s'."), *PrimitiveStage.FilePath, *UniqueFilePath); return false; } } else { if (!PlatformFile.MoveFile(*UniqueFilePath, *PrimitiveStage.FilePath)) { UE_LOG(LogChaosFleshCache, Error, TEXT("Failed to rename file from '%s' to '%s'."), *PrimitiveStage.FilePath, *UniqueFilePath); return false; } } } } else { if (!bRestartSimulation && !PlatformFile.DeleteFile(*PrimitiveStage.FilePath)) { UE_LOG(LogChaosFleshCache, Error, TEXT("Failed to remove existing cache file: '%s'"), *PrimitiveStage.FilePath); return false; } } if (PrimitiveStage.MonolithStage) { UE::ChaosCachingUSD::CloseStage(PrimitiveStage.MonolithStage); } if (bRestartSimulation) { if (!UE::ChaosCachingUSD::OpenStage(PrimitiveStage.FilePath, PrimitiveStage.MonolithStage)) { UE_LOG(LogChaosFleshCache, Error, TEXT("Failed to read restart USD file: '%s'"), *PrimitiveStage.FilePath); return false; } if (!UE::ChaosCachingUSD::DeletePointsInTimeRange(PrimitiveStage.MonolithStage, PrimitiveStage.PrimPath, RestartTimeStart, RestartTimeEnd)) { UE_LOG(LogChaosFleshCache, Error, TEXT("Failed to delete USD data in file: '%s', delete range: [%f, %f]"), *PrimitiveStage.FilePath, RestartTimeStart, RestartTimeEnd); return false; } } else { if (!UE::ChaosCachingUSD::NewStage(PrimitiveStage.FilePath, PrimitiveStage.MonolithStage)) { UE_LOG(LogChaosFleshCache, Error, TEXT("Failed to create new USD file: '%s'"), *PrimitiveStage.FilePath); return false; } if (!UE::ChaosCachingUSD::WriteTetMesh(PrimitiveStage.MonolithStage, PrimitiveStage.PrimPath, *RestCollection)) { UE_LOG(LogChaosFleshCache, Error, TEXT("Failed to write tetrahedron mesh '%s' to USD file: '%s'"), *PrimitiveStage.PrimPath, *PrimitiveStage.FilePath); return false; } } } #endif // USE_USD_SDK && DO_USD_CACHING } } return true; } bool FFleshCacheAdapter::InitializeForPlayback(UPrimitiveComponent* InComponent, FObservedComponent& InObserved, float InTime) { EnsureIsInGameThreadContext(); if (FDeformableSolver* Solver = GetDeformableSolver(InComponent)) { FDeformableSolver::FGameThreadAccess GameThreadAccess(Solver, Softs::FGameThreadAccessor()); if (UDeformableTetrahedralComponent* FleshComp = CastChecked(InComponent)) { FleshComp->ResetDynamicCollection(); if (FleshComp->GetPhysicsProxy()->As()) { FleshComp->GetPhysicsProxy()->As()->SetIsCached(true); } #if USE_USD_SDK && DO_USD_CACHING // // USD caching // FPrimitiveStage& PrimitiveStage = PrimitiveStages.FindOrAdd(InComponent); PrimitiveStage.PrimPath = UsdUtils::GetPrimPathForObject(FleshComp); if (bUseMonolith) { bReadOnly = true; PrimitiveStage.FilePath = GetUSDCacheFilePathRO(InObserved, FleshComp); FPlatformFileManager& FileManager = FPlatformFileManager::Get(); IPlatformFile& PlatformFile = FileManager.GetPlatformFile(); if (PlatformFile.FileExists(*PrimitiveStage.FilePath)) { if (PrimitiveStage.MonolithStage) { UE::ChaosCachingUSD::CloseStage(PrimitiveStage.MonolithStage); } if (!UE::ChaosCachingUSD::OpenStage(PrimitiveStage.FilePath, PrimitiveStage.MonolithStage)) { UE_LOG(LogChaosFleshCache, Error, TEXT("Failed to open USD cache file: '%s'"), *PrimitiveStage.FilePath); return false; } } else { UE_LOG(LogChaosFleshCache, Error, TEXT("USD cache file not found: '%s'"), *PrimitiveStage.FilePath); return false; } } if (!bUseMonolith) { UE_LOG(LogChaosFleshCache, Warning, TEXT("No USD file structure selected (bMonolith).")); } #endif // USE_USD_SDK && DO_USD_CACHING } } return true; } void FFleshCacheAdapter::Finalize() { #if USE_USD_SDK && DO_USD_CACHING for(TPair& PrimitiveStage : PrimitiveStages) { // Detach shared memory arrays. if (PrimitiveStage.Value.MonolithStage) { if (!bReadOnly) { UE::ChaosCachingUSD::SaveStage(PrimitiveStage.Value.MonolithStage, PrimitiveStage.Value.MinTime, PrimitiveStage.Value.MaxTime); } UE::ChaosCachingUSD::CloseStage(PrimitiveStage.Value.MonolithStage); PrimitiveStage.Value.MonolithStage = UE::FUsdStage(); } PrimitiveStage.Value.PrimPath.Empty(); PrimitiveStage.Value.FilePath.Empty(); PrimitiveStage.Value.MinTime = TNumericLimits::Max(); PrimitiveStage.Value.MaxTime = TNumericLimits::Lowest(); } PrimitiveStages.Reset(); #endif // USE_USD_SDK && DO_USD_CACHING } } // namespace Chaos