453 lines
17 KiB
C++
453 lines
17 KiB
C++
/*******************************************************************************
|
|
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);
|
|
}
|
|
}
|
|
|