diff --git a/trinity/CMakeLists.txt b/trinity/CMakeLists.txt index ac9a00d10..ba40ebe53 100644 --- a/trinity/CMakeLists.txt +++ b/trinity/CMakeLists.txt @@ -789,6 +789,9 @@ set(_SOURCES Eve/EveTriggerVolume.cpp Eve/EveTriggerVolume.h Eve/EveTriggerVolume_Blue.cpp + Eve/EveVolumeObject.cpp + Eve/EveVolumeObject.h + Eve/EveVolumeObject_Blue.cpp Eve/EveEffectRoot2.cpp Eve/EveEffectRoot2.h Eve/EveEffectRoot2_Blue.cpp diff --git a/trinity/Eve/EveVolumeObject.cpp b/trinity/Eve/EveVolumeObject.cpp new file mode 100644 index 000000000..52bd22dcb --- /dev/null +++ b/trinity/Eve/EveVolumeObject.cpp @@ -0,0 +1,229 @@ +// Copyright © 2026 CCP ehf. + +#include "StdAfx.h" +#include "EveVolumeObject.h" +#include "Eve/Volume/EveBoxVolume.h" + +EveVolumeObject::EveVolumeObject( IRoot* lockobj ) : + EveEntity( lockobj ), + m_worldTransform( IdentityMatrix() ), + m_enabled( true ), + m_lastSentTransform( IdentityMatrix() ), + m_hasSentTransform( false ), + m_lastSentEnabled( true ), + m_resendOnRegister( false ) +{ +} + +EveVolumeObject::~EveVolumeObject() +{ + // scenes don't unregister their objects when they are destroyed + RemoveFromConsumer(); +} + +ITr2BoxVolumePtr EveVolumeObject::GetConsumer() const +{ + return m_consumer; +} + +void EveVolumeObject::SetConsumer( ITr2BoxVolume* consumer ) +{ + if( m_consumer.p == consumer ) + { + return; + } + + RemoveFromConsumer(); + m_consumer = consumer; +} + +void EveVolumeObject::RebuildBoundingSphere() +{ + m_boundingSphere = m_boxVolume ? m_boxVolume->GetBoundingSphere() : CcpMath::Sphere(); +} + +void EveVolumeObject::UpdateWorldTransform( Be::Time time ) +{ + Quaternion rotation; + Vector3 translation; + + if( m_ballPosition ) + { + m_ballPosition->Update( &translation, time ); + } + else + { + translation = Vector3( 0.0f, 0.0f, 0.0f ); + } + + if( m_ballRotation ) + { + m_ballRotation->Update( &rotation, time ); + } + else + { + rotation = Quaternion( 0.0f, 0.0f, 0.0f, 1.0f ); + } + + m_worldTransform = RotationMatrix( rotation ) * TranslationMatrix( translation ); +} + +void EveVolumeObject::SendTransform( const Matrix& unitBoxToWorld ) +{ + m_consumer->SetTransform( unitBoxToWorld ); + m_lastSentTransform = unitBoxToWorld; + m_hasSentTransform = true; +} + +void EveVolumeObject::RemoveFromConsumer() +{ + if( m_consumer && m_hasSentTransform ) + { + m_consumer->Remove(); + } + m_hasSentTransform = false; +} + +bool EveVolumeObject::TransformsNearlyEqual( const Matrix& a, const Matrix& b ) +{ + const XMVECTOR axisEpsilon = XMVectorReplicate( 1e-4f ); + const XMVECTOR translationEpsilon = XMVectorReplicate( 1e-2f ); // 1 cm + + return XMVector3NearEqual( a.GetX(), b.GetX(), axisEpsilon ) && XMVector3NearEqual( a.GetY(), b.GetY(), axisEpsilon ) && XMVector3NearEqual( a.GetZ(), b.GetZ(), axisEpsilon ) && XMVector3NearEqual( a.GetTranslation(), b.GetTranslation(), translationEpsilon ); +} + +void EveVolumeObject::PushToConsumer() +{ + if( !m_consumer ) + { + return; + } + + if( !m_boxVolume ) + { + RemoveFromConsumer(); + return; + } + + // a consumer starts enabled, so the enabled state goes before the shape + if( !m_hasSentTransform || m_enabled != m_lastSentEnabled ) + { + m_consumer->SetEnabled( m_enabled ); + m_lastSentEnabled = m_enabled; + } + + // relative to the ego ball, so this changes whenever the player moves + const Matrix unitBoxToWorld = m_boxVolume->GetBoxTransform() * m_worldTransform; + if( !m_hasSentTransform || !TransformsNearlyEqual( unitBoxToWorld, m_lastSentTransform ) ) + { + SendTransform( unitBoxToWorld ); + } +} + +// IEveSpaceObject2 +void EveVolumeObject::UpdateSyncronous( const EveUpdateContext& updateContext ) +{ + TRINITY_STATS_ZONE( __FUNCTION__ ); + + UpdateWorldTransform( updateContext.GetTime() ); + + RebuildBoundingSphere(); + + PushToConsumer(); +} + +void EveVolumeObject::UpdateAsyncronous( const EveUpdateContext& updateContext ) +{ +} + +void EveVolumeObject::UpdateVisibility( const EveUpdateContext& updateContext, const Matrix& parentTransform ) +{ +} + +void EveVolumeObject::GetRenderables( std::vector& renderables, Tr2ImpostorManager* impostors ) +{ +} + +bool EveVolumeObject::GetBoundingSphere( Vector4& sphere, BoundingSphereQuery query ) const +{ + Vector3 worldCenter = Transform( m_boundingSphere.center, m_worldTransform ).GetXYZ(); + sphere = Vector4( worldCenter.x, worldCenter.y, worldCenter.z, std::max( m_boundingSphere.radius, 1.0f ) ); + return true; +} + +void EveVolumeObject::UpdateModelCenterWorldPosition( Vector3& position, Be::Time t ) +{ + UpdateWorldTransform( t ); + GetModelCenterWorldPosition( position ); +} + +void EveVolumeObject::GetModelCenterWorldPosition( Vector3& position ) const +{ + position = Transform( m_boundingSphere.center, m_worldTransform ).GetXYZ(); +} + +bool EveVolumeObject::GetLocalBoundingBox( Vector3& min, Vector3& max ) +{ + // Fall back to a unit box when no box is set up yet, so the object stays pickable in Graphite. + float radius = std::max( m_boundingSphere.radius, 1.0f ); + Vector3 extent( radius, radius, radius ); + + min = m_boundingSphere.center - extent; + max = m_boundingSphere.center + extent; + return true; +} + +void EveVolumeObject::GetLocalToWorldTransform( Matrix& transform ) const +{ + transform = m_worldTransform; +} + +Vector3 EveVolumeObject::GetWorldPosition() +{ + return m_worldTransform.GetTranslation(); +} + +Quaternion EveVolumeObject::GetWorldRotation() +{ + return Normalize( RotationQuaternion( m_worldTransform ) ); +} + +bool EveVolumeObject::Initialize() +{ + UpdateWorldTransform( Be::Time( 0.0 ) ); + RebuildBoundingSphere(); + return true; +} + +void EveVolumeObject::GetDebugOptions( Tr2DebugRendererOptions& options ) +{ + options.insert( "Volume Objects" ); +} + +void EveVolumeObject::RenderDebugInfo( ITr2DebugRenderer2& renderer ) +{ + if( m_boxVolume && renderer.HasOption( GetRawRoot(), "Volume Objects" ) ) + { + // cyan when active, grey when disabled + const Color color = m_enabled ? 0xFF33DDFF : 0xFF666666; + + m_boxVolume->RenderDebugInfo( renderer, m_worldTransform, color ); + } +} + +// EveEntity +void EveVolumeObject::RegisterComponents() +{ + // ReregisterEntities: put the shape straight back + if( m_resendOnRegister ) + { + PushToConsumer(); + } + m_resendOnRegister = false; +} + +void EveVolumeObject::UnRegisterComponents() +{ + m_resendOnRegister = m_hasSentTransform; + RemoveFromConsumer(); +} diff --git a/trinity/Eve/EveVolumeObject.h b/trinity/Eve/EveVolumeObject.h new file mode 100644 index 000000000..5467b7e1c --- /dev/null +++ b/trinity/Eve/EveVolumeObject.h @@ -0,0 +1,139 @@ +// Copyright © 2026 CCP ehf. + +#pragma once + +#ifndef EveVolumeObject_h +#define EveVolumeObject_h + +#include "IWorldPosition.h" +#include "IEveSpaceObject2.h" +#include "EveEntity.h" +#include "Tr2DebugRenderer.h" + +#ifdef BLUE_USE_LOCAL_ITr2DebugRenderer2 + +#include "Include/ITr2DebugRenderer2.h" +#else +#include +#endif + +#include +#include + +BLUE_DECLARE( EveBoxVolume ); +BLUE_DECLARE_INTERFACE( ITr2BoxVolume ); +BLUE_DECLARE( EveVolumeObject ); + +/** + * @class EveVolumeObject + * @brief A standalone scene object that places a volume in the world and publishes its shape. + * + * + * The shape is pushed every frame it changes, the scene origin follows the ego ball and so + * moves the volume whenever the player moves. The consumer has to stay in step with everything else + * placed in the scene. The consumer only holds the shape while this object is in a scene. + */ + +BLUE_CLASS( EveVolumeObject ) : + public IWorldPosition, + public IEveSpaceObject2, + public IInitialize, + public ITr2DebugRenderable, + public EveEntity +{ +public: + EXPOSE_TO_BLUE(); + + using IInitialize::Lock; + using IInitialize::Unlock; + + EveVolumeObject( IRoot* lockobj = NULL ); + ~EveVolumeObject(); + + // IEveSpaceObject2 + void UpdateSyncronous( const EveUpdateContext& updateContext ) override; + void UpdateAsyncronous( const EveUpdateContext& updateContext ) override; + void UpdateVisibility( const EveUpdateContext& updateContext, const Matrix& parentTransform ) override; + void GetRenderables( std::vector & renderables, Tr2ImpostorManager * impostors ) override; + bool GetBoundingSphere( Vector4 & sphere, BoundingSphereQuery query = EVE_BOUNDS_NORMAL ) const override; + void UpdateModelCenterWorldPosition( Vector3 & position, Be::Time t ) override; + void GetModelCenterWorldPosition( Vector3 & position ) const override; + bool GetLocalBoundingBox( Vector3 & min, Vector3 & max ) override; + void GetLocalToWorldTransform( Matrix & transform ) const override; + + // IWorldPosition + Vector3 GetWorldPosition() override; + Quaternion GetWorldRotation() override; + + // IInitialize + bool Initialize() override; + + // ITr2DebugRenderable + void GetDebugOptions( Tr2DebugRendererOptions & options ) override; + void RenderDebugInfo( ITr2DebugRenderer2 & renderer ) override; + + // EveEntity + void RegisterComponents() override; + void UnRegisterComponents() override; + +private: + ITr2BoxVolumePtr GetConsumer() const; + + /** + * @brief Takes the shape back from the current consumer before switching to the new one. + */ + void SetConsumer( ITr2BoxVolume * consumer ); + + /** + * @brief Recomputes the broad-phase bounding sphere from the box volume. + */ + void RebuildBoundingSphere(); + + /** + * @brief Rebuilds the world transform from the position and rotation curves. + */ + void UpdateWorldTransform( Be::Time time ); + + /** + * @brief Pushes the enabled state and, when it changed, the unit box world transform to the consumer. + */ + void PushToConsumer(); + + /** + * @brief Sends the transform now and records what was sent. + */ + void SendTransform( const Matrix& unitBoxToWorld ); + + /** + * @brief Takes the shape back from the consumer it was pushed to; the next push sends it again. + */ + void RemoveFromConsumer(); + + /** + * @brief True when two transforms are close enough that a consumer would not notice the difference. + */ + static bool TransformsNearlyEqual( const Matrix& a, const Matrix& b ); + + std::string m_name; + EveBoxVolumePtr m_boxVolume; + + ITriVectorFunctionPtr m_ballPosition; + ITriQuaternionFunctionPtr m_ballRotation; + + Matrix m_worldTransform; + CcpMath::Sphere m_boundingSphere; + bool m_enabled; + + /// Whoever the scene file attached to receive the shape. + ITr2BoxVolumePtr m_consumer; + + Matrix m_lastSentTransform; + bool m_hasSentTransform; + bool m_lastSentEnabled; + /// Set when leaving a scene with a shape out, so registering again. + bool m_resendOnRegister; +}; + +TYPEDEF_BLUECLASS( EveVolumeObject ); + +#endif diff --git a/trinity/Eve/EveVolumeObject_Blue.cpp b/trinity/Eve/EveVolumeObject_Blue.cpp new file mode 100644 index 000000000..d914dd30d --- /dev/null +++ b/trinity/Eve/EveVolumeObject_Blue.cpp @@ -0,0 +1,60 @@ +// Copyright © 2026 CCP ehf. + +#include "StdAfx.h" +#include "EveVolumeObject.h" + +BLUE_DEFINE( EveVolumeObject ); +BLUE_DEFINE_INTERFACE( ITr2BoxVolume ); + +const Be::ClassInfo* EveVolumeObject::ExposeToBlue() +{ + EXPOSURE_BEGIN( EveVolumeObject, "A standalone scene object that places a volume in the world and publishes its shape to the attached consumer" ) + MAP_INTERFACE( IEveSpaceObject2 ) + MAP_INTERFACE( IInitialize ) + MAP_INTERFACE( IWorldPosition ) + MAP_INTERFACE( ITr2DebugRenderable ) + MAP_INTERFACE( EveEntity ) + + MAP_ATTRIBUTE( + "name", + m_name, + "Name identifier of the volume object", + Be::READWRITE | Be::PERSIST ) + + MAP_ATTRIBUTE( + "boxVolume", + m_boxVolume, + "The box volume defining the shape", + Be::READWRITE | Be::PERSIST ) + + MAP_ATTRIBUTE( + "consumer", + m_consumer, + "", + Be::PERSISTONLY ) + MAP_PROPERTY( + "consumer", + GetConsumer, + SetConsumer, + "The object that receives the shape of this volume, attached by the scene file" ) + + MAP_ATTRIBUTE( + "enabled", + m_enabled, + "Whether the volume is active for its consumer", + Be::READWRITE | Be::PERSIST ) + + MAP_ATTRIBUTE( + "translationCurve", + m_ballPosition, + "Vector function slot for attaching a destiny ball to set the position of the volume object", + Be::READWRITE | Be::PERSIST ) + + MAP_ATTRIBUTE( + "rotationCurve", + m_ballRotation, + "Quaternion function slot for attaching a destiny ball to set the rotation of the volume object", + Be::READWRITE | Be::PERSIST ) + + EXPOSURE_END() +} diff --git a/trinity/Eve/Volume/EveBoxVolume.h b/trinity/Eve/Volume/EveBoxVolume.h index 4afa5d7ea..6073bb12b 100644 --- a/trinity/Eve/Volume/EveBoxVolume.h +++ b/trinity/Eve/Volume/EveBoxVolume.h @@ -36,6 +36,12 @@ BLUE_CLASS( EveBoxVolume ) : // INotify bool OnModified( Be::Var * val ); + /// Maps the unit box [-0.5, 0.5]^3 to the owner's object space (scaling, rotation, position). + const Matrix& GetBoxTransform() const + { + return m_boxTransform; + } + private: void Setup(); BlueSharedString m_name; diff --git a/vcpkg.json b/vcpkg.json index 0391b38c6..d8e514cf6 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -43,7 +43,7 @@ }, { "name": "carbon-trinityaudioapi", - "version>=": "2.0.3" + "version>=": "3.1.0" }, { "name": "freetype",