diff --git a/src/AudEmitter.cpp b/src/AudEmitter.cpp index aca59bb..d461ca7 100644 --- a/src/AudEmitter.cpp +++ b/src/AudEmitter.cpp @@ -8,6 +8,13 @@ #include "DebugUtilities.h" #include "Vector3.h" +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"; +} + AudEmitter::AudEmitter( IRoot* lockobj ) : AudGameObjResource( lockobj ), m_normalizeAttenuationScaling( false ), @@ -15,7 +22,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 +38,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), @@ -68,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 ) @@ -115,67 +122,91 @@ 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; } + + 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 ) + { + float scaledRadius = m_visualizationRadius * m_scalingFactor; + renderer.DrawSphere( this, m_position, scaledRadius, debugSphereSegments, ITr2DebugRenderer2::Wireframe, Tr2DebugColor( m_simulationColor ) ); + } + + float emitterRange = std::max( m_visualizationRadius, 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; + 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 * 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( + this, + m_position, + m_position + direction * arrowLength, + arrowRadius, + ARROW_POINTER_LENGTH, + ARROW_SEGMENTS, + 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..24ae1a7 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_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 ) 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..e40ce62 100644 --- a/src/AudGameObjResource.cpp +++ b/src/AudGameObjResource.cpp @@ -18,10 +18,19 @@ static AkGameObjectID GenerateEntityID() return s_currentID++; } +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_authoredRotation( 0.0f, 0.0f, 0.0f, 1.0f ), m_mutex( "AudGameObjResource", "m_mutex" ), m_gameObjRegistered( false ), m_culled( true ), @@ -58,6 +67,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_authoredRotation( 0.0f, 0.0f, 0.0f, 1.0f ), m_mutex( "AudGameObjResource", "m_mutex" ), m_gameObjRegistered( false ), m_culled( true ), @@ -72,6 +84,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"") { @@ -364,34 +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 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::ApplyEffectivePlacement( const Vector3& front, const Vector3& top, const Vector3& position ) { + const Orientation corrected = Orthonormalize( front, top ); + m_position = position; + 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; - Vector3 correctFront = Normalize( front ); - Vector3 correctUp = Normalize( top ); - correctUp = Normalize( Cross( Cross( correctFront, correctUp ), correctFront ) ); - 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 m_authoredRotation != IdentityQuaternion(); +} + +AudGameObjResource::Orientation AudGameObjResource::GetEffectiveOrientation() const +{ + // 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( + Vector3( XMVector3Rotate( m_parentOrientation.front, rotation ) ), + Vector3( XMVector3Rotate( m_parentOrientation.top, rotation ) ) + ); +} + +void AudGameObjResource::RefreshPlacementFromRotation() +{ + const Orientation effectiveOrientation = GetEffectiveOrientation(); + ApplyEffectivePlacement( effectiveOrientation.front, effectiveOrientation.top, m_position ); +} + bool AudGameObjResource::Initialize() { RegisterWwiseObject(); - SetPositionHelper( Vector3( 1,0,0 ), Vector3( 0,1,0 ), m_position ); + SetPlacementFromParent( Vector3( 0, 0, 1 ), Vector3( 0, 1, 0 ), m_position ); if ( !m_eventName.empty() ) { @@ -418,6 +469,12 @@ void AudGameObjResource::OnListModified( long event, ssize_t key, ssize_t key2, bool AudGameObjResource::OnModified( Be::Var* value ) { + if ( ( Be::Var* )&m_authoredRotation == value ) + { + RefreshPlacementFromRotation(); + return true; + } + if ( IsMatch( value, m_eventName ) ) { StopAll(); @@ -581,8 +638,8 @@ void AudGameObjResource::Wake() return; } - RegisterWwiseObject(); - SetPositionHelper( Vector3( 1,0,0 ), Vector3( 0,1,0 ), m_position ); + RegisterWwiseObject(); + ApplyEffectivePlacement( m_effectiveOrientation.front, m_effectiveOrientation.top, m_position ); m_culled = false; if ( m_waitingOneShotInRange.second != L"" && m_listenerInRange ) { @@ -1002,6 +1059,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..eaf6c6e 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,14 +105,26 @@ BLUE_CLASS( AudGameObjResource ) : public IInitialize void ReleaseForcedCullingState(); protected: + struct Orientation + { + Orientation( const Vector3& front_, const Vector3& top_ ); + + Vector3 front; + 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 ); + // 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. @@ -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_authoredRotation; // Whether this game object is currently registered with Wwise. bool m_gameObjRegistered; // Whether this game object is culled or not. @@ -178,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" 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 590fd4c..d732a46 100644 --- a/tests/python/audiotests/test/test_audemitter_exposure.py +++ b/tests/python/audiotests/test/test_audemitter_exposure.py @@ -13,4 +13,84 @@ 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_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) + + 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)