/******************************************************************************* The content of this file includes portions of the proprietary AUDIOKINETIC Wwise Technology released in source code form as part of the game integration package. The content of this file may not be used without valid licenses to the AUDIOKINETIC Wwise Technology. Note that the use of the game engine is subject to the Unreal(R) Engine End User License Agreement at https://www.unrealengine.com/en-US/eula/unreal License Usage Licensees holding valid licenses to the AUDIOKINETIC Wwise Technology may use this file in accordance with the end user license agreement provided with the software or, alternatively, in accordance with the terms contained in a written agreement between you and Audiokinetic Inc. Copyright (c) 2023 Audiokinetic Inc. *******************************************************************************/ /*============================================================================= AkObstructionAndOcclusionService.cpp: =============================================================================*/ #include "ObstructionAndOcclusionService/AkObstructionAndOcclusionService.h" #include "AkAudioDevice.h" #include "AkComponent.h" #include "AkSpatialAudioHelper.h" #include "AkAcousticPortal.h" #include "Engine/World.h" #include "Engine/Engine.h" #include "Components/PrimitiveComponent.h" #include "Async/Async.h" #include "GameFramework/PlayerController.h" #include "GameFramework/Pawn.h" #define AK_DEBUG_OCCLUSION_PRINT 0 #if AK_DEBUG_OCCLUSION_PRINT static int framecounter = 0; #endif #define AK_DEBUG_OCCLUSION 0 #if AK_DEBUG_OCCLUSION #include "DrawDebugHelpers.h" #endif FAkListenerObstructionAndOcclusion::FAkListenerObstructionAndOcclusion(float in_TargetValue, float in_CurrentValue) : CurrentValue(in_CurrentValue) , TargetValue(in_TargetValue) , Rate(0.0f) {} void FAkListenerObstructionAndOcclusion::SetTarget(float in_TargetValue) { TargetValue = FMath::Clamp(in_TargetValue, 0.0f, 1.0f); const float UAkComponent_OCCLUSION_FADE_RATE = 2.0f; // from 0.0 to 1.0 in 0.5 seconds Rate = FMath::Sign(TargetValue - CurrentValue) * UAkComponent_OCCLUSION_FADE_RATE; } bool FAkListenerObstructionAndOcclusion::Update(float DeltaTime) { auto OldValue = CurrentValue; if (OldValue != TargetValue) { const auto NewValue = OldValue + Rate * DeltaTime; if (OldValue > TargetValue) CurrentValue = FMath::Clamp(NewValue, TargetValue, OldValue); else CurrentValue = FMath::Clamp(NewValue, OldValue, TargetValue); AKASSERT(CurrentValue >= 0.f && CurrentValue <= 1.f); return true; } return false; } bool FAkListenerObstructionAndOcclusion::ReachedTarget() { return CurrentValue == TargetValue; } //===================================================================================== // FAkListenerObstructionAndOcclusionPair //===================================================================================== FAkListenerObstructionAndOcclusionPair::FAkListenerObstructionAndOcclusionPair() { SourceRayCollisions.AddZeroed(NUM_BOUNDING_BOX_TRACE_POINTS); ListenerRayCollisions.AddZeroed(NUM_BOUNDING_BOX_TRACE_POINTS); SourceTraceHandles.AddDefaulted(NUM_BOUNDING_BOX_TRACE_POINTS); ListenerTraceHandles.AddDefaulted(NUM_BOUNDING_BOX_TRACE_POINTS); } bool FAkListenerObstructionAndOcclusionPair::Update(float DeltaTime) { if (CurrentCollisionCount != GetCollisionCount()) { CurrentCollisionCount = GetCollisionCount(); const float ratio = (float)CurrentCollisionCount / NUM_BOUNDING_BOX_TRACE_POINTS; Occ.SetTarget(ratio); Obs.SetTarget(ratio); } const bool bObsChanged = Obs.Update(DeltaTime); const bool bOccChanged = Occ.Update(DeltaTime); return bObsChanged || bOccChanged; } void FAkListenerObstructionAndOcclusionPair::Reset() { for (int i = 0; i < NUM_BOUNDING_BOX_TRACE_POINTS; ++i) { SourceRayCollisions[i] = ListenerRayCollisions[i] = false; } } bool FAkListenerObstructionAndOcclusionPair::ReachedTarget() { return Obs.ReachedTarget() && Occ.ReachedTarget(); } void FAkListenerObstructionAndOcclusionPair::AsyncTraceFromSource(const FVector& SourcePosition, const FVector& EndPosition, int BoundingBoxPointIndex, ECollisionChannel CollisionChannel, UWorld* World, const FCollisionQueryParams& CollisionParams) { ensure(BoundingBoxPointIndex < NUM_BOUNDING_BOX_TRACE_POINTS); // Check that we're not stacking another async trace on top of one that hasn't completed yet. if (!World->IsTraceHandleValid(SourceTraceHandles[BoundingBoxPointIndex], false)) { SourceTraceHandles[BoundingBoxPointIndex] = World->AsyncLineTraceByChannel(EAsyncTraceType::Single, SourcePosition, EndPosition, CollisionChannel, CollisionParams); } } void FAkListenerObstructionAndOcclusionPair::AsyncTraceFromListener(const FVector& ListenerPosition, const FVector& EndPosition, int BoundingBoxPointIndex, ECollisionChannel CollisionChannel, UWorld* World, const FCollisionQueryParams& CollisionParams) { ensure(BoundingBoxPointIndex < NUM_BOUNDING_BOX_TRACE_POINTS); // Check that we're not stacking another async trace on top of one that hasn't completed yet. if (!World->IsTraceHandleValid(ListenerTraceHandles[BoundingBoxPointIndex], false)) { ListenerTraceHandles[BoundingBoxPointIndex] = World->AsyncLineTraceByChannel(EAsyncTraceType::Single, ListenerPosition, EndPosition, CollisionChannel, CollisionParams); } } int FAkListenerObstructionAndOcclusionPair::GetCollisionCount() { int CollisionCount = 0; for (int i = 0; i < NUM_BOUNDING_BOX_TRACE_POINTS; ++i) { CollisionCount += (SourceRayCollisions[i] || ListenerRayCollisions[i]) ? 1 : 0; } return CollisionCount; } void FAkListenerObstructionAndOcclusionPair::CheckTraceResults(UWorld* World) { CheckListenerTraceHandles(World); CheckSourceTraceHandles(World); } void FAkListenerObstructionAndOcclusionPair::CheckListenerTraceHandles(UWorld* World) { for (int BoundingBoxPointIndex = 0; BoundingBoxPointIndex < NUM_BOUNDING_BOX_TRACE_POINTS; ++BoundingBoxPointIndex) { if (ListenerTraceHandles[BoundingBoxPointIndex]._Data.FrameNumber != 0) { FTraceDatum OutData; if (World->QueryTraceData(ListenerTraceHandles[BoundingBoxPointIndex], OutData)) { ListenerTraceHandles[BoundingBoxPointIndex]._Data.FrameNumber = 0; ListenerRayCollisions[BoundingBoxPointIndex] = OutData.OutHits.Num() > 0; } } } } void FAkListenerObstructionAndOcclusionPair::CheckSourceTraceHandles(UWorld* World) { for (int BoundingBoxPointIndex = 0; BoundingBoxPointIndex < NUM_BOUNDING_BOX_TRACE_POINTS; ++BoundingBoxPointIndex) { if (SourceTraceHandles[BoundingBoxPointIndex]._Data.FrameNumber != 0) { FTraceDatum OutData; if (World->QueryTraceData(SourceTraceHandles[BoundingBoxPointIndex], OutData)) { SourceTraceHandles[BoundingBoxPointIndex]._Data.FrameNumber = 0; SourceRayCollisions[BoundingBoxPointIndex] = OutData.OutHits.Num() > 0; } } } } //===================================================================================== // AkObstructionAndOcclusionService //===================================================================================== void AkObstructionAndOcclusionService::_Init(UWorld* in_world, float in_refreshInterval) { if (in_refreshInterval > 0 && in_world != nullptr) LastObstructionAndOcclusionRefresh = in_world->GetTimeSeconds() + FMath::RandRange(0.0f, in_refreshInterval); else LastObstructionAndOcclusionRefresh = -1; } void AkObstructionAndOcclusionService::RefreshObstructionAndOcclusion(const UAkComponentSet& in_Listeners, const FVector& SourcePosition, const AActor* Actor, AkRoomID RoomID, ECollisionChannel in_collisionChannel, const float DeltaTime, float OcclusionRefreshInterval) { auto AudioDevice = FAkAudioDevice::Get(); // Fade the active occlusions bool StillClearingObsOcc = false; for (auto It = ListenerInfoMap.CreateIterator(); It; ++It) { AkGameObjectID Listener = It->Key; if (in_Listeners.Find((UAkComponent*)Listener) == nullptr) { It.RemoveCurrent(); continue; } FAkListenerObstructionAndOcclusionPair& ObsOccPair = It->Value; ObsOccPair.CheckTraceResults(Actor->GetWorld()); if (ObsOccPair.Update(DeltaTime) && AudioDevice) { SetObstructionAndOcclusion(Listener, ObsOccPair.Obs.CurrentValue); } if (ClearingObstructionAndOcclusion) { StillClearingObsOcc |= !ObsOccPair.ReachedTarget(); } } if (ClearingObstructionAndOcclusion) { ClearingObstructionAndOcclusion = StillClearingObsOcc; return; } // Compute occlusion only when needed. // Have to have "LastObstructionAndOcclusionRefresh == -1" because GetWorld() might return nullptr in UAkComponent's constructor, // preventing us from initializing it to something smart. const UWorld* CurrentWorld = Actor ? Actor->GetWorld() : nullptr; if (CurrentWorld) { float CurrentTime = CurrentWorld->GetTimeSeconds(); if (CurrentTime < LastObstructionAndOcclusionRefresh && LastObstructionAndOcclusionRefresh - CurrentTime > OcclusionRefreshInterval) { // Occlusion refresh interval was made shorter since the last refresh, we need to re-distribute the next random calculation LastObstructionAndOcclusionRefresh = CurrentTime + FMath::RandRange(0.0f, OcclusionRefreshInterval); } if (LastObstructionAndOcclusionRefresh == -1 || (CurrentTime - LastObstructionAndOcclusionRefresh) >= OcclusionRefreshInterval) { LastObstructionAndOcclusionRefresh = CurrentTime; for (auto& Listener : in_Listeners) { auto& MapEntry = ListenerInfoMap.FindOrAdd(Listener->GetAkGameObjectID()); MapEntry.Position = Listener->GetPosition(); } CalculateObstructionAndOcclusionValues(in_Listeners, SourcePosition, Actor, RoomID, in_collisionChannel); } } } void AkObstructionAndOcclusionService::CalculateObstructionAndOcclusionValues(const UAkComponentSet& in_Listeners, const FVector& SourcePosition, const AActor* Actor, AkRoomID RoomID, ECollisionChannel in_collisionChannel, bool bAsync /* = true */) { auto CurrentWorld = Actor->GetWorld(); if (!CurrentWorld) return; static const FName NAME_SoundOcclusion = TEXT("SoundOcclusion"); FCollisionQueryParams CollisionParams(NAME_SoundOcclusion, true, Actor); auto PlayerController = GEngine->GetFirstLocalPlayerController(CurrentWorld); if (PlayerController) CollisionParams.AddIgnoredActor(PlayerController->GetPawn()); for (auto& Listener : in_Listeners) { if (RoomID != Listener->GetSpatialAudioRoom()) continue; auto MapEntry = ListenerInfoMap.Find(Listener->GetAkGameObjectID()); if (MapEntry == nullptr) continue; const FVector ListenerPosition = MapEntry->Position; FHitResult OutHit; const bool bNowOccluded = CurrentWorld->LineTraceSingleByChannel(OutHit, SourcePosition, ListenerPosition, in_collisionChannel, CollisionParams); if (bNowOccluded) { FBox BoundingBox; AActor* HitActor = AkSpatialAudioHelper::GetActorFromHitResult(OutHit); if (HitActor) { BoundingBox = HitActor->GetComponentsBoundingBox(); } else if (OutHit.Component.IsValid()) { BoundingBox = OutHit.Component->Bounds.GetBox(); } // Translate the impact point to the bounding box of the obstacle const FVector Points[] = { FVector(OutHit.ImpactPoint.X, BoundingBox.Min.Y, BoundingBox.Min.Z), FVector(OutHit.ImpactPoint.X, BoundingBox.Min.Y, BoundingBox.Max.Z), FVector(OutHit.ImpactPoint.X, BoundingBox.Max.Y, BoundingBox.Min.Z), FVector(OutHit.ImpactPoint.X, BoundingBox.Max.Y, BoundingBox.Max.Z), FVector(BoundingBox.Min.X, OutHit.ImpactPoint.Y, BoundingBox.Min.Z), FVector(BoundingBox.Min.X, OutHit.ImpactPoint.Y, BoundingBox.Max.Z), FVector(BoundingBox.Max.X, OutHit.ImpactPoint.Y, BoundingBox.Min.Z), FVector(BoundingBox.Max.X, OutHit.ImpactPoint.Y, BoundingBox.Max.Z), FVector(BoundingBox.Min.X, BoundingBox.Min.Y, OutHit.ImpactPoint.Z), FVector(BoundingBox.Min.X, BoundingBox.Max.Y, OutHit.ImpactPoint.Z), FVector(BoundingBox.Max.X, BoundingBox.Min.Y, OutHit.ImpactPoint.Z), FVector(BoundingBox.Max.X, BoundingBox.Max.Y, OutHit.ImpactPoint.Z) }; if (bAsync) { for (int PointIndex = 0; PointIndex < NUM_BOUNDING_BOX_TRACE_POINTS; ++PointIndex) { auto Point = Points[PointIndex]; MapEntry->AsyncTraceFromListener(ListenerPosition, Point, PointIndex, in_collisionChannel, CurrentWorld, CollisionParams); MapEntry->AsyncTraceFromSource(SourcePosition, Point, PointIndex, in_collisionChannel, CurrentWorld, CollisionParams); } } else { // Compute the number of "second order paths" that are also obstructed. This will allow us to approximate // "how obstructed" the source is. int32 NumObstructedPaths = 0; for (const auto& Point : Points) { if (CurrentWorld->LineTraceSingleByChannel(OutHit, ListenerPosition, Point, in_collisionChannel, CollisionParams) || CurrentWorld->LineTraceSingleByChannel(OutHit, SourcePosition, Point, in_collisionChannel, CollisionParams)) ++NumObstructedPaths; } // Modulate occlusion by blocked secondary paths. const float ratio = (float)NumObstructedPaths / NUM_BOUNDING_BOX_TRACE_POINTS; MapEntry->Occ.SetTarget(ratio); MapEntry->Obs.SetTarget(ratio); } #if AK_DEBUG_OCCLUSION check(IsInGameThread()); // Draw bounding box and "second order paths" //UE_LOG(LogAkAudio, Log, TEXT("Target Occlusion level: %f"), ListenerOcclusionInfo[ListenerIdx].TargetValue); FlushPersistentDebugLines(CurrentWorld); FlushDebugStrings(CurrentWorld); DrawDebugBox(CurrentWorld, BoundingBox.GetCenter(), BoundingBox.GetExtent(), FColor::White, false, 4); DrawDebugPoint(CurrentWorld, ListenerPosition, 10.0f, FColor(0, 255, 0), false, 4); DrawDebugPoint(CurrentWorld, SourcePosition, 10.0f, FColor(0, 255, 0), false, 4); DrawDebugPoint(CurrentWorld, OutHit.ImpactPoint, 10.0f, FColor(0, 255, 0), false, 4); for (int32 i = 0; i < NUM_BOUNDING_BOX_TRACE_POINTS; i++) { DrawDebugPoint(CurrentWorld, Points[i], 10.0f, FColor(255, 255, 0), false, 4); DrawDebugString(CurrentWorld, Points[i], FString::Printf(TEXT("%d"), i), nullptr, FColor::White, 4); DrawDebugLine(CurrentWorld, Points[i], ListenerPosition, FColor::Cyan, false, 4); DrawDebugLine(CurrentWorld, Points[i], SourcePosition, FColor::Cyan, false, 4); } FColor LineColor = FColor::MakeRedToGreenColorFromScalar(1.0f - MapEntry->Occ.TargetValue); DrawDebugLine(CurrentWorld, ListenerPosition, SourcePosition, LineColor, false, 4); #endif // AK_DEBUG_OCCLUSION } else { MapEntry->Occ.SetTarget(0.0f); MapEntry->Obs.SetTarget(0.0f); MapEntry->Reset(); } } } void AkObstructionAndOcclusionService::SetObstructionAndOcclusion(const UAkComponentSet& in_Listeners, AkRoomID RoomID) { FAkAudioDevice* AkAudioDevice = FAkAudioDevice::Get(); if (!AkAudioDevice) return; for (auto& Listener : in_Listeners) { if (RoomID != Listener->GetSpatialAudioRoom()) continue; auto MapEntry = ListenerInfoMap.Find(Listener->GetAkGameObjectID()); if (MapEntry == nullptr) continue; MapEntry->Occ.CurrentValue = MapEntry->Occ.TargetValue; SetObstructionAndOcclusion(Listener->GetAkGameObjectID(), MapEntry->Obs.CurrentValue/*, Occlusion.CurrentValue*/); } } void AkObstructionAndOcclusionService::ClearOcclusionValues() { ClearingObstructionAndOcclusion = false; for (auto& ListenerPack : ListenerInfoMap) { FAkListenerObstructionAndOcclusionPair& Pair = ListenerPack.Value; Pair.Occ.SetTarget(0.0f); Pair.Obs.SetTarget(0.0f); ClearingObstructionAndOcclusion |= !Pair.ReachedTarget(); } } void AkObstructionAndOcclusionService::Tick(const UAkComponentSet& in_Listeners, const FVector& SourcePosition, const AActor* Actor, AkRoomID RoomID, ECollisionChannel in_collisionChannel, float DeltaTime, float OcclusionRefreshInterval) { // Check Occlusion/Obstruction, if enabled if (OcclusionRefreshInterval > 0.0f || ClearingObstructionAndOcclusion) { RefreshObstructionAndOcclusion(in_Listeners, SourcePosition, Actor, RoomID, in_collisionChannel, DeltaTime, OcclusionRefreshInterval); } else if (OcclusionRefreshInterval != PreviousRefreshInterval) { // Reset the occlusion obstruction pairs so that the occlusion is correctly recalculated. for (auto& ListenerPack : ListenerInfoMap) { FAkListenerObstructionAndOcclusionPair& Pair = ListenerPack.Value; Pair.Reset(); } if (OcclusionRefreshInterval <= 0.0f) ClearOcclusionValues(); } PreviousRefreshInterval = OcclusionRefreshInterval; } void AkObstructionAndOcclusionService::UpdateObstructionAndOcclusion(const UAkComponentSet& in_Listeners, const FVector& SourcePosition, const AActor* Actor, AkRoomID RoomID, ECollisionChannel in_collisionChannel, float OcclusionRefreshInterval) { if ((OcclusionRefreshInterval > 0.f || ClearingObstructionAndOcclusion) && Actor) { for (auto& Listener : in_Listeners) { auto& MapEntry = ListenerInfoMap.FindOrAdd(Listener->GetAkGameObjectID()); MapEntry.Position = Listener->GetPosition(); } CalculateObstructionAndOcclusionValues(in_Listeners, SourcePosition, Actor, RoomID, in_collisionChannel, false); for (auto& ListenerPair : ListenerInfoMap) { ListenerPair.Value.Obs.CurrentValue = ListenerPair.Value.Obs.TargetValue; ListenerPair.Value.Occ.CurrentValue = ListenerPair.Value.Occ.TargetValue; } SetObstructionAndOcclusion(in_Listeners, RoomID); } }