From ce7e331c51066668bcf559f3ea952ce0e6a7b7eb Mon Sep 17 00:00:00 2001 From: CCP Zoetrope Date: Fri, 5 Jun 2026 19:02:41 +0000 Subject: [PATCH 1/4] Add ability to author AudEmitter rotation and show this in debug rendering * Allow AudEmitter instances to carry an authored local rotation that is applied on top of their parent/source placement. Store parent orientation separately from the effective Wwise orientation so repeated placement updates do not compound rotation, and preserve the effective orientation across wake for sound prioritization. * Expose emitter placement in Blue/Python as SetPlacement because Graphite automatically introspects whether an object has a SetEmitter method and causes errors when rotating that object. * Add ability to show the direction an audio emitter is pointing using a separate debug rendering option called "Audio Emitter Direction". * Refactor debug rendering for audio emitter's to allow for toggling multiple types of rendering options. * Remove clickable radius from audio emitter debug rendering because it never really fulfilled it's intended purpose and just made things look uglier and more confusing.. * Add tests to cover rotation and SetPlacement. Testing: * Tested in Graphite and client on a scanning effect that was the original catalyst for this change to begin with. * Made sure that the engine audio emitter was continuing to point the correct direction as it has always been the only audio emitter in Frontier/EVE that has had cone attenuation to begin with. * I verified that no python code actually called "SetPosition" in the first place so changing to SetPlacement will not cause unintended consequences. The only place in python that uses SetPosition is the Listener which defines its own function for this anyway. https://fenriscreations.atlassian.net/browse/EF-14176 Co-authored-by: Copilot --- src/AudEmitter.cpp | 120 ++++++++++-------- src/AudEmitter.h | 7 +- src/AudEmitter_Blue.cpp | 7 +- src/AudGameObjResource.cpp | 111 +++++++++++++++- src/AudGameObjResource.h | 19 +++ .../test/test_audemitter_exposure.py | 68 +++++++++- .../test/test_audgameobj_enabled_exposure.py | 2 +- .../test/test_auduiplayer_exposure.py | 6 +- .../test/test_python_audiomanager.py | 6 +- 9 files changed, 277 insertions(+), 69 deletions(-) diff --git a/src/AudEmitter.cpp b/src/AudEmitter.cpp index aca59bb..b2fc26b 100644 --- a/src/AudEmitter.cpp +++ b/src/AudEmitter.cpp @@ -8,6 +8,12 @@ #include "DebugUtilities.h" #include "Vector3.h" +namespace +{ + constexpr const char* AUDIO_ATTENUATION_SPHERE_DEBUG_OPTION = "Audio Attenuation Sphere"; + constexpr const char* AUDIO_EMITTER_DIRECTION_DEBUG_OPTION = "Audio Emitter Direction"; +} + AudEmitter::AudEmitter( IRoot* lockobj ) : AudGameObjResource( lockobj ), m_normalizeAttenuationScaling( false ), @@ -15,7 +21,7 @@ AudEmitter::AudEmitter( IRoot* lockobj ) : m_maxNormalizedValue( 9000.f), // ^ m_minNormalizedScalingFactor( 0.4f ), m_maxNormalizedScalingFactor( 3.5f ), - m_debugColor(0, 0, 0, 0), + m_debugColor( DebugUtilities::GenerateDebugColor( 0.4f, 0.9f ) ), m_simulationColor(0xff00ff00), // Green m_visualizationRadius(0.f), m_listenerDistanceScaleFactor(0.003f), @@ -31,7 +37,7 @@ AudEmitter::AudEmitter( AkGameObjectID gameObjID, IRoot* lockobj ) : m_maxNormalizedValue( 9000.f), // ^ m_minNormalizedScalingFactor( 0.4f ), m_maxNormalizedScalingFactor( 3.5f ), - m_debugColor(0, 0, 0, 0), + m_debugColor( DebugUtilities::GenerateDebugColor( 0.4f, 0.9f ) ), m_simulationColor(0xff00ff00), // Green m_visualizationRadius(0.f), m_listenerDistanceScaleFactor(0.003f), @@ -115,67 +121,81 @@ void AudEmitter::SetVisibility( bool isVisible ) m_isVisible = isVisible; } -// Debug void AudEmitter::GetDebugOptions( Tr2DebugRendererOptions& options ) { - options.insert( "Audio Attenuation Sphere" ); + options.insert( AUDIO_ATTENUATION_SPHERE_DEBUG_OPTION ); + options.insert( AUDIO_EMITTER_DIRECTION_DEBUG_OPTION ); } void AudEmitter::RenderDebugInfo( ITr2DebugRenderer2& renderer ) { - if ( g_audioManager != nullptr && g_audioManager->GetState() == AudioState::Enabled ) + if ( m_culled || g_audioManager == nullptr || g_audioManager->GetState() != AudioState::Enabled ) + { + return; + } + + RenderDebugBoundingSphere( renderer ); + RenderDebugDirection( renderer ); + RenderDebugName( renderer ); +} + +void AudEmitter::RenderDebugBoundingSphere( ITr2DebugRenderer2& renderer ) +{ + if ( !g_debugDisplayAllEmitters && !renderer.HasOption( GetRawRoot(), AUDIO_ATTENUATION_SPHERE_DEBUG_OPTION ) ) { - if ( !g_debugDisplayAllEmitters ) - { - if ( !renderer.HasOption( GetRawRoot(), "Audio Attenuation Sphere" ) ) - { - return; - } - } - - uint32_t debugSphereSegments = static_cast(8.f + m_visualizationRadius / 5000.f); - debugSphereSegments = (debugSphereSegments < 25) ? debugSphereSegments : 25; - - if( m_visualizationRadius > 0.f ) - { - float scaledRadius = m_visualizationRadius * m_scalingFactor; - renderer.DrawSphere( this, m_position, scaledRadius, debugSphereSegments, ITr2DebugRenderer2::Wireframe, Tr2DebugColor( m_simulationColor ) ); - } - - if ( !m_culled ) - { - if ( Dot(m_debugColor, m_debugColor) == 0 ) - { - float minRange = 0.4f; - float maxRange = 0.9f; - m_debugColor = DebugUtilities::GenerateDebugColor( minRange, maxRange ); - } - - const float emitterRange = AK::SoundEngine::Query::GetMaxRadius( m_ID ); - renderer.DrawSphere( this, m_position, emitterRange, debugSphereSegments, ITr2DebugRenderer2::Wireframe, Tr2DebugColor( m_debugColor ) ); - - DrawClickableRadius(renderer); - - std::string debugName = m_name + "(" + std::to_string(m_ID) + ")"; - renderer.DrawText(TRI_DBG_FONT_SMALL, m_position, m_debugColor, debugName.c_str()); - } + return; } + + uint32_t debugSphereSegments = static_cast(8.f + m_visualizationRadius / 5000.f); + debugSphereSegments = (debugSphereSegments < 25) ? debugSphereSegments : 25; + + if( m_visualizationRadius > 0.f ) + { + float scaledRadius = m_visualizationRadius * m_scalingFactor; + renderer.DrawSphere( this, m_position, scaledRadius, debugSphereSegments, ITr2DebugRenderer2::Wireframe, Tr2DebugColor( m_simulationColor ) ); + } + + float emitterRange = m_visualizationRadius; + emitterRange = std::max( emitterRange, AK::SoundEngine::Query::GetMaxRadius( m_ID ) ); + renderer.DrawSphere( this, m_position, emitterRange, debugSphereSegments, ITr2DebugRenderer2::Wireframe, Tr2DebugColor( m_debugColor ) ); } -void AudEmitter::DrawClickableRadius(ITr2DebugRenderer2& renderer) +void AudEmitter::RenderDebugDirection( ITr2DebugRenderer2& renderer ) { - // In order to scale the clickable radius we need to know the distance to the camera. This is not so - // easy to get from our library so we use the listener position because the listener follows the camera. - AudListenerPtr listener = g_audioManager->GetListener(); - Vector3 listenerPos = listener->GetPosition(); - float distanceToListener = Length( m_position - listenerPos ); - float scaleFactor = distanceToListener * m_listenerDistanceScaleFactor; + if ( !renderer.HasOption( GetRawRoot(), AUDIO_EMITTER_DIRECTION_DEBUG_OPTION ) ) + { + return; + } - float textWidth = m_name.length() * m_debugFontCharWidth * scaleFactor; - float radius = textWidth * m_radiusToTextWidthRatio; + AudListenerPtr listener = g_audioManager->GetListener(); + const float distanceToListener = Length( m_position - listener->GetPosition() ); + const float arrowLength = std::min( std::max( distanceToListener * 0.025f, 25.0f ), 250.0f ); + const float arrowRadius = std::max( arrowLength * 0.035f, 1.5f ); + Vector3 direction = Normalize( m_effectiveOrientation.front ); + + renderer.DrawArrow( + this, + m_position, + m_position + direction * arrowLength, + arrowRadius, + 0.22f, + 12, + ITr2DebugRenderer2::Solid, + Tr2DebugColor( m_debugColor ) + ); +} + +void AudEmitter::RenderDebugName( ITr2DebugRenderer2& renderer ) +{ + if ( !g_debugDisplayAllEmitters && + !renderer.HasOption( GetRawRoot(), AUDIO_ATTENUATION_SPHERE_DEBUG_OPTION ) && + !renderer.HasOption( GetRawRoot(), AUDIO_EMITTER_DIRECTION_DEBUG_OPTION ) ) + { + return; + } - // Draw barely visible transparent sphere - renderer.DrawSphere( this, m_position, radius, 8, ITr2DebugRenderer2::Solid, Tr2DebugColor( 0x08000000 ) ); + std::string debugName = m_name + "(" + std::to_string(m_ID) + ")"; + renderer.DrawText(TRI_DBG_FONT_SMALL, m_position, m_debugColor, debugName.c_str()); } void AudEmitter::Mute() diff --git a/src/AudEmitter.h b/src/AudEmitter.h index 36986bd..dd0083e 100644 --- a/src/AudEmitter.h +++ b/src/AudEmitter.h @@ -62,10 +62,9 @@ BLUE_CLASS( AudEmitter ) : virtual void RenderDebugInfo( ITr2DebugRenderer2& renderer ) override; protected: AudEmitter( AkGameObjectID gameObjID, IRoot* lockobj = NULL ); - /** - * @brief Draw a transparent sphere that makes this clickable in our editor. Scales based on distance to camera. - */ - void DrawClickableRadius(ITr2DebugRenderer2& renderer); + void RenderDebugBoundingSphere( ITr2DebugRenderer2& renderer ); + void RenderDebugDirection( ITr2DebugRenderer2& renderer ); + void RenderDebugName( ITr2DebugRenderer2& renderer ); // Properties used for normalizing attenuation scaling for this audio emitter. bool m_normalizeAttenuationScaling; float m_minNormalizedValue; diff --git a/src/AudEmitter_Blue.cpp b/src/AudEmitter_Blue.cpp index 01b7b8a..13311c4 100644 --- a/src/AudEmitter_Blue.cpp +++ b/src/AudEmitter_Blue.cpp @@ -22,6 +22,9 @@ const Be::ClassInfo* AudEmitter::ExposeToBlue() MAP_INTERFACE( ITr2AudEmitter ) MAP_INTERFACE( ITr2DebugRenderable ) + MAP_ATTRIBUTE( "rotation", m_rotation, "Authored local audio rotation used by placement providers.\n:jessica-group: Audio Placement", Be::READWRITE | Be::PERSIST | Be::NOTIFY ) + MAP_PROPERTY_READONLY( "front", GetFront, "Effective front vector sent to Wwise.\n:jessica-hidden: True" ) + MAP_PROPERTY_READONLY( "top", GetTop, "Effective top vector sent to Wwise.\n:jessica-hidden: True" ) MAP_ATTRIBUTE( "normalizeAttenuationScaling", m_normalizeAttenuationScaling, "Determines whether attenuation scaling for this audio emitter should be normalized\n:jessica-group: Attenuation Normalization", Be::READWRITE | Be::PERSIST ) MAP_ATTRIBUTE( "minNormalizedValue", m_minNormalizedValue, "The minimum number to use for attenuation normalization.\n:jessica-group: Attenuation Normalization", Be::READWRITE | Be::PERSIST ) MAP_ATTRIBUTE( "maxNormalizedValue", m_maxNormalizedValue, "The maximum number to use for attenuation normalization.\n:jessica-group: Attenuation Normalization", Be::READWRITE | Be::PERSIST ) @@ -49,7 +52,7 @@ const Be::ClassInfo* AudEmitter::ExposeToBlue() ) MAP_METHOD_AND_WRAP ( - "SetPosition", + "SetPlacement", SetPosition, "Update orientation and position of an audio emitter.\n" ":param front: Vector3 representing the orientation of the object.\n" @@ -57,4 +60,4 @@ const Be::ClassInfo* AudEmitter::ExposeToBlue() ":param position: Vector3 representing the world position .\n" ) EXPOSURE_CHAINTO( AudGameObjResource ) -} \ No newline at end of file +} diff --git a/src/AudGameObjResource.cpp b/src/AudGameObjResource.cpp index 9c43271..4f1761f 100644 --- a/src/AudGameObjResource.cpp +++ b/src/AudGameObjResource.cpp @@ -18,10 +18,53 @@ static AkGameObjectID GenerateEntityID() return s_currentID++; } +namespace +{ + bool IsIdentityRotation( const Quaternion& rotation ) + { + return rotation.x == 0.0f && rotation.y == 0.0f && rotation.z == 0.0f && rotation.w == 1.0f; + } + + Quaternion NormalizeAudioRotation( const Quaternion& rotation ) + { + const float lengthSq = rotation.x * rotation.x + rotation.y * rotation.y + rotation.z * rotation.z + rotation.w * rotation.w; + if ( lengthSq <= 0.00000001f ) + { + return Quaternion( 0.0f, 0.0f, 0.0f, 1.0f ); + } + + const float inverseLength = 1.0f / sqrtf( lengthSq ); + return Quaternion( rotation.x * inverseLength, rotation.y * inverseLength, rotation.z * inverseLength, rotation.w * inverseLength ); + } + + Vector3 RotateAudioVector( const Vector3& vector, const Quaternion& rotation ) + { + const Quaternion normalizedRotation = NormalizeAudioRotation( rotation ); + const float tx = 2.0f * ( normalizedRotation.y * vector.z - normalizedRotation.z * vector.y ); + const float ty = 2.0f * ( normalizedRotation.z * vector.x - normalizedRotation.x * vector.z ); + const float tz = 2.0f * ( normalizedRotation.x * vector.y - normalizedRotation.y * vector.x ); + + return Vector3( + vector.x + normalizedRotation.w * tx + normalizedRotation.y * tz - normalizedRotation.z * ty, + vector.y + normalizedRotation.w * ty + normalizedRotation.z * tx - normalizedRotation.x * tz, + vector.z + normalizedRotation.w * tz + normalizedRotation.x * ty - normalizedRotation.y * tx + ); + } +} + +AudGameObjResource::Orientation::Orientation( const Vector3& front_, const Vector3& top_ ) : + front( front_ ), + top( top_ ) +{ +} + AudGameObjResource::AudGameObjResource( IRoot* lockobj ) : PARENTLOCK( m_parameters ), m_eventPrefix(L""), m_scalingFactor( 1.0 ), m_position( WWISE_INIT_POSITION ), + m_parentOrientation( Vector3( 0, 0, 1 ), Vector3( 0, 1, 0 ) ), + m_effectiveOrientation( Vector3( 0, 0, 1 ), Vector3( 0, 1, 0 ) ), + m_rotation( 0.0f, 0.0f, 0.0f, 1.0f ), m_mutex( "AudGameObjResource", "m_mutex" ), m_gameObjRegistered( false ), m_culled( true ), @@ -58,6 +101,9 @@ AudGameObjResource::AudGameObjResource( AkGameObjectID gameObjID, IRoot* lockobj m_eventPrefix(L""), m_scalingFactor( 1.0 ), m_position( WWISE_INIT_POSITION ), + m_parentOrientation( Vector3( 0, 0, 1 ), Vector3( 0, 1, 0 ) ), + m_effectiveOrientation( Vector3( 0, 0, 1 ), Vector3( 0, 1, 0 ) ), + m_rotation( 0.0f, 0.0f, 0.0f, 1.0f ), m_mutex( "AudGameObjResource", "m_mutex" ), m_gameObjRegistered( false ), m_culled( true ), @@ -72,6 +118,7 @@ AudGameObjResource::AudGameObjResource( AkGameObjectID gameObjID, IRoot* lockobj m_additionalCullingWeight( 0.0f ), m_cumulativeWeight( 0.0f ), m_maxAttenuationRadiusSq( 0.0f ), + m_hasReceivedPosition(false), m_waitingOneShotInRange( std::pair( std::chrono::steady_clock::now(), L"" ) ), m_eventName(L"") { @@ -366,16 +413,25 @@ bool AudGameObjResource::SetAttenuationScalingFactor( float value ) int AudGameObjResource::SetPositionHelper( const Vector3& front, const Vector3& top, const Vector3& position ) { + m_parentOrientation = Orientation( front, top ); + const Orientation effectiveOrientation = GetEffectiveOrientation(); + return SetEffectivePositionHelper( effectiveOrientation.front, effectiveOrientation.top, position ); +} + +int AudGameObjResource::SetEffectivePositionHelper( const Vector3& front, const Vector3& top, const Vector3& position ) +{ + Vector3 correctFront = Normalize( front ); + Vector3 correctUp = Normalize( top ); + correctUp = Normalize( Cross( Cross( correctFront, correctUp ), correctFront ) ); + m_position = position; + m_effectiveOrientation = Orientation( correctFront, correctUp ); if( g_audioManager != nullptr && g_audioManager->GetState() == AudioState::Enabled ) { if( m_gameObjRegistered ) { AkSoundPosition tmp; - Vector3 correctFront = Normalize( front ); - Vector3 correctUp = Normalize( top ); - correctUp = Normalize( Cross( Cross( correctFront, correctUp ), correctFront ) ); tmp.Set( MakeAkVector(position), MakeAkVector(correctFront), MakeAkVector(correctUp) ); // all vectors come in RH, but WWISE is LH, so convert @@ -388,10 +444,39 @@ int AudGameObjResource::SetPositionHelper( const Vector3& front, const Vector3& return AK_Success; } +bool AudGameObjResource::HasAuthoredRotation() const +{ + return !IsIdentityRotation( m_rotation ); +} + +AudGameObjResource::Orientation AudGameObjResource::GetEffectiveOrientation() const +{ + if ( !HasAuthoredRotation() ) + { + return m_parentOrientation; + } + + return Orientation( + RotateAudioVector( m_parentOrientation.front, m_rotation ), + RotateAudioVector( m_parentOrientation.top, m_rotation ) + ); +} + +void AudGameObjResource::RefreshPlacementFromRotation() +{ + if ( !m_hasReceivedPosition ) + { + return; + } + + const Orientation effectiveOrientation = GetEffectiveOrientation(); + SetEffectivePositionHelper( effectiveOrientation.front, effectiveOrientation.top, m_position ); +} + bool AudGameObjResource::Initialize() { RegisterWwiseObject(); - SetPositionHelper( Vector3( 1,0,0 ), Vector3( 0,1,0 ), m_position ); + SetPositionHelper( Vector3( 0, 0, 1 ), Vector3( 0, 1, 0 ), m_position ); if ( !m_eventName.empty() ) { @@ -418,6 +503,12 @@ void AudGameObjResource::OnListModified( long event, ssize_t key, ssize_t key2, bool AudGameObjResource::OnModified( Be::Var* value ) { + if ( ( Be::Var* )&m_rotation == value ) + { + RefreshPlacementFromRotation(); + return true; + } + if ( IsMatch( value, m_eventName ) ) { StopAll(); @@ -582,7 +673,7 @@ void AudGameObjResource::Wake() } RegisterWwiseObject(); - SetPositionHelper( Vector3( 1,0,0 ), Vector3( 0,1,0 ), m_position ); + SetEffectivePositionHelper( m_effectiveOrientation.front, m_effectiveOrientation.top, m_position ); m_culled = false; if ( m_waitingOneShotInRange.second != L"" && m_listenerInRange ) { @@ -1002,6 +1093,16 @@ Vector3 AudGameObjResource::GetPosition() const return m_position; } +Vector3 AudGameObjResource::GetFront() const +{ + return m_effectiveOrientation.front; +} + +Vector3 AudGameObjResource::GetTop() const +{ + return m_effectiveOrientation.top; +} + std::wstring AudGameObjResource::GetEventName() { return m_eventName; diff --git a/src/AudGameObjResource.h b/src/AudGameObjResource.h index 02c420b..2b2a935 100644 --- a/src/AudGameObjResource.h +++ b/src/AudGameObjResource.h @@ -76,6 +76,10 @@ BLUE_CLASS( AudGameObjResource ) : public IInitialize std::wstring GetEventName(); // Get the current position of this game object. Vector3 GetPosition() const override; + // Get the effective front vector sent to Wwise. + Vector3 GetFront() const; + // Get the effective top vector sent to Wwise. + Vector3 GetTop() const; // Set the squared length of the distance that this game object sits from the listener. void SetDistanceSqFromListener(float distanceSq) override; // Set the Wwise event to be sent to this game object when it is initialized. @@ -101,6 +105,14 @@ BLUE_CLASS( AudGameObjResource ) : public IInitialize void ReleaseForcedCullingState(); protected: + struct Orientation + { + Orientation( const Vector3& front_, const Vector3& top_ ); + + Vector3 front; + Vector3 top; + }; + enum ActionTypes { Stop = AkActionOnEventType_Stop, @@ -109,6 +121,10 @@ BLUE_CLASS( AudGameObjResource ) : public IInitialize AudGameObjResource( AkGameObjectID gameObjID, IRoot* lockobj = NULL ); // Convert a Trinity RH vector to a Wwise LH vector and set the position for this game object in Wwise. virtual int SetPositionHelper( const Vector3& front, const Vector3& top, const Vector3& position ); + int SetEffectivePositionHelper( const Vector3& front, const Vector3& top, const Vector3& position ); + bool HasAuthoredRotation() const; + Orientation GetEffectiveOrientation() const; + void RefreshPlacementFromRotation(); // Prepend an event prefix if one exists on the given event. std::wstring PrepareEvent( const std::wstring& event, bool bypassPrefix ); // Propagate any wwise callbacks received for this game object. @@ -133,6 +149,9 @@ BLUE_CLASS( AudGameObjResource ) : public IInitialize std::wstring m_eventName; PAudParameterVector m_parameters; Vector3 m_position; + Orientation m_parentOrientation; + Orientation m_effectiveOrientation; + Quaternion m_rotation; // Whether this game object is currently registered with Wwise. bool m_gameObjRegistered; // Whether this game object is culled or not. diff --git a/tests/python/audiotests/test/test_audemitter_exposure.py b/tests/python/audiotests/test/test_audemitter_exposure.py index 590fd4c..bab295f 100644 --- a/tests/python/audiotests/test/test_audemitter_exposure.py +++ b/tests/python/audiotests/test/test_audemitter_exposure.py @@ -13,4 +13,70 @@ def setUp(self): def test_audioemitter_debug(self): """Test that all methods having to do with debugging in AudEmitter work.""" - self.emitter.SetPosition((1,0,0), (0,1,0), (0,50,0)) + self.emitter.SetPlacement((1,0,0), (0,1,0), (0,50,0)) + + def assertVectorAlmostEqual(self, actual, expected): + self.assertAlmostEqual(actual[0], expected[0], places=4) + self.assertAlmostEqual(actual[1], expected[1], places=4) + self.assertAlmostEqual(actual[2], expected[2], places=4) + + def test_setplacement_updates_orientation_and_position(self): + """Blue placement exposure accepts front, top, and position.""" + self.emitter.SetPlacement((1, 0, 0), (0, 1, 0), (0, 50, 0)) + self.assertVectorAlmostEqual(self.emitter.front, (1, 0, 0)) + self.assertVectorAlmostEqual(self.emitter.top, (0, 1, 0)) + + def test_front_and_top_readback_match_corrected_wwise_orientation(self): + self.emitter.SetPlacement((2, 0, 0), (1, 1, 0), (0, 50, 0)) + + self.assertVectorAlmostEqual(self.emitter.front, (1, 0, 0)) + self.assertVectorAlmostEqual(self.emitter.top, (0, 1, 0)) + + def test_setposition_is_not_exposed(self): + """AudEmitter does not expose transform-style SetPosition in Blue.""" + with self.assertRaises(AttributeError): + self.emitter.SetPosition((0, 60, 0)) + + def test_translation_is_not_exposed(self): + """AudEmitter has authored rotation, but no standalone transform position.""" + self.assertFalse(hasattr(self.emitter, "translation")) + + def test_rotation_adds_to_incoming_placement_direction_and_survives_wake(self): + """Upstream placement supplies the parent orientation and emitter rotation is additive.""" + self.emitter.rotation = (0, 1, 0, 0) + + self.emitter.SetPlacement((1, 0, 0), (0, 1, 0), (0, 50, 0)) + self.assertVectorAlmostEqual(self.emitter.front, (-1, 0, 0)) + self.assertVectorAlmostEqual(self.emitter.top, (0, 1, 0)) + + self.emitter.Cull() + self.assertTrue(self.emitter.IsCulled()) + self.emitter.Wake() + self.assertFalse(self.emitter.IsCulled()) + + self.assertVectorAlmostEqual(self.emitter.front, (-1, 0, 0)) + self.assertVectorAlmostEqual(self.emitter.top, (0, 1, 0)) + + def test_rotation_attribute_updates_existing_placement(self): + self.emitter.SetPlacement((1, 0, 0), (0, 1, 0), (0, 50, 0)) + + self.emitter.rotation = (0, 1, 0, 0) + + self.assertVectorAlmostEqual(self.emitter.front, (-1, 0, 0)) + self.assertVectorAlmostEqual(self.emitter.top, (0, 1, 0)) + + def test_repeated_parent_placement_updates_do_not_compound_rotation(self): + self.emitter.rotation = (0, 1, 0, 0) + + self.emitter.SetPlacement((1, 0, 0), (0, 1, 0), (0, 50, 0)) + self.emitter.SetPlacement((1, 0, 0), (0, 1, 0), (0, 50, 0)) + + self.assertVectorAlmostEqual(self.emitter.front, (-1, 0, 0)) + self.assertVectorAlmostEqual(self.emitter.top, (0, 1, 0)) + + def test_listener_does_not_expose_emitter_orientation_readback(self): + import audio2 + listener = audio2.GetListener() + + self.assertFalse(hasattr(listener, "front")) + self.assertFalse(hasattr(listener, "top")) diff --git a/tests/python/audiotests/test/test_audgameobj_enabled_exposure.py b/tests/python/audiotests/test/test_audgameobj_enabled_exposure.py index ec5909c..b772a09 100644 --- a/tests/python/audiotests/test/test_audgameobj_enabled_exposure.py +++ b/tests/python/audiotests/test/test_audgameobj_enabled_exposure.py @@ -26,7 +26,7 @@ def setUp(self): # through other audio emitter classes. That is why these tests use AudEmitter. import audio2 self.emitter = audio2.AudEmitter("emitter1") - self.emitter.SetPosition((0,0,0), (0,0,0), (0,0,0)) + self.emitter.SetPlacement((0,0,0), (0,0,0), (0,0,0)) self.audioManager.Enable() self.listener = audio2.GetListener() diff --git a/tests/python/audiotests/test/test_auduiplayer_exposure.py b/tests/python/audiotests/test/test_auduiplayer_exposure.py index fc70690..40688a9 100644 --- a/tests/python/audiotests/test/test_auduiplayer_exposure.py +++ b/tests/python/audiotests/test/test_auduiplayer_exposure.py @@ -32,7 +32,7 @@ def test_auduiplayer_sendevent(self): """Test that SendEvent is properly exposed through AudUIPlayer.""" import audio2 uiPlayer = audio2.GetUIPlayer() - uiPlayer.SetPosition((0,0,0), (0,0,0), (0,0,0)) + uiPlayer.SetPlacement((0,0,0), (0,0,0), (0,0,0)) PumpOSWithTimeout(self.alwaysTrueBoolean, maxTries=3) playingID = uiPlayer.SendEvent(LOOP_EVENT) self.assertTrue(playingID > 0) @@ -55,7 +55,7 @@ def test_auduiplayer_postdialogueevent(self): """Test that AudUIPlayer::PostDialogueEvent allows you to get the current playing position of an event.""" import audio2 uiPlayer = audio2.GetUIPlayer() - uiPlayer.SetPosition((0,0,0), (0,0,0), (0,0,0)) + uiPlayer.SetPlacement((0,0,0), (0,0,0), (0,0,0)) PumpOSWithTimeout(self.alwaysTrueBoolean, maxTries=3) playingID = uiPlayer.PostDialogueEvent(ONE_SHOT_EVENT) self.assertTrue(playingID > 0) @@ -70,7 +70,7 @@ def test_auduiplayer_postdialogueevent(self): def test_auduiplayer_geteventplayposition_returns_invalid_when_disabled(self): import audio2 uiPlayer = audio2.GetUIPlayer() - uiPlayer.SetPosition((0,0,0), (0,0,0), (0,0,0)) + uiPlayer.SetPlacement((0,0,0), (0,0,0), (0,0,0)) PumpOSWithTimeout(self.alwaysTrueBoolean, maxTries=3) playingID = uiPlayer.PostDialogueEvent(ONE_SHOT_EVENT) self.assertTrue(playingID > 0) diff --git a/tests/python/audiotests/test/test_python_audiomanager.py b/tests/python/audiotests/test/test_python_audiomanager.py index 249c3f7..48b197a 100644 --- a/tests/python/audiotests/test/test_python_audiomanager.py +++ b/tests/python/audiotests/test/test_python_audiomanager.py @@ -65,7 +65,7 @@ def test_disable_then_enable_can_load_bank_and_play_event(self): self.assertEqual(set(self.audioManager.GetLoadedSoundBanks()), set([INIT_BANK, COMMON_BNK, LOOP_BNK])) emitter = audio2.AudEmitter("lifecycleEmitter") - emitter.SetPosition((0,0,0), (0,0,0), (0,0,0)) + emitter.SetPlacement((0,0,0), (0,0,0), (0,0,0)) listener = audio2.GetListener() listener.SetPosition((0,0,0), (0,0,0), (0,0,0)) PumpOSWithTimeout(self.alwaysTrueBoolean, maxTries=3) @@ -80,7 +80,7 @@ def test_stopped_loop_does_not_resume_after_disable_enable(self): self.pumpUntil(lambda: self.expectedSoundBanksAreLoaded([COMMON_BNK, LOOP_BNK])) emitter = audio2.AudEmitter("stoppedLoopLifecycleEmitter") - emitter.SetPosition((0,0,0), (0,0,0), (0,0,0)) + emitter.SetPlacement((0,0,0), (0,0,0), (0,0,0)) listener = audio2.GetListener() listener.SetPosition((0,0,0), (0,0,0), (0,0,0)) PumpOSWithTimeout(self.alwaysTrueBoolean, maxTries=3) @@ -107,7 +107,7 @@ def test_loop_stopped_by_posted_event_does_not_resume_after_disable_enable(self) self.pumpUntil(lambda: self.expectedSoundBanksAreLoaded([COMMON_BNK, LOOP_BNK, ONE_SHOT_BNK])) emitter = audio2.AudEmitter("postedStopLifecycleEmitter") - emitter.SetPosition((0,0,0), (0,0,0), (0,0,0)) + emitter.SetPlacement((0,0,0), (0,0,0), (0,0,0)) listener = audio2.GetListener() listener.SetPosition((0,0,0), (0,0,0), (0,0,0)) PumpOSWithTimeout(self.alwaysTrueBoolean, maxTries=3) From aaafeb2e5de6a3d4558e297758db9bd350aaa0eb Mon Sep 17 00:00:00 2001 From: CCP Zoetrope Date: Fri, 5 Jun 2026 19:07:21 +0000 Subject: [PATCH 2/4] Change m_rotation to m_authoredRotation --- src/AudEmitter_Blue.cpp | 2 +- src/AudGameObjResource.cpp | 12 ++++++------ src/AudGameObjResource.h | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/AudEmitter_Blue.cpp b/src/AudEmitter_Blue.cpp index 13311c4..24ae1a7 100644 --- a/src/AudEmitter_Blue.cpp +++ b/src/AudEmitter_Blue.cpp @@ -22,7 +22,7 @@ const Be::ClassInfo* AudEmitter::ExposeToBlue() MAP_INTERFACE( ITr2AudEmitter ) MAP_INTERFACE( ITr2DebugRenderable ) - MAP_ATTRIBUTE( "rotation", m_rotation, "Authored local audio rotation used by placement providers.\n:jessica-group: Audio Placement", Be::READWRITE | Be::PERSIST | Be::NOTIFY ) + MAP_ATTRIBUTE( "rotation", m_authoredRotation, "Authored local audio rotation used by placement providers.\n:jessica-group: Audio Placement", Be::READWRITE | Be::PERSIST | Be::NOTIFY ) MAP_PROPERTY_READONLY( "front", GetFront, "Effective front vector sent to Wwise.\n:jessica-hidden: True" ) MAP_PROPERTY_READONLY( "top", GetTop, "Effective top vector sent to Wwise.\n:jessica-hidden: True" ) MAP_ATTRIBUTE( "normalizeAttenuationScaling", m_normalizeAttenuationScaling, "Determines whether attenuation scaling for this audio emitter should be normalized\n:jessica-group: Attenuation Normalization", Be::READWRITE | Be::PERSIST ) diff --git a/src/AudGameObjResource.cpp b/src/AudGameObjResource.cpp index 4f1761f..4efe593 100644 --- a/src/AudGameObjResource.cpp +++ b/src/AudGameObjResource.cpp @@ -64,7 +64,7 @@ AudGameObjResource::AudGameObjResource( IRoot* lockobj ) : PARENTLOCK( m_paramet m_position( WWISE_INIT_POSITION ), m_parentOrientation( Vector3( 0, 0, 1 ), Vector3( 0, 1, 0 ) ), m_effectiveOrientation( Vector3( 0, 0, 1 ), Vector3( 0, 1, 0 ) ), - m_rotation( 0.0f, 0.0f, 0.0f, 1.0f ), + m_authoredRotation( 0.0f, 0.0f, 0.0f, 1.0f ), m_mutex( "AudGameObjResource", "m_mutex" ), m_gameObjRegistered( false ), m_culled( true ), @@ -103,7 +103,7 @@ AudGameObjResource::AudGameObjResource( AkGameObjectID gameObjID, IRoot* lockobj m_position( WWISE_INIT_POSITION ), m_parentOrientation( Vector3( 0, 0, 1 ), Vector3( 0, 1, 0 ) ), m_effectiveOrientation( Vector3( 0, 0, 1 ), Vector3( 0, 1, 0 ) ), - m_rotation( 0.0f, 0.0f, 0.0f, 1.0f ), + m_authoredRotation( 0.0f, 0.0f, 0.0f, 1.0f ), m_mutex( "AudGameObjResource", "m_mutex" ), m_gameObjRegistered( false ), m_culled( true ), @@ -446,7 +446,7 @@ int AudGameObjResource::SetEffectivePositionHelper( const Vector3& front, const bool AudGameObjResource::HasAuthoredRotation() const { - return !IsIdentityRotation( m_rotation ); + return !IsIdentityRotation( m_authoredRotation ); } AudGameObjResource::Orientation AudGameObjResource::GetEffectiveOrientation() const @@ -457,8 +457,8 @@ AudGameObjResource::Orientation AudGameObjResource::GetEffectiveOrientation() co } return Orientation( - RotateAudioVector( m_parentOrientation.front, m_rotation ), - RotateAudioVector( m_parentOrientation.top, m_rotation ) + RotateAudioVector( m_parentOrientation.front, m_authoredRotation ), + RotateAudioVector( m_parentOrientation.top, m_authoredRotation ) ); } @@ -503,7 +503,7 @@ void AudGameObjResource::OnListModified( long event, ssize_t key, ssize_t key2, bool AudGameObjResource::OnModified( Be::Var* value ) { - if ( ( Be::Var* )&m_rotation == value ) + if ( ( Be::Var* )&m_authoredRotation == value ) { RefreshPlacementFromRotation(); return true; diff --git a/src/AudGameObjResource.h b/src/AudGameObjResource.h index 2b2a935..24f71d3 100644 --- a/src/AudGameObjResource.h +++ b/src/AudGameObjResource.h @@ -151,7 +151,7 @@ BLUE_CLASS( AudGameObjResource ) : public IInitialize Vector3 m_position; Orientation m_parentOrientation; Orientation m_effectiveOrientation; - Quaternion m_rotation; + Quaternion m_authoredRotation; // Whether this game object is currently registered with Wwise. bool m_gameObjRegistered; // Whether this game object is culled or not. From 146396d4c94cf7214f5ed4a8e6e67136183a5661 Mon Sep 17 00:00:00 2001 From: CCP Zoetrope Date: Mon, 8 Jun 2026 21:37:18 +0000 Subject: [PATCH 3/4] Make stretch audio follow the stretch direction Use the current source-to-destination vector for StretchAudio emitter placement instead of always using a fixed orientation. This keeps authored emitter rotation as a local offset from the actual stretch direction. Also let authored emitter rotation update before parent placement arrives. This particular commit fixes https://fenriscreations.atlassian.net/browse/EF-14176 and the previous commit did not fully fix this because StretchAudio needed to be updated to update rotation in relation to the world transform of the effect while in the client. --- src/AudGameObjResource.cpp | 5 ---- src/Components/StretchAudio.cpp | 30 ++++++++++++++++++- .../test/test_audemitter_exposure.py | 14 +++++++++ 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/src/AudGameObjResource.cpp b/src/AudGameObjResource.cpp index 4efe593..d5c5749 100644 --- a/src/AudGameObjResource.cpp +++ b/src/AudGameObjResource.cpp @@ -464,11 +464,6 @@ AudGameObjResource::Orientation AudGameObjResource::GetEffectiveOrientation() co void AudGameObjResource::RefreshPlacementFromRotation() { - if ( !m_hasReceivedPosition ) - { - return; - } - const Orientation effectiveOrientation = GetEffectiveOrientation(); SetEffectivePositionHelper( effectiveOrientation.front, effectiveOrientation.top, m_position ); } diff --git a/src/Components/StretchAudio.cpp b/src/Components/StretchAudio.cpp index ee3a60a..a2dbd2a 100644 --- a/src/Components/StretchAudio.cpp +++ b/src/Components/StretchAudio.cpp @@ -4,6 +4,31 @@ #include "StretchAudio.h" #include "AudManager.h" +namespace +{ + void GetStretchOrientation( const Vector3& sourcePosition, const Vector3& destPosition, Vector3& front, Vector3& top ) + { + const Vector3 segment = destPosition - sourcePosition; + const float segmentLengthSquared = Dot( segment, segment ); + if ( segmentLengthSquared < 1e-6f ) + { + front = Vector3( 0, 1, 0 ); + top = Vector3( 0, 0, 1 ); + return; + } + + front = Normalize( segment ); + Vector3 preferredTop( 0, 0, 1 ); + Vector3 topCandidate = preferredTop - front * Dot( preferredTop, front ); + if ( Dot( topCandidate, topCandidate ) < 1e-6f ) + { + preferredTop = Vector3( 0, 1, 0 ); + topCandidate = preferredTop - front * Dot( preferredTop, front ); + } + top = Normalize( topCandidate ); + } +} + StretchAudio::StretchAudio( IRoot* lockobj ) : m_impactEvent( L"" ), m_outburstEvent( L"" ), @@ -48,7 +73,10 @@ void StretchAudio::Update( Vector3& sourcePosition, Vector3& destPosition ) return; } - Vector3 front(0,1,0), top(0,0,1); + Vector3 front; + Vector3 top; + GetStretchOrientation( sourcePosition, destPosition, front, top ); + if ( nullptr != m_sourceEmitter ) { m_sourceEmitter->SetPosition( front, top, sourcePosition ); diff --git a/tests/python/audiotests/test/test_audemitter_exposure.py b/tests/python/audiotests/test/test_audemitter_exposure.py index bab295f..d732a46 100644 --- a/tests/python/audiotests/test/test_audemitter_exposure.py +++ b/tests/python/audiotests/test/test_audemitter_exposure.py @@ -65,6 +65,20 @@ def test_rotation_attribute_updates_existing_placement(self): self.assertVectorAlmostEqual(self.emitter.front, (-1, 0, 0)) self.assertVectorAlmostEqual(self.emitter.top, (0, 1, 0)) + def test_rotation_attribute_updates_default_orientation_before_placement(self): + self.emitter.rotation = (0.7071068, 0, 0, 0.7071067) + + self.assertVectorAlmostEqual(self.emitter.front, (0, -1, 0)) + self.assertVectorAlmostEqual(self.emitter.top, (0, 0, 1)) + + def test_rotation_attribute_adds_to_stretchaudio_placement_convention(self): + self.emitter.rotation = (0.7071068, 0, 0, 0.7071067) + + self.emitter.SetPlacement((0, 1, 0), (0, 0, 1), (0, 50, 0)) + + self.assertVectorAlmostEqual(self.emitter.front, (0, 0, 1)) + self.assertVectorAlmostEqual(self.emitter.top, (0, -1, 0)) + def test_repeated_parent_placement_updates_do_not_compound_rotation(self): self.emitter.rotation = (0, 1, 0, 0) From 6325975dc34a5fd9a6a94411853e7555673382d7 Mon Sep 17 00:00:00 2001 From: CCP Zoetrope Date: Wed, 8 Jul 2026 18:05:03 +0000 Subject: [PATCH 4/4] Refactor audio placement helpers and tidy emitter debug rendering Follow-up cleanup on the authored-emitter-rotation work. No behavior change. - Rename SetPositionHelper -> SetPlacementFromParent and SetEffectivePositionHelper -> ApplyEffectivePlacement to make the "record parent orientation, resolve authored rotation, apply" split read clearly. AudListener and its Blue exposure follow the rename. - Replace hand-rolled quaternion normalization and rotation with the canonical carbon-math helpers (Normalize, IdentityQuaternion, XMVector3Rotate) - Extract AudGameObjResource::Orthonormalize() and share it between the emitter base and the listener, removing the duplicated orientation normalization of geometry that previously lived in both paths. - Name the debug-render tuning values as locally scoped constexpr instead of inline literals. --- src/AudEmitter.cpp | 29 +++++++++---- src/AudGameObjResource.cpp | 89 +++++++++++++------------------------- src/AudGameObjResource.h | 19 +++++--- src/AudListener.cpp | 13 +++--- src/AudListener.h | 2 +- src/AudListener_Blue.cpp | 4 +- 6 files changed, 72 insertions(+), 84 deletions(-) diff --git a/src/AudEmitter.cpp b/src/AudEmitter.cpp index b2fc26b..d461ca7 100644 --- a/src/AudEmitter.cpp +++ b/src/AudEmitter.cpp @@ -10,6 +10,7 @@ namespace { + // Debug render toggle names constexpr const char* AUDIO_ATTENUATION_SPHERE_DEBUG_OPTION = "Audio Attenuation Sphere"; constexpr const char* AUDIO_EMITTER_DIRECTION_DEBUG_OPTION = "Audio Emitter Direction"; } @@ -74,7 +75,7 @@ void AudEmitter::SetPrefix( const std::wstring& prefix ) int AudEmitter::SetPosition( const Vector3& front, const Vector3& top, const Vector3& pos ) { m_hasReceivedPosition = true; - return SetPositionHelper( front, top, pos ); + return SetPlacementFromParent( front, top, pos ); } unsigned int AudEmitter::SendEvent( const std::wstring& name, bool bypassPrefix ) @@ -146,8 +147,11 @@ void AudEmitter::RenderDebugBoundingSphere( ITr2DebugRenderer2& renderer ) return; } - uint32_t debugSphereSegments = static_cast(8.f + m_visualizationRadius / 5000.f); - debugSphereSegments = (debugSphereSegments < 25) ? debugSphereSegments : 25; + constexpr float BASE_SEGMENTS = 8.f; + constexpr float RADIUS_PER_SEGMENT = 5000.f; + constexpr uint32_t MAX_SEGMENTS = 25; + uint32_t debugSphereSegments = static_cast( BASE_SEGMENTS + m_visualizationRadius / RADIUS_PER_SEGMENT ); + debugSphereSegments = std::min( debugSphereSegments, MAX_SEGMENTS ); if( m_visualizationRadius > 0.f ) { @@ -155,8 +159,7 @@ void AudEmitter::RenderDebugBoundingSphere( ITr2DebugRenderer2& renderer ) renderer.DrawSphere( this, m_position, scaledRadius, debugSphereSegments, ITr2DebugRenderer2::Wireframe, Tr2DebugColor( m_simulationColor ) ); } - float emitterRange = m_visualizationRadius; - emitterRange = std::max( emitterRange, AK::SoundEngine::Query::GetMaxRadius( m_ID ) ); + float emitterRange = std::max( m_visualizationRadius, AK::SoundEngine::Query::GetMaxRadius( m_ID ) ); renderer.DrawSphere( this, m_position, emitterRange, debugSphereSegments, ITr2DebugRenderer2::Wireframe, Tr2DebugColor( m_debugColor ) ); } @@ -167,10 +170,18 @@ void AudEmitter::RenderDebugDirection( ITr2DebugRenderer2& renderer ) return; } + constexpr float ARROW_LENGTH_PER_DISTANCE = 0.025f; + constexpr float ARROW_MIN_LENGTH = 25.0f; + constexpr float ARROW_MAX_LENGTH = 250.0f; + constexpr float ARROW_RADIUS_PER_LENGTH = 0.035f; + constexpr float ARROW_MIN_RADIUS = 1.5f; + constexpr float ARROW_POINTER_LENGTH = 0.22f; + constexpr uint32_t ARROW_SEGMENTS = 12; + AudListenerPtr listener = g_audioManager->GetListener(); const float distanceToListener = Length( m_position - listener->GetPosition() ); - const float arrowLength = std::min( std::max( distanceToListener * 0.025f, 25.0f ), 250.0f ); - const float arrowRadius = std::max( arrowLength * 0.035f, 1.5f ); + const float arrowLength = std::min( std::max( distanceToListener * ARROW_LENGTH_PER_DISTANCE, ARROW_MIN_LENGTH ), ARROW_MAX_LENGTH ); + const float arrowRadius = std::max( arrowLength * ARROW_RADIUS_PER_LENGTH, ARROW_MIN_RADIUS ); Vector3 direction = Normalize( m_effectiveOrientation.front ); renderer.DrawArrow( @@ -178,8 +189,8 @@ void AudEmitter::RenderDebugDirection( ITr2DebugRenderer2& renderer ) m_position, m_position + direction * arrowLength, arrowRadius, - 0.22f, - 12, + ARROW_POINTER_LENGTH, + ARROW_SEGMENTS, ITr2DebugRenderer2::Solid, Tr2DebugColor( m_debugColor ) ); diff --git a/src/AudGameObjResource.cpp b/src/AudGameObjResource.cpp index d5c5749..e40ce62 100644 --- a/src/AudGameObjResource.cpp +++ b/src/AudGameObjResource.cpp @@ -18,40 +18,6 @@ static AkGameObjectID GenerateEntityID() return s_currentID++; } -namespace -{ - bool IsIdentityRotation( const Quaternion& rotation ) - { - return rotation.x == 0.0f && rotation.y == 0.0f && rotation.z == 0.0f && rotation.w == 1.0f; - } - - Quaternion NormalizeAudioRotation( const Quaternion& rotation ) - { - const float lengthSq = rotation.x * rotation.x + rotation.y * rotation.y + rotation.z * rotation.z + rotation.w * rotation.w; - if ( lengthSq <= 0.00000001f ) - { - return Quaternion( 0.0f, 0.0f, 0.0f, 1.0f ); - } - - const float inverseLength = 1.0f / sqrtf( lengthSq ); - return Quaternion( rotation.x * inverseLength, rotation.y * inverseLength, rotation.z * inverseLength, rotation.w * inverseLength ); - } - - Vector3 RotateAudioVector( const Vector3& vector, const Quaternion& rotation ) - { - const Quaternion normalizedRotation = NormalizeAudioRotation( rotation ); - const float tx = 2.0f * ( normalizedRotation.y * vector.z - normalizedRotation.z * vector.y ); - const float ty = 2.0f * ( normalizedRotation.z * vector.x - normalizedRotation.x * vector.z ); - const float tz = 2.0f * ( normalizedRotation.x * vector.y - normalizedRotation.y * vector.x ); - - return Vector3( - vector.x + normalizedRotation.w * tx + normalizedRotation.y * tz - normalizedRotation.z * ty, - vector.y + normalizedRotation.w * ty + normalizedRotation.z * tx - normalizedRotation.x * tz, - vector.z + normalizedRotation.w * tz + normalizedRotation.x * ty - normalizedRotation.y * tx - ); - } -} - AudGameObjResource::Orientation::Orientation( const Vector3& front_, const Vector3& top_ ) : front( front_ ), top( top_ ) @@ -411,67 +377,72 @@ bool AudGameObjResource::SetAttenuationScalingFactor( float value ) return false; } -int AudGameObjResource::SetPositionHelper( const Vector3& front, const Vector3& top, const Vector3& position ) +int AudGameObjResource::SetPlacementFromParent( const Vector3& front, const Vector3& top, const Vector3& position ) { m_parentOrientation = Orientation( front, top ); const Orientation effectiveOrientation = GetEffectiveOrientation(); - return SetEffectivePositionHelper( effectiveOrientation.front, effectiveOrientation.top, position ); + return ApplyEffectivePlacement( effectiveOrientation.front, effectiveOrientation.top, position ); +} + +AudGameObjResource::Orientation AudGameObjResource::Orthonormalize( const Vector3& front, const Vector3& top ) +{ + const Vector3 correctFront = Normalize( front ); + const Vector3 correctUp = Normalize( Cross( Cross( correctFront, Normalize( top ) ), correctFront ) ); + return Orientation( correctFront, correctUp ); } -int AudGameObjResource::SetEffectivePositionHelper( const Vector3& front, const Vector3& top, const Vector3& position ) +int AudGameObjResource::ApplyEffectivePlacement( const Vector3& front, const Vector3& top, const Vector3& position ) { - Vector3 correctFront = Normalize( front ); - Vector3 correctUp = Normalize( top ); - correctUp = Normalize( Cross( Cross( correctFront, correctUp ), correctFront ) ); + const Orientation corrected = Orthonormalize( front, top ); m_position = position; - m_effectiveOrientation = Orientation( correctFront, correctUp ); + m_effectiveOrientation = corrected; - if( g_audioManager != nullptr && g_audioManager->GetState() == AudioState::Enabled ) + if( g_audioManager != nullptr && g_audioManager->GetState() == AudioState::Enabled && m_gameObjRegistered ) { - if( m_gameObjRegistered ) - { - AkSoundPosition tmp; - tmp.Set( MakeAkVector(position), MakeAkVector(correctFront), MakeAkVector(correctUp) ); + AkSoundPosition tmp; + tmp.Set( MakeAkVector( position ), MakeAkVector( corrected.front ), MakeAkVector( corrected.top ) ); - // all vectors come in RH, but WWISE is LH, so convert - AkSoundPosition soundPosLH; - RH2LH::convertEmitter( &soundPosLH, &tmp); + // all vectors come in RH, but WWISE is LH, so convert + AkSoundPosition soundPosLH; + RH2LH::convertEmitter( &soundPosLH, &tmp ); - AKRESULT result = AK::SoundEngine::SetPosition( m_ID, soundPosLH ); - } + AK::SoundEngine::SetPosition( m_ID, soundPosLH ); } return AK_Success; } bool AudGameObjResource::HasAuthoredRotation() const { - return !IsIdentityRotation( m_authoredRotation ); + return m_authoredRotation != IdentityQuaternion(); } AudGameObjResource::Orientation AudGameObjResource::GetEffectiveOrientation() const { - if ( !HasAuthoredRotation() ) + // XMVector3Rotate requires a unit quaternion; ignore identity and degenerate + // (e.g. zero-length) authored rotations so placement never becomes NaN. + if ( !HasAuthoredRotation() || LengthSq( m_authoredRotation ) <= 0.0f ) { return m_parentOrientation; } + const Quaternion rotation = Normalize( m_authoredRotation ); return Orientation( - RotateAudioVector( m_parentOrientation.front, m_authoredRotation ), - RotateAudioVector( m_parentOrientation.top, m_authoredRotation ) + Vector3( XMVector3Rotate( m_parentOrientation.front, rotation ) ), + Vector3( XMVector3Rotate( m_parentOrientation.top, rotation ) ) ); } void AudGameObjResource::RefreshPlacementFromRotation() { const Orientation effectiveOrientation = GetEffectiveOrientation(); - SetEffectivePositionHelper( effectiveOrientation.front, effectiveOrientation.top, m_position ); + ApplyEffectivePlacement( effectiveOrientation.front, effectiveOrientation.top, m_position ); } bool AudGameObjResource::Initialize() { RegisterWwiseObject(); - SetPositionHelper( Vector3( 0, 0, 1 ), Vector3( 0, 1, 0 ), m_position ); + SetPlacementFromParent( Vector3( 0, 0, 1 ), Vector3( 0, 1, 0 ), m_position ); if ( !m_eventName.empty() ) { @@ -667,8 +638,8 @@ void AudGameObjResource::Wake() return; } - RegisterWwiseObject(); - SetEffectivePositionHelper( m_effectiveOrientation.front, m_effectiveOrientation.top, m_position ); + RegisterWwiseObject(); + ApplyEffectivePlacement( m_effectiveOrientation.front, m_effectiveOrientation.top, m_position ); m_culled = false; if ( m_waitingOneShotInRange.second != L"" && m_listenerInRange ) { diff --git a/src/AudGameObjResource.h b/src/AudGameObjResource.h index 24f71d3..eaf6c6e 100644 --- a/src/AudGameObjResource.h +++ b/src/AudGameObjResource.h @@ -113,18 +113,18 @@ BLUE_CLASS( AudGameObjResource ) : public IInitialize Vector3 top; }; + // Force (front, top) into a valid orientation: unit length and mutually perpendicular, + // keeping front's direction and snapping top square to it (Gram-Schmidt). + static Orientation Orthonormalize( const Vector3& front, const Vector3& top ); + enum ActionTypes { Stop = AkActionOnEventType_Stop, Break = AkActionOnEventType_Break, }; AudGameObjResource( AkGameObjectID gameObjID, IRoot* lockobj = NULL ); - // Convert a Trinity RH vector to a Wwise LH vector and set the position for this game object in Wwise. - virtual int SetPositionHelper( const Vector3& front, const Vector3& top, const Vector3& position ); - int SetEffectivePositionHelper( const Vector3& front, const Vector3& top, const Vector3& position ); - bool HasAuthoredRotation() const; - Orientation GetEffectiveOrientation() const; - void RefreshPlacementFromRotation(); + // Records a new parent orientation, resolves it against any authored rotation, and applies the result. + virtual int SetPlacementFromParent( const Vector3& front, const Vector3& top, const Vector3& position ); // Prepend an event prefix if one exists on the given event. std::wstring PrepareEvent( const std::wstring& event, bool bypassPrefix ); // Propagate any wwise callbacks received for this game object. @@ -197,6 +197,13 @@ BLUE_CLASS( AudGameObjResource ) : public IInitialize // A mutex to be used when working with m_playingEvents, m_pendingStoppedPlayingIDs and m_eventsOnWake as they are accessed in different threads. CcpMutex m_mutex; + +private: + // Applies an already-resolved effective orientation to this game object and pushes it to Wwise. + int ApplyEffectivePlacement( const Vector3& front, const Vector3& top, const Vector3& position ); + bool HasAuthoredRotation() const; + Orientation GetEffectiveOrientation() const; + void RefreshPlacementFromRotation(); }; TYPEDEF_BLUECLASS( AudGameObjResource ); diff --git a/src/AudListener.cpp b/src/AudListener.cpp index 2d79f5e..bc5a8d8 100644 --- a/src/AudListener.cpp +++ b/src/AudListener.cpp @@ -49,24 +49,23 @@ void AudListener::RegisterWwiseObject() } } -int AudListener::SetPositionHelper( const Vector3& front, const Vector3& top, const Vector3& position ) +int AudListener::SetPlacementFromParent( const Vector3& front, const Vector3& top, const Vector3& position ) { if( g_audioManager != nullptr && g_audioManager->GetState() != AudioState::Uninitialized ) { m_position = position; if( m_gameObjRegistered ) { + const Orientation corrected = Orthonormalize( front, top ); + AkSoundPosition tmp; - Vector3 correctFront = Normalize( front ); - Vector3 correctUp = Normalize( top ); - correctUp = Normalize( Cross( Cross( correctFront, correctUp ), correctFront ) ); - tmp.Set( MakeAkVector( position ), MakeAkVector( correctFront ), MakeAkVector( correctUp ) ); + tmp.Set( MakeAkVector( position ), MakeAkVector( corrected.front ), MakeAkVector( corrected.top ) ); // all vectors come in RH, but WWISE is LH, so convert AkSoundPosition soundPosLH; - RH2LH::convertListener( &soundPosLH, &tmp); + RH2LH::convertListener( &soundPosLH, &tmp ); - AKRESULT result = AK::SoundEngine::SetPosition( m_ID, soundPosLH ); + AK::SoundEngine::SetPosition( m_ID, soundPosLH ); } } return AK_Success; diff --git a/src/AudListener.h b/src/AudListener.h index eeb411b..27a422e 100644 --- a/src/AudListener.h +++ b/src/AudListener.h @@ -24,7 +24,7 @@ BLUE_CLASS( AudListener ) : public AudGameObjResource EXPOSE_TO_BLUE(); void RegisterWwiseObject() override; - int SetPositionHelper( const Vector3& front, const Vector3& top, const Vector3& position ) override; + int SetPlacementFromParent( const Vector3& front, const Vector3& top, const Vector3& position ) override; }; TYPEDEF_BLUECLASS( AudListener ); diff --git a/src/AudListener_Blue.cpp b/src/AudListener_Blue.cpp index 1c5e4ae..54611a6 100644 --- a/src/AudListener_Blue.cpp +++ b/src/AudListener_Blue.cpp @@ -15,8 +15,8 @@ const Be::ClassInfo* AudListener::ExposeToBlue() MAP_METHOD_AND_WRAP ( - "SetPosition", - SetPositionHelper, + "SetPosition", + SetPlacementFromParent, "Updates the entities orientation and position of an entity.\n" ":param front: Vector3 representing the orientation of the object.\n" ":param top: Vector3 representing the up vector of the object.\n"