diff --git a/CMakeLists.txt b/CMakeLists.txt index 1631dd3..3bda5ea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,9 +45,12 @@ 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/AudUnitBox.cpp src/SpatialAudioSettings.cpp src/audio2.cpp src/AudGameObjResource_Blue.cpp @@ -89,8 +92,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..937a311 --- /dev/null +++ b/src/AudPortal.cpp @@ -0,0 +1,109 @@ +//////////////////////////////////////////////////////////// +// +// Creator: Phevos Rinis +// Creation Date: Oct 2026 +// Copyright (c) 2026 Fenris Creations +// + +#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_obstruction( 0.0f ), + m_occlusion( 0.0f ), + m_obstructionOcclusionTransition( false ), + m_enabled( true ), + m_shapeEnabled( true ), + m_warnedDegenerate( 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_obstructionOcclusionSent( false ), + m_sentObstruction( 0.0f ), + m_sentOcclusion( 0.0f ), + m_sentTransition( false ), + m_setPortalFailureLogged( false ), + m_obstructionOcclusionFailureLogged( 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..92e495c --- /dev/null +++ b/src/AudPortal.h @@ -0,0 +1,176 @@ +//////////////////////////////////////////////////////////// +// +// Creator: Phevos Rinis +// Creation Date: Oct 2026 +// Copyright (c) 2026 Fenris Creations +// + +#pragma once + +#include + +#include "AudUnitBox.h" + +#include +#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(); + + /** + * @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; + + /** + * @brief Called when the volume object is enabled or disabled. + * + * @param enabled Whether the volume object is enabled. + */ + void SetEnabled( bool enabled ) override; + + /** + * @brief Clears the box and removes the portal from Wwise. + */ + void Remove() override; + + /** + * @brief Called by Blue when an attribute changes. + * + * @param value The attribute that changed. + * + * @return Always true. + */ + bool OnModified( Be::Var* value ) override; + + /** + * @brief Portal ID, from the same ID space as game objects. + */ + AkUInt64 GetPortalID() const { return m_portalID; } + + /** + * @brief Name shown in the Wwise profiler. + */ + const std::string& GetName() const { return m_name; } + + /** + * @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; } + + /** + * @brief Obstruction of the direct sound through the portal. + */ + float GetObstruction() const { return m_obstruction; } + + /** + * @brief Occlusion of all sound through the portal, e.g. a door that is partly closed. + */ + float GetOcclusion() const { return m_occlusion; } + + /** + * @brief Whether obstruction and occlusion fade over the depth of the portal. + */ + bool GetObstructionOcclusionTransition() const { return m_obstructionOcclusionTransition; } + + /** + * @brief True when the portal and its volume object are both enabled. + */ + bool IsEnabled() const { return m_enabled && m_shapeEnabled; } + + /** + * @brief Box of the portal, set through ITr2BoxVolume. + */ + const AudUnitBox& GetBox() const { return m_box; } + + /** + * @brief Whether the portal is in Wwise. + */ + bool IsRegistered() const { return m_sentToWwise; } + + /** + * @brief Room on the front side, as sent to Wwise. + */ + AkUInt64 GetFrontRoomID() const { return m_frontRoomID; } + + /** + * @brief Room on the back side, as sent to Wwise. + */ + AkUInt64 GetBackRoomID() const { return m_backRoomID; } + +private: + friend class AudRoomManager; + + /** + * @brief Registers with the room manager once it exists. + * + * @return True when the portal is registered. + */ + bool EnsureRegistered(); + + /** + * @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; + + 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. + bool m_shapeEnabled; + + AudUnitBox m_box; + bool m_warnedDegenerate; + 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. + AkUInt64 m_foundFrontRoomID; + 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 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/AudPortal_Blue.cpp b/src/AudPortal_Blue.cpp new file mode 100644 index 0000000..db4afbf --- /dev/null +++ b/src/AudPortal_Blue.cpp @@ -0,0 +1,32 @@ +//////////////////////////////////////////////////////////// +// +// Creator: Phevos Rinis +// Creation Date: Oct 2026 +// Copyright (c) 2026 Fenris Creations +// + +#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( "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 ) + 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/AudRoom.cpp b/src/AudRoom.cpp index 9247486..9e9db52 100644 --- a/src/AudRoom.cpp +++ b/src/AudRoom.cpp @@ -2,7 +2,7 @@ // // Creator: Phevos Rinis // Creation Date: Sep 2026 -// Copyright (c) 2026 CCP Games +// Copyright (c) 2026 Fenris Creations // #include "stdafx.h" @@ -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..acb6772 100644 --- a/src/AudRoom.h +++ b/src/AudRoom.h @@ -2,13 +2,15 @@ // // Creator: Phevos Rinis // Creation Date: Sep 2026 -// Copyright (c) 2026 CCP Games +// Copyright (c) 2026 Fenris Creations // #pragma once #include +#include "AudUnitBox.h" + #include #include @@ -27,49 +29,114 @@ BLUE_CLASS( AudRoom ) : EXPOSE_TO_BLUE(); - // ITr2BoxVolume + /** + * @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; + + /** + * @brief Called when the volume object is enabled or disabled. + * + * @param enabled Whether the volume object is enabled. + */ void SetEnabled( bool enabled ) override; + + /** + * @brief Clears the box and removes the room from Wwise. + */ void Remove() override; - // INotify + /** + * @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; } - 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; } - /// 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; } + /** + * @brief Box of the room, set through ITr2BoxVolume. + */ + const AudUnitBox& GetBox() const { return m_box; } + + /** + * @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; @@ -87,11 +154,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..2fd1d71 100644 --- a/src/AudRoomManager.cpp +++ b/src/AudRoomManager.cpp @@ -2,13 +2,14 @@ // // Creator: Phevos Rinis // Creation Date: Sep 2026 -// Copyright (c) 2026 CCP Games +// Copyright (c) 2026 Fenris Creations // #include "stdafx.h" #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,16 +49,42 @@ namespace namespace { - const AkUInt64 OUTDOOR_ROOM_ID = static_cast( AK::SpatialAudio::kOutdoorRoomID ); + /// 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; + + /** + * @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 ) @@ -70,6 +98,25 @@ namespace && a.DistanceBehavior == b.DistanceBehavior && a.RoomGameObj_KeepRegistered == b.RoomGameObj_KeepRegistered; } + + /** + * @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() ) + && 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 ) : @@ -162,6 +209,8 @@ void AudRoomManager::Update() } } + SyncPortalsLocked(); + if( !m_assignmentsDirty ) { return; @@ -180,7 +229,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 +238,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; } @@ -285,7 +334,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 ); @@ -298,7 +347,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 +366,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; @@ -412,6 +461,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(); @@ -541,13 +591,15 @@ 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 ); } void AudRoomManager::RemoveAllFromWwise() { CcpAutoMutex lock( m_mutex ); + RemoveAllPortalsLocked(); + for( auto& entry : m_rooms ) { RemoveLocked( *entry.second ); @@ -580,6 +632,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; @@ -587,3 +640,208 @@ 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 ) +{ + 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 ) + { + 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; + } + + // 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; + 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() ); + + 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_setPortalFailureLogged ) + { + CCP_LOGERR_CH( s_ch, "Failed to set portal '%s' (%llu), AKRESULT: %d", name.c_str(), portal.GetPortalID(), result ); + portal.m_setPortalFailureLogged = true; + } + return; + } + + portal.m_setPortalFailureLogged = false; + 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; + + SendPortalObstructionOcclusionLocked( 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 ); + const bool transition = portal.GetObstructionOcclusionTransition(); + + if( portal.m_obstructionOcclusionSent + && obstruction == portal.m_sentObstruction + && occlusion == portal.m_sentOcclusion + && transition == portal.m_sentTransition ) + { + return; + } + + const AKRESULT result = AK::SpatialAudio::SetPortalObstructionAndOcclusion( portal.GetPortalID(), obstruction, occlusion, transition ); + if( result != AK_Success ) + { + 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_obstructionOcclusionFailureLogged = true; + } + return; + } + + portal.m_obstructionOcclusionFailureLogged = false; + portal.m_obstructionOcclusionSent = true; + portal.m_sentObstruction = obstruction; + portal.m_sentOcclusion = occlusion; + portal.m_sentTransition = transition; +} + +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.h b/src/AudRoomManager.h index 27002f5..c26d824 100644 --- a/src/AudRoomManager.h +++ b/src/AudRoomManager.h @@ -2,12 +2,13 @@ // // Creator: Phevos Rinis // Creation Date: Sep 2026 -// Copyright (c) 2026 CCP Games +// Copyright (c) 2026 Fenris Creations // #pragma once #include +#include #include #include "Matrix.h" @@ -17,47 +18,126 @@ #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. */ 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(); - /// 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 ); - /// Removes every room from Wwise, for Disable() and the rooms switch. + /** + * @brief Called by AudPortal on construction and destruction. + * + * @param portal The portal to add or remove. + */ + void RegisterPortal( AudPortal* portal ); + void UnregisterPortal( AudPortal* portal ); + + /** + * @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 ); + + /** + * @brief Clears the portal's box and removes the portal from Wwise. + * + * @param portal The portal whose box is removed. + */ + void RemovePortalShape( AudPortal& portal ); + + /** + * @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 ); - /// 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: @@ -75,31 +155,141 @@ 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. 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 ); + /** + * @brief Updates every portal in Wwise. Caller holds m_mutex. + */ + void SyncPortalsLocked(); + + /** + * @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 ); + + /** + * @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. + */ + void SendPortalObstructionOcclusionLocked( AudPortal& portal ); + + /** + * @brief Removes one portal from Wwise. Caller holds m_mutex. + * + * @param portal The portal to remove. + */ + void RemovePortalLocked( AudPortal& portal ); + + /** + * @brief Removes every portal from Wwise. Caller holds m_mutex. + */ + void RemoveAllPortalsLocked(); + + /** + * @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; 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/AudUnitBox.cpp b/src/AudUnitBox.cpp new file mode 100644 index 0000000..5a96874 --- /dev/null +++ b/src/AudUnitBox.cpp @@ -0,0 +1,67 @@ +//////////////////////////////////////////////////////////// +// +// Creator: Phevos Rinis +// Creation Date: Oct 2026 +// Copyright (c) 2026 Fenris Creations +// + +#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..30fc582 --- /dev/null +++ b/src/AudUnitBox.h @@ -0,0 +1,72 @@ +//////////////////////////////////////////////////////////// +// +// Creator: Phevos Rinis +// Creation Date: Oct 2026 +// Copyright (c) 2026 Fenris Creations +// + +#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(); + + /** + * @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 ); + + /** + * @brief Forgets the transform. + */ + void ClearTransform(); + + /** + * @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; } + + /** + * @brief Volume of the box. + */ + float GetVolume() const { return m_volume; } + + /** + * @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; + + /** + * @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: + Matrix m_unitBoxToWorld; + float m_volume; + bool m_usable; +}; 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)