// Copyright Epic Games, Inc. All Rights Reserved. #include "BuoyancySubsystem.h" #include "BuoyancyAlgorithms.h" #include "BuoyancyStats.h" #include "BuoyancyRuntimeSettings.h" #include "BuoyancyEventInterface.h" #include "Engine/World.h" #include "Physics/Experimental/PhysScene_Chaos.h" #include "Physics/PhysicsFiltering.h" #include "Chaos/MidPhaseModification.h" #include "Chaos/MassProperties.h" #include "DrawDebugHelpers.h" #include "WaterBodyActor.h" #include "Components/SplineComponent.h" #include "WaterSubsystem.h" #include "WaterBodyManager.h" #include "WaterSplineComponent.h" #include "BakedShallowWaterSimulationComponent.h" #include "Chaos/PhysicsObject.h" #include "PBDRigidsSolver.h" #include "Chaos/PBDRigidsEvolutionGBF.h" #include "Chaos/PerParticleGravity.h" #include "Chaos/DebugDrawQueue.h" #include "Templates/SharedPointer.h" #include "PhysicsProxy/SingleParticlePhysicsProxy.h" #include "Chaos/GeometryParticlesfwd.h" #include "Chaos/ParticleHandle.h" #include "Chaos/DebugDrawQueue.h" #include "Chaos/CastingUtilities.h" #include "Chaos/Utilities.h" #include "Chaos/PBDRigidsEvolutionGBF.h" #include "Chaos/Collision/CollisionFilter.h" #include "Chaos/Collision/CollisionUtil.h" #include "Chaos/Sphere.h" // // CVars // // Jira to remove: PLAY-21231 bool bBuoyancyCallbackDataEnabled = true; FAutoConsoleVariableRef CVarBuoyancyCallbackDataEnabled(TEXT("p.Buoyancy.CallbackData.Enabled"), bBuoyancyCallbackDataEnabled, TEXT("")); // Added for jira PLAY-37027 bool bBuoyancyCallbackDataParticleValidation = true; FAutoConsoleVariableRef CVarBuoyancyCallbackDataParticleValidation(TEXT("p.Buoyancy.CallbackData.ParticleValidation"), bBuoyancyCallbackDataParticleValidation, TEXT("")); bool bBuoyancyDebugDraw = false; #if ENABLE_DRAW_DEBUG FAutoConsoleVariableRef CVarBuoyancyDebugDraw(TEXT("p.Buoyancy.DebugDraw"), bBuoyancyDebugDraw, TEXT("")); #endif int32 bUseShallowWaterSimulation = 1; static FAutoConsoleVariableRef CVarUseShallowWaterSimulation( TEXT("p.Buoyancy.bUseShallowWaterSimulation"), bUseShallowWaterSimulation, TEXT("new buoancy method"), ECVF_Scalability ); int32 bUseAccurateIntegrationForSplines = 0; static FAutoConsoleVariableRef CVarUseAccurateIntegrationForSplines( TEXT("p.Buoyancy.bUseAccurateIntegrationForSplines"), bUseAccurateIntegrationForSplines, TEXT("new buoancy method for splines"), ECVF_Scalability ); // // Logging // DEFINE_LOG_CATEGORY(LogBuoyancySubsystem); // // Console Commands // #if WITH_BUOYANCY_MEMORY_TRACKING static FAutoConsoleCommandWithWorld BuoyancyLogMemory( TEXT("p.Buoyancy.LogMemory"), TEXT("Log how much memory is being used by the buoyancy subsystem"), FConsoleCommandWithWorldDelegate::CreateLambda([](UWorld* World) { if (UBuoyancySubsystem* BuoyancySubsystem = World != nullptr ? World->GetSubsystem() : nullptr) { const int32 AllocatedSize = BuoyancySubsystem->GetAllocatedSize(); UE_LOG(LogBuoyancySubsystem, Warning, TEXT("Buoyancy Subsystem Allocated Bytes: %d"), AllocatedSize); } })); #endif // WITH_BUOYANCY_MEMORY_TRACKING // // Buoyancy Subsystem // bool UBuoyancySubsystem::SetEnabled(const bool bEnabled) { if (bEnabled != IsEnabled()) { if (bEnabled) { CreateSimCallback(); } else { DestroySimCallback(); } return IsEnabled() == bEnabled; } // Already had whatever setting return true; } bool UBuoyancySubsystem::IsEnabled() const { return SimCallback != nullptr; } bool UBuoyancySubsystem::SetEnabledWithUpdatedNetModeCallback(const bool bEnabled) { bool bEnabledResult = SetEnabled(bEnabled); if (IsEnabled()) { if (SimCallback) { if (FBuoyancySubsystemSimCallbackInput* AsyncInput = SimCallback->GetProducerInputData_External()) { AsyncInput->NetMode = NetMode; } } } return bEnabledResult; } #if WITH_BUOYANCY_MEMORY_TRACKING SIZE_T UBuoyancySubsystem::GetAllocatedSize() const { return SimCallback_AllocatedSize; } #endif void UBuoyancySubsystem::CreateSimCallback() { // Sometimes in PIE, sim callbacks have not been freed at this point. // I think it is an issue with world ticking subsystem Deinitialize(). DestroySimCallback(); // Create sim callback if (Chaos::FPhysicsSolver* Solver = GetSolver()) { // Create the callback for keeping spline data in sync ensureAlwaysMsgf(SplineData == nullptr, TEXT("UBuoyancySubsystem::CreateSimCallback: Creating new FBuoyancyWaterSplineDataManager before releasing previous SplineData")); SplineData = Solver->CreateAndRegisterSimCallbackObject_External(); // Create the main buoyancy sim callback ensureAlwaysMsgf(SimCallback == nullptr, TEXT("UBuoyancySubsystem::CreateSimCallback: Creating new FBuoyancySubsystemSimCallback before releasing previous SimCallback")); SimCallback = Solver->CreateAndRegisterSimCallbackObject_External(); // Give the buoyancy sim callback a reference to the spline data callback, // so that it'll have access to per-particle spline data if (ensureAlwaysMsgf(SimCallback != nullptr && SplineData != nullptr, TEXT("Either SimCallback or SplineData were not properly initialized in UBuoyancySubsystem::CreateSimCallback!"))) { if (FBuoyancySubsystemSimCallbackInput* AsyncInput = SimCallback->GetProducerInputData_External()) { AsyncInput->SplineData = SplineData; } } // Populate an initial async input so that the sim callbacks have the most up-to-date info UpdateAllAsyncInputs(); } } void UBuoyancySubsystem::DestroySimCallback() { // Destroy the main sim callback for buoyancy if (SimCallback) { if (Chaos::FPhysicsSolverBase* Solver = SimCallback->GetSolver()) { Solver->UnregisterAndFreeSimCallbackObject_External(SimCallback); SimCallback = nullptr; } } // Destroy the sim callback for keeping spline data in sync if (SplineData) { if (Chaos::FPhysicsSolverBase* Solver = SplineData->GetSolver()) { Solver->UnregisterAndFreeSimCallbackObject_External(SplineData); SplineData = nullptr; } } } void UBuoyancySubsystem::PostInitialize() { Super::PostInitialize(); // Apply initial runtime settings ApplyRuntimeSettings(GetDefault(), EPropertyChangeType::ValueSet); // Setup callback for when waterbodies are added/removed if (FWaterBodyManager* WaterBodyManager = UWaterSubsystem::GetWaterBodyManager(GetWorld())) { WaterBodyManager->OnWaterBodyAdded.AddUObject(this, &UBuoyancySubsystem::OnWaterBodyAdded); WaterBodyManager->OnWaterBodyRemoved.AddUObject(this, &UBuoyancySubsystem::OnWaterBodyRemoved); bWaterObjectsChanged = true; } // Set up callback for when runtime settings change in editor #if WITH_EDITOR GetDefault()->OnSettingsChange.AddUObject(this, &UBuoyancySubsystem::ApplyRuntimeSettings); #endif //WITH_EDITOR } void UBuoyancySubsystem::Deinitialize() { DestroySimCallback(); Super::Deinitialize(); } void UBuoyancySubsystem::ApplyRuntimeSettings(const UBuoyancyRuntimeSettings* InSettings, EPropertyChangeType::Type ChangeType) { bBuoyancySettingsChanged = true; // Runtime settings presents water density in g/cm^3, but we want it in kg/cm^3 // so introduce a factor of 10^-3 here. BuoyancySettings.WaterDensity = Chaos::GCm3ToKgCm3(InSettings->WaterDensity); BuoyancySettings.WaterDrag = InSettings->WaterDrag; BuoyancySettings.WaterCollisionChannel = InSettings->CollisionChannelForWaterObjects; BuoyancySettings.bKeepAwake = InSettings->bKeepFloatingObjectsAwake; BuoyancySettings.MaxNumBoundsSubdivisions = InSettings->MaxNumBoundsSubdivisions; BuoyancySettings.MinBoundsSubdivisionVol = InSettings->MinBoundsSubdivisionVol; BuoyancySettings.MinVelocityForSurfaceTouchCallback = InSettings->MinVelocityForSurfaceTouchCallback; BuoyancySettings.bSplineKeyCacheGrid = InSettings->bEnableSplineKeyCacheGrid; BuoyancySettings.SplineKeyCacheGridSize = InSettings->SplineKeyCacheGridSize; BuoyancySettings.SplineKeyCacheLimit = InSettings->SplineKeyCacheLimit; // Based on server/client/editor, determine if we should generate callbacks. // If we're editor, always generate callbacks. If we're not editor, only // generate callbacks on client. #if WITH_EDITOR BuoyancySettings.SurfaceTouchCallbackFlags = InSettings->SurfaceTouchCallbackFlags; #else UWorld* World = GetWorld(); BuoyancySettings.SurfaceTouchCallbackFlags = (World && World->IsNetMode(NM_Client)) ? InSettings->SurfaceTouchCallbackFlags : EBuoyancyEventFlags::None; #endif // Enable or disable SetEnabled(InSettings->bBuoyancyEnabled); } void UBuoyancySubsystem::Tick(float DeltaTime) { SCOPE_CYCLE_COUNTER(STAT_BuoyancySubsystem_Tick) // // The entire point of this tick is to send runtime data to // the physics thread which might have changed, and to process // outputs which may effect water bodies or result in // callbacks. // Super::Tick(DeltaTime); UWorld* World = GetWorld(); if (World == nullptr) { return; } if (SimCallback == nullptr) { return; } // Update spline info for all water bodies & internal arrays of water objects if (bWaterObjectsChanged) { UpdateSplineData(); } // Only bother sending new async inputs if our buoyancy settings actually changed if (bBuoyancySettingsChanged) { UpdateBuoyancySettings(); } if (NetMode != World->GetNetMode()) { NetMode = World->GetNetMode(); UpdateNetMode(); } // Process surface-touched callbacks if (BuoyancySettings.SurfaceTouchCallbackFlags != 0) { ProcessSurfaceTouchCallbacks(); } } void UBuoyancySubsystem::UpdateAllAsyncInputs() { UpdateNetMode(); UpdateSplineData(); UpdateBuoyancySettings(); } void UBuoyancySubsystem::UpdateNetMode() { // Send net mode to PT if (SimCallback) { if (FBuoyancySubsystemSimCallbackInput* AsyncInput = SimCallback->GetProducerInputData_External()) { AsyncInput->NetMode = NetMode; } } } void UBuoyancySubsystem::UpdateSplineData() { SCOPE_CYCLE_COUNTER(STAT_BuoyancySubsystem_UpdateWaterBodiesList) if (bWaterObjectsChanged == false) { return; } if (SplineData == nullptr) { return; } if (FWaterBodyManager* WaterBodyManager = UWaterSubsystem::GetWaterBodyManager(GetWorld())) { // Clear the flag to rebuild water object data bWaterObjectsChanged = false; // Clear out existing old water object data // // NOTE: the "false" argument here prevents a resize, since we're just going to // repopulate the underlying sparse array again immediately anyway. SplineData->ClearData_GT(false); // Loop over every registered water body WaterBodyManager->ForEachWaterBodyComponent(GetWorld(), [this](UWaterBodyComponent* WaterBodyComponent) { // Get the metadata object if there is one, and the spline component UWaterSplineMetadata* SplineMetadata = WaterBodyComponent->GetWaterSplineMetadata(); UBakedShallowWaterSimulationComponent *BakedSim = WaterBodyComponent->GetBakedShallowWaterSimulation(); if (const UWaterSplineComponent* SplineComponent = WaterBodyComponent->GetWaterSpline()) { // Copy out water spline data into a shared ptr, to be associated with all // child particles and marshaled to PT. const Chaos::FRigidTransform3 WaterTransform = WaterBodyComponent->GetComponentTransform(); const TOptional EmptyOptionalFloat; const TOptional EmptyOptionalSimGrid; TSharedPtr WaterSplineData = MakeShared( WaterTransform, SplineComponent->GetSplinePointsPosition(), WaterBodyComponent->GetWaterBodyType(), SplineMetadata ? SplineMetadata->RiverWidth : EmptyOptionalFloat, SplineMetadata ? SplineMetadata->WaterVelocityScalar : EmptyOptionalFloat, bUseShallowWaterSimulation && WaterBodyComponent->UseBakedSimulationForQueriesAndPhysics() ? BakedSim->SimulationData : EmptyOptionalSimGrid ); // Go over each physics object in each primitive component which was generated // from this spline, and associate the spline with the particle. for (UPrimitiveComponent* WaterPrimitiveComponent : WaterBodyComponent->GetCollisionComponents(true)) { // Add each object (probably just one) to the objects list if (WaterPrimitiveComponent) { FBodyInstance& BodyInstance = WaterPrimitiveComponent->BodyInstance; if (BodyInstance.IsValidBodyInstance()) { if (Chaos::FSingleParticlePhysicsProxy* WaterProxy = BodyInstance.GetPhysicsActor()) { SplineData->SetData_GT(WaterProxy->GetGameThreadAPI(), WaterSplineData); } } } } } return true; }); } } void UBuoyancySubsystem::UpdateBuoyancySettings() { SCOPE_CYCLE_COUNTER(STAT_BuoyancySubsystem_UpdateBuoyancySettings) if (SimCallback) { if (FBuoyancySubsystemSimCallbackInput* AsyncInput = SimCallback->GetProducerInputData_External()) { bBuoyancySettingsChanged = false; AsyncInput->BuoyancySettings = MakeUnique(BuoyancySettings); } } } void UBuoyancySubsystem::ProcessSurfaceTouchCallbacks() { SCOPE_CYCLE_COUNTER(STAT_BuoyancySubsystem_DispatchCallbacks) UWorld* World = GetWorld(); if (World == nullptr) { return; } FPhysScene* PhysScene = World->GetPhysicsScene(); if (PhysScene == nullptr) { return; } while (Chaos::TSimCallbackOutputHandle AsyncOutput = SimCallback->PopFutureOutputData_External()) { // Process surface touches for (const FBuoyancySubsystemSimCallbackOutput::FSurfaceTouch& SurfaceTouch : AsyncOutput->SurfaceTouches) { // Skip if we mask out this touch type if ((SurfaceTouch.Flag & BuoyancySettings.SurfaceTouchCallbackFlags) == 0) { continue; } // Extract primitive components UPrimitiveComponent* WaterComponent = PhysScene->GetOwningComponent(SurfaceTouch.WaterProxy); UPrimitiveComponent* RigidComponent = PhysScene->GetOwningComponent(SurfaceTouch.RigidProxy); // Skip if either of the components were not valid if (WaterComponent == nullptr || RigidComponent == nullptr) { continue; } // Get the parental water body component AWaterBody* WaterActor = WaterComponent->GetOwner(); const auto DispatchEvent = [&](AActor* Actor) { // TODO: Actor relevancy check? // If the actor implements the event interface, call the surface touched callback if (IBuoyancyEventInterface* InterfaceInstance = Cast(Actor)) { switch (SurfaceTouch.Flag) { case EBuoyancyEventFlags::Begin: InterfaceInstance->OnSurfaceTouchBegin_Native( WaterComponent, RigidComponent, SurfaceTouch.Vol, SurfaceTouch.CoM, SurfaceTouch.Vel); IBuoyancyEventInterface::Execute_OnSurfaceTouchBegin( Actor, WaterActor, WaterComponent, RigidComponent, SurfaceTouch.Vol, SurfaceTouch.CoM, SurfaceTouch.Vel); break; case EBuoyancyEventFlags::Continue: InterfaceInstance->OnSurfaceTouching_Native( WaterComponent, RigidComponent, SurfaceTouch.Vol, SurfaceTouch.CoM, SurfaceTouch.Vel); IBuoyancyEventInterface::Execute_OnSurfaceTouching( Actor, WaterActor, WaterComponent, RigidComponent, SurfaceTouch.Vol, SurfaceTouch.CoM, SurfaceTouch.Vel); break; case EBuoyancyEventFlags::End: InterfaceInstance->OnSurfaceTouchEnd_Native( WaterComponent, RigidComponent, SurfaceTouch.Vol, SurfaceTouch.CoM, SurfaceTouch.Vel); IBuoyancyEventInterface::Execute_OnSurfaceTouchEnd( Actor, WaterActor, WaterComponent, RigidComponent, SurfaceTouch.Vol, SurfaceTouch.CoM, SurfaceTouch.Vel); break; } } }; DispatchEvent(WaterActor); DispatchEvent(RigidComponent->GetOwner()); } #if WITH_BUOYANCY_MEMORY_TRACKING // Process changes to tracking of memory allocated on physics thread if (AsyncOutput->AllocatedSize.IsSet()) { SimCallback_AllocatedSize = *AsyncOutput->AllocatedSize; } #endif } } TStatId UBuoyancySubsystem::GetStatId() const { RETURN_QUICK_DECLARE_CYCLE_STAT(UBuoyancySubsystem, STATGROUP_Tickables); } void UBuoyancySubsystem::OnWaterBodyAdded(UWaterBodyComponent* WaterBodyComponent) { bWaterObjectsChanged = true; } void UBuoyancySubsystem::OnWaterBodyRemoved(UWaterBodyComponent* WaterBodyComponent) { bWaterObjectsChanged = true; } Chaos::FPhysicsSolver* UBuoyancySubsystem::GetSolver() const { if (UWorld* World = GetWorld()) { if (FPhysScene* Scene = World->GetPhysicsScene()) { if (Chaos::FPhysicsSolver* Solver = Scene->GetSolver()) { return Solver; } } } return nullptr; } // // Buoyancy Sim Callback // void FBuoyancySubsystemSimCallbackInput::Reset() { SplineData.Reset(); BuoyancySettings.Reset(); NetMode.Reset(); } void FBuoyancySubsystemSimCallbackOutput::Reset() { SurfaceTouches.Reset(); #if WITH_BUOYANCY_MEMORY_TRACKING AllocatedSize.Reset(); #endif } SIZE_T FBuoyancySubsystemSimCallback::GetAllocatedSize() const { return BuoyancyParticleData.GetAllocatedSize() + SplineKeyCache.GetAllocatedSize(); } void FBuoyancySubsystemSimCallback::OnPreSimulate_Internal() { SCOPE_CYCLE_COUNTER(STAT_BuoyancySubsystem_OnPreSimulate) // If we were sent new buoyancy settings or data, update our local sim copy if (const FBuoyancySubsystemSimCallbackInput* Input = GetConsumerInput_Internal()) { if (Input->NetMode.IsSet()) { NetMode = *Input->NetMode; } if (Input->SplineData.IsSet()) { SplineData = *Input->SplineData; // New spline data means we need to reset our spline key cache if (!bUseAccurateIntegrationForSplines) { SplineKeyCache.Reset(); } // New spline data also means that any internal storage of water particles // might be out of date. Clear all data that potentially contains references // to old water particles. // // NOTE: This may cause new-submersion events to trigger on already submerged // objects when water splines are added/removed, depending on velocity- // filtering options. It might be desirable to instead add a mechanism to // suppress events for one frame instead, or to remove only the data which // relate to water particles which have been removed. BuoyancyParticleData.Interactions.Reset(); BuoyancyParticleData.SubmersionMetaData.Reset(); BuoyancyParticleData.PrevSubmersionMetaData.Reset(); } if (Input->BuoyancySettings.IsValid()) { BuoyancySettings = MoveTemp(Input->BuoyancySettings); // Set key cache properties if (!bUseAccurateIntegrationForSplines) { SplineKeyCache.SetGridSize(BuoyancySettings->SplineKeyCacheGridSize); SplineKeyCache.SetCacheLimit(BuoyancySettings->SplineKeyCacheLimit); } } } // If we don't have a valid buoyancy settings object, don't continue if (BuoyancySettings.IsValid() == false) { return; } // If we have debug draw enabled #if ENABLE_DRAW_DEBUG if (bBuoyancyDebugDraw) { // If we're using spline key cache grid, debug draw all cached points if (BuoyancySettings->bSplineKeyCacheGrid && !bUseAccurateIntegrationForSplines) { SplineKeyCache.ForEachSplineKey([this](const FBuoyancyWaterSplineData& WaterSpline, const FVector& LocalPos, float SplineKey) { // Draw a box representing this spline's grid cell const FVector WorldPos = WaterSpline.Transform.TransformPosition(LocalPos); Chaos::FDebugDrawQueue::GetInstance().DrawDebugBox(WorldPos, FVector(SplineKeyCache.GetGridSize() * .5f), FQuat::Identity, FColor::White, false, -1.f, -1, 1.f); // Draw an arrow from the center to the closest const FVector ClosestPointLocal = WaterSpline.Position.Eval(SplineKey); const FVector ClosestPoint = WaterSpline.Transform.TransformPosition(ClosestPointLocal); Chaos::FDebugDrawQueue::GetInstance().DrawDebugDirectionalArrow(WorldPos, ClosestPoint, 20.f, FColor::Silver, false, -1.f, -1, 1.f); }); } } #endif } void FBuoyancySubsystemSimCallback::OnMidPhaseModification_Internal(Chaos::FMidPhaseModifierAccessor& MidPhaseAccessor) { SCOPE_CYCLE_COUNTER(STAT_BuoyancySubsystem_OnMidPhaseModification) // Don't do any processing until we know what machine we're on if (NetMode == ENetMode::NM_MAX) { return; } // If we don't have a spline data manager, early out if (SplineData == nullptr) { return; } // If we don't have a valid buoyancy settings object, don't continue if (BuoyancySettings.IsValid() == false) { return; } // Get the solver Chaos::FPhysicsSolverBase* SolverBase = GetSolver(); if (SolverBase == nullptr) { return; } // Why does cast-checked return a ref? That makes me think // it's not actually doing a check... Chaos::FPBDRigidsSolver& PBDSolver = SolverBase->CastChecked(); // Get the evolution Chaos::FPBDRigidsEvolution* Evolution = PBDSolver.GetEvolution(); if (Evolution == nullptr) { return; } // SparseArray implements move semantics, so these swaps should amount to pointer swaps. // This way array memories stick around even when reset/swapped so we don't do many // new allocations. Swap(BuoyancyParticleData.Submersions, BuoyancyParticleData.PrevSubmersions); Swap(BuoyancyParticleData.SubmersionMetaData, BuoyancyParticleData.PrevSubmersionMetaData); // Clear arrays for the following phases, but keep their memory allocated BuoyancyParticleData.Interactions.Reset(); BuoyancyParticleData.Submersions.Reset(); BuoyancyParticleData.SubmergedShapes.Reset(); BuoyancyParticleData.SubmersionMetaData.Reset(); // Build list of "submersions" TrackInteractions(PBDSolver, *Evolution, MidPhaseAccessor); // Process the list of interactions that we built from the midphases ProcessInteractions(*Evolution); // Apply buoyant forces resulting from submersions ApplyBuoyantForces(*Evolution); // Generate async outputs for callback data GenerateCallbackData(); #if WITH_BUOYANCY_MEMORY_TRACKING // Generate async outputs for allocation data GenerateAllocationData(); #endif } void FBuoyancySubsystemSimCallback::TrackInteractions( Chaos::FPBDRigidsSolver& PBDSolver, Chaos::FPBDRigidsEvolution& Evolution, Chaos::FMidPhaseModifierAccessor& MidPhaseAccessor) { SCOPE_CYCLE_COUNTER(STAT_BuoyancySubsystem_VisitMidphases) // The spline userdatapt manager contains a PT array of all water splines indexed on // water particle unique index. Therefore, we can use it to loop over all water body // particles and check their midphases. SplineData->VisitAllData_PT([this, &PBDSolver, &Evolution, &MidPhaseAccessor](Chaos::FUniqueIdx ParticleIdx, const TSharedPtr& WaterData) { // If the WaterData shared ptr is invalid, skip this one if (WaterData.IsValid() == false) { return; } // Get water body particle proxy from index Chaos::FSingleParticlePhysicsProxy* ParticleProxy = PBDSolver.GetParticleProxy_PT(ParticleIdx); if (ParticleProxy == nullptr || ParticleProxy->GetMarkedDeleted()) { return; } // Get water body particle handle from proxy Chaos::FGeometryParticleHandle* WaterParticle = ParticleProxy->GetHandle_LowLevel(); if (WaterParticle == nullptr) { return; } // Get all midphases which involve this particle Chaos::FMidPhaseModifierParticleRange MidPhases = MidPhaseAccessor.GetMidPhases(WaterParticle); // Loop over all midphases for (auto& MidPhase : MidPhases) { // Make sure we have two valid particles Chaos::FGeometryParticleHandle* OtherParticle = MidPhase.GetOtherParticle(WaterParticle); if (WaterParticle == nullptr || OtherParticle == nullptr) { continue; } // Make sure the non-water particle is backed by a rigid Chaos::FPBDRigidParticleHandle* RigidParticle = OtherParticle->CastToRigidParticle(); if (RigidParticle == nullptr) { continue; } // Finally... we know for sure this is a midphase that we wanna process TrackInteraction(Evolution, WaterParticle, RigidParticle, *WaterData.Get(), MidPhase); } }); } void FBuoyancySubsystemSimCallback::TrackInteraction( Chaos::FPBDRigidsEvolution& Evolution, Chaos::FGeometryParticleHandle* WaterParticle, Chaos::FPBDRigidParticleHandle* RigidParticle, const FBuoyancyWaterSplineData& WaterSpline, Chaos::FMidPhaseModifier& MidPhase) { SCOPE_CYCLE_COUNTER(STAT_BuoyancySubsystem_TrackInteraction) { SCOPE_CYCLE_COUNTER(STAT_BuoyancySubsystem_DisableMidPhase) // Always disable midphases with water MidPhase.Disable(); } TSharedPtr WaterSampler = nullptr; if (WaterSpline.ShouldSampleFromShallowWaterSimulation()) { // eval shallow water - closest point is just the current point adjusted upward in Z const FVector ParticlePos = RigidParticle->XCom(); FVector WaterVelocity; float WaterHeight; float WaterDepth; WaterSpline.ShallowWaterSimData->SampleShallowWaterSimulationAtPosition(ParticlePos, WaterVelocity, WaterHeight, WaterDepth); // #todo(dmp): if there is no water here, then return. This is an approximation that there is water underneath this rigid that might cause interaction // ideally, we'd multisample this since we can lose some water interactions this way if (WaterDepth < 1e-5) { return; } FVector ClosestWaterPointToParticle = ParticlePos; ClosestWaterPointToParticle.Z = WaterHeight; FShallowWaterSimulationGrid DefaultShallowWaterSimulationGrid; const FShallowWaterSimulationGrid& ShallowWaterSimData = WaterSpline.ShallowWaterSimData.Get(DefaultShallowWaterSimulationGrid); WaterSampler = TSharedPtr(new FBuoyancyShallowWaterSampler(ShallowWaterSimData, ClosestWaterPointToParticle, WaterVelocity)); } else { SCOPE_CYCLE_COUNTER(STAT_BuoyancySubsystem_SplineEvaluation) float ClosestSplineKey = 0; FVector ClosestPoint = FVector::ZeroVector; FVector WaterN = FVector::ZeroVector; FVector WaterVel = FVector::ZeroVector; // Find water surface at the nearest point on the spline const FVector ParticlePos = RigidParticle->XCom(); { SCOPE_CYCLE_COUNTER(STAT_BuoyancySubsystem_SplineEvaluation_FindNearest) if (BuoyancySettings->bSplineKeyCacheGrid) { ClosestSplineKey = SplineKeyCache.GetClosestSplineKey(WaterSpline, ParticlePos); } else { const FVector ParticleLocalPos = WaterSpline.Transform.InverseTransformPosition(ParticlePos); float ParticleDistance; ClosestSplineKey = WaterSpline.Position.FindNearest(ParticleLocalPos, ParticleDistance); } } { SCOPE_CYCLE_COUNTER(STAT_BuoyancySubsystem_SplineEvaluation_Eval) ClosestPoint = WaterSpline.Transform.TransformPosition(WaterSpline.Position.Eval(ClosestSplineKey)); } // Get water surface level and normal // Don't build interaction if outside of water body { // Find water surface at the nearest point on the spline FVector ClosestPointDerivative = WaterSpline.Transform.TransformVector( WaterSpline.Position.EvalDerivative(ClosestSplineKey)); // Water normal direction depends on body type if (WaterSpline.BodyType == EWaterBodyType::River) { // River water normal can be determined by the relationship of the // derivative of the spline position to the up vector. // // NOTE: This calculation breaks down in the limit of purely // vertical water const FVector SplineRight = FVector::CrossProduct(FVector::UpVector, ClosestPointDerivative); const FVector SplineUp = FVector::CrossProduct(ClosestPointDerivative, SplineRight); WaterN = SplineUp.GetSafeNormal(); } else { WaterN = FVector::UpVector; } // Project the position difference onto the water surface const FVector Diff = ClosestPoint - ParticlePos; const FVector LateralDiff = Diff - (WaterN * FVector::DotProduct(WaterN, Diff)); // Different water body types have different ways of determining // whether a point is laterally inside their volume. switch (WaterSpline.BodyType) { case EWaterBodyType::River: { if (WaterSpline.Width.IsSet()) { // If distance to spline is greater than the width of the spline, // then this is a river and we're outside of it. const float Width = WaterSpline.Width->Eval(ClosestSplineKey); const float DistSq = FVector::DotProduct(LateralDiff, LateralDiff); const float WidthSq = Width * Width * .25f; if (DistSq > WidthSq) { return; } } break; } case EWaterBodyType::Lake: { // Determine if we're inside the lake by projecting the horizontal spline // diff onto the cross product of the spline direction and the up-vector // (ie, the right-vector) const FVector RightVector = FVector::CrossProduct(ClosestPointDerivative, FVector::UpVector); const float DiffProj = FVector::DotProduct(RightVector, LateralDiff); if (DiffProj < SMALL_NUMBER) { return; } break; } } WaterSampler = TSharedPtr(new FBuoyancyConstantSplineSampler(WaterSpline, ClosestPoint, ClosestPointDerivative, ClosestSplineKey, WaterN)); #if ENABLE_DRAW_DEBUG if (bBuoyancyDebugDraw && !bUseAccurateIntegrationForSplines) { // Spline Color const FColor SplineColor = FColor::Cyan; // Draw projection onto the line const FVector SurfacePoint = ClosestPoint - LateralDiff; Chaos::FDebugDrawQueue::GetInstance().DrawDebugLine(ParticlePos, SurfacePoint, SplineColor, false, -1.f, -1, 6.f); Chaos::FDebugDrawQueue::GetInstance().DrawDebugLine(SurfacePoint, ClosestPoint, SplineColor, false, -1.f, -1, 3.f); // Draw a section of the spline near the spline key Chaos::FVec3 PrevPoint; bool bFirst = true; for (float SplineKey = ClosestSplineKey - .1f; SplineKey <= ClosestSplineKey + .1f; SplineKey += .05f) { const FVector SplinePoint = WaterSpline.Transform.TransformPosition(WaterSpline.Position.Eval(SplineKey)); if (bFirst) { bFirst = false; } else { Chaos::FDebugDrawQueue::GetInstance().DrawDebugDirectionalArrow(PrevPoint, SplinePoint, 15.f, SplineColor, false, -1.f, -1, 3.f); } PrevPoint = SplinePoint; } // Draw water surface normal at the surface point Chaos::FDebugDrawQueue::GetInstance().DrawDebugDirectionalArrow(SurfacePoint, SurfacePoint + (WaterN * 100.f), 20.f, FColor::Green, false, -1.f, -1, 3.f); } #endif } } { SCOPE_CYCLE_COUNTER(STAT_BuoyancySubsystem_AddInteraction) // If this particle already has a list of interactions, add to it. Otherwise // make a new one. BuoyancyParticleData.GetInteractions(*RigidParticle).Add({ RigidParticle, WaterParticle, WaterSampler }); } } void FBuoyancySubsystemSimCallback::ProcessInteractions(Chaos::FPBDRigidsEvolution& Evolution) { SCOPE_CYCLE_COUNTER(STAT_BuoyancySubsystem_ProcessInteractions) for (FBuoyancyInteractionArray& ParticleInteractions : BuoyancyParticleData.Interactions) { // Sort this particle's water interactions by water level - highest to lowest ParticleInteractions.Sort([](const FBuoyancyInteraction& A, const FBuoyancyInteraction& B) { return A.WaterSampler->GetClosestWaterPointToParticle().Z > B.WaterSampler->GetClosestWaterPointToParticle().Z; }); // Process each interaction for (FBuoyancyInteraction& Interaction : ParticleInteractions) { if (!Interaction.WaterSampler.IsValid()) { UE_LOG(LogBuoyancySubsystem, Warning, TEXT("Skipped invalid buoyancy interaction")); return; } else if (Interaction.WaterSampler->GetSamplerType() == EWaterSamplerType::ShallowWater || bUseAccurateIntegrationForSplines) { ProcessAccurateInteraction(Evolution, Interaction, Interaction.WaterSampler); } else { ProcessInteraction(Evolution, Interaction); } } } } void FBuoyancySubsystemSimCallback::ProcessAccurateInteraction(Chaos::FPBDRigidsEvolution& Evolution, FBuoyancyInteraction& Interaction, TSharedPtr WaterSampler) { const Chaos::FReal DeltaSeconds = GetDeltaTime_Internal(); float SubmergedVol = 0; Chaos::FVec3 SubmergedCoM = Chaos::FVec3::ZeroVector; Chaos::FVec3 WorldForce = Chaos::FVec3::ZeroVector; Chaos::FVec3 WorldTorque = Chaos::FVec3::ZeroVector; float TotalParticleVol = 0; if (!Interaction.WaterSampler.IsValid()) { UE_LOG(LogBuoyancySubsystem, Warning, TEXT("Skipped invalid buoyancy interaction ")); return; } else if (Interaction.WaterSampler->GetSamplerType() == EWaterSamplerType::ConstantSpline) { BuoyancyAlgorithms::ComputeSubmergedVolumeAndForcesForParticle(BuoyancyParticleData, Interaction.RigidParticle, Interaction.WaterParticle, StaticCastSharedPtr(Interaction.WaterSampler), Evolution, DeltaSeconds, BuoyancySettings->WaterDensity, BuoyancySettings->WaterDrag, TotalParticleVol, SubmergedVol, SubmergedCoM, WorldForce, WorldTorque); } else if (Interaction.WaterSampler->GetSamplerType() == EWaterSamplerType::ShallowWater) { BuoyancyAlgorithms::ComputeSubmergedVolumeAndForcesForParticle(BuoyancyParticleData, Interaction.RigidParticle, Interaction.WaterParticle, StaticCastSharedPtr(Interaction.WaterSampler), Evolution, DeltaSeconds, BuoyancySettings->WaterDensity, BuoyancySettings->WaterDrag, TotalParticleVol, SubmergedVol, SubmergedCoM, WorldForce, WorldTorque); } // Apply forces to the particle { SCOPE_CYCLE_COUNTER(STAT_BuoyancySubsystem_ShallowWaterIntegrateForces) Chaos::FConstGenericParticleHandle RigidGeneric(Interaction.RigidParticle); #if ENABLE_DRAW_DEBUG if (bBuoyancyDebugDraw) { Chaos::FDebugDrawQueue::GetInstance().DrawDebugSphere(SubmergedCoM, 20, 10, FColor::Green, false, -1.f, -1, 2.0f); Chaos::FDebugDrawQueue::GetInstance().DrawDebugSphere(RigidGeneric->PCom(), 20, 10, FColor::Blue, false, -1.f, -1, 2.0f); Chaos::FDebugDrawQueue::GetInstance().DrawDebugDirectionalArrow(RigidGeneric->PCom(), RigidGeneric->PCom() + WorldForce, 20, FColor::Blue, false, -1.f, -1, 2.f); } #endif // #todo(dmp): if we do this integration to accelerations per shape, then each shape can have different densities per shape. // This will change to integrate the accumulated accelerations instead of forces // Use inertia to convert forces to accelerations const Chaos::FVec3 LinearAccel = RigidGeneric->InvM() * WorldForce; const Chaos::FMatrix33 WorldInvI = Chaos::Utilities::ComputeWorldSpaceInertia(RigidGeneric->RCom(), RigidGeneric->ConditionedInvI()); const Chaos::FVec3 AngularAccel = WorldInvI * WorldTorque; // Integrate to get delta velocities Chaos::FVec3 DeltaV = LinearAccel * DeltaSeconds; Chaos::FVec3 DeltaW = AngularAccel * DeltaSeconds; if (SubmergedVol > SMALL_NUMBER) { // Get the submersion for this particle. If it's new, the particle ptr will be null. // If it's not new, make sure the particle ptr is the right particle. FBuoyancySubmersion& Submersion = BuoyancyParticleData.GetSubmersion(*Interaction.RigidParticle); if (Submersion.Particle != nullptr) { ensureMsgf(Submersion.Particle == Interaction.RigidParticle, TEXT("Something went wrong - there's a particle index mismatch in the Submersions sparse array")); } Submersion.Particle = Interaction.RigidParticle; Chaos::FSingleParticlePhysicsProxy* RigidProxy = static_cast(Submersion.Particle->PhysicsProxy()); Submersion.SyncTimestamp = RigidProxy->GetSyncTimestamp(); // Get the weighted-average CoM // NOTE: The unchecked division should be safe since we already // know SubmergedVol > SMALL_NUMBER const float TotalSubmergedVol = Submersion.Vol + SubmergedVol; Submersion.CoM = ((Submersion.CoM * Submersion.Vol) + (SubmergedCoM * SubmergedVol)) / TotalSubmergedVol; // Sum the volumes Submersion.Vol = TotalSubmergedVol; // Get the weighted-average slerped water surface norm const float VolRatio = SubmergedVol / TotalSubmergedVol; // #todo(dmp): Split out submersion since we don't need norm here- only deltav and deltaw // #todo(dmp): output the perimeter of intersection Submersion.Vel = FMath::Lerp(Submersion.Vel, WaterSampler->GetAverageWaterVelocity(), VolRatio); Submersion.HasIntegratedQuantities = true; Submersion.DeltaV = FMath::Lerp(Submersion.DeltaV, DeltaV, VolRatio); Submersion.DeltaW = FMath::Lerp(Submersion.DeltaW, DeltaW, VolRatio); // potentially add a surface callback AddSurfaceTouchCallback(Interaction, Submersion, TotalParticleVol, SubmergedVol, SubmergedCoM); } } } void FBuoyancySubsystemSimCallback::ProcessInteraction(Chaos::FPBDRigidsEvolution& Evolution, FBuoyancyInteraction& Interaction) { SCOPE_CYCLE_COUNTER(STAT_BuoyancySubsystem_ProcessInteraction) TSharedPtr ConstantSplineSampler; if (!Interaction.WaterSampler.IsValid()) { UE_LOG(LogBuoyancySubsystem, Warning, TEXT("Skipped invalid buoyancy interaction ")); return; } else if (Interaction.WaterSampler->GetSamplerType() == EWaterSamplerType::ConstantSpline) { ConstantSplineSampler = StaticCastSharedPtr(Interaction.WaterSampler); } else { UE_LOG(LogBuoyancySubsystem, Warning, TEXT("Skipped buoyancy interaction that isn't based on water splines")); return; } // Compute submerged volume and CoM float SubmergedVol; Chaos::FVec3 SubmergedCoM; float TotalVol; if (BuoyancyAlgorithms::ComputeSubmergedVolume(BuoyancyParticleData, Evolution, Interaction.RigidParticle, Interaction.WaterParticle, ConstantSplineSampler->GetClosestWaterPointToParticle(), ConstantSplineSampler->WaterN, BuoyancySettings->MaxNumBoundsSubdivisions, BuoyancySettings->MinBoundsSubdivisionVol, SubmergedVol, SubmergedCoM, TotalVol)) { SCOPE_CYCLE_COUNTER(STAT_BuoyancySubsystem_BuildSubmersions) // If any volume was submerged, take the weighted average of the centers of mass to get the // approximate submerged center of mass, and apply the buoyancy force there. if (SubmergedVol > SMALL_NUMBER) { // sample water spline for velocity with this call ConstantSplineSampler->InitializeForSubmersion(); // Get the submersion for this particle. If it's new, the particle ptr will be null. // If it's not new, make sure the particle ptr is the right particle. FBuoyancySubmersion& Submersion = BuoyancyParticleData.GetSubmersion(*Interaction.RigidParticle); if (Submersion.Particle != nullptr) { ensureMsgf(Submersion.Particle == Interaction.RigidParticle, TEXT("Something went wrong - there's a particle index mismatch in the Submersions sparse array")); } Submersion.Particle = Interaction.RigidParticle; Chaos::FSingleParticlePhysicsProxy* RigidProxy = static_cast(Submersion.Particle->PhysicsProxy()); Submersion.SyncTimestamp = RigidProxy->GetSyncTimestamp(); // Get the weighted-average CoM // NOTE: The unchecked division should be safe since we already // know SubmergedVol > SMALL_NUMBER const float TotalSubmergedVol = Submersion.Vol + SubmergedVol; Submersion.CoM = ((Submersion.CoM * Submersion.Vol) + (SubmergedCoM * SubmergedVol)) / TotalSubmergedVol; // Sum the volumes Submersion.Vol = TotalSubmergedVol; // Get the weighted-average slerped water surface norm const float VolRatio = SubmergedVol / TotalSubmergedVol; const FQuat NormRot = FQuat::FindBetweenNormals(Submersion.Norm, ConstantSplineSampler->WaterN); Submersion.Norm = NormRot.Slerp(FQuat::Identity, NormRot, VolRatio).GetUpVector(); // Blend water velocities Submersion.Vel = FMath::Lerp(Submersion.Vel, ConstantSplineSampler->WaterVel, VolRatio); // potentially add a surface callback AddSurfaceTouchCallback(Interaction, Submersion, TotalVol, SubmergedVol, SubmergedCoM); } } } void FBuoyancySubsystemSimCallback::AddSurfaceTouchCallback(FBuoyancyInteraction& Interaction, FBuoyancySubmersion& Submersion, float TotalVol, float SubmergedVol, FVector SubmergedCoM) { // If this is a surface touch record it for callback, if if (BuoyancySettings->SurfaceTouchCallbackFlags != 0 && TotalVol > SubmergedVol * (1.f + UE_KINDA_SMALL_NUMBER)) { SCOPE_CYCLE_COUNTER(STAT_BuoyancySubsystem_BuildSubmersionCallbackData) // Proceed only if this CoM is moving fast enough to generate events const Chaos::FVec3 CoMDiff = Submersion.CoM - Interaction.RigidParticle->XCom(); const Chaos::FVec3 CoMVel = Interaction.RigidParticle->GetV() + Chaos::FVec3::CrossProduct(Interaction.RigidParticle->GetW(), CoMDiff); const float CoMVelSq = Chaos::FVec3::DotProduct(CoMVel, CoMVel); const float MinVel = BuoyancySettings->MinVelocityForSurfaceTouchCallback; const float MinVelSq = MinVel * MinVel; if (CoMVelSq > MinVelSq) { // If we don't have a metadata for this particle yet, add one FBuoyancySubmersionMetaData& MetaData = BuoyancyParticleData.GetSubmersionMetaData(*Interaction.RigidParticle); // If we haven't already maxed out on water contacts, add one if (MetaData.WaterContacts.Num() < FBuoyancySubmersionMetaData::MaxNumWaterContacts) { Chaos::FSingleParticlePhysicsProxy* WaterProxy = static_cast(Interaction.WaterParticle->PhysicsProxy()); MetaData.WaterContacts.Add({ Interaction.WaterParticle, WaterProxy->GetSyncTimestamp(), SubmergedVol, SubmergedCoM, CoMVel }); } } } } void FBuoyancySubsystemSimCallback::ApplyBuoyantForces(Chaos::FPBDRigidsEvolution& Evolution) { SCOPE_CYCLE_COUNTER(STAT_BuoyancySubsystem_ApplyBuoyantForces) // How much time has the sim ticked this frame const Chaos::FReal DeltaSeconds = GetDeltaTime_Internal(); // Get perparticle gravity rule, for figuring out the effective gravity on buoyant objects const Chaos::FPerParticleGravity* PerParticleGravity = &Evolution.GetGravityForces(); // Apply all buoyant forces for (const FBuoyancySubmersion& Submersion : BuoyancyParticleData.Submersions) { Chaos::FVec3 DeltaV; Chaos::FVec3 DeltaW; bool IsValidForce = true; // If the submersion has integrated velocity and torque, use that, otherwise, compute it if (Submersion.HasIntegratedQuantities) { DeltaV = Submersion.DeltaV; DeltaW = Submersion.DeltaW; } else { // Figure out the gravity level of the particle const int32 GravityGroupIndex = Submersion.Particle->GravityGroupIndex(); const Chaos::FVec3 GravityAccel = PerParticleGravity != nullptr && GravityGroupIndex != INDEX_NONE ? (Chaos::FVec3)PerParticleGravity->GetAcceleration(GravityGroupIndex) : Chaos::FVec3::DownVector * 980.f; // Default to "regular" gravity // Compute delta linear and angular velocities due to buoyancy. If they're big enough to // matter, apply them IsValidForce = BuoyancyAlgorithms::ComputeBuoyantForce(Submersion.Particle, DeltaSeconds, BuoyancySettings->WaterDensity, BuoyancySettings->WaterDrag, GravityAccel, Submersion.CoM, Submersion.Vol, Submersion.Vel, Submersion.Norm, DeltaV, DeltaW); } if (IsValidForce) { DeltaV = DeltaV.GetClampedToSize(0.f, BuoyancySettings->MaxDeltaV); DeltaW = DeltaW.GetClampedToSize(0.f, BuoyancySettings->MaxDeltaW); // Compute new velocity const Chaos::FVec3 NewV = Submersion.Particle->GetV() + DeltaV; const Chaos::FVec3 NewW = Submersion.Particle->GetW() + DeltaW; // Apply the deltas Submersion.Particle->SetV(NewV); Submersion.Particle->SetW(NewW); // Wake up the body?? if (BuoyancySettings->bKeepAwake) { Evolution.SetParticleObjectState(Submersion.Particle, Chaos::EObjectStateType::Dynamic); } } } } namespace { bool IsParticleValid(Chaos::FGeometryParticleHandle* ParticleHandle, TSharedPtr SyncTimestamp) { if (bBuoyancyCallbackDataParticleValidation == false) { return true; } if (ParticleHandle == nullptr) { return false; } if (SyncTimestamp.IsValid() == false) { return false; } if (SyncTimestamp->bDeleted) { return false; } IPhysicsProxyBase* Proxy = ParticleHandle->PhysicsProxy(); if (Proxy == nullptr) { return false; } if (!ensureMsgf(Proxy->GetSyncTimestamp() == SyncTimestamp, TEXT("particle's sync timestamp doesn't match the sync timestamp that was passed to the particle validity check!"))) { return false; } return true; }; } void FBuoyancySubsystemSimCallback::GenerateCallbackData() { if (bBuoyancyCallbackDataEnabled == false) { return; } // Get refs to internal sparse arrays of submersion data TSparseArray& PrevSubmersions = BuoyancyParticleData.PrevSubmersions; TSparseArray& MetaDatas = BuoyancyParticleData.SubmersionMetaData; TSparseArray& PrevMetaDatas = BuoyancyParticleData.PrevSubmersionMetaData; // Generate callback data if we're into that sort of thing const uint8 CallbackFlags = BuoyancySettings->SurfaceTouchCallbackFlags; if (CallbackFlags != 0) { SCOPE_CYCLE_COUNTER(STAT_BuoyancySubsystem_ProduceSurfaceTouches) // Get the async output struct to write to FBuoyancySubsystemSimCallbackOutput& Output = GetProducerOutputData_Internal(); // Process every surface touch and queue up some of them to return // to game thread for callback dispatch Output.SurfaceTouches.Reserve(MetaDatas.Num() * FBuoyancySubmersionMetaData::MaxNumWaterContacts); for (auto Iter = MetaDatas.CreateIterator(); Iter; ++Iter) { const FBuoyancySubmersionMetaData& MetaData = *Iter; const int32 ObjectIndex = Iter.GetIndex(); const FBuoyancySubmersion& Submersion = BuoyancyParticleData.Submersions[ObjectIndex]; // Mark this as a new or continuing contact based on whether or // not we have a bit from the previous-submersions array. const bool bPrevSubmerged = PrevMetaDatas.IsValidIndex(ObjectIndex) && PrevMetaDatas.IsAllocated(ObjectIndex); const EBuoyancyEventFlags TouchFlag = bPrevSubmerged ? EBuoyancyEventFlags::Continue : EBuoyancyEventFlags::Begin; // Clear out the "prev" entry for this one's metadata so that // we can loop over the prev metadata for lost-contacts. Only // bother doing this work if we're tracking removals if (bPrevSubmerged && (CallbackFlags & EBuoyancyEventFlags::End) != 0) { PrevMetaDatas.RemoveAt(ObjectIndex); } // Only continue if we're tracking this touch type if ((TouchFlag & CallbackFlags) == 0) { continue; } if (!ensureMsgf(IsParticleValid(Submersion.Particle, Submersion.SyncTimestamp.Pin()), TEXT("Submersion data for buoyancy callback includes invalid submerged particle handle"))) { continue; } // Build up output of new and continuing surface touches for (const FBuoyancySubmersionMetaData::FWaterContact& WaterContact : MetaData.WaterContacts) { if (!ensureMsgf(IsParticleValid(WaterContact.Water, WaterContact.SyncTimestamp.Pin()), TEXT("Submersion data for buoyancy callback includes invalid water body particle handle"))) { continue; } Output.SurfaceTouches.Add({ TouchFlag, Submersion.Particle->PhysicsProxy(), WaterContact.Water->PhysicsProxy(), WaterContact.Vol, WaterContact.CoM, WaterContact.Vel }); } } // The remaining previous submersion metadata will correspond with lost contacts // // NOTE: // At the moment, lost contact callbacks will only occur when an entire object // loses contact, not just when one part of it loses contact. if ((CallbackFlags & EBuoyancyEventFlags::End) != 0) { for (auto Iter = PrevMetaDatas.CreateIterator(); Iter; ++Iter) { const FBuoyancySubmersionMetaData& MetaData = *Iter; const int32 ObjectIndex = Iter.GetIndex(); const FBuoyancySubmersion& Submersion = PrevSubmersions[ObjectIndex]; if (!IsParticleValid(Submersion.Particle, Submersion.SyncTimestamp.Pin())) { continue; } for (const FBuoyancySubmersionMetaData::FWaterContact& WaterContact : MetaData.WaterContacts) { if (!IsParticleValid(WaterContact.Water, WaterContact.SyncTimestamp.Pin())) { continue; } Output.SurfaceTouches.Add({ EBuoyancyEventFlags::End, Submersion.Particle->PhysicsProxy(), WaterContact.Water->PhysicsProxy(), WaterContact.Vol, WaterContact.CoM, WaterContact.Vel }); } } } } } #if WITH_BUOYANCY_MEMORY_TRACKING void FBuoyancySubsystemSimCallback::GenerateAllocationData() { SCOPE_CYCLE_COUNTER(STAT_BuoyancySubsystem_GenerateAllocationData) // NOTE: For now I'm guarding this inside WITH_BUOYANCY_MEMORY_TRACKING. // The current use case for the game thread GetAllocatedSize is a console // command which is editor-only. If this is to become useful // more generally, we can find a way to do it without relying on // continual calls to GetAllocatedSize(). const uint32 NewAllocatedSize = GetAllocatedSize(); if (AllocatedSize != NewAllocatedSize) { // If the number of allocated bytes has changed, update our cached num bytes AllocatedSize = NewAllocatedSize; // Get the async output struct to write to FBuoyancySubsystemSimCallbackOutput& Output = GetProducerOutputData_Internal(); Output.AllocatedSize = AllocatedSize; } } #endif