From b006c1a996a6b5dc848d34db54cd6ee22efea26f Mon Sep 17 00:00:00 2001 From: Phevos Rinis Date: Thu, 15 Oct 2026 10:14:51 +0000 Subject: [PATCH 01/11] create the AudPortal class to wrap around AkPortal - rooms and portals share the new AudUnitBox class for their box --- CMakeLists.txt | 6 +++ src/AudPortal.cpp | 94 ++++++++++++++++++++++++++++++++++ src/AudPortal.h | 74 ++++++++++++++++++++++++++ src/AudPortal_Blue.cpp | 26 ++++++++++ src/AudRoom.cpp | 52 ++----------------- src/AudRoom.h | 18 ++----- src/AudRoomManager.cpp | 12 ++--- src/AudRoomManager.h | 15 +++++- src/AudRoomManager_Portals.cpp | 35 +++++++++++++ src/AudUnitBox.cpp | 67 ++++++++++++++++++++++++ src/AudUnitBox.h | 41 +++++++++++++++ 11 files changed, 370 insertions(+), 70 deletions(-) create mode 100644 src/AudPortal.cpp create mode 100644 src/AudPortal.h create mode 100644 src/AudPortal_Blue.cpp create mode 100644 src/AudRoomManager_Portals.cpp create mode 100644 src/AudUnitBox.cpp create mode 100644 src/AudUnitBox.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 1631dd3..b4ef4f6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,9 +45,13 @@ ccp_add_library(CarbonAudio SHARED src/AudGeometry.cpp src/AudGeometry_Blue.cpp src/AudObstructionOcclusion.cpp + src/AudPortal.cpp + src/AudPortal_Blue.cpp src/AudRoom.cpp src/AudRoom_Blue.cpp src/AudRoomManager.cpp + src/AudRoomManager_Portals.cpp + src/AudUnitBox.cpp src/SpatialAudioSettings.cpp src/audio2.cpp src/AudGameObjResource_Blue.cpp @@ -89,8 +93,10 @@ target_sources(CarbonAudio PRIVATE src/AudMusicPlayer.h src/AudGeometry.h src/AudObstructionOcclusion.h + src/AudPortal.h src/AudRoom.h src/AudRoomManager.h + src/AudUnitBox.h src/SpatialAudioSettings.h src/autoversion.h src/DebugUtilities.h diff --git a/src/AudPortal.cpp b/src/AudPortal.cpp new file mode 100644 index 0000000..9745eea --- /dev/null +++ b/src/AudPortal.cpp @@ -0,0 +1,94 @@ +//////////////////////////////////////////////////////////// +// +// Creator: Phevos Rinis +// Creation Date: Oct 2026 +// Copyright (c) 2026 CCP Games +// + +#include "stdafx.h" +#include "AudPortal.h" + +#include "AudManager.h" +#include "AudRoomManager.h" + +AudPortal::AudPortal( IRoot* lockobj ) : + m_portalID( AllocateGameObjectID() ), + m_open( true ), + m_adjacentRoomBleed( 1.0f ), + m_enabled( true ), + m_shapeEnabled( true ), + m_warnedDegenerate( false ), + m_registeredWithManager( false ) +{ + EnsureRegistered(); +} + +AudPortal::~AudPortal() +{ + if( g_audioManager != nullptr && m_registeredWithManager ) + { + g_audioManager->GetRoomManager().UnregisterPortal( this ); + } +} + +bool AudPortal::EnsureRegistered() +{ + if( m_registeredWithManager ) + { + return true; + } + + if( g_audioManager == nullptr ) + { + return false; + } + + g_audioManager->GetRoomManager().RegisterPortal( this ); + m_registeredWithManager = true; + return true; +} + +void AudPortal::SetTransform( const Matrix& unitBoxToWorld ) +{ + if( EnsureRegistered() ) + { + g_audioManager->GetRoomManager().SetPortalTransform( *this, unitBoxToWorld ); + } + else + { + ApplyTransform( unitBoxToWorld ); + } +} + +void AudPortal::ApplyTransform( const Matrix& unitBoxToWorld ) +{ + if( !m_box.SetTransform( unitBoxToWorld ) && !m_warnedDegenerate ) + { + CCP_LOGWARN( "Portal '%s' (%llu) has a box with a zero-length axis; it is not sent to Wwise until it is scaled.", m_name.c_str(), m_portalID ); + m_warnedDegenerate = true; + } +} + +void AudPortal::SetEnabled( bool enabled ) +{ + m_shapeEnabled = enabled; + EnsureRegistered(); +} + +void AudPortal::Remove() +{ + if( EnsureRegistered() ) + { + g_audioManager->GetRoomManager().RemovePortalShape( *this ); + } + else + { + m_box.ClearTransform(); + } +} + +bool AudPortal::OnModified( Be::Var* value ) +{ + EnsureRegistered(); + return true; +} diff --git a/src/AudPortal.h b/src/AudPortal.h new file mode 100644 index 0000000..940f694 --- /dev/null +++ b/src/AudPortal.h @@ -0,0 +1,74 @@ +//////////////////////////////////////////////////////////// +// +// Creator: Phevos Rinis +// Creation Date: Oct 2026 +// Copyright (c) 2026 CCP Games +// + +#pragma once + +#include + +#include "AudUnitBox.h" + +#include + +#include + +/** + * @brief A Wwise Spatial Audio portal, placed as a unit box through ITr2BoxVolume, with the front room on the box's local -Z side. + */ +BLUE_CLASS( AudPortal ) : + public ITr2BoxVolume, + public INotify +{ +public: + AudPortal( IRoot* lockobj = NULL ); + virtual ~AudPortal(); + + EXPOSE_TO_BLUE(); + + // ITr2BoxVolume + void SetTransform( const Matrix& unitBoxToWorld ) override; + void SetEnabled( bool enabled ) override; + void Remove() override; + + // INotify + bool OnModified( Be::Var* value ) override; + + /// Portal ID, from the same ID space as game objects. + AkUInt64 GetPortalID() const { return m_portalID; } + /// Name shown in the Wwise profiler. + const std::string& GetName() const { return m_name; } + /// Whether sound passes through the portal, e.g. an open door. + bool IsOpen() const { return m_open; } + float GetAdjacentRoomBleed() const { return m_adjacentRoomBleed; } + /// True when the portal and its volume object are both enabled. + bool IsEnabled() const { return m_enabled && m_shapeEnabled; } + /// Box of the portal, set through ITr2BoxVolume. + const AudUnitBox& GetBox() const { return m_box; } + +private: + friend class AudRoomManager; + + /// Registers with the room manager once it exists. + bool EnsureRegistered(); + /// Stores the box. Called by AudRoomManager under its lock. + void ApplyTransform( const Matrix& unitBoxToWorld ); + + AkUInt64 m_portalID; + + std::string m_name; + bool m_open; + float m_adjacentRoomBleed; + /// Enabled attribute of the portal. + bool m_enabled; + /// Set through ITr2BoxVolume::SetEnabled. + bool m_shapeEnabled; + + AudUnitBox m_box; + bool m_warnedDegenerate; + bool m_registeredWithManager; +}; + +TYPEDEF_BLUECLASS( AudPortal ); diff --git a/src/AudPortal_Blue.cpp b/src/AudPortal_Blue.cpp new file mode 100644 index 0000000..221c2d5 --- /dev/null +++ b/src/AudPortal_Blue.cpp @@ -0,0 +1,26 @@ +//////////////////////////////////////////////////////////// +// +// Creator: Phevos Rinis +// Creation Date: Oct 2026 +// Copyright (c) 2026 CCP Games +// + +#include "stdafx.h" +#include "AudPortal.h" + +BLUE_DEFINE( AudPortal ); + +const Be::ClassInfo* AudPortal::ExposeToBlue() +{ + EXPOSURE_BEGIN( AudPortal, "A Wwise Spatial Audio portal, placed as a unit box through ITr2BoxVolume" ) + MAP_INTERFACE( ITr2BoxVolume ) + MAP_INTERFACE( INotify ) + + MAP_ATTRIBUTE( "name", m_name, "Name of the portal, shown in the Wwise profiler.", Be::READWRITE | Be::PERSIST | Be::NOTIFY ) + MAP_ATTRIBUTE( "open", m_open, "Whether sound passes through the portal, e.g. an open door.\n:jessica-group: Acoustics", Be::READWRITE | Be::PERSIST | Be::NOTIFY ) + MAP_ATTRIBUTE( "adjacentRoomBleed", m_adjacentRoomBleed, "Scales how much reverb of the room on the other side is heard through the portal.\n:jessica-group: Acoustics", Be::READWRITE | Be::PERSIST | Be::NOTIFY ) + MAP_ATTRIBUTE( "enabled", m_enabled, "Whether the portal is active. It is only sent to Wwise when its volume object is enabled too.", Be::READWRITE | Be::PERSIST | Be::NOTIFY ) + + MAP_ATTRIBUTE( "portalID", m_portalID, "Wwise portal identifier.\n:jessica-hidden: True", Be::READ ) + EXPOSURE_END() +} diff --git a/src/AudRoom.cpp b/src/AudRoom.cpp index 9247486..319868a 100644 --- a/src/AudRoom.cpp +++ b/src/AudRoom.cpp @@ -11,8 +11,6 @@ #include "AudManager.h" #include "AudRoomManager.h" -#include - AudRoom::AudRoom( IRoot* lockobj ) : m_roomID( AllocateGameObjectID() ), m_reverbLevel( 1.0f ), @@ -22,11 +20,7 @@ AudRoom::AudRoom( IRoot* lockobj ) : m_keepRegistered( false ), m_enabled( true ), m_shapeEnabled( true ), - m_unitBoxToWorld( IdentityMatrix() ), - m_volume( 0.0f ), - m_shapeValid( false ), m_warnedDegenerate( false ), - m_hasTransform( false ), m_sentToWwise( false ), m_registeredWithManager( false ), m_roomTonePlayingID( AK_INVALID_PLAYING_ID ), @@ -68,7 +62,7 @@ void AudRoom::Sync() } AudRoomManager& manager = g_audioManager->GetRoomManager(); - if( IsEnabled() && m_hasTransform && m_shapeValid ) + if( IsEnabled() && m_box.IsUsable() ) { manager.Push( *this ); } @@ -92,53 +86,13 @@ void AudRoom::SetTransform( const Matrix& unitBoxToWorld ) void AudRoom::ApplyTransform( const Matrix& unitBoxToWorld ) { - m_unitBoxToWorld = unitBoxToWorld; - m_volume = std::fabs( Determinant( m_unitBoxToWorld ) ); - m_hasTransform = true; - - const Matrix& m = m_unitBoxToWorld; - constexpr float minAxisLengthSq = 1e-6f; - m_shapeValid = LengthSq( Vector3( m._11, m._12, m._13 ) ) > minAxisLengthSq - && LengthSq( Vector3( m._21, m._22, m._23 ) ) > minAxisLengthSq - && LengthSq( Vector3( m._31, m._32, m._33 ) ) > minAxisLengthSq; - if( !m_shapeValid && !m_warnedDegenerate ) + if( !m_box.SetTransform( unitBoxToWorld ) && !m_warnedDegenerate ) { CCP_LOGWARN( "Room '%s' (%llu) has a box with a zero-length axis; it is not sent to Wwise until it is scaled.", m_name.c_str(), m_roomID ); m_warnedDegenerate = true; } } -bool AudRoom::ContainsPoint( const Vector3& worldPosition ) const -{ - if( !m_hasTransform || !m_shapeValid ) - { - return false; - } - - const Matrix& m = m_unitBoxToWorld; - const Vector3 axes[3] = - { - Vector3( m._11, m._12, m._13 ), - Vector3( m._21, m._22, m._23 ), - Vector3( m._31, m._32, m._33 ), - }; - const Vector3 offset = worldPosition - Vector3( m._41, m._42, m._43 ); - - for( const Vector3& axis : axes ) - { - const float lengthSq = LengthSq( axis ); - if( lengthSq <= 1e-12f ) - { - return false; - } - if( std::fabs( Dot( offset, axis ) ) > 0.5f * lengthSq ) - { - return false; - } - } - return true; -} - void AudRoom::SetEnabled( bool enabled ) { if( m_shapeEnabled == enabled ) @@ -158,7 +112,7 @@ void AudRoom::Remove() } else { - m_hasTransform = false; + m_box.ClearTransform(); } } diff --git a/src/AudRoom.h b/src/AudRoom.h index f6bface..7c8a5d9 100644 --- a/src/AudRoom.h +++ b/src/AudRoom.h @@ -9,6 +9,8 @@ #include +#include "AudUnitBox.h" + #include #include @@ -50,18 +52,11 @@ BLUE_CLASS( AudRoom ) : const std::wstring& GetRoomToneEvent() const { return m_roomToneEvent; } /// True when the room and its volume object are both enabled. bool IsEnabled() const { return m_enabled && m_shapeEnabled; } - bool HasTransform() const { return m_hasTransform; } - /// False when the box has a zero-length axis. - bool HasUsableShape() const { return m_shapeValid; } - const Matrix& GetUnitBoxToWorld() const { return m_unitBoxToWorld; } + /// Box of the room, set through ITr2BoxVolume. + const AudUnitBox& GetBox() const { return m_box; } /// Whether the room is in Wwise. bool IsRegistered() const { return m_sentToWwise; } - /// Returns whether the point is inside the room, in right-handed world space. - bool ContainsPoint( const Vector3& worldPosition ) const; - /// Volume of the box, the smaller room wins when priorities are equal. - float GetVolume() const { return m_volume; } - private: friend class AudRoomManager; @@ -87,11 +82,8 @@ BLUE_CLASS( AudRoom ) : /// Set through ITr2BoxVolume::SetEnabled. bool m_shapeEnabled; - Matrix m_unitBoxToWorld; - float m_volume; - bool m_shapeValid; + AudUnitBox m_box; bool m_warnedDegenerate; - bool m_hasTransform; bool m_sentToWwise; bool m_registeredWithManager; diff --git a/src/AudRoomManager.cpp b/src/AudRoomManager.cpp index 5615e39..63320b8 100644 --- a/src/AudRoomManager.cpp +++ b/src/AudRoomManager.cpp @@ -180,7 +180,7 @@ AkUInt64 AudRoomManager::ResolveRoomLocked( const Vector3& position ) const for( const auto& entry : m_rooms ) { const AudRoom* room = entry.second; - if( !room->m_sentToWwise || !room->ContainsPoint( position ) ) + if( !room->m_sentToWwise || !room->GetBox().ContainsPoint( position ) ) { continue; } @@ -189,7 +189,7 @@ AkUInt64 AudRoomManager::ResolveRoomLocked( const Vector3& position ) const const float bestPriority = best != nullptr ? best->m_sentRoomParams.RoomPriority : 0.0f; if( best == nullptr || priority > bestPriority - || ( priority == bestPriority && room->GetVolume() < best->GetVolume() ) ) + || ( priority == bestPriority && room->GetBox().GetVolume() < best->GetBox().GetVolume() ) ) { best = room; } @@ -298,7 +298,7 @@ bool AudRoomManager::EnsureOutdoorRoomConfigured() void AudRoomManager::PushLocked( AudRoom& room ) { - if( !room.IsEnabled() || !room.HasTransform() || !room.HasUsableShape() ) + if( !room.IsEnabled() || !room.GetBox().IsUsable() ) { RemoveLocked( room ); return; @@ -317,11 +317,11 @@ void AudRoomManager::PushLocked( AudRoom& room ) const AkUInt64 instanceID = GeometryInstanceIDForRoom( room ); AkTransform transform; - RH2LH::convertTransform( room.GetUnitBoxToWorld(), transform ); + RH2LH::convertTransform( room.GetBox().GetUnitBoxToWorld(), transform ); AkGeometryInstanceParams instanceParams; instanceParams.PositionAndOrientation = transform; - instanceParams.Scale = RH2LH::extractScale( room.GetUnitBoxToWorld() ); + instanceParams.Scale = RH2LH::extractScale( room.GetBox().GetUnitBoxToWorld() ); instanceParams.GeometrySetID = SHARED_CUBE_GEOMETRY_SET_ID; instanceParams.UseForReflectionAndDiffraction = false; instanceParams.BypassPortalSubtraction = false; @@ -541,7 +541,7 @@ void AudRoomManager::SetTransform( AudRoom& room, const Matrix& unitBoxToWorld ) void AudRoomManager::RemoveShape( AudRoom& room ) { CcpAutoMutex lock( m_mutex ); - room.m_hasTransform = false; + room.m_box.ClearTransform(); RemoveLocked( room ); } diff --git a/src/AudRoomManager.h b/src/AudRoomManager.h index 27002f5..6703a46 100644 --- a/src/AudRoomManager.h +++ b/src/AudRoomManager.h @@ -17,10 +17,11 @@ #include class AudManager; +class AudPortal; class AudRoom; /** - * @brief Sends every AudRoom to Wwise and assigns game objects to rooms. + * @brief Sends every AudRoom and AudPortal to Wwise and assigns game objects to rooms. * * Lock order: m_mutex can be held while taking AudManager's SoundBank and action-log locks, never the other way round. */ @@ -43,6 +44,15 @@ class AudRoomManager /// Clears the room's box and removes the room from Wwise. void RemoveShape( AudRoom& room ); + /// Called by AudPortal on construction and destruction. + void RegisterPortal( AudPortal* portal ); + void UnregisterPortal( AudPortal* portal ); + + /// Stores the portal's box under the lock. + void SetPortalTransform( AudPortal& portal, const Matrix& unitBoxToWorld ); + /// Clears the portal's box. + void RemovePortalShape( AudPortal& portal ); + /// Removes every room from Wwise, for Disable() and the rooms switch. void RemoveAllFromWwise(); /// Sends every enabled room to Wwise, for Enable() and the rooms switch. @@ -93,13 +103,14 @@ class AudRoomManager /// Plays the room tone, or waits for its SoundBanks. Caller holds m_mutex. void PostRoomToneLocked( AudRoom& room, const std::wstring& eventName ); - /// Returns the room containing the position, or the outdoor room. Caller holds m_mutex. + /// Returns the room containing the position, or the outdoor room. The smaller room wins when priorities are equal. Caller holds m_mutex. AkUInt64 ResolveRoomLocked( const Vector3& position ) const; /// Calls SetGameObjectInRoom when the object's room changed. Caller holds m_mutex. void AssignLocked( AkGameObjectID gameObjectID, TrackedGameObject& tracked ); AudManager* m_audioManager; std::unordered_map m_rooms; + std::unordered_map m_portals; std::unordered_map m_trackedObjects; bool m_cubeSent; bool m_outdoorConfigured; diff --git a/src/AudRoomManager_Portals.cpp b/src/AudRoomManager_Portals.cpp new file mode 100644 index 0000000..c050dbd --- /dev/null +++ b/src/AudRoomManager_Portals.cpp @@ -0,0 +1,35 @@ +//////////////////////////////////////////////////////////// +// +// Creator: Phevos Rinis +// Creation Date: Oct 2026 +// Copyright (c) 2026 CCP Games +// + +#include "stdafx.h" +#include "AudRoomManager.h" + +#include "AudPortal.h" + +void AudRoomManager::RegisterPortal( AudPortal* portal ) +{ + CcpAutoMutex lock( m_mutex ); + m_portals[portal->GetPortalID()] = portal; +} + +void AudRoomManager::UnregisterPortal( AudPortal* portal ) +{ + CcpAutoMutex lock( m_mutex ); + m_portals.erase( portal->GetPortalID() ); +} + +void AudRoomManager::SetPortalTransform( AudPortal& portal, const Matrix& unitBoxToWorld ) +{ + CcpAutoMutex lock( m_mutex ); + portal.ApplyTransform( unitBoxToWorld ); +} + +void AudRoomManager::RemovePortalShape( AudPortal& portal ) +{ + CcpAutoMutex lock( m_mutex ); + portal.m_box.ClearTransform(); +} diff --git a/src/AudUnitBox.cpp b/src/AudUnitBox.cpp new file mode 100644 index 0000000..4984381 --- /dev/null +++ b/src/AudUnitBox.cpp @@ -0,0 +1,67 @@ +//////////////////////////////////////////////////////////// +// +// Creator: Phevos Rinis +// Creation Date: Oct 2026 +// Copyright (c) 2026 CCP Games +// + +#include "stdafx.h" +#include "AudUnitBox.h" + +#include + +AudUnitBox::AudUnitBox() : + m_unitBoxToWorld( IdentityMatrix() ), + m_volume( 0.0f ), + m_usable( false ) +{ +} + +bool AudUnitBox::SetTransform( const Matrix& unitBoxToWorld ) +{ + m_unitBoxToWorld = unitBoxToWorld; + m_volume = std::fabs( Determinant( m_unitBoxToWorld ) ); + + const Matrix& m = m_unitBoxToWorld; + constexpr float minAxisLengthSq = 1e-6f; + m_usable = LengthSq( Vector3( m._11, m._12, m._13 ) ) > minAxisLengthSq + && LengthSq( Vector3( m._21, m._22, m._23 ) ) > minAxisLengthSq + && LengthSq( Vector3( m._31, m._32, m._33 ) ) > minAxisLengthSq; + return m_usable; +} + +void AudUnitBox::ClearTransform() +{ + m_usable = false; +} + +bool AudUnitBox::ContainsPoint( const Vector3& worldPosition ) const +{ + if( !m_usable ) + { + return false; + } + + const Matrix& m = m_unitBoxToWorld; + const Vector3 axes[3] = + { + Vector3( m._11, m._12, m._13 ), + Vector3( m._21, m._22, m._23 ), + Vector3( m._31, m._32, m._33 ), + }; + const Vector3 offset = worldPosition - Vector3( m._41, m._42, m._43 ); + + for( const Vector3& axis : axes ) + { + if( std::fabs( Dot( offset, axis ) ) > 0.5f * LengthSq( axis ) ) + { + return false; + } + } + return true; +} + +Vector3 AudUnitBox::ToWorld( const Vector3& unitBoxPosition ) const +{ + return TransformCoord( unitBoxPosition, m_unitBoxToWorld ); +} diff --git a/src/AudUnitBox.h b/src/AudUnitBox.h new file mode 100644 index 0000000..c225405 --- /dev/null +++ b/src/AudUnitBox.h @@ -0,0 +1,41 @@ +//////////////////////////////////////////////////////////// +// +// Creator: Phevos Rinis +// Creation Date: Oct 2026 +// Copyright (c) 2026 CCP Games +// + +#pragma once + +#include "Matrix.h" +#include "Vector3.h" + +/** + * @brief The unit box [-0.5, 0.5]^3 placed by a transform in right-handed world space. + */ +class AudUnitBox +{ +public: + AudUnitBox(); + + /// Stores the transform. Returns false when the box has a zero-length axis. + bool SetTransform( const Matrix& unitBoxToWorld ); + /// Forgets the transform. + void ClearTransform(); + + /// True when the box has a transform with no zero-length axis. + bool IsUsable() const { return m_usable; } + const Matrix& GetUnitBoxToWorld() const { return m_unitBoxToWorld; } + /// Volume of the box. + float GetVolume() const { return m_volume; } + + /// Returns whether the point is inside the box, in right-handed world space. + bool ContainsPoint( const Vector3& worldPosition ) const; + /// Returns the world position of a point in unit box space. + Vector3 ToWorld( const Vector3& unitBoxPosition ) const; + +private: + Matrix m_unitBoxToWorld; + float m_volume; + bool m_usable; +}; From d1295a5251e093cf7558e521215e2f3d4fd85e8c Mon Sep 17 00:00:00 2001 From: Phevos Rinis Date: Thu, 15 Oct 2026 10:16:36 +0000 Subject: [PATCH 02/11] send portals to wwise --- src/AudPortal.cpp | 9 ++- src/AudPortal.h | 17 +++++ src/AudPortal_Blue.cpp | 3 + src/AudRoomManager.cpp | 9 ++- src/AudRoomManager.h | 21 ++++-- src/AudRoomManager_Portals.cpp | 120 +++++++++++++++++++++++++++++++++ 6 files changed, 171 insertions(+), 8 deletions(-) diff --git a/src/AudPortal.cpp b/src/AudPortal.cpp index 9745eea..93fb1db 100644 --- a/src/AudPortal.cpp +++ b/src/AudPortal.cpp @@ -18,7 +18,14 @@ AudPortal::AudPortal( IRoot* lockobj ) : m_enabled( true ), m_shapeEnabled( true ), m_warnedDegenerate( false ), - m_registeredWithManager( false ) + m_sentToWwise( false ), + m_registeredWithManager( false ), + m_frontRoomID( AudRoomManager::OUTDOOR_ROOM_ID ), + m_backRoomID( AudRoomManager::OUTDOOR_ROOM_ID ), + m_foundFrontRoomID( AudRoomManager::OUTDOOR_ROOM_ID ), + m_foundBackRoomID( AudRoomManager::OUTDOOR_ROOM_ID ), + m_foundTicks( 0 ), + m_failureLogged( false ) { EnsureRegistered(); } diff --git a/src/AudPortal.h b/src/AudPortal.h index 940f694..1729cbb 100644 --- a/src/AudPortal.h +++ b/src/AudPortal.h @@ -47,6 +47,12 @@ BLUE_CLASS( AudPortal ) : bool IsEnabled() const { return m_enabled && m_shapeEnabled; } /// Box of the portal, set through ITr2BoxVolume. const AudUnitBox& GetBox() const { return m_box; } + /// Whether the portal is in Wwise. + bool IsRegistered() const { return m_sentToWwise; } + /// Room on the front side, as sent to Wwise. + AkUInt64 GetFrontRoomID() const { return m_frontRoomID; } + /// Room on the back side, as sent to Wwise. + AkUInt64 GetBackRoomID() const { return m_backRoomID; } private: friend class AudRoomManager; @@ -68,7 +74,18 @@ BLUE_CLASS( AudPortal ) : AudUnitBox m_box; bool m_warnedDegenerate; + bool m_sentToWwise; bool m_registeredWithManager; + + AkUInt64 m_frontRoomID; + AkUInt64 m_backRoomID; + /// Rooms found at the faces on the last update. + AkUInt64 m_foundFrontRoomID; + AkUInt64 m_foundBackRoomID; + /// Updates in a row the same rooms were found. + int m_foundTicks; + /// Set after a failed Wwise call is logged, so it is not logged again every update. + bool m_failureLogged; }; TYPEDEF_BLUECLASS( AudPortal ); diff --git a/src/AudPortal_Blue.cpp b/src/AudPortal_Blue.cpp index 221c2d5..f0e4530 100644 --- a/src/AudPortal_Blue.cpp +++ b/src/AudPortal_Blue.cpp @@ -22,5 +22,8 @@ const Be::ClassInfo* AudPortal::ExposeToBlue() MAP_ATTRIBUTE( "enabled", m_enabled, "Whether the portal is active. It is only sent to Wwise when its volume object is enabled too.", Be::READWRITE | Be::PERSIST | Be::NOTIFY ) MAP_ATTRIBUTE( "portalID", m_portalID, "Wwise portal identifier.\n:jessica-hidden: True", Be::READ ) + MAP_ATTRIBUTE( "isRegistered", m_sentToWwise, "Whether the portal is in Wwise.\n:jessica-hidden: True", Be::READ ) + MAP_ATTRIBUTE( "frontRoomID", m_frontRoomID, "Room on the front side of the portal in Wwise.\n:jessica-hidden: True", Be::READ ) + MAP_ATTRIBUTE( "backRoomID", m_backRoomID, "Room on the back side of the portal in Wwise.\n:jessica-hidden: True", Be::READ ) EXPOSURE_END() } diff --git a/src/AudRoomManager.cpp b/src/AudRoomManager.cpp index 63320b8..1072673 100644 --- a/src/AudRoomManager.cpp +++ b/src/AudRoomManager.cpp @@ -47,8 +47,6 @@ namespace namespace { - const AkUInt64 OUTDOOR_ROOM_ID = static_cast( AK::SpatialAudio::kOutdoorRoomID ); - /// Compares orientations with a small tolerance for float errors. bool OrientationsNearlyEqual( const AkVector& a, const AkVector& b ) { @@ -162,6 +160,8 @@ void AudRoomManager::Update() } } + SyncPortalsLocked(); + if( !m_assignmentsDirty ) { return; @@ -285,7 +285,7 @@ bool AudRoomManager::EnsureOutdoorRoomConfigured() params.RoomGameObj_KeepRegistered = false; params.GeometryInstanceID = AkGeometryInstanceID(); - AKRESULT result = AK::SpatialAudio::SetRoom( AK::SpatialAudio::kOutdoorRoomID, params, "Outdoors" ); + AKRESULT result = AK::SpatialAudio::SetRoom( AkRoomID( OUTDOOR_ROOM_ID ), params, "Outdoors" ); if( result != AK_Success ) { CCP_LOGERR_CH( s_ch, "Failed to configure the outdoor room, AKRESULT: %d", result ); @@ -548,6 +548,8 @@ void AudRoomManager::RemoveShape( AudRoom& room ) void AudRoomManager::RemoveAllFromWwise() { CcpAutoMutex lock( m_mutex ); + RemoveAllPortalsLocked(); + for( auto& entry : m_rooms ) { RemoveLocked( *entry.second ); @@ -580,6 +582,7 @@ void AudRoomManager::ForgetWwiseState() room.m_roomTonePending = false; room.m_postedRoomTone.clear(); } + RemoveAllPortalsLocked(); m_pendingRoomTones = 0; m_roomsInWwise.store( 0, std::memory_order_release ); m_cubeSent = false; diff --git a/src/AudRoomManager.h b/src/AudRoomManager.h index 6703a46..56735e3 100644 --- a/src/AudRoomManager.h +++ b/src/AudRoomManager.h @@ -8,6 +8,7 @@ #pragma once #include +#include #include #include "Matrix.h" @@ -28,6 +29,9 @@ class AudRoom; class AudRoomManager { public: + /// Room ID of the Wwise outdoor room. + static constexpr AkUInt64 OUTDOOR_ROOM_ID = AK_OUTDOORS_ROOM_ID; + explicit AudRoomManager( AudManager* audioManager ); ~AudRoomManager(); @@ -48,12 +52,12 @@ class AudRoomManager void RegisterPortal( AudPortal* portal ); void UnregisterPortal( AudPortal* portal ); - /// Stores the portal's box under the lock. + /// Stores the portal's box under the lock. The portal is sent on the next Update(). void SetPortalTransform( AudPortal& portal, const Matrix& unitBoxToWorld ); - /// Clears the portal's box. + /// Clears the portal's box and removes the portal from Wwise. void RemovePortalShape( AudPortal& portal ); - /// Removes every room from Wwise, for Disable() and the rooms switch. + /// Removes every portal and room from Wwise, for Disable() and the rooms switch. void RemoveAllFromWwise(); /// Sends every enabled room to Wwise, for Enable() and the rooms switch. void ResendAll(); @@ -64,7 +68,7 @@ class AudRoomManager void UpdateGameObjectPosition( AkGameObjectID gameObjectID, const Vector3& position ); /// Removes a game object that was unregistered from Wwise. void ForgetGameObject( AkGameObjectID gameObjectID ); - /// Assigns game objects again when rooms changed and retries room tones waiting for SoundBanks. + /// Sends portals, assigns game objects again when rooms changed and retries room tones waiting for SoundBanks. void Update(); size_t GetRoomCount() const; @@ -108,6 +112,15 @@ class AudRoomManager /// Calls SetGameObjectInRoom when the object's room changed. Caller holds m_mutex. void AssignLocked( AkGameObjectID gameObjectID, TrackedGameObject& tracked ); + /// Updates every portal in Wwise. Caller holds m_mutex. + void SyncPortalsLocked(); + /// Sends one portal between the rooms at its two faces, or removes it. Caller holds m_mutex. + void SyncPortalLocked( AudPortal& portal ); + /// Removes one portal from Wwise. Caller holds m_mutex. + void RemovePortalLocked( AudPortal& portal ); + /// Removes every portal from Wwise. Caller holds m_mutex. + void RemoveAllPortalsLocked(); + AudManager* m_audioManager; std::unordered_map m_rooms; std::unordered_map m_portals; diff --git a/src/AudRoomManager_Portals.cpp b/src/AudRoomManager_Portals.cpp index c050dbd..7da1c55 100644 --- a/src/AudRoomManager_Portals.cpp +++ b/src/AudRoomManager_Portals.cpp @@ -8,7 +8,22 @@ #include "stdafx.h" #include "AudRoomManager.h" +#include "AudManager.h" #include "AudPortal.h" +#include "Utilities.h" + +#include +#include + +static CcpLogChannel_t s_ch = CCP_LOG_DEFINE_CHANNEL( "AudRoomManager" ); + +namespace +{ + /// Updates in a row a portal can have the same room on both sides before it is reported. + constexpr int PORTAL_SAME_ROOM_WARNING_TICKS = 30; + /// Smallest adjacent room bleed sent to Wwise, which needs it above zero. + constexpr float MIN_ADJACENT_ROOM_BLEED = 1e-3f; +} void AudRoomManager::RegisterPortal( AudPortal* portal ) { @@ -19,6 +34,7 @@ void AudRoomManager::RegisterPortal( AudPortal* portal ) void AudRoomManager::UnregisterPortal( AudPortal* portal ) { CcpAutoMutex lock( m_mutex ); + RemovePortalLocked( *portal ); m_portals.erase( portal->GetPortalID() ); } @@ -32,4 +48,108 @@ void AudRoomManager::RemovePortalShape( AudPortal& portal ) { CcpAutoMutex lock( m_mutex ); portal.m_box.ClearTransform(); + RemovePortalLocked( portal ); +} + +void AudRoomManager::SyncPortalsLocked() +{ + for( auto& entry : m_portals ) + { + SyncPortalLocked( *entry.second ); + } +} + +void AudRoomManager::SyncPortalLocked( AudPortal& portal ) +{ + const AudUnitBox& box = portal.GetBox(); + if( !portal.IsEnabled() || !box.IsUsable() ) + { + portal.m_foundTicks = 0; + RemovePortalLocked( portal ); + return; + } + + const AkUInt64 frontRoomID = ResolveRoomLocked( box.ToWorld( Vector3( 0.0f, 0.0f, -0.5f ) ) ); + const AkUInt64 backRoomID = ResolveRoomLocked( box.ToWorld( Vector3( 0.0f, 0.0f, 0.5f ) ) ); + if( frontRoomID == portal.m_foundFrontRoomID && backRoomID == portal.m_foundBackRoomID ) + { + portal.m_foundTicks = std::min( portal.m_foundTicks + 1, PORTAL_SAME_ROOM_WARNING_TICKS + 1 ); + } + else + { + portal.m_foundFrontRoomID = frontRoomID; + portal.m_foundBackRoomID = backRoomID; + portal.m_foundTicks = 1; + } + + if( frontRoomID == backRoomID ) + { + RemovePortalLocked( portal ); + if( portal.m_foundTicks == PORTAL_SAME_ROOM_WARNING_TICKS ) + { + CCP_LOGWARN_CH( s_ch, "Portal '%s' (%llu) has room %llu on both sides; it is not sent to Wwise until it crosses a room wall.", + portal.GetName().c_str(), portal.GetPortalID(), frontRoomID ); + } + return; + } + + AkTransform transform; + RH2LH::convertTransform( box.GetUnitBoxToWorld(), transform ); + const AkVector scale = RH2LH::extractScale( box.GetUnitBoxToWorld() ); + + AkPortalParams params; + params.Transform = AK::ConvertAkTransformToAkWorldTransform( transform ); + params.Extent = AkExtent( 0.5f * scale.X, 0.5f * scale.Y, 0.5f * scale.Z ); + params.FrontRoom = AkRoomID( frontRoomID ); + params.BackRoom = AkRoomID( backRoomID ); + params.AdjacentRoomBleed = std::max( portal.GetAdjacentRoomBleed(), MIN_ADJACENT_ROOM_BLEED ); + params.bEnabled = portal.IsOpen(); + + const std::string name = portal.GetName().empty() ? "Portal " + std::to_string( portal.GetPortalID() ) : portal.GetName(); + const AKRESULT result = AK::SpatialAudio::SetPortal( portal.GetPortalID(), params, name.c_str() ); + if( result != AK_Success ) + { + if( !portal.m_failureLogged ) + { + CCP_LOGERR_CH( s_ch, "Failed to set portal '%s' (%llu), AKRESULT: %d", name.c_str(), portal.GetPortalID(), result ); + portal.m_failureLogged = true; + } + return; + } + + if( !portal.m_sentToWwise || portal.m_frontRoomID != frontRoomID || portal.m_backRoomID != backRoomID ) + { + CCP_LOG_CH( s_ch, "Portal '%s' (%llu) sent to Wwise with front room %llu and back room %llu.", + name.c_str(), portal.GetPortalID(), frontRoomID, backRoomID ); + } + + portal.m_sentToWwise = true; + portal.m_frontRoomID = frontRoomID; + portal.m_backRoomID = backRoomID; + portal.m_failureLogged = false; +} + +void AudRoomManager::RemovePortalLocked( AudPortal& portal ) +{ + if( !portal.m_sentToWwise ) + { + return; + } + + portal.m_sentToWwise = false; + portal.m_frontRoomID = OUTDOOR_ROOM_ID; + portal.m_backRoomID = OUTDOOR_ROOM_ID; + + if( AK::SoundEngine::IsInitialized() ) + { + AK::SpatialAudio::RemovePortal( portal.GetPortalID() ); + } +} + +void AudRoomManager::RemoveAllPortalsLocked() +{ + for( auto& entry : m_portals ) + { + RemovePortalLocked( *entry.second ); + } } From 6c65835e426325407a567a75b0bb524bdb189c6e Mon Sep 17 00:00:00 2001 From: Phevos Rinis Date: Thu, 15 Oct 2026 10:17:04 +0000 Subject: [PATCH 03/11] remove portals before the rooms they connect --- src/AudRoomManager.cpp | 1 + src/AudRoomManager.h | 2 ++ src/AudRoomManager_Portals.cpp | 12 ++++++++++++ 3 files changed, 15 insertions(+) diff --git a/src/AudRoomManager.cpp b/src/AudRoomManager.cpp index 1072673..4451db4 100644 --- a/src/AudRoomManager.cpp +++ b/src/AudRoomManager.cpp @@ -412,6 +412,7 @@ void AudRoomManager::RemoveLocked( AudRoom& room ) room.m_sentToWwise = false; m_assignmentsDirty = true; + RemovePortalsConnectedToLocked( room.GetRoomID() ); const bool lastRoom = m_roomsInWwise.fetch_sub( 1, std::memory_order_acq_rel ) == 1; const bool soundEngineUp = AK::SoundEngine::IsInitialized(); diff --git a/src/AudRoomManager.h b/src/AudRoomManager.h index 56735e3..25b56cd 100644 --- a/src/AudRoomManager.h +++ b/src/AudRoomManager.h @@ -120,6 +120,8 @@ class AudRoomManager void RemovePortalLocked( AudPortal& portal ); /// Removes every portal from Wwise. Caller holds m_mutex. void RemoveAllPortalsLocked(); + /// Removes the portals connected to the room before the room leaves Wwise. Caller holds m_mutex. + void RemovePortalsConnectedToLocked( AkUInt64 roomID ); AudManager* m_audioManager; std::unordered_map m_rooms; diff --git a/src/AudRoomManager_Portals.cpp b/src/AudRoomManager_Portals.cpp index 7da1c55..f1800fa 100644 --- a/src/AudRoomManager_Portals.cpp +++ b/src/AudRoomManager_Portals.cpp @@ -153,3 +153,15 @@ void AudRoomManager::RemoveAllPortalsLocked() RemovePortalLocked( *entry.second ); } } + +void AudRoomManager::RemovePortalsConnectedToLocked( AkUInt64 roomID ) +{ + for( auto& entry : m_portals ) + { + AudPortal& portal = *entry.second; + if( portal.m_frontRoomID == roomID || portal.m_backRoomID == roomID ) + { + RemovePortalLocked( portal ); + } + } +} From db6a906f68a0fe542a78ef416f4375a28860bce1 Mon Sep 17 00:00:00 2001 From: Phevos Rinis Date: Thu, 15 Oct 2026 10:17:43 +0000 Subject: [PATCH 04/11] only resend a portal when its parameters change --- src/AudPortal.h | 4 +++ src/AudRoomManager_Portals.cpp | 52 ++++++++++++++++++++++++++++++---- 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/src/AudPortal.h b/src/AudPortal.h index 1729cbb..9aff8ed 100644 --- a/src/AudPortal.h +++ b/src/AudPortal.h @@ -12,6 +12,7 @@ #include "AudUnitBox.h" #include +#include #include @@ -77,6 +78,9 @@ BLUE_CLASS( AudPortal ) : bool m_sentToWwise; bool m_registeredWithManager; + /// Last parameters sent to Wwise. + AkPortalParams m_sentPortalParams; + std::string m_sentName; AkUInt64 m_frontRoomID; AkUInt64 m_backRoomID; /// Rooms found at the faces on the last update. diff --git a/src/AudRoomManager_Portals.cpp b/src/AudRoomManager_Portals.cpp index f1800fa..77ec9a7 100644 --- a/src/AudRoomManager_Portals.cpp +++ b/src/AudRoomManager_Portals.cpp @@ -13,6 +13,7 @@ #include "Utilities.h" #include +#include #include static CcpLogChannel_t s_ch = CCP_LOG_DEFINE_CHANNEL( "AudRoomManager" ); @@ -23,6 +24,35 @@ namespace constexpr int PORTAL_SAME_ROOM_WARNING_TICKS = 30; /// Smallest adjacent room bleed sent to Wwise, which needs it above zero. constexpr float MIN_ADJACENT_ROOM_BLEED = 1e-3f; + + /// Compares positions with a 1 mm tolerance. + bool PositionsNearlyEqual( const AkVector64& a, const AkVector64& b ) + { + constexpr AkReal64 epsilon = 1e-3; + return std::fabs( a.X - b.X ) <= epsilon && std::fabs( a.Y - b.Y ) <= epsilon && std::fabs( a.Z - b.Z ) <= epsilon; + } + + /// Compares orientations with a small tolerance for float errors. + bool OrientationsNearlyEqual( const AkVector& a, const AkVector& b ) + { + constexpr float epsilon = 1e-4f; + return std::fabs( a.X - b.X ) <= epsilon && std::fabs( a.Y - b.Y ) <= epsilon && std::fabs( a.Z - b.Z ) <= epsilon; + } + + /// Compares every parameter SetPortal uses. + bool PortalParamsEqual( const AkPortalParams& a, const AkPortalParams& b ) + { + return PositionsNearlyEqual( a.Transform.Position(), b.Transform.Position() ) + && OrientationsNearlyEqual( a.Transform.OrientationFront(), b.Transform.OrientationFront() ) + && OrientationsNearlyEqual( a.Transform.OrientationTop(), b.Transform.OrientationTop() ) + && a.Extent.halfWidth == b.Extent.halfWidth + && a.Extent.halfHeight == b.Extent.halfHeight + && a.Extent.halfDepth == b.Extent.halfDepth + && a.FrontRoom == b.FrontRoom + && a.BackRoom == b.BackRoom + && a.AdjacentRoomBleed == b.AdjacentRoomBleed + && a.bEnabled == b.bEnabled; + } } void AudRoomManager::RegisterPortal( AudPortal* portal ) @@ -106,15 +136,25 @@ void AudRoomManager::SyncPortalLocked( AudPortal& portal ) params.bEnabled = portal.IsOpen(); const std::string name = portal.GetName().empty() ? "Portal " + std::to_string( portal.GetPortalID() ) : portal.GetName(); - const AKRESULT result = AK::SpatialAudio::SetPortal( portal.GetPortalID(), params, name.c_str() ); - if( result != AK_Success ) + const bool portalChanged = !portal.m_sentToWwise + || !PortalParamsEqual( params, portal.m_sentPortalParams ) + || name != portal.m_sentName; + + if( portalChanged ) { - if( !portal.m_failureLogged ) + const AKRESULT result = AK::SpatialAudio::SetPortal( portal.GetPortalID(), params, name.c_str() ); + if( result != AK_Success ) { - CCP_LOGERR_CH( s_ch, "Failed to set portal '%s' (%llu), AKRESULT: %d", name.c_str(), portal.GetPortalID(), result ); - portal.m_failureLogged = true; + if( !portal.m_failureLogged ) + { + CCP_LOGERR_CH( s_ch, "Failed to set portal '%s' (%llu), AKRESULT: %d", name.c_str(), portal.GetPortalID(), result ); + portal.m_failureLogged = true; + } + return; } - return; + + portal.m_sentPortalParams = params; + portal.m_sentName = name; } if( !portal.m_sentToWwise || portal.m_frontRoomID != frontRoomID || portal.m_backRoomID != backRoomID ) From 8239501fd584f9c67018cfde10b4cc2d1042ca95 Mon Sep 17 00:00:00 2001 From: Phevos Rinis Date: Thu, 15 Oct 2026 10:18:35 +0000 Subject: [PATCH 05/11] add door obstruction and occlusion to portals --- src/AudPortal.cpp | 7 ++++++ src/AudPortal.h | 14 ++++++++++++ src/AudPortal_Blue.cpp | 3 +++ src/AudRoomManager.h | 2 ++ src/AudRoomManager_Portals.cpp | 40 +++++++++++++++++++++++++++++++++- 5 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/AudPortal.cpp b/src/AudPortal.cpp index 93fb1db..ad07bb9 100644 --- a/src/AudPortal.cpp +++ b/src/AudPortal.cpp @@ -15,6 +15,9 @@ AudPortal::AudPortal( IRoot* lockobj ) : m_portalID( AllocateGameObjectID() ), m_open( true ), m_adjacentRoomBleed( 1.0f ), + m_obstruction( 0.0f ), + m_occlusion( 0.0f ), + m_obstructionOcclusionTransition( false ), m_enabled( true ), m_shapeEnabled( true ), m_warnedDegenerate( false ), @@ -25,6 +28,10 @@ AudPortal::AudPortal( IRoot* lockobj ) : m_foundFrontRoomID( AudRoomManager::OUTDOOR_ROOM_ID ), m_foundBackRoomID( AudRoomManager::OUTDOOR_ROOM_ID ), m_foundTicks( 0 ), + m_obstructionOcclusionSent( false ), + m_sentObstruction( 0.0f ), + m_sentOcclusion( 0.0f ), + m_sentTransition( false ), m_failureLogged( false ) { EnsureRegistered(); diff --git a/src/AudPortal.h b/src/AudPortal.h index 9aff8ed..53bec00 100644 --- a/src/AudPortal.h +++ b/src/AudPortal.h @@ -44,6 +44,12 @@ BLUE_CLASS( AudPortal ) : /// Whether sound passes through the portal, e.g. an open door. bool IsOpen() const { return m_open; } float GetAdjacentRoomBleed() const { return m_adjacentRoomBleed; } + /// Obstruction of the direct sound through the portal. + float GetObstruction() const { return m_obstruction; } + /// Occlusion of all sound through the portal, e.g. a door that is partly closed. + float GetOcclusion() const { return m_occlusion; } + /// Whether obstruction and occlusion fade over the depth of the portal. + bool GetObstructionOcclusionTransition() const { return m_obstructionOcclusionTransition; } /// True when the portal and its volume object are both enabled. bool IsEnabled() const { return m_enabled && m_shapeEnabled; } /// Box of the portal, set through ITr2BoxVolume. @@ -68,6 +74,9 @@ BLUE_CLASS( AudPortal ) : std::string m_name; bool m_open; float m_adjacentRoomBleed; + float m_obstruction; + float m_occlusion; + bool m_obstructionOcclusionTransition; /// Enabled attribute of the portal. bool m_enabled; /// Set through ITr2BoxVolume::SetEnabled. @@ -88,6 +97,11 @@ BLUE_CLASS( AudPortal ) : AkUInt64 m_foundBackRoomID; /// Updates in a row the same rooms were found. int m_foundTicks; + /// Obstruction and occlusion last sent to Wwise. + bool m_obstructionOcclusionSent; + float m_sentObstruction; + float m_sentOcclusion; + bool m_sentTransition; /// Set after a failed Wwise call is logged, so it is not logged again every update. bool m_failureLogged; }; diff --git a/src/AudPortal_Blue.cpp b/src/AudPortal_Blue.cpp index f0e4530..5a1b417 100644 --- a/src/AudPortal_Blue.cpp +++ b/src/AudPortal_Blue.cpp @@ -19,6 +19,9 @@ const Be::ClassInfo* AudPortal::ExposeToBlue() MAP_ATTRIBUTE( "name", m_name, "Name of the portal, shown in the Wwise profiler.", Be::READWRITE | Be::PERSIST | Be::NOTIFY ) MAP_ATTRIBUTE( "open", m_open, "Whether sound passes through the portal, e.g. an open door.\n:jessica-group: Acoustics", Be::READWRITE | Be::PERSIST | Be::NOTIFY ) MAP_ATTRIBUTE( "adjacentRoomBleed", m_adjacentRoomBleed, "Scales how much reverb of the room on the other side is heard through the portal.\n:jessica-group: Acoustics", Be::READWRITE | Be::PERSIST | Be::NOTIFY ) + MAP_ATTRIBUTE( "obstruction", m_obstruction, "Obstruction [0, 1] of the direct sound through the portal.\n:jessica-group: Acoustics", Be::READWRITE | Be::PERSIST | Be::NOTIFY ) + MAP_ATTRIBUTE( "occlusion", m_occlusion, "Occlusion [0, 1] of all sound through the portal, e.g. a door that is partly closed.\n:jessica-group: Acoustics", Be::READWRITE | Be::PERSIST | Be::NOTIFY ) + MAP_ATTRIBUTE( "obstructionOcclusionTransition", m_obstructionOcclusionTransition, "Fade obstruction and occlusion over the depth of the portal.\n:jessica-group: Acoustics", Be::READWRITE | Be::PERSIST | Be::NOTIFY ) MAP_ATTRIBUTE( "enabled", m_enabled, "Whether the portal is active. It is only sent to Wwise when its volume object is enabled too.", Be::READWRITE | Be::PERSIST | Be::NOTIFY ) MAP_ATTRIBUTE( "portalID", m_portalID, "Wwise portal identifier.\n:jessica-hidden: True", Be::READ ) diff --git a/src/AudRoomManager.h b/src/AudRoomManager.h index 25b56cd..1771981 100644 --- a/src/AudRoomManager.h +++ b/src/AudRoomManager.h @@ -116,6 +116,8 @@ class AudRoomManager void SyncPortalsLocked(); /// Sends one portal between the rooms at its two faces, or removes it. Caller holds m_mutex. void SyncPortalLocked( AudPortal& portal ); + /// Sends the portal's obstruction and occlusion when they changed. Returns false when Wwise refused them. Caller holds m_mutex. + bool SendPortalObstructionOcclusionLocked( AudPortal& portal ); /// Removes one portal from Wwise. Caller holds m_mutex. void RemovePortalLocked( AudPortal& portal ); /// Removes every portal from Wwise. Caller holds m_mutex. diff --git a/src/AudRoomManager_Portals.cpp b/src/AudRoomManager_Portals.cpp index 77ec9a7..fc1705b 100644 --- a/src/AudRoomManager_Portals.cpp +++ b/src/AudRoomManager_Portals.cpp @@ -166,7 +166,44 @@ void AudRoomManager::SyncPortalLocked( AudPortal& portal ) portal.m_sentToWwise = true; portal.m_frontRoomID = frontRoomID; portal.m_backRoomID = backRoomID; - portal.m_failureLogged = false; + + if( SendPortalObstructionOcclusionLocked( portal ) ) + { + portal.m_failureLogged = false; + } +} + +bool AudRoomManager::SendPortalObstructionOcclusionLocked( AudPortal& portal ) +{ + const float obstruction = std::clamp( portal.GetObstruction(), 0.0f, 1.0f ); + const float occlusion = std::clamp( portal.GetOcclusion(), 0.0f, 1.0f ); + const bool transition = portal.GetObstructionOcclusionTransition(); + + if( portal.m_obstructionOcclusionSent + && obstruction == portal.m_sentObstruction + && occlusion == portal.m_sentOcclusion + && transition == portal.m_sentTransition ) + { + return true; + } + + const AKRESULT result = AK::SpatialAudio::SetPortalObstructionAndOcclusion( portal.GetPortalID(), obstruction, occlusion, transition ); + if( result != AK_Success ) + { + if( !portal.m_failureLogged ) + { + CCP_LOGWARN_CH( s_ch, "Failed to set obstruction and occlusion on portal '%s' (%llu), AKRESULT: %d", + portal.GetName().c_str(), portal.GetPortalID(), result ); + portal.m_failureLogged = true; + } + return false; + } + + portal.m_obstructionOcclusionSent = true; + portal.m_sentObstruction = obstruction; + portal.m_sentOcclusion = occlusion; + portal.m_sentTransition = transition; + return true; } void AudRoomManager::RemovePortalLocked( AudPortal& portal ) @@ -179,6 +216,7 @@ void AudRoomManager::RemovePortalLocked( AudPortal& portal ) portal.m_sentToWwise = false; portal.m_frontRoomID = OUTDOOR_ROOM_ID; portal.m_backRoomID = OUTDOOR_ROOM_ID; + portal.m_obstructionOcclusionSent = false; if( AK::SoundEngine::IsInitialized() ) { From a000d88053b89ee45acc7e42e5c117c98d91b3de Mon Sep 17 00:00:00 2001 From: Phevos Rinis Date: Thu, 15 Oct 2026 10:19:03 +0000 Subject: [PATCH 06/11] keep portal rooms stable while the player moves --- src/AudRoomManager_Portals.cpp | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/AudRoomManager_Portals.cpp b/src/AudRoomManager_Portals.cpp index fc1705b..2de20c9 100644 --- a/src/AudRoomManager_Portals.cpp +++ b/src/AudRoomManager_Portals.cpp @@ -22,6 +22,8 @@ namespace { /// Updates in a row a portal can have the same room on both sides before it is reported. constexpr int PORTAL_SAME_ROOM_WARNING_TICKS = 30; + /// Updates in a row new rooms must be found at a portal's faces before they replace the rooms in Wwise. + constexpr int PORTAL_ROOM_CHANGE_TICKS = 3; /// Smallest adjacent room bleed sent to Wwise, which needs it above zero. constexpr float MIN_ADJACENT_ROOM_BLEED = 1e-3f; @@ -99,19 +101,23 @@ void AudRoomManager::SyncPortalLocked( AudPortal& portal ) return; } - const AkUInt64 frontRoomID = ResolveRoomLocked( box.ToWorld( Vector3( 0.0f, 0.0f, -0.5f ) ) ); - const AkUInt64 backRoomID = ResolveRoomLocked( box.ToWorld( Vector3( 0.0f, 0.0f, 0.5f ) ) ); - if( frontRoomID == portal.m_foundFrontRoomID && backRoomID == portal.m_foundBackRoomID ) + const AkUInt64 foundFrontRoomID = ResolveRoomLocked( box.ToWorld( Vector3( 0.0f, 0.0f, -0.5f ) ) ); + const AkUInt64 foundBackRoomID = ResolveRoomLocked( box.ToWorld( Vector3( 0.0f, 0.0f, 0.5f ) ) ); + if( foundFrontRoomID == portal.m_foundFrontRoomID && foundBackRoomID == portal.m_foundBackRoomID ) { portal.m_foundTicks = std::min( portal.m_foundTicks + 1, PORTAL_SAME_ROOM_WARNING_TICKS + 1 ); } else { - portal.m_foundFrontRoomID = frontRoomID; - portal.m_foundBackRoomID = backRoomID; + portal.m_foundFrontRoomID = foundFrontRoomID; + portal.m_foundBackRoomID = foundBackRoomID; portal.m_foundTicks = 1; } + const bool keepSentRooms = portal.m_sentToWwise && portal.m_foundTicks < PORTAL_ROOM_CHANGE_TICKS; + const AkUInt64 frontRoomID = keepSentRooms ? portal.m_frontRoomID : foundFrontRoomID; + const AkUInt64 backRoomID = keepSentRooms ? portal.m_backRoomID : foundBackRoomID; + if( frontRoomID == backRoomID ) { RemovePortalLocked( portal ); From 9cf17eba7a725ec21d0219a4aaba42edf740553c Mon Sep 17 00:00:00 2001 From: Phevos Rinis Date: Thu, 15 Oct 2026 10:08:44 +0000 Subject: [PATCH 07/11] look up portal rooms a quarter of the portal depth past each face - a portal as deep as its wall could miss a room by a rounding error and connect to outdoors --- src/AudRoomManager.h | 2 +- src/AudRoomManager_Portals.cpp | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/AudRoomManager.h b/src/AudRoomManager.h index 1771981..22e0068 100644 --- a/src/AudRoomManager.h +++ b/src/AudRoomManager.h @@ -114,7 +114,7 @@ class AudRoomManager /// Updates every portal in Wwise. Caller holds m_mutex. void SyncPortalsLocked(); - /// Sends one portal between the rooms at its two faces, or removes it. Caller holds m_mutex. + /// Sends one portal between the rooms on its two sides, or removes it. Caller holds m_mutex. void SyncPortalLocked( AudPortal& portal ); /// Sends the portal's obstruction and occlusion when they changed. Returns false when Wwise refused them. Caller holds m_mutex. bool SendPortalObstructionOcclusionLocked( AudPortal& portal ); diff --git a/src/AudRoomManager_Portals.cpp b/src/AudRoomManager_Portals.cpp index 2de20c9..12f4a22 100644 --- a/src/AudRoomManager_Portals.cpp +++ b/src/AudRoomManager_Portals.cpp @@ -26,6 +26,8 @@ namespace constexpr int PORTAL_ROOM_CHANGE_TICKS = 3; /// Smallest adjacent room bleed sent to Wwise, which needs it above zero. constexpr float MIN_ADJACENT_ROOM_BLEED = 1e-3f; + /// Unit box Z where a portal looks up its rooms, a quarter of its depth past each face. + constexpr float PORTAL_ROOM_LOOKUP_Z = 0.75f; /// Compares positions with a 1 mm tolerance. bool PositionsNearlyEqual( const AkVector64& a, const AkVector64& b ) @@ -101,8 +103,8 @@ void AudRoomManager::SyncPortalLocked( AudPortal& portal ) return; } - const AkUInt64 foundFrontRoomID = ResolveRoomLocked( box.ToWorld( Vector3( 0.0f, 0.0f, -0.5f ) ) ); - const AkUInt64 foundBackRoomID = ResolveRoomLocked( box.ToWorld( Vector3( 0.0f, 0.0f, 0.5f ) ) ); + const AkUInt64 foundFrontRoomID = ResolveRoomLocked( box.ToWorld( Vector3( 0.0f, 0.0f, -PORTAL_ROOM_LOOKUP_Z ) ) ); + const AkUInt64 foundBackRoomID = ResolveRoomLocked( box.ToWorld( Vector3( 0.0f, 0.0f, PORTAL_ROOM_LOOKUP_Z ) ) ); if( foundFrontRoomID == portal.m_foundFrontRoomID && foundBackRoomID == portal.m_foundBackRoomID ) { portal.m_foundTicks = std::min( portal.m_foundTicks + 1, PORTAL_SAME_ROOM_WARNING_TICKS + 1 ); From d8022b2b3deeafdf9fbebd45cec22dd110ddf891 Mon Sep 17 00:00:00 2001 From: Phevos Rinis Date: Thu, 15 Oct 2026 10:36:41 +0000 Subject: [PATCH 08/11] move the portal code into AudRoomManager.cpp --- CMakeLists.txt | 1 - src/AudRoomManager.cpp | 226 +++++++++++++++++++++++++++++ src/AudRoomManager_Portals.cpp | 253 --------------------------------- 3 files changed, 226 insertions(+), 254 deletions(-) delete mode 100644 src/AudRoomManager_Portals.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index b4ef4f6..3bda5ea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -50,7 +50,6 @@ ccp_add_library(CarbonAudio SHARED src/AudRoom.cpp src/AudRoom_Blue.cpp src/AudRoomManager.cpp - src/AudRoomManager_Portals.cpp src/AudUnitBox.cpp src/SpatialAudioSettings.cpp src/audio2.cpp diff --git a/src/AudRoomManager.cpp b/src/AudRoomManager.cpp index 4451db4..acc0112 100644 --- a/src/AudRoomManager.cpp +++ b/src/AudRoomManager.cpp @@ -9,6 +9,7 @@ #include "AudRoomManager.h" #include "AudManager.h" +#include "AudPortal.h" #include "AudRoom.h" #include "AudStaticDataRepository.h" #include "Utilities.h" @@ -16,6 +17,7 @@ #include #include #include +#include static CcpLogChannel_t s_ch = CCP_LOG_DEFINE_CHANNEL( "AudRoomManager" ); @@ -47,6 +49,22 @@ namespace namespace { + /// Updates in a row a portal can have the same room on both sides before it is reported. + constexpr int PORTAL_SAME_ROOM_WARNING_TICKS = 30; + /// Updates in a row new rooms must be found at a portal's faces before they replace the rooms in Wwise. + constexpr int PORTAL_ROOM_CHANGE_TICKS = 3; + /// Smallest adjacent room bleed sent to Wwise, which needs it above zero. + constexpr float MIN_ADJACENT_ROOM_BLEED = 1e-3f; + /// Unit box Z where a portal looks up its rooms, a quarter of its depth past each face. + constexpr float PORTAL_ROOM_LOOKUP_Z = 0.75f; + + /// Compares positions with a 1 mm tolerance. + bool PositionsNearlyEqual( const AkVector64& a, const AkVector64& b ) + { + constexpr AkReal64 epsilon = 1e-3; + return std::fabs( a.X - b.X ) <= epsilon && std::fabs( a.Y - b.Y ) <= epsilon && std::fabs( a.Z - b.Z ) <= epsilon; + } + /// Compares orientations with a small tolerance for float errors. bool OrientationsNearlyEqual( const AkVector& a, const AkVector& b ) { @@ -68,6 +86,21 @@ namespace && a.DistanceBehavior == b.DistanceBehavior && a.RoomGameObj_KeepRegistered == b.RoomGameObj_KeepRegistered; } + + /// Compares every parameter SetPortal uses. + bool PortalParamsEqual( const AkPortalParams& a, const AkPortalParams& b ) + { + return PositionsNearlyEqual( a.Transform.Position(), b.Transform.Position() ) + && OrientationsNearlyEqual( a.Transform.OrientationFront(), b.Transform.OrientationFront() ) + && OrientationsNearlyEqual( a.Transform.OrientationTop(), b.Transform.OrientationTop() ) + && a.Extent.halfWidth == b.Extent.halfWidth + && a.Extent.halfHeight == b.Extent.halfHeight + && a.Extent.halfDepth == b.Extent.halfDepth + && a.FrontRoom == b.FrontRoom + && a.BackRoom == b.BackRoom + && a.AdjacentRoomBleed == b.AdjacentRoomBleed + && a.bEnabled == b.bEnabled; + } } AudRoomManager::AudRoomManager( AudManager* audioManager ) : @@ -591,3 +624,196 @@ void AudRoomManager::ForgetWwiseState() m_trackedObjects.clear(); m_assignmentsDirty = false; } + +void AudRoomManager::RegisterPortal( AudPortal* portal ) +{ + CcpAutoMutex lock( m_mutex ); + m_portals[portal->GetPortalID()] = portal; +} + +void AudRoomManager::UnregisterPortal( AudPortal* portal ) +{ + CcpAutoMutex lock( m_mutex ); + RemovePortalLocked( *portal ); + m_portals.erase( portal->GetPortalID() ); +} + +void AudRoomManager::SetPortalTransform( AudPortal& portal, const Matrix& unitBoxToWorld ) +{ + CcpAutoMutex lock( m_mutex ); + portal.ApplyTransform( unitBoxToWorld ); +} + +void AudRoomManager::RemovePortalShape( AudPortal& portal ) +{ + CcpAutoMutex lock( m_mutex ); + portal.m_box.ClearTransform(); + RemovePortalLocked( portal ); +} + +void AudRoomManager::SyncPortalsLocked() +{ + for( auto& entry : m_portals ) + { + SyncPortalLocked( *entry.second ); + } +} + +void AudRoomManager::SyncPortalLocked( AudPortal& portal ) +{ + const AudUnitBox& box = portal.GetBox(); + if( !portal.IsEnabled() || !box.IsUsable() ) + { + portal.m_foundTicks = 0; + RemovePortalLocked( portal ); + return; + } + + const AkUInt64 foundFrontRoomID = ResolveRoomLocked( box.ToWorld( Vector3( 0.0f, 0.0f, -PORTAL_ROOM_LOOKUP_Z ) ) ); + const AkUInt64 foundBackRoomID = ResolveRoomLocked( box.ToWorld( Vector3( 0.0f, 0.0f, PORTAL_ROOM_LOOKUP_Z ) ) ); + if( foundFrontRoomID == portal.m_foundFrontRoomID && foundBackRoomID == portal.m_foundBackRoomID ) + { + portal.m_foundTicks = std::min( portal.m_foundTicks + 1, PORTAL_SAME_ROOM_WARNING_TICKS + 1 ); + } + else + { + portal.m_foundFrontRoomID = foundFrontRoomID; + portal.m_foundBackRoomID = foundBackRoomID; + portal.m_foundTicks = 1; + } + + const bool keepSentRooms = portal.m_sentToWwise && portal.m_foundTicks < PORTAL_ROOM_CHANGE_TICKS; + const AkUInt64 frontRoomID = keepSentRooms ? portal.m_frontRoomID : foundFrontRoomID; + const AkUInt64 backRoomID = keepSentRooms ? portal.m_backRoomID : foundBackRoomID; + + if( frontRoomID == backRoomID ) + { + RemovePortalLocked( portal ); + if( portal.m_foundTicks == PORTAL_SAME_ROOM_WARNING_TICKS ) + { + CCP_LOGWARN_CH( s_ch, "Portal '%s' (%llu) has room %llu on both sides; it is not sent to Wwise until it crosses a room wall.", + portal.GetName().c_str(), portal.GetPortalID(), frontRoomID ); + } + return; + } + + AkTransform transform; + RH2LH::convertTransform( box.GetUnitBoxToWorld(), transform ); + const AkVector scale = RH2LH::extractScale( box.GetUnitBoxToWorld() ); + + AkPortalParams params; + params.Transform = AK::ConvertAkTransformToAkWorldTransform( transform ); + params.Extent = AkExtent( 0.5f * scale.X, 0.5f * scale.Y, 0.5f * scale.Z ); + params.FrontRoom = AkRoomID( frontRoomID ); + params.BackRoom = AkRoomID( backRoomID ); + params.AdjacentRoomBleed = std::max( portal.GetAdjacentRoomBleed(), MIN_ADJACENT_ROOM_BLEED ); + params.bEnabled = portal.IsOpen(); + + const std::string name = portal.GetName().empty() ? "Portal " + std::to_string( portal.GetPortalID() ) : portal.GetName(); + const bool portalChanged = !portal.m_sentToWwise + || !PortalParamsEqual( params, portal.m_sentPortalParams ) + || name != portal.m_sentName; + + if( portalChanged ) + { + const AKRESULT result = AK::SpatialAudio::SetPortal( portal.GetPortalID(), params, name.c_str() ); + if( result != AK_Success ) + { + if( !portal.m_failureLogged ) + { + CCP_LOGERR_CH( s_ch, "Failed to set portal '%s' (%llu), AKRESULT: %d", name.c_str(), portal.GetPortalID(), result ); + portal.m_failureLogged = true; + } + return; + } + + portal.m_sentPortalParams = params; + portal.m_sentName = name; + } + + if( !portal.m_sentToWwise || portal.m_frontRoomID != frontRoomID || portal.m_backRoomID != backRoomID ) + { + CCP_LOG_CH( s_ch, "Portal '%s' (%llu) sent to Wwise with front room %llu and back room %llu.", + name.c_str(), portal.GetPortalID(), frontRoomID, backRoomID ); + } + + portal.m_sentToWwise = true; + portal.m_frontRoomID = frontRoomID; + portal.m_backRoomID = backRoomID; + + if( SendPortalObstructionOcclusionLocked( portal ) ) + { + portal.m_failureLogged = false; + } +} + +bool AudRoomManager::SendPortalObstructionOcclusionLocked( AudPortal& portal ) +{ + const float obstruction = std::clamp( portal.GetObstruction(), 0.0f, 1.0f ); + const float occlusion = std::clamp( portal.GetOcclusion(), 0.0f, 1.0f ); + const bool transition = portal.GetObstructionOcclusionTransition(); + + if( portal.m_obstructionOcclusionSent + && obstruction == portal.m_sentObstruction + && occlusion == portal.m_sentOcclusion + && transition == portal.m_sentTransition ) + { + return true; + } + + const AKRESULT result = AK::SpatialAudio::SetPortalObstructionAndOcclusion( portal.GetPortalID(), obstruction, occlusion, transition ); + if( result != AK_Success ) + { + if( !portal.m_failureLogged ) + { + CCP_LOGWARN_CH( s_ch, "Failed to set obstruction and occlusion on portal '%s' (%llu), AKRESULT: %d", + portal.GetName().c_str(), portal.GetPortalID(), result ); + portal.m_failureLogged = true; + } + return false; + } + + portal.m_obstructionOcclusionSent = true; + portal.m_sentObstruction = obstruction; + portal.m_sentOcclusion = occlusion; + portal.m_sentTransition = transition; + return true; +} + +void AudRoomManager::RemovePortalLocked( AudPortal& portal ) +{ + if( !portal.m_sentToWwise ) + { + return; + } + + portal.m_sentToWwise = false; + portal.m_frontRoomID = OUTDOOR_ROOM_ID; + portal.m_backRoomID = OUTDOOR_ROOM_ID; + portal.m_obstructionOcclusionSent = false; + + if( AK::SoundEngine::IsInitialized() ) + { + AK::SpatialAudio::RemovePortal( portal.GetPortalID() ); + } +} + +void AudRoomManager::RemoveAllPortalsLocked() +{ + for( auto& entry : m_portals ) + { + RemovePortalLocked( *entry.second ); + } +} + +void AudRoomManager::RemovePortalsConnectedToLocked( AkUInt64 roomID ) +{ + for( auto& entry : m_portals ) + { + AudPortal& portal = *entry.second; + if( portal.m_frontRoomID == roomID || portal.m_backRoomID == roomID ) + { + RemovePortalLocked( portal ); + } + } +} diff --git a/src/AudRoomManager_Portals.cpp b/src/AudRoomManager_Portals.cpp deleted file mode 100644 index 12f4a22..0000000 --- a/src/AudRoomManager_Portals.cpp +++ /dev/null @@ -1,253 +0,0 @@ -//////////////////////////////////////////////////////////// -// -// Creator: Phevos Rinis -// Creation Date: Oct 2026 -// Copyright (c) 2026 CCP Games -// - -#include "stdafx.h" -#include "AudRoomManager.h" - -#include "AudManager.h" -#include "AudPortal.h" -#include "Utilities.h" - -#include -#include -#include - -static CcpLogChannel_t s_ch = CCP_LOG_DEFINE_CHANNEL( "AudRoomManager" ); - -namespace -{ - /// Updates in a row a portal can have the same room on both sides before it is reported. - constexpr int PORTAL_SAME_ROOM_WARNING_TICKS = 30; - /// Updates in a row new rooms must be found at a portal's faces before they replace the rooms in Wwise. - constexpr int PORTAL_ROOM_CHANGE_TICKS = 3; - /// Smallest adjacent room bleed sent to Wwise, which needs it above zero. - constexpr float MIN_ADJACENT_ROOM_BLEED = 1e-3f; - /// Unit box Z where a portal looks up its rooms, a quarter of its depth past each face. - constexpr float PORTAL_ROOM_LOOKUP_Z = 0.75f; - - /// Compares positions with a 1 mm tolerance. - bool PositionsNearlyEqual( const AkVector64& a, const AkVector64& b ) - { - constexpr AkReal64 epsilon = 1e-3; - return std::fabs( a.X - b.X ) <= epsilon && std::fabs( a.Y - b.Y ) <= epsilon && std::fabs( a.Z - b.Z ) <= epsilon; - } - - /// Compares orientations with a small tolerance for float errors. - bool OrientationsNearlyEqual( const AkVector& a, const AkVector& b ) - { - constexpr float epsilon = 1e-4f; - return std::fabs( a.X - b.X ) <= epsilon && std::fabs( a.Y - b.Y ) <= epsilon && std::fabs( a.Z - b.Z ) <= epsilon; - } - - /// Compares every parameter SetPortal uses. - bool PortalParamsEqual( const AkPortalParams& a, const AkPortalParams& b ) - { - return PositionsNearlyEqual( a.Transform.Position(), b.Transform.Position() ) - && OrientationsNearlyEqual( a.Transform.OrientationFront(), b.Transform.OrientationFront() ) - && OrientationsNearlyEqual( a.Transform.OrientationTop(), b.Transform.OrientationTop() ) - && a.Extent.halfWidth == b.Extent.halfWidth - && a.Extent.halfHeight == b.Extent.halfHeight - && a.Extent.halfDepth == b.Extent.halfDepth - && a.FrontRoom == b.FrontRoom - && a.BackRoom == b.BackRoom - && a.AdjacentRoomBleed == b.AdjacentRoomBleed - && a.bEnabled == b.bEnabled; - } -} - -void AudRoomManager::RegisterPortal( AudPortal* portal ) -{ - CcpAutoMutex lock( m_mutex ); - m_portals[portal->GetPortalID()] = portal; -} - -void AudRoomManager::UnregisterPortal( AudPortal* portal ) -{ - CcpAutoMutex lock( m_mutex ); - RemovePortalLocked( *portal ); - m_portals.erase( portal->GetPortalID() ); -} - -void AudRoomManager::SetPortalTransform( AudPortal& portal, const Matrix& unitBoxToWorld ) -{ - CcpAutoMutex lock( m_mutex ); - portal.ApplyTransform( unitBoxToWorld ); -} - -void AudRoomManager::RemovePortalShape( AudPortal& portal ) -{ - CcpAutoMutex lock( m_mutex ); - portal.m_box.ClearTransform(); - RemovePortalLocked( portal ); -} - -void AudRoomManager::SyncPortalsLocked() -{ - for( auto& entry : m_portals ) - { - SyncPortalLocked( *entry.second ); - } -} - -void AudRoomManager::SyncPortalLocked( AudPortal& portal ) -{ - const AudUnitBox& box = portal.GetBox(); - if( !portal.IsEnabled() || !box.IsUsable() ) - { - portal.m_foundTicks = 0; - RemovePortalLocked( portal ); - return; - } - - const AkUInt64 foundFrontRoomID = ResolveRoomLocked( box.ToWorld( Vector3( 0.0f, 0.0f, -PORTAL_ROOM_LOOKUP_Z ) ) ); - const AkUInt64 foundBackRoomID = ResolveRoomLocked( box.ToWorld( Vector3( 0.0f, 0.0f, PORTAL_ROOM_LOOKUP_Z ) ) ); - if( foundFrontRoomID == portal.m_foundFrontRoomID && foundBackRoomID == portal.m_foundBackRoomID ) - { - portal.m_foundTicks = std::min( portal.m_foundTicks + 1, PORTAL_SAME_ROOM_WARNING_TICKS + 1 ); - } - else - { - portal.m_foundFrontRoomID = foundFrontRoomID; - portal.m_foundBackRoomID = foundBackRoomID; - portal.m_foundTicks = 1; - } - - const bool keepSentRooms = portal.m_sentToWwise && portal.m_foundTicks < PORTAL_ROOM_CHANGE_TICKS; - const AkUInt64 frontRoomID = keepSentRooms ? portal.m_frontRoomID : foundFrontRoomID; - const AkUInt64 backRoomID = keepSentRooms ? portal.m_backRoomID : foundBackRoomID; - - if( frontRoomID == backRoomID ) - { - RemovePortalLocked( portal ); - if( portal.m_foundTicks == PORTAL_SAME_ROOM_WARNING_TICKS ) - { - CCP_LOGWARN_CH( s_ch, "Portal '%s' (%llu) has room %llu on both sides; it is not sent to Wwise until it crosses a room wall.", - portal.GetName().c_str(), portal.GetPortalID(), frontRoomID ); - } - return; - } - - AkTransform transform; - RH2LH::convertTransform( box.GetUnitBoxToWorld(), transform ); - const AkVector scale = RH2LH::extractScale( box.GetUnitBoxToWorld() ); - - AkPortalParams params; - params.Transform = AK::ConvertAkTransformToAkWorldTransform( transform ); - params.Extent = AkExtent( 0.5f * scale.X, 0.5f * scale.Y, 0.5f * scale.Z ); - params.FrontRoom = AkRoomID( frontRoomID ); - params.BackRoom = AkRoomID( backRoomID ); - params.AdjacentRoomBleed = std::max( portal.GetAdjacentRoomBleed(), MIN_ADJACENT_ROOM_BLEED ); - params.bEnabled = portal.IsOpen(); - - const std::string name = portal.GetName().empty() ? "Portal " + std::to_string( portal.GetPortalID() ) : portal.GetName(); - const bool portalChanged = !portal.m_sentToWwise - || !PortalParamsEqual( params, portal.m_sentPortalParams ) - || name != portal.m_sentName; - - if( portalChanged ) - { - const AKRESULT result = AK::SpatialAudio::SetPortal( portal.GetPortalID(), params, name.c_str() ); - if( result != AK_Success ) - { - if( !portal.m_failureLogged ) - { - CCP_LOGERR_CH( s_ch, "Failed to set portal '%s' (%llu), AKRESULT: %d", name.c_str(), portal.GetPortalID(), result ); - portal.m_failureLogged = true; - } - return; - } - - portal.m_sentPortalParams = params; - portal.m_sentName = name; - } - - if( !portal.m_sentToWwise || portal.m_frontRoomID != frontRoomID || portal.m_backRoomID != backRoomID ) - { - CCP_LOG_CH( s_ch, "Portal '%s' (%llu) sent to Wwise with front room %llu and back room %llu.", - name.c_str(), portal.GetPortalID(), frontRoomID, backRoomID ); - } - - portal.m_sentToWwise = true; - portal.m_frontRoomID = frontRoomID; - portal.m_backRoomID = backRoomID; - - if( SendPortalObstructionOcclusionLocked( portal ) ) - { - portal.m_failureLogged = false; - } -} - -bool AudRoomManager::SendPortalObstructionOcclusionLocked( AudPortal& portal ) -{ - const float obstruction = std::clamp( portal.GetObstruction(), 0.0f, 1.0f ); - const float occlusion = std::clamp( portal.GetOcclusion(), 0.0f, 1.0f ); - const bool transition = portal.GetObstructionOcclusionTransition(); - - if( portal.m_obstructionOcclusionSent - && obstruction == portal.m_sentObstruction - && occlusion == portal.m_sentOcclusion - && transition == portal.m_sentTransition ) - { - return true; - } - - const AKRESULT result = AK::SpatialAudio::SetPortalObstructionAndOcclusion( portal.GetPortalID(), obstruction, occlusion, transition ); - if( result != AK_Success ) - { - if( !portal.m_failureLogged ) - { - CCP_LOGWARN_CH( s_ch, "Failed to set obstruction and occlusion on portal '%s' (%llu), AKRESULT: %d", - portal.GetName().c_str(), portal.GetPortalID(), result ); - portal.m_failureLogged = true; - } - return false; - } - - portal.m_obstructionOcclusionSent = true; - portal.m_sentObstruction = obstruction; - portal.m_sentOcclusion = occlusion; - portal.m_sentTransition = transition; - return true; -} - -void AudRoomManager::RemovePortalLocked( AudPortal& portal ) -{ - if( !portal.m_sentToWwise ) - { - return; - } - - portal.m_sentToWwise = false; - portal.m_frontRoomID = OUTDOOR_ROOM_ID; - portal.m_backRoomID = OUTDOOR_ROOM_ID; - portal.m_obstructionOcclusionSent = false; - - if( AK::SoundEngine::IsInitialized() ) - { - AK::SpatialAudio::RemovePortal( portal.GetPortalID() ); - } -} - -void AudRoomManager::RemoveAllPortalsLocked() -{ - for( auto& entry : m_portals ) - { - RemovePortalLocked( *entry.second ); - } -} - -void AudRoomManager::RemovePortalsConnectedToLocked( AkUInt64 roomID ) -{ - for( auto& entry : m_portals ) - { - AudPortal& portal = *entry.second; - if( portal.m_frontRoomID == roomID || portal.m_backRoomID == roomID ) - { - RemovePortalLocked( portal ); - } - } -} From 0e464bbeede7ea38eb1322860599736c9c6e93cc Mon Sep 17 00:00:00 2001 From: Phevos Rinis Date: Thu, 15 Oct 2026 10:43:56 +0000 Subject: [PATCH 09/11] use doxygen for the room and portal file headers and interface comments - the copyright is now Fenris Creations ehf. --- src/AudPortal.cpp | 12 ++++++------ src/AudPortal.h | 18 ++++++++++-------- src/AudPortal_Blue.cpp | 12 ++++++------ src/AudRoom.cpp | 12 ++++++------ src/AudRoom.h | 18 ++++++++++-------- src/AudRoomManager.cpp | 12 ++++++------ src/AudRoomManager.h | 12 ++++++------ src/AudUnitBox.cpp | 12 ++++++------ src/AudUnitBox.h | 12 ++++++------ 9 files changed, 62 insertions(+), 58 deletions(-) diff --git a/src/AudPortal.cpp b/src/AudPortal.cpp index ad07bb9..d1098c8 100644 --- a/src/AudPortal.cpp +++ b/src/AudPortal.cpp @@ -1,9 +1,9 @@ -//////////////////////////////////////////////////////////// -// -// Creator: Phevos Rinis -// Creation Date: Oct 2026 -// Copyright (c) 2026 CCP Games -// +/** + * @file AudPortal.cpp + * @author Phevos Rinis + * @date Oct 2026 + * @copyright © 2026 Fenris Creations ehf. + */ #include "stdafx.h" #include "AudPortal.h" diff --git a/src/AudPortal.h b/src/AudPortal.h index 53bec00..05f370a 100644 --- a/src/AudPortal.h +++ b/src/AudPortal.h @@ -1,9 +1,9 @@ -//////////////////////////////////////////////////////////// -// -// Creator: Phevos Rinis -// Creation Date: Oct 2026 -// Copyright (c) 2026 CCP Games -// +/** + * @file AudPortal.h + * @author Phevos Rinis + * @date Oct 2026 + * @copyright © 2026 Fenris Creations ehf. + */ #pragma once @@ -29,12 +29,14 @@ BLUE_CLASS( AudPortal ) : EXPOSE_TO_BLUE(); - // ITr2BoxVolume + /// Stores the portal's box. The portal is sent to Wwise on the next update. void SetTransform( const Matrix& unitBoxToWorld ) override; + /// Called when the volume object is enabled or disabled. void SetEnabled( bool enabled ) override; + /// Clears the box and removes the portal from Wwise. void Remove() override; - // INotify + /// Called by Blue when an attribute changes. bool OnModified( Be::Var* value ) override; /// Portal ID, from the same ID space as game objects. diff --git a/src/AudPortal_Blue.cpp b/src/AudPortal_Blue.cpp index 5a1b417..4eb6054 100644 --- a/src/AudPortal_Blue.cpp +++ b/src/AudPortal_Blue.cpp @@ -1,9 +1,9 @@ -//////////////////////////////////////////////////////////// -// -// Creator: Phevos Rinis -// Creation Date: Oct 2026 -// Copyright (c) 2026 CCP Games -// +/** + * @file AudPortal_Blue.cpp + * @author Phevos Rinis + * @date Oct 2026 + * @copyright © 2026 Fenris Creations ehf. + */ #include "stdafx.h" #include "AudPortal.h" diff --git a/src/AudRoom.cpp b/src/AudRoom.cpp index 319868a..e0fc88c 100644 --- a/src/AudRoom.cpp +++ b/src/AudRoom.cpp @@ -1,9 +1,9 @@ -//////////////////////////////////////////////////////////// -// -// Creator: Phevos Rinis -// Creation Date: Sep 2026 -// Copyright (c) 2026 CCP Games -// +/** + * @file AudRoom.cpp + * @author Phevos Rinis + * @date Sep 2026 + * @copyright © 2026 Fenris Creations ehf. + */ #include "stdafx.h" #include "AudRoom.h" diff --git a/src/AudRoom.h b/src/AudRoom.h index 7c8a5d9..ab2f0fd 100644 --- a/src/AudRoom.h +++ b/src/AudRoom.h @@ -1,9 +1,9 @@ -//////////////////////////////////////////////////////////// -// -// Creator: Phevos Rinis -// Creation Date: Sep 2026 -// Copyright (c) 2026 CCP Games -// +/** + * @file AudRoom.h + * @author Phevos Rinis + * @date Sep 2026 + * @copyright © 2026 Fenris Creations ehf. + */ #pragma once @@ -29,12 +29,14 @@ BLUE_CLASS( AudRoom ) : EXPOSE_TO_BLUE(); - // ITr2BoxVolume + /// Stores the room's box and sends the room to Wwise. void SetTransform( const Matrix& unitBoxToWorld ) override; + /// Called when the volume object is enabled or disabled. void SetEnabled( bool enabled ) override; + /// Clears the box and removes the room from Wwise. void Remove() override; - // INotify + /// Called by Blue when an attribute changes. bool OnModified( Be::Var* value ) override; /// Room ID, from the same ID space as game objects. diff --git a/src/AudRoomManager.cpp b/src/AudRoomManager.cpp index acc0112..779c163 100644 --- a/src/AudRoomManager.cpp +++ b/src/AudRoomManager.cpp @@ -1,9 +1,9 @@ -//////////////////////////////////////////////////////////// -// -// Creator: Phevos Rinis -// Creation Date: Sep 2026 -// Copyright (c) 2026 CCP Games -// +/** + * @file AudRoomManager.cpp + * @author Phevos Rinis + * @date Sep 2026 + * @copyright © 2026 Fenris Creations ehf. + */ #include "stdafx.h" #include "AudRoomManager.h" diff --git a/src/AudRoomManager.h b/src/AudRoomManager.h index 22e0068..ef1772e 100644 --- a/src/AudRoomManager.h +++ b/src/AudRoomManager.h @@ -1,9 +1,9 @@ -//////////////////////////////////////////////////////////// -// -// Creator: Phevos Rinis -// Creation Date: Sep 2026 -// Copyright (c) 2026 CCP Games -// +/** + * @file AudRoomManager.h + * @author Phevos Rinis + * @date Sep 2026 + * @copyright © 2026 Fenris Creations ehf. + */ #pragma once diff --git a/src/AudUnitBox.cpp b/src/AudUnitBox.cpp index 4984381..c0d8f0c 100644 --- a/src/AudUnitBox.cpp +++ b/src/AudUnitBox.cpp @@ -1,9 +1,9 @@ -//////////////////////////////////////////////////////////// -// -// Creator: Phevos Rinis -// Creation Date: Oct 2026 -// Copyright (c) 2026 CCP Games -// +/** + * @file AudUnitBox.cpp + * @author Phevos Rinis + * @date Oct 2026 + * @copyright © 2026 Fenris Creations ehf. + */ #include "stdafx.h" #include "AudUnitBox.h" diff --git a/src/AudUnitBox.h b/src/AudUnitBox.h index c225405..54f82b3 100644 --- a/src/AudUnitBox.h +++ b/src/AudUnitBox.h @@ -1,9 +1,9 @@ -//////////////////////////////////////////////////////////// -// -// Creator: Phevos Rinis -// Creation Date: Oct 2026 -// Copyright (c) 2026 CCP Games -// +/** + * @file AudUnitBox.h + * @author Phevos Rinis + * @date Oct 2026 + * @copyright © 2026 Fenris Creations ehf. + */ #pragma once From ca297eddbbe3ae98353c1404537a8bebe5789d34 Mon Sep 17 00:00:00 2001 From: Phevos Rinis Date: Thu, 15 Oct 2026 10:54:39 +0000 Subject: [PATCH 10/11] document the room and portal functions with doxygen blocks - restore the file banners, with the Fenris Creations copyright --- src/AudPortal.cpp | 12 +-- src/AudPortal.h | 109 ++++++++++++++++----- src/AudPortal_Blue.cpp | 12 +-- src/AudRoom.cpp | 12 +-- src/AudRoom.h | 110 +++++++++++++++++---- src/AudRoomManager.cpp | 36 +++++-- src/AudRoomManager.h | 218 ++++++++++++++++++++++++++++++++++------- src/AudUnitBox.cpp | 12 +-- src/AudUnitBox.h | 55 ++++++++--- 9 files changed, 451 insertions(+), 125 deletions(-) diff --git a/src/AudPortal.cpp b/src/AudPortal.cpp index d1098c8..65ced57 100644 --- a/src/AudPortal.cpp +++ b/src/AudPortal.cpp @@ -1,9 +1,9 @@ -/** - * @file AudPortal.cpp - * @author Phevos Rinis - * @date Oct 2026 - * @copyright © 2026 Fenris Creations ehf. - */ +//////////////////////////////////////////////////////////// +// +// Creator: Phevos Rinis +// Creation Date: Oct 2026 +// Copyright (c) 2026 Fenris Creations +// #include "stdafx.h" #include "AudPortal.h" diff --git a/src/AudPortal.h b/src/AudPortal.h index 05f370a..ed83f5c 100644 --- a/src/AudPortal.h +++ b/src/AudPortal.h @@ -1,9 +1,9 @@ -/** - * @file AudPortal.h - * @author Phevos Rinis - * @date Oct 2026 - * @copyright © 2026 Fenris Creations ehf. - */ +//////////////////////////////////////////////////////////// +// +// Creator: Phevos Rinis +// Creation Date: Oct 2026 +// Copyright (c) 2026 Fenris Creations +// #pragma once @@ -29,46 +29,109 @@ BLUE_CLASS( AudPortal ) : EXPOSE_TO_BLUE(); - /// Stores the portal's box. The portal is sent to Wwise on the next update. + /** + * @brief Stores the portal's box. The portal is sent to Wwise on the next update. + * + * @param unitBoxToWorld Maps the unit box to world space. + */ void SetTransform( const Matrix& unitBoxToWorld ) override; - /// Called when the volume object is enabled or disabled. + + /** + * @brief Called when the volume object is enabled or disabled. + * + * @param enabled Whether the volume object is enabled. + */ void SetEnabled( bool enabled ) override; - /// Clears the box and removes the portal from Wwise. + + /** + * @brief Clears the box and removes the portal from Wwise. + */ void Remove() override; - /// Called by Blue when an attribute changes. + /** + * @brief Called by Blue when an attribute changes. + * + * @param value The attribute that changed. + * + * @return Always true. + */ bool OnModified( Be::Var* value ) override; - /// Portal ID, from the same ID space as game objects. + /** + * @brief Portal ID, from the same ID space as game objects. + */ AkUInt64 GetPortalID() const { return m_portalID; } - /// Name shown in the Wwise profiler. + + /** + * @brief Name shown in the Wwise profiler. + */ const std::string& GetName() const { return m_name; } - /// Whether sound passes through the portal, e.g. an open door. + + /** + * @brief Whether sound passes through the portal, e.g. an open door. + */ bool IsOpen() const { return m_open; } + + /** + * @brief Scales how much reverb of the room on the other side is heard through the portal. + */ float GetAdjacentRoomBleed() const { return m_adjacentRoomBleed; } - /// Obstruction of the direct sound through the portal. + + /** + * @brief Obstruction of the direct sound through the portal. + */ float GetObstruction() const { return m_obstruction; } - /// Occlusion of all sound through the portal, e.g. a door that is partly closed. + + /** + * @brief Occlusion of all sound through the portal, e.g. a door that is partly closed. + */ float GetOcclusion() const { return m_occlusion; } - /// Whether obstruction and occlusion fade over the depth of the portal. + + /** + * @brief Whether obstruction and occlusion fade over the depth of the portal. + */ bool GetObstructionOcclusionTransition() const { return m_obstructionOcclusionTransition; } - /// True when the portal and its volume object are both enabled. + + /** + * @brief True when the portal and its volume object are both enabled. + */ bool IsEnabled() const { return m_enabled && m_shapeEnabled; } - /// Box of the portal, set through ITr2BoxVolume. + + /** + * @brief Box of the portal, set through ITr2BoxVolume. + */ const AudUnitBox& GetBox() const { return m_box; } - /// Whether the portal is in Wwise. + + /** + * @brief Whether the portal is in Wwise. + */ bool IsRegistered() const { return m_sentToWwise; } - /// Room on the front side, as sent to Wwise. + + /** + * @brief Room on the front side, as sent to Wwise. + */ AkUInt64 GetFrontRoomID() const { return m_frontRoomID; } - /// Room on the back side, as sent to Wwise. + + /** + * @brief Room on the back side, as sent to Wwise. + */ AkUInt64 GetBackRoomID() const { return m_backRoomID; } private: friend class AudRoomManager; - /// Registers with the room manager once it exists. + /** + * @brief Registers with the room manager once it exists. + * + * @return True when the portal is registered. + */ bool EnsureRegistered(); - /// Stores the box. Called by AudRoomManager under its lock. + + /** + * @brief Stores the box. Called by AudRoomManager under its lock. + * + * @param unitBoxToWorld Maps the unit box to world space. + */ void ApplyTransform( const Matrix& unitBoxToWorld ); AkUInt64 m_portalID; diff --git a/src/AudPortal_Blue.cpp b/src/AudPortal_Blue.cpp index 4eb6054..db4afbf 100644 --- a/src/AudPortal_Blue.cpp +++ b/src/AudPortal_Blue.cpp @@ -1,9 +1,9 @@ -/** - * @file AudPortal_Blue.cpp - * @author Phevos Rinis - * @date Oct 2026 - * @copyright © 2026 Fenris Creations ehf. - */ +//////////////////////////////////////////////////////////// +// +// Creator: Phevos Rinis +// Creation Date: Oct 2026 +// Copyright (c) 2026 Fenris Creations +// #include "stdafx.h" #include "AudPortal.h" diff --git a/src/AudRoom.cpp b/src/AudRoom.cpp index e0fc88c..9e9db52 100644 --- a/src/AudRoom.cpp +++ b/src/AudRoom.cpp @@ -1,9 +1,9 @@ -/** - * @file AudRoom.cpp - * @author Phevos Rinis - * @date Sep 2026 - * @copyright © 2026 Fenris Creations ehf. - */ +//////////////////////////////////////////////////////////// +// +// Creator: Phevos Rinis +// Creation Date: Sep 2026 +// Copyright (c) 2026 Fenris Creations +// #include "stdafx.h" #include "AudRoom.h" diff --git a/src/AudRoom.h b/src/AudRoom.h index ab2f0fd..acb6772 100644 --- a/src/AudRoom.h +++ b/src/AudRoom.h @@ -1,9 +1,9 @@ -/** - * @file AudRoom.h - * @author Phevos Rinis - * @date Sep 2026 - * @copyright © 2026 Fenris Creations ehf. - */ +//////////////////////////////////////////////////////////// +// +// Creator: Phevos Rinis +// Creation Date: Sep 2026 +// Copyright (c) 2026 Fenris Creations +// #pragma once @@ -29,44 +29,114 @@ BLUE_CLASS( AudRoom ) : EXPOSE_TO_BLUE(); - /// Stores the room's box and sends the room to Wwise. + /** + * @brief Stores the room's box and sends the room to Wwise. + * + * @param unitBoxToWorld Maps the unit box to world space. + */ void SetTransform( const Matrix& unitBoxToWorld ) override; - /// Called when the volume object is enabled or disabled. + + /** + * @brief Called when the volume object is enabled or disabled. + * + * @param enabled Whether the volume object is enabled. + */ void SetEnabled( bool enabled ) override; - /// Clears the box and removes the room from Wwise. + + /** + * @brief Clears the box and removes the room from Wwise. + */ void Remove() override; - /// Called by Blue when an attribute changes. + /** + * @brief Called by Blue when an attribute changes. + * + * @param value The attribute that changed. + * + * @return Always true. + */ bool OnModified( Be::Var* value ) override; - /// Room ID, from the same ID space as game objects. + /** + * @brief Room ID, from the same ID space as game objects. + */ AkUInt64 GetRoomID() const { return m_roomID; } - /// Name shown in the Wwise profiler. + + /** + * @brief Name shown in the Wwise profiler. + */ const std::string& GetName() const { return m_name; } - /// Reverb aux bus name. Empty means no reverb. + + /** + * @brief Reverb aux bus name. Empty means no reverb. + */ const std::string& GetReverbAuxBus() const { return m_reverbAuxBus; } + + /** + * @brief Send level [0, 1] from emitters in the room to the reverb bus. + */ float GetReverbLevel() const { return m_reverbLevel; } + + /** + * @brief Transmission loss [0, 1] through the walls of the room. + */ float GetTransmissionLoss() const { return m_transmissionLoss; } + + /** + * @brief The room with the highest priority wins when rooms overlap. + */ float GetPriority() const { return m_priority; } + + /** + * @brief Send level [0, 1] from the room tone to the reverb bus. + */ float GetAuxSendLevelToSelf() const { return m_auxSendLevelToSelf; } + + /** + * @brief Whether the room game object stays registered in Wwise. + */ bool GetKeepRegistered() const { return m_keepRegistered; } - /// Room tone event. Empty means no room tone. + + /** + * @brief Room tone event. Empty means no room tone. + */ const std::wstring& GetRoomToneEvent() const { return m_roomToneEvent; } - /// True when the room and its volume object are both enabled. + + /** + * @brief True when the room and its volume object are both enabled. + */ bool IsEnabled() const { return m_enabled && m_shapeEnabled; } - /// Box of the room, set through ITr2BoxVolume. + + /** + * @brief Box of the room, set through ITr2BoxVolume. + */ const AudUnitBox& GetBox() const { return m_box; } - /// Whether the room is in Wwise. + + /** + * @brief Whether the room is in Wwise. + */ bool IsRegistered() const { return m_sentToWwise; } private: friend class AudRoomManager; - /// Registers with the room manager once it exists. + /** + * @brief Registers with the room manager once it exists. + * + * @return True when the room is registered. + */ bool EnsureRegistered(); - /// Sends the room to Wwise, or removes it when disabled. + + /** + * @brief Sends the room to Wwise, or removes it when disabled. + */ void Sync(); - /// Stores the box. Called by AudRoomManager under its lock. + + /** + * @brief Stores the box. Called by AudRoomManager under its lock. + * + * @param unitBoxToWorld Maps the unit box to world space. + */ void ApplyTransform( const Matrix& unitBoxToWorld ); AkUInt64 m_roomID; diff --git a/src/AudRoomManager.cpp b/src/AudRoomManager.cpp index 779c163..00e6124 100644 --- a/src/AudRoomManager.cpp +++ b/src/AudRoomManager.cpp @@ -1,9 +1,9 @@ -/** - * @file AudRoomManager.cpp - * @author Phevos Rinis - * @date Sep 2026 - * @copyright © 2026 Fenris Creations ehf. - */ +//////////////////////////////////////////////////////////// +// +// Creator: Phevos Rinis +// Creation Date: Sep 2026 +// Copyright (c) 2026 Fenris Creations +// #include "stdafx.h" #include "AudRoomManager.h" @@ -58,21 +58,33 @@ namespace /// Unit box Z where a portal looks up its rooms, a quarter of its depth past each face. constexpr float PORTAL_ROOM_LOOKUP_Z = 0.75f; - /// Compares positions with a 1 mm tolerance. + /** + * @brief Compares positions with a 1 mm tolerance. + * + * @return True when every axis is within the tolerance. + */ bool PositionsNearlyEqual( const AkVector64& a, const AkVector64& b ) { constexpr AkReal64 epsilon = 1e-3; return std::fabs( a.X - b.X ) <= epsilon && std::fabs( a.Y - b.Y ) <= epsilon && std::fabs( a.Z - b.Z ) <= epsilon; } - /// Compares orientations with a small tolerance for float errors. + /** + * @brief Compares orientations with a small tolerance for float errors. + * + * @return True when every axis is within the tolerance. + */ bool OrientationsNearlyEqual( const AkVector& a, const AkVector& b ) { constexpr float epsilon = 1e-4f; return std::fabs( a.X - b.X ) <= epsilon && std::fabs( a.Y - b.Y ) <= epsilon && std::fabs( a.Z - b.Z ) <= epsilon; } - /// Compares every parameter SetRoom uses. + /** + * @brief Compares every parameter SetRoom uses. + * + * @return True when SetRoom would not change anything. + */ bool RoomParamsEqual( const AkRoomParams& a, const AkRoomParams& b ) { return OrientationsNearlyEqual( a.Front, b.Front ) @@ -87,7 +99,11 @@ namespace && a.RoomGameObj_KeepRegistered == b.RoomGameObj_KeepRegistered; } - /// Compares every parameter SetPortal uses. + /** + * @brief Compares every parameter SetPortal uses. + * + * @return True when SetPortal would not change anything. + */ bool PortalParamsEqual( const AkPortalParams& a, const AkPortalParams& b ) { return PositionsNearlyEqual( a.Transform.Position(), b.Transform.Position() ) diff --git a/src/AudRoomManager.h b/src/AudRoomManager.h index ef1772e..9abc8f6 100644 --- a/src/AudRoomManager.h +++ b/src/AudRoomManager.h @@ -1,9 +1,9 @@ -/** - * @file AudRoomManager.h - * @author Phevos Rinis - * @date Sep 2026 - * @copyright © 2026 Fenris Creations ehf. - */ +//////////////////////////////////////////////////////////// +// +// Creator: Phevos Rinis +// Creation Date: Sep 2026 +// Copyright (c) 2026 Fenris Creations +// #pragma once @@ -35,43 +35,109 @@ class AudRoomManager explicit AudRoomManager( AudManager* audioManager ); ~AudRoomManager(); - /// Called by AudRoom on construction and destruction. + /** + * @brief Called by AudRoom on construction and destruction. + * + * @param room The room to add or remove. + */ void RegisterRoom( AudRoom* room ); void UnregisterRoom( AudRoom* room ); - /// Creates or updates the room in Wwise. + /** + * @brief Creates or updates the room in Wwise. + * + * @param room The room to send. + */ void Push( AudRoom& room ); - /// Removes the room from Wwise. + + /** + * @brief Removes the room from Wwise. + * + * @param room The room to remove. + */ void Remove( AudRoom& room ); - /// Stores the room's box under the lock and sends the room. + + /** + * @brief Stores the room's box under the lock and sends the room. + * + * @param room The room that moved. + * @param unitBoxToWorld Maps the unit box to world space. + */ void SetTransform( AudRoom& room, const Matrix& unitBoxToWorld ); - /// Clears the room's box and removes the room from Wwise. + + /** + * @brief Clears the room's box and removes the room from Wwise. + * + * @param room The room whose box is removed. + */ void RemoveShape( AudRoom& room ); - /// Called by AudPortal on construction and destruction. + /** + * @brief Called by AudPortal on construction and destruction. + * + * @param portal The portal to add or remove. + */ void RegisterPortal( AudPortal* portal ); void UnregisterPortal( AudPortal* portal ); - /// Stores the portal's box under the lock. The portal is sent on the next Update(). + /** + * @brief Stores the portal's box under the lock. The portal is sent on the next Update(). + * + * @param portal The portal that moved. + * @param unitBoxToWorld Maps the unit box to world space. + */ void SetPortalTransform( AudPortal& portal, const Matrix& unitBoxToWorld ); - /// Clears the portal's box and removes the portal from Wwise. + + /** + * @brief Clears the portal's box and removes the portal from Wwise. + * + * @param portal The portal whose box is removed. + */ void RemovePortalShape( AudPortal& portal ); - /// Removes every portal and room from Wwise, for Disable() and the rooms switch. + /** + * @brief Removes every portal and room from Wwise, for Disable() and the rooms switch. + */ void RemoveAllFromWwise(); - /// Sends every enabled room to Wwise, for Enable() and the rooms switch. + + /** + * @brief Sends every enabled room to Wwise, for Enable() and the rooms switch. + */ void ResendAll(); - /// Resets the Wwise state after the sound engine is terminated. + + /** + * @brief Resets the Wwise state after the sound engine is terminated. + */ void ForgetWwiseState(); - /// Assigns the game object to the room it is in. Position is in right-handed world space. + /** + * @brief Assigns the game object to the room it is in. + * + * @param gameObjectID The game object that moved. + * @param position Its position in right-handed world space. + */ void UpdateGameObjectPosition( AkGameObjectID gameObjectID, const Vector3& position ); - /// Removes a game object that was unregistered from Wwise. + + /** + * @brief Removes a game object that was unregistered from Wwise. + * + * @param gameObjectID The game object to forget. + */ void ForgetGameObject( AkGameObjectID gameObjectID ); - /// Sends portals, assigns game objects again when rooms changed and retries room tones waiting for SoundBanks. + + /** + * @brief Sends portals, assigns game objects again when rooms changed and retries room tones waiting for SoundBanks. + */ void Update(); + /** + * @brief Number of rooms registered with the manager. + */ size_t GetRoomCount() const; + + /** + * @brief Number of game objects whose room is tracked. + */ size_t GetTrackedGameObjectCount() const; private: @@ -89,40 +155,120 @@ class AudRoomManager static constexpr AkUInt64 ROOM_SPATIAL_ID_TAG = 1ull << 62; static constexpr AkUInt64 SHARED_CUBE_GEOMETRY_SET_ID = ROOM_SPATIAL_ID_TAG | 1ull; - /// Geometry instance ID of the room, tagged so it can't collide with trinity's geometry IDs. + /** + * @brief Geometry instance ID of the room, tagged so it can't collide with trinity's geometry IDs. + * + * @param room The room. + * + * @return The tagged geometry instance ID. + */ static AkUInt64 GeometryInstanceIDForRoom( const AudRoom& room ); - /// Sends the shared unit cube once. Caller holds m_mutex. + /** + * @brief Sends the shared unit cube once. Caller holds m_mutex. + * + * @return True when the cube is in Wwise. + */ bool EnsureSharedCubeGeometry(); - /// Removes the shared unit cube. Caller holds m_mutex. + + /** + * @brief Removes the shared unit cube. Caller holds m_mutex. + */ void ReleaseSharedCubeGeometry(); - /// Sets the outdoor room to no transmission loss and no reverb. Caller holds m_mutex. + + /** + * @brief Sets the outdoor room to no transmission loss and no reverb. Caller holds m_mutex. + * + * @return True when the outdoor room is set. + */ bool EnsureOutdoorRoomConfigured(); - /// Sends one room. Caller holds m_mutex. + + /** + * @brief Sends one room. Caller holds m_mutex. + * + * @param room The room to send. + */ void PushLocked( AudRoom& room ); - /// Removes one room with its room tone and game object assignments. Caller holds m_mutex. + + /** + * @brief Removes one room with its room tone and game object assignments. Caller holds m_mutex. + * + * @param room The room to remove. + */ void RemoveLocked( AudRoom& room ); - /// Stops the room tone. Caller holds m_mutex. + + /** + * @brief Stops the room tone. Caller holds m_mutex. + * + * @param room The room whose tone stops. + */ void StopRoomToneLocked( AudRoom& room ); - /// Plays the room tone, or waits for its SoundBanks. Caller holds m_mutex. + + /** + * @brief Plays the room tone, or waits for its SoundBanks. Caller holds m_mutex. + * + * @param room The room that plays the tone. + * @param eventName The room tone event. + */ void PostRoomToneLocked( AudRoom& room, const std::wstring& eventName ); - /// Returns the room containing the position, or the outdoor room. The smaller room wins when priorities are equal. Caller holds m_mutex. + /** + * @brief Returns the room containing the position, or the outdoor room. Caller holds m_mutex. + * + * The smaller room wins when priorities are equal. + * + * @param position Position in right-handed world space. + * + * @return The room ID. + */ AkUInt64 ResolveRoomLocked( const Vector3& position ) const; - /// Calls SetGameObjectInRoom when the object's room changed. Caller holds m_mutex. + + /** + * @brief Calls SetGameObjectInRoom when the object's room changed. Caller holds m_mutex. + * + * @param gameObjectID The game object to assign. + * @param tracked Its tracked position and room. + */ void AssignLocked( AkGameObjectID gameObjectID, TrackedGameObject& tracked ); - /// Updates every portal in Wwise. Caller holds m_mutex. + /** + * @brief Updates every portal in Wwise. Caller holds m_mutex. + */ void SyncPortalsLocked(); - /// Sends one portal between the rooms on its two sides, or removes it. Caller holds m_mutex. + + /** + * @brief Sends one portal between the rooms on its two sides, or removes it. Caller holds m_mutex. + * + * @param portal The portal to send. + */ void SyncPortalLocked( AudPortal& portal ); - /// Sends the portal's obstruction and occlusion when they changed. Returns false when Wwise refused them. Caller holds m_mutex. + + /** + * @brief Sends the portal's obstruction and occlusion when they changed. Caller holds m_mutex. + * + * @param portal The portal to send them for. + * + * @return False when Wwise refused them. + */ bool SendPortalObstructionOcclusionLocked( AudPortal& portal ); - /// Removes one portal from Wwise. Caller holds m_mutex. + + /** + * @brief Removes one portal from Wwise. Caller holds m_mutex. + * + * @param portal The portal to remove. + */ void RemovePortalLocked( AudPortal& portal ); - /// Removes every portal from Wwise. Caller holds m_mutex. + + /** + * @brief Removes every portal from Wwise. Caller holds m_mutex. + */ void RemoveAllPortalsLocked(); - /// Removes the portals connected to the room before the room leaves Wwise. Caller holds m_mutex. + + /** + * @brief Removes the portals connected to the room before the room leaves Wwise. Caller holds m_mutex. + * + * @param roomID The room that leaves Wwise. + */ void RemovePortalsConnectedToLocked( AkUInt64 roomID ); AudManager* m_audioManager; diff --git a/src/AudUnitBox.cpp b/src/AudUnitBox.cpp index c0d8f0c..5a96874 100644 --- a/src/AudUnitBox.cpp +++ b/src/AudUnitBox.cpp @@ -1,9 +1,9 @@ -/** - * @file AudUnitBox.cpp - * @author Phevos Rinis - * @date Oct 2026 - * @copyright © 2026 Fenris Creations ehf. - */ +//////////////////////////////////////////////////////////// +// +// Creator: Phevos Rinis +// Creation Date: Oct 2026 +// Copyright (c) 2026 Fenris Creations +// #include "stdafx.h" #include "AudUnitBox.h" diff --git a/src/AudUnitBox.h b/src/AudUnitBox.h index 54f82b3..30fc582 100644 --- a/src/AudUnitBox.h +++ b/src/AudUnitBox.h @@ -1,9 +1,9 @@ -/** - * @file AudUnitBox.h - * @author Phevos Rinis - * @date Oct 2026 - * @copyright © 2026 Fenris Creations ehf. - */ +//////////////////////////////////////////////////////////// +// +// Creator: Phevos Rinis +// Creation Date: Oct 2026 +// Copyright (c) 2026 Fenris Creations +// #pragma once @@ -18,20 +18,51 @@ class AudUnitBox public: AudUnitBox(); - /// Stores the transform. Returns false when the box has a zero-length axis. + /** + * @brief Stores the transform. + * + * @param unitBoxToWorld Maps the unit box to world space. + * + * @return False when the box has a zero-length axis. + */ bool SetTransform( const Matrix& unitBoxToWorld ); - /// Forgets the transform. + + /** + * @brief Forgets the transform. + */ void ClearTransform(); - /// True when the box has a transform with no zero-length axis. + /** + * @brief True when the box has a transform with no zero-length axis. + */ bool IsUsable() const { return m_usable; } + + /** + * @brief The transform from the unit box to world space. + */ const Matrix& GetUnitBoxToWorld() const { return m_unitBoxToWorld; } - /// Volume of the box. + + /** + * @brief Volume of the box. + */ float GetVolume() const { return m_volume; } - /// Returns whether the point is inside the box, in right-handed world space. + /** + * @brief Returns whether the point is inside the box. + * + * @param worldPosition Position in right-handed world space. + * + * @return True when the point is inside the box or on its surface. + */ bool ContainsPoint( const Vector3& worldPosition ) const; - /// Returns the world position of a point in unit box space. + + /** + * @brief Returns the world position of a point in unit box space. + * + * @param unitBoxPosition Position in unit box space, where [-0.5, 0.5] on every axis is inside the box. + * + * @return The position in right-handed world space. + */ Vector3 ToWorld( const Vector3& unitBoxPosition ) const; private: From 757f65d2a464857b283920817c3dc8a8c2361f59 Mon Sep 17 00:00:00 2001 From: phevosccp Date: Mon, 26 Oct 2026 11:00:30 +0000 Subject: [PATCH 11/11] requested reviewer changes --- src/AudPortal.cpp | 3 +- src/AudPortal.h | 6 +- src/AudRoomManager.cpp | 66 ++++++---- src/AudRoomManager.h | 22 +++- tests/native/CMakeLists.txt | 1 + tests/native/FakeVolumeObject.cpp | 59 +++++++++ .../audiotests/test/test_portal_exposure.py | 124 ++++++++++++++++++ 7 files changed, 248 insertions(+), 33 deletions(-) create mode 100644 tests/native/FakeVolumeObject.cpp create mode 100644 tests/python/audiotests/test/test_portal_exposure.py diff --git a/src/AudPortal.cpp b/src/AudPortal.cpp index 65ced57..937a311 100644 --- a/src/AudPortal.cpp +++ b/src/AudPortal.cpp @@ -32,7 +32,8 @@ AudPortal::AudPortal( IRoot* lockobj ) : m_sentObstruction( 0.0f ), m_sentOcclusion( 0.0f ), m_sentTransition( false ), - m_failureLogged( false ) + m_setPortalFailureLogged( false ), + m_obstructionOcclusionFailureLogged( false ) { EnsureRegistered(); } diff --git a/src/AudPortal.h b/src/AudPortal.h index ed83f5c..92e495c 100644 --- a/src/AudPortal.h +++ b/src/AudPortal.h @@ -167,8 +167,10 @@ BLUE_CLASS( AudPortal ) : float m_sentObstruction; float m_sentOcclusion; bool m_sentTransition; - /// Set after a failed Wwise call is logged, so it is not logged again every update. - bool m_failureLogged; + /// Set after a failed SetPortal is logged, so it is not logged again every update. + bool m_setPortalFailureLogged; + /// Set after a failed SetPortalObstructionAndOcclusion is logged, so it is not logged again every update. + bool m_obstructionOcclusionFailureLogged; }; TYPEDEF_BLUECLASS( AudPortal ); diff --git a/src/AudRoomManager.cpp b/src/AudRoomManager.cpp index 00e6124..2fd1d71 100644 --- a/src/AudRoomManager.cpp +++ b/src/AudRoomManager.cpp @@ -677,14 +677,34 @@ void AudRoomManager::SyncPortalsLocked() void AudRoomManager::SyncPortalLocked( AudPortal& portal ) { - const AudUnitBox& box = portal.GetBox(); - if( !portal.IsEnabled() || !box.IsUsable() ) + if( !portal.IsEnabled() || !portal.GetBox().IsUsable() ) { portal.m_foundTicks = 0; RemovePortalLocked( portal ); return; } + AkUInt64 frontRoomID; + AkUInt64 backRoomID; + ChoosePortalRoomsLocked( portal, frontRoomID, backRoomID ); + + if( frontRoomID == backRoomID ) + { + RemovePortalLocked( portal ); + if( portal.m_foundTicks == PORTAL_SAME_ROOM_WARNING_TICKS ) + { + CCP_LOGWARN_CH( s_ch, "Portal '%s' (%llu) has room %llu on both sides; it is not sent to Wwise until it crosses a room wall.", + portal.GetName().c_str(), portal.GetPortalID(), frontRoomID ); + } + return; + } + + SendPortalLocked( portal, frontRoomID, backRoomID ); +} + +void AudRoomManager::ChoosePortalRoomsLocked( AudPortal& portal, AkUInt64& frontRoomID, AkUInt64& backRoomID ) +{ + const AudUnitBox& box = portal.GetBox(); const AkUInt64 foundFrontRoomID = ResolveRoomLocked( box.ToWorld( Vector3( 0.0f, 0.0f, -PORTAL_ROOM_LOOKUP_Z ) ) ); const AkUInt64 foundBackRoomID = ResolveRoomLocked( box.ToWorld( Vector3( 0.0f, 0.0f, PORTAL_ROOM_LOOKUP_Z ) ) ); if( foundFrontRoomID == portal.m_foundFrontRoomID && foundBackRoomID == portal.m_foundBackRoomID ) @@ -698,21 +718,15 @@ void AudRoomManager::SyncPortalLocked( AudPortal& portal ) portal.m_foundTicks = 1; } + // New rooms must hold for a few updates, so a lookup point close to a room wall cannot make the portal flicker between rooms. const bool keepSentRooms = portal.m_sentToWwise && portal.m_foundTicks < PORTAL_ROOM_CHANGE_TICKS; - const AkUInt64 frontRoomID = keepSentRooms ? portal.m_frontRoomID : foundFrontRoomID; - const AkUInt64 backRoomID = keepSentRooms ? portal.m_backRoomID : foundBackRoomID; - - if( frontRoomID == backRoomID ) - { - RemovePortalLocked( portal ); - if( portal.m_foundTicks == PORTAL_SAME_ROOM_WARNING_TICKS ) - { - CCP_LOGWARN_CH( s_ch, "Portal '%s' (%llu) has room %llu on both sides; it is not sent to Wwise until it crosses a room wall.", - portal.GetName().c_str(), portal.GetPortalID(), frontRoomID ); - } - return; - } + frontRoomID = keepSentRooms ? portal.m_frontRoomID : foundFrontRoomID; + backRoomID = keepSentRooms ? portal.m_backRoomID : foundBackRoomID; +} +void AudRoomManager::SendPortalLocked( AudPortal& portal, AkUInt64 frontRoomID, AkUInt64 backRoomID ) +{ + const AudUnitBox& box = portal.GetBox(); AkTransform transform; RH2LH::convertTransform( box.GetUnitBoxToWorld(), transform ); const AkVector scale = RH2LH::extractScale( box.GetUnitBoxToWorld() ); @@ -735,14 +749,15 @@ void AudRoomManager::SyncPortalLocked( AudPortal& portal ) const AKRESULT result = AK::SpatialAudio::SetPortal( portal.GetPortalID(), params, name.c_str() ); if( result != AK_Success ) { - if( !portal.m_failureLogged ) + if( !portal.m_setPortalFailureLogged ) { CCP_LOGERR_CH( s_ch, "Failed to set portal '%s' (%llu), AKRESULT: %d", name.c_str(), portal.GetPortalID(), result ); - portal.m_failureLogged = true; + portal.m_setPortalFailureLogged = true; } return; } + portal.m_setPortalFailureLogged = false; portal.m_sentPortalParams = params; portal.m_sentName = name; } @@ -757,13 +772,10 @@ void AudRoomManager::SyncPortalLocked( AudPortal& portal ) portal.m_frontRoomID = frontRoomID; portal.m_backRoomID = backRoomID; - if( SendPortalObstructionOcclusionLocked( portal ) ) - { - portal.m_failureLogged = false; - } + SendPortalObstructionOcclusionLocked( portal ); } -bool AudRoomManager::SendPortalObstructionOcclusionLocked( AudPortal& portal ) +void AudRoomManager::SendPortalObstructionOcclusionLocked( AudPortal& portal ) { const float obstruction = std::clamp( portal.GetObstruction(), 0.0f, 1.0f ); const float occlusion = std::clamp( portal.GetOcclusion(), 0.0f, 1.0f ); @@ -774,26 +786,26 @@ bool AudRoomManager::SendPortalObstructionOcclusionLocked( AudPortal& portal ) && occlusion == portal.m_sentOcclusion && transition == portal.m_sentTransition ) { - return true; + return; } const AKRESULT result = AK::SpatialAudio::SetPortalObstructionAndOcclusion( portal.GetPortalID(), obstruction, occlusion, transition ); if( result != AK_Success ) { - if( !portal.m_failureLogged ) + if( !portal.m_obstructionOcclusionFailureLogged ) { CCP_LOGWARN_CH( s_ch, "Failed to set obstruction and occlusion on portal '%s' (%llu), AKRESULT: %d", portal.GetName().c_str(), portal.GetPortalID(), result ); - portal.m_failureLogged = true; + portal.m_obstructionOcclusionFailureLogged = true; } - return false; + return; } + portal.m_obstructionOcclusionFailureLogged = false; portal.m_obstructionOcclusionSent = true; portal.m_sentObstruction = obstruction; portal.m_sentOcclusion = occlusion; portal.m_sentTransition = transition; - return true; } void AudRoomManager::RemovePortalLocked( AudPortal& portal ) diff --git a/src/AudRoomManager.h b/src/AudRoomManager.h index 9abc8f6..c26d824 100644 --- a/src/AudRoomManager.h +++ b/src/AudRoomManager.h @@ -243,14 +243,30 @@ class AudRoomManager */ void SyncPortalLocked( AudPortal& portal ); + /** + * @brief Finds the rooms past the portal's front and back faces. Caller holds m_mutex. + * + * @param portal The portal. + * @param frontRoomID Set to the room on the front side. + * @param backRoomID Set to the room on the back side. + */ + void ChoosePortalRoomsLocked( AudPortal& portal, AkUInt64& frontRoomID, AkUInt64& backRoomID ); + + /** + * @brief Sends the portal between two different rooms when anything changed. Caller holds m_mutex. + * + * @param portal The portal to send. + * @param frontRoomID Room on the front side. + * @param backRoomID Room on the back side. + */ + void SendPortalLocked( AudPortal& portal, AkUInt64 frontRoomID, AkUInt64 backRoomID ); + /** * @brief Sends the portal's obstruction and occlusion when they changed. Caller holds m_mutex. * * @param portal The portal to send them for. - * - * @return False when Wwise refused them. */ - bool SendPortalObstructionOcclusionLocked( AudPortal& portal ); + void SendPortalObstructionOcclusionLocked( AudPortal& portal ); /** * @brief Removes one portal from Wwise. Caller holds m_mutex. diff --git a/tests/native/CMakeLists.txt b/tests/native/CMakeLists.txt index 11f4224..76a06c0 100644 --- a/tests/native/CMakeLists.txt +++ b/tests/native/CMakeLists.txt @@ -3,6 +3,7 @@ # Test-only Blue module for the Python tests. Built with the tests, never installed. ccp_add_library(CarbonAudioTests SHARED FakeObstructionQuery.cpp + FakeVolumeObject.cpp ) target_link_libraries(CarbonAudioTests PRIVATE Blue) diff --git a/tests/native/FakeVolumeObject.cpp b/tests/native/FakeVolumeObject.cpp new file mode 100644 index 0000000..39ad8c2 --- /dev/null +++ b/tests/native/FakeVolumeObject.cpp @@ -0,0 +1,59 @@ +// Copyright © 2026 CCP ehf. + +// Test-only stand-in for trinity's EveVolumeObject, so the tests can place AudRoom and AudPortal boxes. + +#include +#include +#include + +BLUE_DEFINE_INTERFACE( ITr2BoxVolume ); +BLUE_DECLARE_INTERFACE( ITr2BoxVolume ); + +// Places an axis-aligned box on its consumer. A negative size turns that axis round. +BLUE_CLASS( FakeVolumeObject ) : public IRoot +{ +public: + FakeVolumeObject( IRoot* lockobj = nullptr ) {} + + EXPOSE_TO_BLUE(); + + void Place( const Vector3& center, const Vector3& size ) + { + if( !m_consumer ) + { + return; + } + + Matrix unitBoxToWorld = {}; + unitBoxToWorld._11 = size.x; + unitBoxToWorld._22 = size.y; + unitBoxToWorld._33 = size.z; + unitBoxToWorld._41 = center.x; + unitBoxToWorld._42 = center.y; + unitBoxToWorld._43 = center.z; + unitBoxToWorld._44 = 1.0f; + m_consumer->SetTransform( unitBoxToWorld ); + } + +private: + ITr2BoxVolumePtr m_consumer; +}; + +TYPEDEF_BLUECLASS( FakeVolumeObject ); +BLUE_DEFINE( FakeVolumeObject ); + +const Be::ClassInfo* FakeVolumeObject::ExposeToBlue() +{ + EXPOSURE_BEGIN( FakeVolumeObject, "Stands in for trinity's EveVolumeObject in the tests, placing a box on an AudRoom or AudPortal." ) + MAP_INTERFACE( FakeVolumeObject ) + MAP_ATTRIBUTE( "consumer", m_consumer, "The AudRoom or AudPortal that receives the box.", Be::READWRITE ) + MAP_METHOD_AND_WRAP + ( + "Place", + Place, + "Places the box on the consumer.\n" + ":param center: Centre of the box.\n" + ":param size: Width, height and depth of the box." + ) + EXPOSURE_END() +} diff --git a/tests/python/audiotests/test/test_portal_exposure.py b/tests/python/audiotests/test/test_portal_exposure.py new file mode 100644 index 0000000..f8d9688 --- /dev/null +++ b/tests/python/audiotests/test/test_portal_exposure.py @@ -0,0 +1,124 @@ +# Copyright © 2026 CCP ehf. + +from audiotests.base_test_class import BaseAudio2TestClass + + +OUTDOORS_ROOM_ID = 0xFFFFFFFFFFFFFFFF +ROOM_SIZE = (20.0, 20.0, 20.0) +PORTAL_SIZE = (4.0, 4.0, 2.0) +ROOM_CHANGE_UPDATES = 3 + +# Two rooms that share the wall at z = 0, with the front room on the -z side. +FRONT_ROOM_CENTER = (0.0, 0.0, -10.0) +BACK_ROOM_CENTER = (0.0, 0.0, 10.0) +FAR_AWAY = (0.0, 0.0, 1000.0) + + +class TestPortalExposure(BaseAudio2TestClass): + """Tests which rooms a portal joins and when it is sent to Wwise.""" + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.Initialize(cls) + + def setUp(self): + self.audioManager.Enable() + self.manager = self.audioManager.manager + self.manager.spatialAudioRoomsEnabled = True + if not self.manager.spatialAudioRoomsEnabled: + self.skipTest("Spatial audio rooms are not available on this platform.") + self.volumes = [] + + def tearDown(self): + self.manager.spatialAudioRoomsEnabled = False + self.volumes = [] + self.audioManager.Disable() + + def Update(self, times=1): + """Let the engine run one frame per time, which runs AudManager::Process() once.""" + import blue + for _ in range(times): + blue.pyos.synchro.Yield() + + def Place(self, consumer, center, size): + import blue + volume = blue.LoadExtension("_audiotests").FakeVolumeObject() + volume.consumer = consumer + volume.Place(center, size) + self.volumes.append(volume) + return volume + + def AddRoom(self, name, center): + import audio2 + room = audio2.AudRoom() + room.name = name + volume = self.Place(room, center, ROOM_SIZE) + self.assertTrue(room.isRegistered, "Room '%s' was not sent to Wwise." % name) + return room, volume + + def AddPortal(self, center, size=PORTAL_SIZE): + import audio2 + portal = audio2.AudPortal() + portal.name = "testPortal" + volume = self.Place(portal, center, size) + return portal, volume + + def AssertRooms(self, portal, frontRoomID, backRoomID, message): + self.assertTrue(portal.isRegistered, message) + self.assertEqual((portal.frontRoomID, portal.backRoomID), (frontRoomID, backRoomID), message) + + def test_a_portal_across_a_room_wall_joins_the_rooms_on_its_sides(self): + frontRoom, _ = self.AddRoom("frontRoom", FRONT_ROOM_CENTER) + backRoom, _ = self.AddRoom("backRoom", BACK_ROOM_CENTER) + portal, _ = self.AddPortal((0.0, 0.0, 0.0)) + self.Update() + self.AssertRooms(portal, frontRoom.roomID, backRoom.roomID, "The portal did not join the rooms in front of and behind it.") + + turnedPortal, _ = self.AddPortal((0.0, 0.0, 0.0), (-PORTAL_SIZE[0], PORTAL_SIZE[1], -PORTAL_SIZE[2])) + self.Update() + self.AssertRooms(turnedPortal, backRoom.roomID, frontRoom.roomID, "Turning the portal round did not swap its rooms.") + + def test_a_portal_on_an_outer_wall_leads_outdoors(self): + backRoom, _ = self.AddRoom("backRoom", BACK_ROOM_CENTER) + portal, _ = self.AddPortal((0.0, 0.0, 0.0)) + self.Update() + self.AssertRooms(portal, OUTDOORS_ROOM_ID, backRoom.roomID, "The portal did not lead from the room to outdoors.") + + def test_a_portal_with_the_same_room_on_both_sides_is_not_sent(self): + self.AddRoom("frontRoom", FRONT_ROOM_CENTER) + self.AddRoom("backRoom", BACK_ROOM_CENTER) + portal, portalVolume = self.AddPortal(FRONT_ROOM_CENTER) + self.Update() + self.assertFalse(portal.isRegistered, "A portal inside one room was sent to Wwise.") + + portalVolume.Place((0.0, 0.0, 0.0), PORTAL_SIZE) + self.Update() + self.assertTrue(portal.isRegistered, "The portal was not sent once it crossed the room wall.") + + portalVolume.Place(FRONT_ROOM_CENTER, PORTAL_SIZE) + self.Update(ROOM_CHANGE_UPDATES) + self.assertFalse(portal.isRegistered, "The portal stayed in Wwise after moving inside one room.") + self.assertEqual((portal.frontRoomID, portal.backRoomID), (OUTDOORS_ROOM_ID, OUTDOORS_ROOM_ID), "The removed portal kept its rooms.") + + def test_new_rooms_replace_the_sent_rooms_only_after_three_updates_in_a_row(self): + frontRoom, _ = self.AddRoom("frontRoom", FRONT_ROOM_CENTER) + backRoom, backRoomVolume = self.AddRoom("backRoom", BACK_ROOM_CENTER) + portal, _ = self.AddPortal((0.0, 0.0, 0.0)) + self.Update() + self.AssertRooms(portal, frontRoom.roomID, backRoom.roomID, "The portal did not join the two rooms.") + + for _ in range(ROOM_CHANGE_UPDATES): + backRoomVolume.Place(FAR_AWAY, ROOM_SIZE) + self.Update() + self.AssertRooms(portal, frontRoom.roomID, backRoom.roomID, "Rooms found for one update replaced the sent ones.") + backRoomVolume.Place(BACK_ROOM_CENTER, ROOM_SIZE) + self.Update() + self.AssertRooms(portal, frontRoom.roomID, backRoom.roomID, "The rooms changed when the room came back.") + + backRoomVolume.Place(FAR_AWAY, ROOM_SIZE) + for update in range(1, ROOM_CHANGE_UPDATES): + self.Update() + self.AssertRooms(portal, frontRoom.roomID, backRoom.roomID, "The rooms changed after %d update(s)." % update) + self.Update() + self.AssertRooms(portal, frontRoom.roomID, OUTDOORS_ROOM_ID, "The rooms did not change after %d updates in a row." % ROOM_CHANGE_UPDATES)