From 88261f66d799cd4cfff39f50242e389575486532 Mon Sep 17 00:00:00 2001 From: Phevos Rinis Date: Thu, 8 Oct 2026 14:43:16 +0000 Subject: [PATCH 01/15] create the EveVolumeObject class to place box volumes in a scene --- trinity/CMakeLists.txt | 3 + trinity/Eve/EveVolumeObject.cpp | 167 +++++++++++++++++++++++++++ trinity/Eve/EveVolumeObject.h | 91 +++++++++++++++ trinity/Eve/EveVolumeObject_Blue.cpp | 47 ++++++++ 4 files changed, 308 insertions(+) create mode 100644 trinity/Eve/EveVolumeObject.cpp create mode 100644 trinity/Eve/EveVolumeObject.h create mode 100644 trinity/Eve/EveVolumeObject_Blue.cpp 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..dd16dca34 --- /dev/null +++ b/trinity/Eve/EveVolumeObject.cpp @@ -0,0 +1,167 @@ +// Copyright © 2026 CCP ehf. + +#include "StdAfx.h" +#include "EveVolumeObject.h" + +EveVolumeObject::EveVolumeObject( IRoot* lockobj ) : + PARENTLOCK( m_volumes ), + m_worldTransform( IdentityMatrix() ), + m_enabled( true ) +{ +} + +EveVolumeObject::~EveVolumeObject() +{ +} + +void EveVolumeObject::RebuildBoundingSphere() +{ + TRINITY_STATS_ZONE( __FUNCTION__ ); + + m_boundingSphere = CcpMath::Sphere(); + + for( const auto& volume : m_volumes ) + { + auto volumeSphere = volume->GetBoundingSphere(); + + if( !volumeSphere.IsInitialized() ) + { + continue; + } + + if( !m_boundingSphere.IsInitialized() || volumeSphere.IsSphereInside( m_boundingSphere ) ) + { + m_boundingSphere = volumeSphere; + continue; + } + + if( m_boundingSphere.IsSphereInside( volumeSphere ) ) + { + continue; + } + + Vector3 delta = volumeSphere.center - m_boundingSphere.center; + float deltaLen = Length( delta ); + + m_boundingSphere.center += 0.5f * ( 1.f + ( volumeSphere.radius - m_boundingSphere.radius ) / deltaLen ) * delta; + m_boundingSphere.radius = 0.5f * ( m_boundingSphere.radius + volumeSphere.radius + deltaLen ); + } +} + +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 ); +} + +// IEveSpaceObject2 +void EveVolumeObject::UpdateSyncronous( const EveUpdateContext& updateContext ) +{ + TRINITY_STATS_ZONE( __FUNCTION__ ); + + UpdateWorldTransform( updateContext.GetTime() ); + + RebuildBoundingSphere(); +} + +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 volumes are 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( renderer.HasOption( GetRawRoot(), "Volume Objects" ) ) + { + // cyan when active, grey when disabled + const Color color = m_enabled ? 0xFF33DDFF : 0xFF666666; + + for( const auto& volume : m_volumes ) + { + volume->RenderDebugInfo( renderer, m_worldTransform, color ); + } + } +} diff --git a/trinity/Eve/EveVolumeObject.h b/trinity/Eve/EveVolumeObject.h new file mode 100644 index 000000000..a2b5a4a2c --- /dev/null +++ b/trinity/Eve/EveVolumeObject.h @@ -0,0 +1,91 @@ +// Copyright © 2026 CCP ehf. + +#pragma once + +#ifndef EveVolumeObject_h +#define EveVolumeObject_h + +#include "IWorldPosition.h" +#include "IEveSpaceObject2.h" +#include "Tr2DebugRenderer.h" +#include "Eve/Volume/IEveVolume.h" + +#ifdef BLUE_USE_LOCAL_ITr2DebugRenderer2 +// This is only needed for py2 as the file now belongs in blue. +#include "Include/ITr2DebugRenderer2.h" +#else +#include +#endif + +#include + +BLUE_DECLARE_INTERFACE( IEveVolume ); +BLUE_DECLARE_IVECTOR( IEveVolume ); +BLUE_DECLARE( EveVolumeObject ); + +/** + * @class EveVolumeObject + * @brief A standalone scene object that places a volume in the world. + * + * This is a trigger volume without the triggering: it owns its volumes and a destiny-ball driven + * transform, and nothing else. + */ +BLUE_CLASS( EveVolumeObject ) : + public IWorldPosition, + public IEveSpaceObject2, + public IInitialize, + public ITr2DebugRenderable +{ +public: + EXPOSE_TO_BLUE(); + + 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; + +private: + /** + * @brief Recomputes the broad-phase bounding sphere from the volume list. + */ + void RebuildBoundingSphere(); + + /** + * @brief Rebuilds the world transform from the position and rotation curves. + */ + void UpdateWorldTransform( Be::Time time ); + + std::string m_name; + PIEveVolumeVector m_volumes; + + ITriVectorFunctionPtr m_ballPosition; + ITriQuaternionFunctionPtr m_ballRotation; + + Matrix m_worldTransform; + CcpMath::Sphere m_boundingSphere; + bool m_enabled; +}; + +TYPEDEF_BLUECLASS( EveVolumeObject ); + +#endif diff --git a/trinity/Eve/EveVolumeObject_Blue.cpp b/trinity/Eve/EveVolumeObject_Blue.cpp new file mode 100644 index 000000000..29133061f --- /dev/null +++ b/trinity/Eve/EveVolumeObject_Blue.cpp @@ -0,0 +1,47 @@ +// Copyright © 2026 CCP ehf. + +#include "StdAfx.h" +#include "EveVolumeObject.h" + +BLUE_DEFINE( EveVolumeObject ); + +const Be::ClassInfo* EveVolumeObject::ExposeToBlue() +{ + EXPOSURE_BEGIN( EveVolumeObject, "A standalone scene object that places a volume in the world" ) + MAP_INTERFACE( IEveSpaceObject2 ) + MAP_INTERFACE( IInitialize ) + MAP_INTERFACE( IWorldPosition ) + MAP_INTERFACE( ITr2DebugRenderable ) + + MAP_ATTRIBUTE( + "name", + m_name, + "Name identifier of the volume object", + Be::READWRITE | Be::PERSIST ) + + MAP_ATTRIBUTE( + "volumes", + m_volumes, + "The volumes defining the shape.", + Be::READ | Be::PERSIST ) + + MAP_ATTRIBUTE( + "enabled", + m_enabled, + "Whether the volume is active", + 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() +} From 8947887fa5497583cfb5f8237a58738292493610 Mon Sep 17 00:00:00 2001 From: Phevos Rinis Date: Fri, 9 Oct 2026 08:01:30 +0000 Subject: [PATCH 02/15] send the box shape to the ITr2VolumeObject consumer --- trinity/Eve/EveVolumeObject.cpp | 141 ++++++++++++++++++++++++++- trinity/Eve/EveVolumeObject.h | 40 +++++++- trinity/Eve/EveVolumeObject_Blue.cpp | 13 ++- trinity/Eve/Volume/EveBoxVolume.h | 3 + 4 files changed, 190 insertions(+), 7 deletions(-) diff --git a/trinity/Eve/EveVolumeObject.cpp b/trinity/Eve/EveVolumeObject.cpp index dd16dca34..5c0cf49ea 100644 --- a/trinity/Eve/EveVolumeObject.cpp +++ b/trinity/Eve/EveVolumeObject.cpp @@ -3,15 +3,52 @@ #include "StdAfx.h" #include "EveVolumeObject.h" +#include + +namespace +{ + bool NearlyEqual( float a, float b, float epsilon ) + { + return std::fabs( a - b ) <= epsilon; + } + + /// True when two transforms are close enough that a consumer would not notice the difference. + bool TransformsNearlyEqual( const Matrix& a, const Matrix& b ) + { + constexpr float axisEpsilon = 1e-4f; // rotation and scale, in unit-box space + constexpr float translationEpsilon = 1e-2f; // 1 cm + + return NearlyEqual( a._11, b._11, axisEpsilon ) && NearlyEqual( a._12, b._12, axisEpsilon ) && NearlyEqual( a._13, b._13, axisEpsilon ) + && NearlyEqual( a._21, b._21, axisEpsilon ) && NearlyEqual( a._22, b._22, axisEpsilon ) && NearlyEqual( a._23, b._23, axisEpsilon ) + && NearlyEqual( a._31, b._31, axisEpsilon ) && NearlyEqual( a._32, b._32, axisEpsilon ) && NearlyEqual( a._33, b._33, axisEpsilon ) + && NearlyEqual( a._41, b._41, translationEpsilon ) && NearlyEqual( a._42, b._42, translationEpsilon ) && NearlyEqual( a._43, b._43, translationEpsilon ); + } +} + EveVolumeObject::EveVolumeObject( IRoot* lockobj ) : PARENTLOCK( m_volumes ), m_worldTransform( IdentityMatrix() ), - m_enabled( true ) + m_enabled( true ), + m_boxChangeCallbackID( 0 ), + m_warnedAboutVolumes( false ), + m_lastSentTransform( IdentityMatrix() ), + m_hasSentTransform( false ), + m_lastSentEnabled( true ), + m_transformDirty( false ) { } EveVolumeObject::~EveVolumeObject() { + if( m_boxVolume && m_boxChangeCallbackID != 0 ) + { + m_boxVolume->UnregisterForChanges( m_boxChangeCallbackID ); + } + + if( m_consumer ) + { + m_consumer->Remove(); + } } void EveVolumeObject::RebuildBoundingSphere() @@ -74,6 +111,103 @@ void EveVolumeObject::UpdateWorldTransform( Be::Time time ) m_worldTransform = RotationMatrix( rotation ) * TranslationMatrix( translation ); } +void EveVolumeObject::ResolveBoxVolume() +{ + // The box may be replaced or removed while editing; drop it when it is no longer in the list. + if( m_boxVolume ) + { + bool stillListed = false; + for( const auto& volume : m_volumes ) + { + if( volume == static_cast( m_boxVolume.p ) ) + { + stillListed = true; + break; + } + } + + if( !stillListed ) + { + if( m_boxChangeCallbackID != 0 ) + { + m_boxVolume->UnregisterForChanges( m_boxChangeCallbackID ); + m_boxChangeCallbackID = 0; + } + m_boxVolume = nullptr; + m_warnedAboutVolumes = false; + m_transformDirty = true; + } + } + + if( m_boxVolume || m_volumes.empty() ) + { + return; + } + + // The consumer takes exactly one shape, so the first box volume defines it. + for( const auto& volume : m_volumes ) + { + if( EveBoxVolumePtr box = BlueCastPtr( volume ) ) + { + if( !m_boxVolume ) + { + m_boxVolume = box; + } + else if( !m_warnedAboutVolumes ) + { + CCP_LOGWARN( "EveVolumeObject '%s': only the first box volume is used, extra volumes are ignored.", m_name.c_str() ); + m_warnedAboutVolumes = true; + } + } + else if( !m_warnedAboutVolumes ) + { + CCP_LOGWARN( "EveVolumeObject '%s': only box volumes are supported, other volume types are ignored.", m_name.c_str() ); + m_warnedAboutVolumes = true; + } + } + + if( m_boxVolume ) + { + // Fires when the box is edited (e.g. in Graphite); the next update re-sends the transform. + m_boxChangeCallbackID = m_boxVolume->RegisterForChanges( [this]() + { + m_transformDirty = true; + } ); + } +} + +void EveVolumeObject::SendTransform( const Matrix& unitBoxToWorld ) +{ + m_consumer->SetTransform( unitBoxToWorld ); + m_lastSentTransform = unitBoxToWorld; + m_hasSentTransform = true; + m_transformDirty = false; +} + +void EveVolumeObject::PushToConsumer() +{ + if( !m_consumer || !m_boxVolume ) + { + return; + } + + // Enabled state goes first: a consumer starts enabled, so a disabled object must say so before it + // hands over a shape, otherwise the consumer would activate for one frame and deactivate again. + if( m_enabled != m_lastSentEnabled ) + { + m_consumer->SetEnabled( m_enabled ); + m_lastSentEnabled = m_enabled; + } + + // The box lives in this object's space; compose with the world transform, as EveBoxVolume::RenderDebugInfo does. + // The world transform is relative to the ego ball, so this changes every frame the player moves. + const Matrix unitBoxToWorld = m_boxVolume->GetBoxTransform() * m_worldTransform; + if( !m_hasSentTransform || m_transformDirty || !TransformsNearlyEqual( unitBoxToWorld, m_lastSentTransform ) ) + { + SendTransform( unitBoxToWorld ); + } +} + // IEveSpaceObject2 void EveVolumeObject::UpdateSyncronous( const EveUpdateContext& updateContext ) { @@ -82,6 +216,10 @@ void EveVolumeObject::UpdateSyncronous( const EveUpdateContext& updateContext ) UpdateWorldTransform( updateContext.GetTime() ); RebuildBoundingSphere(); + + ResolveBoxVolume(); + + PushToConsumer(); } void EveVolumeObject::UpdateAsyncronous( const EveUpdateContext& updateContext ) @@ -144,6 +282,7 @@ bool EveVolumeObject::Initialize() { UpdateWorldTransform( Be::Time( 0.0 ) ); RebuildBoundingSphere(); + ResolveBoxVolume(); return true; } diff --git a/trinity/Eve/EveVolumeObject.h b/trinity/Eve/EveVolumeObject.h index a2b5a4a2c..5762a5f2e 100644 --- a/trinity/Eve/EveVolumeObject.h +++ b/trinity/Eve/EveVolumeObject.h @@ -9,6 +9,7 @@ #include "IEveSpaceObject2.h" #include "Tr2DebugRenderer.h" #include "Eve/Volume/IEveVolume.h" +#include "Eve/Volume/EveBoxVolume.h" #ifdef BLUE_USE_LOCAL_ITr2DebugRenderer2 // This is only needed for py2 as the file now belongs in blue. @@ -18,17 +19,24 @@ #endif #include +#include BLUE_DECLARE_INTERFACE( IEveVolume ); BLUE_DECLARE_IVECTOR( IEveVolume ); +BLUE_DECLARE_INTERFACE( ITr2VolumeObject ); BLUE_DECLARE( EveVolumeObject ); /** * @class EveVolumeObject - * @brief A standalone scene object that places a volume in the world. + * @brief A standalone scene object that places a volume in the world and publishes its shape. * - * This is a trigger volume without the triggering: it owns its volumes and a destiny-ball driven - * transform, and nothing else. + * This is a trigger volume without the triggering: it owns a box volume and a destiny-ball driven + * transform, and pushes the unit-box world transform to whatever consumer the scene file attached + * to it. The object itself knows nothing about what the consumer does with the shape. + * + * The shape is pushed every frame it changes, because 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. */ BLUE_CLASS( EveVolumeObject ) : public IWorldPosition, @@ -75,6 +83,21 @@ BLUE_CLASS( EveVolumeObject ) : */ void UpdateWorldTransform( Be::Time time ); + /** + * @brief Picks the box volume that defines the shape and listens for edits to it. + */ + void ResolveBoxVolume(); + + /** + * @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 ); + std::string m_name; PIEveVolumeVector m_volumes; @@ -84,6 +107,17 @@ BLUE_CLASS( EveVolumeObject ) : Matrix m_worldTransform; CcpMath::Sphere m_boundingSphere; bool m_enabled; + + /// Whoever the scene file attached to receive the shape. Not created here; the asset decides. + ITr2VolumeObjectPtr m_consumer; + EveBoxVolumePtr m_boxVolume; + uint32_t m_boxChangeCallbackID; + bool m_warnedAboutVolumes; + + Matrix m_lastSentTransform; + bool m_hasSentTransform; + bool m_lastSentEnabled; + bool m_transformDirty; }; TYPEDEF_BLUECLASS( EveVolumeObject ); diff --git a/trinity/Eve/EveVolumeObject_Blue.cpp b/trinity/Eve/EveVolumeObject_Blue.cpp index 29133061f..9abd79735 100644 --- a/trinity/Eve/EveVolumeObject_Blue.cpp +++ b/trinity/Eve/EveVolumeObject_Blue.cpp @@ -4,10 +4,11 @@ #include "EveVolumeObject.h" BLUE_DEFINE( EveVolumeObject ); +BLUE_DEFINE_INTERFACE( ITr2VolumeObject ); const Be::ClassInfo* EveVolumeObject::ExposeToBlue() { - EXPOSURE_BEGIN( EveVolumeObject, "A standalone scene object that places a volume in the world" ) + 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 ) @@ -22,13 +23,19 @@ const Be::ClassInfo* EveVolumeObject::ExposeToBlue() MAP_ATTRIBUTE( "volumes", m_volumes, - "The volumes defining the shape.", + "The volumes defining the shape. Only the first box volume is used.", Be::READ | Be::PERSIST ) + MAP_ATTRIBUTE( + "consumer", + m_consumer, + "The object that receives the shape of this volume, attached by the scene file", + Be::READWRITE | Be::PERSIST ) + MAP_ATTRIBUTE( "enabled", m_enabled, - "Whether the volume is active", + "Whether the volume is active for its consumer", Be::READWRITE | Be::PERSIST ) MAP_ATTRIBUTE( diff --git a/trinity/Eve/Volume/EveBoxVolume.h b/trinity/Eve/Volume/EveBoxVolume.h index 4afa5d7ea..47900839c 100644 --- a/trinity/Eve/Volume/EveBoxVolume.h +++ b/trinity/Eve/Volume/EveBoxVolume.h @@ -36,6 +36,9 @@ 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; From f203a5950086a3f02a94217eab74ead30e70abd9 Mon Sep 17 00:00:00 2001 From: Phevos Rinis Date: Fri, 9 Oct 2026 08:01:31 +0000 Subject: [PATCH 03/15] remove the shape when the volume leaves the scene --- trinity/Eve/EveVolumeObject.cpp | 33 +++++++++++++++++++++++++++- trinity/Eve/EveVolumeObject.h | 20 +++++++++++++++-- trinity/Eve/EveVolumeObject_Blue.cpp | 1 + 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/trinity/Eve/EveVolumeObject.cpp b/trinity/Eve/EveVolumeObject.cpp index 5c0cf49ea..573799fd5 100644 --- a/trinity/Eve/EveVolumeObject.cpp +++ b/trinity/Eve/EveVolumeObject.cpp @@ -26,6 +26,7 @@ namespace } EveVolumeObject::EveVolumeObject( IRoot* lockobj ) : + EveEntity( lockobj ), PARENTLOCK( m_volumes ), m_worldTransform( IdentityMatrix() ), m_enabled( true ), @@ -34,7 +35,8 @@ EveVolumeObject::EveVolumeObject( IRoot* lockobj ) : m_lastSentTransform( IdentityMatrix() ), m_hasSentTransform( false ), m_lastSentEnabled( true ), - m_transformDirty( false ) + m_transformDirty( false ), + m_resendOnRegister( false ) { } @@ -45,6 +47,7 @@ EveVolumeObject::~EveVolumeObject() m_boxVolume->UnregisterForChanges( m_boxChangeCallbackID ); } + // Destroying a scene does not unregister its objects, so take the shape back here too. if( m_consumer ) { m_consumer->Remove(); @@ -184,6 +187,15 @@ void EveVolumeObject::SendTransform( const Matrix& unitBoxToWorld ) m_transformDirty = false; } +void EveVolumeObject::RemoveFromConsumer() +{ + if( m_consumer && m_hasSentTransform ) + { + m_consumer->Remove(); + } + m_hasSentTransform = false; +} + void EveVolumeObject::PushToConsumer() { if( !m_consumer || !m_boxVolume ) @@ -304,3 +316,22 @@ void EveVolumeObject::RenderDebugInfo( ITr2DebugRenderer2& renderer ) } } } + +// EveEntity +void EveVolumeObject::RegisterComponents() +{ + // Unregistered and registered again in one go (ReregisterEntities): put the same shape straight back + // instead of leaving the consumer empty until the next update. A first registration waits for the update. + if( m_resendOnRegister && m_consumer && m_boxVolume ) + { + SendTransform( m_lastSentTransform ); + } + m_resendOnRegister = false; +} + +void EveVolumeObject::UnRegisterComponents() +{ + // Left the scene: nothing updates the shape any more, so the consumer must not keep it. + m_resendOnRegister = m_hasSentTransform; + RemoveFromConsumer(); +} diff --git a/trinity/Eve/EveVolumeObject.h b/trinity/Eve/EveVolumeObject.h index 5762a5f2e..49bea6989 100644 --- a/trinity/Eve/EveVolumeObject.h +++ b/trinity/Eve/EveVolumeObject.h @@ -7,6 +7,7 @@ #include "IWorldPosition.h" #include "IEveSpaceObject2.h" +#include "EveEntity.h" #include "Tr2DebugRenderer.h" #include "Eve/Volume/IEveVolume.h" #include "Eve/Volume/EveBoxVolume.h" @@ -36,17 +37,21 @@ BLUE_DECLARE( EveVolumeObject ); * * The shape is pushed every frame it changes, because 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. + * 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 ITr2DebugRenderable, + public EveEntity { public: EXPOSE_TO_BLUE(); + using IInitialize::Lock; + using IInitialize::Unlock; + EveVolumeObject( IRoot* lockobj = NULL ); ~EveVolumeObject(); @@ -72,6 +77,10 @@ BLUE_CLASS( EveVolumeObject ) : void GetDebugOptions( Tr2DebugRendererOptions & options ) override; void RenderDebugInfo( ITr2DebugRenderer2 & renderer ) override; + // EveEntity + void RegisterComponents() override; + void UnRegisterComponents() override; + private: /** * @brief Recomputes the broad-phase bounding sphere from the volume list. @@ -98,6 +107,11 @@ BLUE_CLASS( EveVolumeObject ) : */ 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(); + std::string m_name; PIEveVolumeVector m_volumes; @@ -118,6 +132,8 @@ BLUE_CLASS( EveVolumeObject ) : bool m_hasSentTransform; bool m_lastSentEnabled; bool m_transformDirty; + /// Set when leaving a scene with a shape out, so registering again (e.g. ReregisterEntities) puts it straight back. + bool m_resendOnRegister; }; TYPEDEF_BLUECLASS( EveVolumeObject ); diff --git a/trinity/Eve/EveVolumeObject_Blue.cpp b/trinity/Eve/EveVolumeObject_Blue.cpp index 9abd79735..ef2eaa6e6 100644 --- a/trinity/Eve/EveVolumeObject_Blue.cpp +++ b/trinity/Eve/EveVolumeObject_Blue.cpp @@ -13,6 +13,7 @@ const Be::ClassInfo* EveVolumeObject::ExposeToBlue() MAP_INTERFACE( IInitialize ) MAP_INTERFACE( IWorldPosition ) MAP_INTERFACE( ITr2DebugRenderable ) + MAP_INTERFACE( EveEntity ) MAP_ATTRIBUTE( "name", From 1397e7956bb82a0761c46273cdbbe7b90f8dd7d8 Mon Sep 17 00:00:00 2001 From: Phevos Rinis Date: Fri, 9 Oct 2026 08:01:32 +0000 Subject: [PATCH 04/15] handle a replaced consumer or box --- trinity/Eve/EveVolumeObject.cpp | 27 +++++++++++++++++++-------- trinity/Eve/EveVolumeObject.h | 2 ++ 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/trinity/Eve/EveVolumeObject.cpp b/trinity/Eve/EveVolumeObject.cpp index 573799fd5..e6ebcf69d 100644 --- a/trinity/Eve/EveVolumeObject.cpp +++ b/trinity/Eve/EveVolumeObject.cpp @@ -48,9 +48,9 @@ EveVolumeObject::~EveVolumeObject() } // Destroying a scene does not unregister its objects, so take the shape back here too. - if( m_consumer ) + if( m_pushedConsumer ) { - m_consumer->Remove(); + m_pushedConsumer->Remove(); } } @@ -139,6 +139,8 @@ void EveVolumeObject::ResolveBoxVolume() m_boxVolume = nullptr; m_warnedAboutVolumes = false; m_transformDirty = true; + // The consumer still holds the old box; nothing is sent until a new box is picked. + RemoveFromConsumer(); } } @@ -181,7 +183,7 @@ void EveVolumeObject::ResolveBoxVolume() void EveVolumeObject::SendTransform( const Matrix& unitBoxToWorld ) { - m_consumer->SetTransform( unitBoxToWorld ); + m_pushedConsumer->SetTransform( unitBoxToWorld ); m_lastSentTransform = unitBoxToWorld; m_hasSentTransform = true; m_transformDirty = false; @@ -189,16 +191,25 @@ void EveVolumeObject::SendTransform( const Matrix& unitBoxToWorld ) void EveVolumeObject::RemoveFromConsumer() { - if( m_consumer && m_hasSentTransform ) + if( m_pushedConsumer && m_hasSentTransform ) { - m_consumer->Remove(); + m_pushedConsumer->Remove(); } m_hasSentTransform = false; } void EveVolumeObject::PushToConsumer() { - if( !m_consumer || !m_boxVolume ) + // The consumer was replaced or cleared (e.g. in Graphite): take the shape back from the old one. + if( m_pushedConsumer.p != m_consumer.p ) + { + RemoveFromConsumer(); + m_pushedConsumer = m_consumer; + // Whatever state the new consumer is in, the enabled state goes out before the first shape. + m_lastSentEnabled = !m_enabled; + } + + if( !m_pushedConsumer || !m_boxVolume ) { return; } @@ -207,7 +218,7 @@ void EveVolumeObject::PushToConsumer() // hands over a shape, otherwise the consumer would activate for one frame and deactivate again. if( m_enabled != m_lastSentEnabled ) { - m_consumer->SetEnabled( m_enabled ); + m_pushedConsumer->SetEnabled( m_enabled ); m_lastSentEnabled = m_enabled; } @@ -322,7 +333,7 @@ void EveVolumeObject::RegisterComponents() { // Unregistered and registered again in one go (ReregisterEntities): put the same shape straight back // instead of leaving the consumer empty until the next update. A first registration waits for the update. - if( m_resendOnRegister && m_consumer && m_boxVolume ) + if( m_resendOnRegister && m_pushedConsumer && m_boxVolume ) { SendTransform( m_lastSentTransform ); } diff --git a/trinity/Eve/EveVolumeObject.h b/trinity/Eve/EveVolumeObject.h index 49bea6989..b857be128 100644 --- a/trinity/Eve/EveVolumeObject.h +++ b/trinity/Eve/EveVolumeObject.h @@ -124,6 +124,8 @@ BLUE_CLASS( EveVolumeObject ) : /// Whoever the scene file attached to receive the shape. Not created here; the asset decides. ITr2VolumeObjectPtr m_consumer; + /// The consumer the shape was pushed to. Differs from m_consumer for one update after it is replaced or cleared. + ITr2VolumeObjectPtr m_pushedConsumer; EveBoxVolumePtr m_boxVolume; uint32_t m_boxChangeCallbackID; bool m_warnedAboutVolumes; From 14264f17b459d79bb5ec26940215dcc2d9fe1a0e Mon Sep 17 00:00:00 2001 From: Phevos Rinis Date: Fri, 9 Oct 2026 08:24:40 +0000 Subject: [PATCH 05/15] clean up EveVolumeObject includes and comments --- trinity/Eve/EveVolumeObject.cpp | 25 +++++++------------------ trinity/Eve/EveVolumeObject.h | 4 +--- 2 files changed, 8 insertions(+), 21 deletions(-) diff --git a/trinity/Eve/EveVolumeObject.cpp b/trinity/Eve/EveVolumeObject.cpp index e6ebcf69d..d4a2e78c0 100644 --- a/trinity/Eve/EveVolumeObject.cpp +++ b/trinity/Eve/EveVolumeObject.cpp @@ -2,6 +2,7 @@ #include "StdAfx.h" #include "EveVolumeObject.h" +#include "Eve/Volume/EveBoxVolume.h" #include @@ -47,11 +48,8 @@ EveVolumeObject::~EveVolumeObject() m_boxVolume->UnregisterForChanges( m_boxChangeCallbackID ); } - // Destroying a scene does not unregister its objects, so take the shape back here too. - if( m_pushedConsumer ) - { - m_pushedConsumer->Remove(); - } + // scenes don't unregister their objects when they are destroyed + RemoveFromConsumer(); } void EveVolumeObject::RebuildBoundingSphere() @@ -116,7 +114,6 @@ void EveVolumeObject::UpdateWorldTransform( Be::Time time ) void EveVolumeObject::ResolveBoxVolume() { - // The box may be replaced or removed while editing; drop it when it is no longer in the list. if( m_boxVolume ) { bool stillListed = false; @@ -139,7 +136,6 @@ void EveVolumeObject::ResolveBoxVolume() m_boxVolume = nullptr; m_warnedAboutVolumes = false; m_transformDirty = true; - // The consumer still holds the old box; nothing is sent until a new box is picked. RemoveFromConsumer(); } } @@ -149,7 +145,6 @@ void EveVolumeObject::ResolveBoxVolume() return; } - // The consumer takes exactly one shape, so the first box volume defines it. for( const auto& volume : m_volumes ) { if( EveBoxVolumePtr box = BlueCastPtr( volume ) ) @@ -173,7 +168,6 @@ void EveVolumeObject::ResolveBoxVolume() if( m_boxVolume ) { - // Fires when the box is edited (e.g. in Graphite); the next update re-sends the transform. m_boxChangeCallbackID = m_boxVolume->RegisterForChanges( [this]() { m_transformDirty = true; @@ -200,12 +194,11 @@ void EveVolumeObject::RemoveFromConsumer() void EveVolumeObject::PushToConsumer() { - // The consumer was replaced or cleared (e.g. in Graphite): take the shape back from the old one. if( m_pushedConsumer.p != m_consumer.p ) { RemoveFromConsumer(); m_pushedConsumer = m_consumer; - // Whatever state the new consumer is in, the enabled state goes out before the first shape. + // make sure a new consumer gets the enabled state m_lastSentEnabled = !m_enabled; } @@ -214,16 +207,14 @@ void EveVolumeObject::PushToConsumer() return; } - // Enabled state goes first: a consumer starts enabled, so a disabled object must say so before it - // hands over a shape, otherwise the consumer would activate for one frame and deactivate again. + // a consumer starts enabled, so the enabled state goes before the shape if( m_enabled != m_lastSentEnabled ) { m_pushedConsumer->SetEnabled( m_enabled ); m_lastSentEnabled = m_enabled; } - // The box lives in this object's space; compose with the world transform, as EveBoxVolume::RenderDebugInfo does. - // The world transform is relative to the ego ball, so this changes every frame the player moves. + // relative to the ego ball, so this changes whenever the player moves const Matrix unitBoxToWorld = m_boxVolume->GetBoxTransform() * m_worldTransform; if( !m_hasSentTransform || m_transformDirty || !TransformsNearlyEqual( unitBoxToWorld, m_lastSentTransform ) ) { @@ -331,8 +322,7 @@ void EveVolumeObject::RenderDebugInfo( ITr2DebugRenderer2& renderer ) // EveEntity void EveVolumeObject::RegisterComponents() { - // Unregistered and registered again in one go (ReregisterEntities): put the same shape straight back - // instead of leaving the consumer empty until the next update. A first registration waits for the update. + // ReregisterEntities: put the shape straight back if( m_resendOnRegister && m_pushedConsumer && m_boxVolume ) { SendTransform( m_lastSentTransform ); @@ -342,7 +332,6 @@ void EveVolumeObject::RegisterComponents() void EveVolumeObject::UnRegisterComponents() { - // Left the scene: nothing updates the shape any more, so the consumer must not keep it. m_resendOnRegister = m_hasSentTransform; RemoveFromConsumer(); } diff --git a/trinity/Eve/EveVolumeObject.h b/trinity/Eve/EveVolumeObject.h index b857be128..615bf8382 100644 --- a/trinity/Eve/EveVolumeObject.h +++ b/trinity/Eve/EveVolumeObject.h @@ -10,7 +10,6 @@ #include "EveEntity.h" #include "Tr2DebugRenderer.h" #include "Eve/Volume/IEveVolume.h" -#include "Eve/Volume/EveBoxVolume.h" #ifdef BLUE_USE_LOCAL_ITr2DebugRenderer2 // This is only needed for py2 as the file now belongs in blue. @@ -22,8 +21,7 @@ #include #include -BLUE_DECLARE_INTERFACE( IEveVolume ); -BLUE_DECLARE_IVECTOR( IEveVolume ); +BLUE_DECLARE( EveBoxVolume ); BLUE_DECLARE_INTERFACE( ITr2VolumeObject ); BLUE_DECLARE( EveVolumeObject ); From e4f32d83075e69b4c4e0d3a366eaeedcc071b857 Mon Sep 17 00:00:00 2001 From: Phevos Rinis Date: Fri, 9 Oct 2026 09:41:09 +0000 Subject: [PATCH 06/15] make TransformsNearlyEqual a private static member --- trinity/Eve/EveVolumeObject.cpp | 33 +++++++++++---------------------- trinity/Eve/EveVolumeObject.h | 21 ++++++++++++--------- 2 files changed, 23 insertions(+), 31 deletions(-) diff --git a/trinity/Eve/EveVolumeObject.cpp b/trinity/Eve/EveVolumeObject.cpp index d4a2e78c0..d5e681118 100644 --- a/trinity/Eve/EveVolumeObject.cpp +++ b/trinity/Eve/EveVolumeObject.cpp @@ -4,28 +4,6 @@ #include "EveVolumeObject.h" #include "Eve/Volume/EveBoxVolume.h" -#include - -namespace -{ - bool NearlyEqual( float a, float b, float epsilon ) - { - return std::fabs( a - b ) <= epsilon; - } - - /// True when two transforms are close enough that a consumer would not notice the difference. - bool TransformsNearlyEqual( const Matrix& a, const Matrix& b ) - { - constexpr float axisEpsilon = 1e-4f; // rotation and scale, in unit-box space - constexpr float translationEpsilon = 1e-2f; // 1 cm - - return NearlyEqual( a._11, b._11, axisEpsilon ) && NearlyEqual( a._12, b._12, axisEpsilon ) && NearlyEqual( a._13, b._13, axisEpsilon ) - && NearlyEqual( a._21, b._21, axisEpsilon ) && NearlyEqual( a._22, b._22, axisEpsilon ) && NearlyEqual( a._23, b._23, axisEpsilon ) - && NearlyEqual( a._31, b._31, axisEpsilon ) && NearlyEqual( a._32, b._32, axisEpsilon ) && NearlyEqual( a._33, b._33, axisEpsilon ) - && NearlyEqual( a._41, b._41, translationEpsilon ) && NearlyEqual( a._42, b._42, translationEpsilon ) && NearlyEqual( a._43, b._43, translationEpsilon ); - } -} - EveVolumeObject::EveVolumeObject( IRoot* lockobj ) : EveEntity( lockobj ), PARENTLOCK( m_volumes ), @@ -192,6 +170,17 @@ void EveVolumeObject::RemoveFromConsumer() 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_pushedConsumer.p != m_consumer.p ) diff --git a/trinity/Eve/EveVolumeObject.h b/trinity/Eve/EveVolumeObject.h index 615bf8382..886ce466b 100644 --- a/trinity/Eve/EveVolumeObject.h +++ b/trinity/Eve/EveVolumeObject.h @@ -12,7 +12,7 @@ #include "Eve/Volume/IEveVolume.h" #ifdef BLUE_USE_LOCAL_ITr2DebugRenderer2 -// This is only needed for py2 as the file now belongs in blue. + #include "Include/ITr2DebugRenderer2.h" #else #include @@ -29,14 +29,12 @@ BLUE_DECLARE( EveVolumeObject ); * @class EveVolumeObject * @brief A standalone scene object that places a volume in the world and publishes its shape. * - * This is a trigger volume without the triggering: it owns a box volume and a destiny-ball driven - * transform, and pushes the unit-box world transform to whatever consumer the scene file attached - * to it. The object itself knows nothing about what the consumer does with the shape. * - * The shape is pushed every frame it changes, because 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 + * 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, @@ -110,6 +108,11 @@ BLUE_CLASS( EveVolumeObject ) : */ 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; PIEveVolumeVector m_volumes; @@ -120,9 +123,9 @@ BLUE_CLASS( EveVolumeObject ) : CcpMath::Sphere m_boundingSphere; bool m_enabled; - /// Whoever the scene file attached to receive the shape. Not created here; the asset decides. + /// Whoever the scene file attached to receive the shape. ITr2VolumeObjectPtr m_consumer; - /// The consumer the shape was pushed to. Differs from m_consumer for one update after it is replaced or cleared. + /// The consumer the shape was pushed to. ITr2VolumeObjectPtr m_pushedConsumer; EveBoxVolumePtr m_boxVolume; uint32_t m_boxChangeCallbackID; @@ -132,7 +135,7 @@ BLUE_CLASS( EveVolumeObject ) : bool m_hasSentTransform; bool m_lastSentEnabled; bool m_transformDirty; - /// Set when leaving a scene with a shape out, so registering again (e.g. ReregisterEntities) puts it straight back. + /// Set when leaving a scene with a shape out, so registering again. bool m_resendOnRegister; }; From 8b7aae962e34aaced04cc0c4e135eef810074131 Mon Sep 17 00:00:00 2001 From: Phevos Rinis <160875214+phevosccp@users.noreply.github.com> Date: Mon, 12 Oct 2026 09:28:38 +0000 Subject: [PATCH 07/15] Apply batched suggestions from code review Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- trinity/Eve/EveVolumeObject.cpp | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/trinity/Eve/EveVolumeObject.cpp b/trinity/Eve/EveVolumeObject.cpp index d5e681118..76f491b51 100644 --- a/trinity/Eve/EveVolumeObject.cpp +++ b/trinity/Eve/EveVolumeObject.cpp @@ -146,8 +146,7 @@ void EveVolumeObject::ResolveBoxVolume() if( m_boxVolume ) { - m_boxChangeCallbackID = m_boxVolume->RegisterForChanges( [this]() - { + m_boxChangeCallbackID = m_boxVolume->RegisterForChanges( [this]() { m_transformDirty = true; } ); } @@ -175,10 +174,7 @@ 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 ); + 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() From 566c5fa276b4b87f7023930ccec42a021f7f7010 Mon Sep 17 00:00:00 2001 From: Phevos Rinis <160875214+phevosccp@users.noreply.github.com> Date: Mon, 12 Oct 2026 09:29:01 +0000 Subject: [PATCH 08/15] Apply suggestion from @github-actions[bot] Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- trinity/Eve/Volume/EveBoxVolume.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/trinity/Eve/Volume/EveBoxVolume.h b/trinity/Eve/Volume/EveBoxVolume.h index 47900839c..6073bb12b 100644 --- a/trinity/Eve/Volume/EveBoxVolume.h +++ b/trinity/Eve/Volume/EveBoxVolume.h @@ -37,7 +37,10 @@ BLUE_CLASS( EveBoxVolume ) : 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; } + const Matrix& GetBoxTransform() const + { + return m_boxTransform; + } private: void Setup(); From 657efb1e85b9e36b3cf1bbccce5ca8f52da235fd Mon Sep 17 00:00:00 2001 From: Phevos Rinis <160875214+phevosccp@users.noreply.github.com> Date: Mon, 12 Oct 2026 09:29:42 +0000 Subject: [PATCH 09/15] Update trinity/Eve/Volume/EveBoxVolume.h Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- trinity/Eve/Volume/EveBoxVolume.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/trinity/Eve/Volume/EveBoxVolume.h b/trinity/Eve/Volume/EveBoxVolume.h index 6073bb12b..e476ff3a6 100644 --- a/trinity/Eve/Volume/EveBoxVolume.h +++ b/trinity/Eve/Volume/EveBoxVolume.h @@ -41,6 +41,9 @@ BLUE_CLASS( EveBoxVolume ) : { return m_boxTransform; } + { + return m_boxTransform; + } private: void Setup(); From 5cfd79d40055874c56f049236b282c45077548b0 Mon Sep 17 00:00:00 2001 From: Phevos Rinis Date: Mon, 12 Oct 2026 09:42:24 +0000 Subject: [PATCH 10/15] rename ITr2VolumeObject to ITr2BoxVolume --- trinity/Eve/EveVolumeObject.h | 8 ++++---- trinity/Eve/EveVolumeObject_Blue.cpp | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/trinity/Eve/EveVolumeObject.h b/trinity/Eve/EveVolumeObject.h index 886ce466b..72c2d8c40 100644 --- a/trinity/Eve/EveVolumeObject.h +++ b/trinity/Eve/EveVolumeObject.h @@ -19,10 +19,10 @@ #endif #include -#include +#include BLUE_DECLARE( EveBoxVolume ); -BLUE_DECLARE_INTERFACE( ITr2VolumeObject ); +BLUE_DECLARE_INTERFACE( ITr2BoxVolume ); BLUE_DECLARE( EveVolumeObject ); /** @@ -124,9 +124,9 @@ BLUE_CLASS( EveVolumeObject ) : bool m_enabled; /// Whoever the scene file attached to receive the shape. - ITr2VolumeObjectPtr m_consumer; + ITr2BoxVolumePtr m_consumer; /// The consumer the shape was pushed to. - ITr2VolumeObjectPtr m_pushedConsumer; + ITr2BoxVolumePtr m_pushedConsumer; EveBoxVolumePtr m_boxVolume; uint32_t m_boxChangeCallbackID; bool m_warnedAboutVolumes; diff --git a/trinity/Eve/EveVolumeObject_Blue.cpp b/trinity/Eve/EveVolumeObject_Blue.cpp index ef2eaa6e6..09e3c59cf 100644 --- a/trinity/Eve/EveVolumeObject_Blue.cpp +++ b/trinity/Eve/EveVolumeObject_Blue.cpp @@ -4,7 +4,7 @@ #include "EveVolumeObject.h" BLUE_DEFINE( EveVolumeObject ); -BLUE_DEFINE_INTERFACE( ITr2VolumeObject ); +BLUE_DEFINE_INTERFACE( ITr2BoxVolume ); const Be::ClassInfo* EveVolumeObject::ExposeToBlue() { From 408624250f2635b50f8e6ab95cf125e416800a03 Mon Sep 17 00:00:00 2001 From: Phevos Rinis Date: Mon, 12 Oct 2026 14:50:05 +0000 Subject: [PATCH 11/15] remove duplicated GetBoxTransform body --- trinity/Eve/Volume/EveBoxVolume.h | 3 --- 1 file changed, 3 deletions(-) diff --git a/trinity/Eve/Volume/EveBoxVolume.h b/trinity/Eve/Volume/EveBoxVolume.h index e476ff3a6..6073bb12b 100644 --- a/trinity/Eve/Volume/EveBoxVolume.h +++ b/trinity/Eve/Volume/EveBoxVolume.h @@ -41,9 +41,6 @@ BLUE_CLASS( EveBoxVolume ) : { return m_boxTransform; } - { - return m_boxTransform; - } private: void Setup(); From 69fae51f4ed915730b4c318287414e1ac59f982b Mon Sep 17 00:00:00 2001 From: Phevos Rinis Date: Mon, 12 Oct 2026 15:37:43 +0000 Subject: [PATCH 12/15] resolve the box and push through the normal path on register --- trinity/Eve/EveVolumeObject.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/trinity/Eve/EveVolumeObject.cpp b/trinity/Eve/EveVolumeObject.cpp index 76f491b51..b7aa4d948 100644 --- a/trinity/Eve/EveVolumeObject.cpp +++ b/trinity/Eve/EveVolumeObject.cpp @@ -308,9 +308,10 @@ void EveVolumeObject::RenderDebugInfo( ITr2DebugRenderer2& renderer ) void EveVolumeObject::RegisterComponents() { // ReregisterEntities: put the shape straight back - if( m_resendOnRegister && m_pushedConsumer && m_boxVolume ) + if( m_resendOnRegister ) { - SendTransform( m_lastSentTransform ); + ResolveBoxVolume(); + PushToConsumer(); } m_resendOnRegister = false; } From 41a719a8fd7377744b43c9a05e5d0078f1cb1327 Mon Sep 17 00:00:00 2001 From: Phevos Rinis Date: Tue, 13 Oct 2026 10:58:02 +0000 Subject: [PATCH 13/15] implement requested changes --- trinity/Eve/EveVolumeObject.cpp | 148 +++++---------------------- trinity/Eve/EveVolumeObject.h | 23 ++--- trinity/Eve/EveVolumeObject_Blue.cpp | 16 +-- 3 files changed, 44 insertions(+), 143 deletions(-) diff --git a/trinity/Eve/EveVolumeObject.cpp b/trinity/Eve/EveVolumeObject.cpp index b7aa4d948..52bd22dcb 100644 --- a/trinity/Eve/EveVolumeObject.cpp +++ b/trinity/Eve/EveVolumeObject.cpp @@ -6,62 +6,40 @@ EveVolumeObject::EveVolumeObject( IRoot* lockobj ) : EveEntity( lockobj ), - PARENTLOCK( m_volumes ), m_worldTransform( IdentityMatrix() ), m_enabled( true ), - m_boxChangeCallbackID( 0 ), - m_warnedAboutVolumes( false ), m_lastSentTransform( IdentityMatrix() ), m_hasSentTransform( false ), m_lastSentEnabled( true ), - m_transformDirty( false ), m_resendOnRegister( false ) { } EveVolumeObject::~EveVolumeObject() { - if( m_boxVolume && m_boxChangeCallbackID != 0 ) - { - m_boxVolume->UnregisterForChanges( m_boxChangeCallbackID ); - } - // scenes don't unregister their objects when they are destroyed RemoveFromConsumer(); } -void EveVolumeObject::RebuildBoundingSphere() +ITr2BoxVolumePtr EveVolumeObject::GetConsumer() const { - TRINITY_STATS_ZONE( __FUNCTION__ ); - - m_boundingSphere = CcpMath::Sphere(); + return m_consumer; +} - for( const auto& volume : m_volumes ) +void EveVolumeObject::SetConsumer( ITr2BoxVolume* consumer ) +{ + if( m_consumer.p == consumer ) { - auto volumeSphere = volume->GetBoundingSphere(); - - if( !volumeSphere.IsInitialized() ) - { - continue; - } - - if( !m_boundingSphere.IsInitialized() || volumeSphere.IsSphereInside( m_boundingSphere ) ) - { - m_boundingSphere = volumeSphere; - continue; - } - - if( m_boundingSphere.IsSphereInside( volumeSphere ) ) - { - continue; - } + return; + } - Vector3 delta = volumeSphere.center - m_boundingSphere.center; - float deltaLen = Length( delta ); + RemoveFromConsumer(); + m_consumer = consumer; +} - m_boundingSphere.center += 0.5f * ( 1.f + ( volumeSphere.radius - m_boundingSphere.radius ) / deltaLen ) * delta; - m_boundingSphere.radius = 0.5f * ( m_boundingSphere.radius + volumeSphere.radius + deltaLen ); - } +void EveVolumeObject::RebuildBoundingSphere() +{ + m_boundingSphere = m_boxVolume ? m_boxVolume->GetBoundingSphere() : CcpMath::Sphere(); } void EveVolumeObject::UpdateWorldTransform( Be::Time time ) @@ -90,81 +68,18 @@ void EveVolumeObject::UpdateWorldTransform( Be::Time time ) m_worldTransform = RotationMatrix( rotation ) * TranslationMatrix( translation ); } -void EveVolumeObject::ResolveBoxVolume() -{ - if( m_boxVolume ) - { - bool stillListed = false; - for( const auto& volume : m_volumes ) - { - if( volume == static_cast( m_boxVolume.p ) ) - { - stillListed = true; - break; - } - } - - if( !stillListed ) - { - if( m_boxChangeCallbackID != 0 ) - { - m_boxVolume->UnregisterForChanges( m_boxChangeCallbackID ); - m_boxChangeCallbackID = 0; - } - m_boxVolume = nullptr; - m_warnedAboutVolumes = false; - m_transformDirty = true; - RemoveFromConsumer(); - } - } - - if( m_boxVolume || m_volumes.empty() ) - { - return; - } - - for( const auto& volume : m_volumes ) - { - if( EveBoxVolumePtr box = BlueCastPtr( volume ) ) - { - if( !m_boxVolume ) - { - m_boxVolume = box; - } - else if( !m_warnedAboutVolumes ) - { - CCP_LOGWARN( "EveVolumeObject '%s': only the first box volume is used, extra volumes are ignored.", m_name.c_str() ); - m_warnedAboutVolumes = true; - } - } - else if( !m_warnedAboutVolumes ) - { - CCP_LOGWARN( "EveVolumeObject '%s': only box volumes are supported, other volume types are ignored.", m_name.c_str() ); - m_warnedAboutVolumes = true; - } - } - - if( m_boxVolume ) - { - m_boxChangeCallbackID = m_boxVolume->RegisterForChanges( [this]() { - m_transformDirty = true; - } ); - } -} - void EveVolumeObject::SendTransform( const Matrix& unitBoxToWorld ) { - m_pushedConsumer->SetTransform( unitBoxToWorld ); + m_consumer->SetTransform( unitBoxToWorld ); m_lastSentTransform = unitBoxToWorld; m_hasSentTransform = true; - m_transformDirty = false; } void EveVolumeObject::RemoveFromConsumer() { - if( m_pushedConsumer && m_hasSentTransform ) + if( m_consumer && m_hasSentTransform ) { - m_pushedConsumer->Remove(); + m_consumer->Remove(); } m_hasSentTransform = false; } @@ -179,29 +94,27 @@ bool EveVolumeObject::TransformsNearlyEqual( const Matrix& a, const Matrix& b ) void EveVolumeObject::PushToConsumer() { - if( m_pushedConsumer.p != m_consumer.p ) + if( !m_consumer ) { - RemoveFromConsumer(); - m_pushedConsumer = m_consumer; - // make sure a new consumer gets the enabled state - m_lastSentEnabled = !m_enabled; + return; } - if( !m_pushedConsumer || !m_boxVolume ) + if( !m_boxVolume ) { + RemoveFromConsumer(); return; } // a consumer starts enabled, so the enabled state goes before the shape - if( m_enabled != m_lastSentEnabled ) + if( !m_hasSentTransform || m_enabled != m_lastSentEnabled ) { - m_pushedConsumer->SetEnabled( m_enabled ); + 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 || m_transformDirty || !TransformsNearlyEqual( unitBoxToWorld, m_lastSentTransform ) ) + if( !m_hasSentTransform || !TransformsNearlyEqual( unitBoxToWorld, m_lastSentTransform ) ) { SendTransform( unitBoxToWorld ); } @@ -216,8 +129,6 @@ void EveVolumeObject::UpdateSyncronous( const EveUpdateContext& updateContext ) RebuildBoundingSphere(); - ResolveBoxVolume(); - PushToConsumer(); } @@ -253,7 +164,7 @@ void EveVolumeObject::GetModelCenterWorldPosition( Vector3& position ) const bool EveVolumeObject::GetLocalBoundingBox( Vector3& min, Vector3& max ) { - // Fall back to a unit box when no volumes are set up yet, so the object stays pickable in Graphite. + // 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 ); @@ -281,7 +192,6 @@ bool EveVolumeObject::Initialize() { UpdateWorldTransform( Be::Time( 0.0 ) ); RebuildBoundingSphere(); - ResolveBoxVolume(); return true; } @@ -292,15 +202,12 @@ void EveVolumeObject::GetDebugOptions( Tr2DebugRendererOptions& options ) void EveVolumeObject::RenderDebugInfo( ITr2DebugRenderer2& renderer ) { - if( renderer.HasOption( GetRawRoot(), "Volume Objects" ) ) + if( m_boxVolume && renderer.HasOption( GetRawRoot(), "Volume Objects" ) ) { // cyan when active, grey when disabled const Color color = m_enabled ? 0xFF33DDFF : 0xFF666666; - for( const auto& volume : m_volumes ) - { - volume->RenderDebugInfo( renderer, m_worldTransform, color ); - } + m_boxVolume->RenderDebugInfo( renderer, m_worldTransform, color ); } } @@ -310,7 +217,6 @@ void EveVolumeObject::RegisterComponents() // ReregisterEntities: put the shape straight back if( m_resendOnRegister ) { - ResolveBoxVolume(); PushToConsumer(); } m_resendOnRegister = false; diff --git a/trinity/Eve/EveVolumeObject.h b/trinity/Eve/EveVolumeObject.h index 72c2d8c40..5467b7e1c 100644 --- a/trinity/Eve/EveVolumeObject.h +++ b/trinity/Eve/EveVolumeObject.h @@ -9,7 +9,6 @@ #include "IEveSpaceObject2.h" #include "EveEntity.h" #include "Tr2DebugRenderer.h" -#include "Eve/Volume/IEveVolume.h" #ifdef BLUE_USE_LOCAL_ITr2DebugRenderer2 @@ -78,20 +77,22 @@ BLUE_CLASS( EveVolumeObject ) : void UnRegisterComponents() override; private: + ITr2BoxVolumePtr GetConsumer() const; + /** - * @brief Recomputes the broad-phase bounding sphere from the volume list. + * @brief Takes the shape back from the current consumer before switching to the new one. */ - void RebuildBoundingSphere(); + void SetConsumer( ITr2BoxVolume * consumer ); /** - * @brief Rebuilds the world transform from the position and rotation curves. + * @brief Recomputes the broad-phase bounding sphere from the box volume. */ - void UpdateWorldTransform( Be::Time time ); + void RebuildBoundingSphere(); /** - * @brief Picks the box volume that defines the shape and listens for edits to it. + * @brief Rebuilds the world transform from the position and rotation curves. */ - void ResolveBoxVolume(); + void UpdateWorldTransform( Be::Time time ); /** * @brief Pushes the enabled state and, when it changed, the unit box world transform to the consumer. @@ -114,7 +115,7 @@ BLUE_CLASS( EveVolumeObject ) : static bool TransformsNearlyEqual( const Matrix& a, const Matrix& b ); std::string m_name; - PIEveVolumeVector m_volumes; + EveBoxVolumePtr m_boxVolume; ITriVectorFunctionPtr m_ballPosition; ITriQuaternionFunctionPtr m_ballRotation; @@ -125,16 +126,10 @@ BLUE_CLASS( EveVolumeObject ) : /// Whoever the scene file attached to receive the shape. ITr2BoxVolumePtr m_consumer; - /// The consumer the shape was pushed to. - ITr2BoxVolumePtr m_pushedConsumer; - EveBoxVolumePtr m_boxVolume; - uint32_t m_boxChangeCallbackID; - bool m_warnedAboutVolumes; Matrix m_lastSentTransform; bool m_hasSentTransform; bool m_lastSentEnabled; - bool m_transformDirty; /// Set when leaving a scene with a shape out, so registering again. bool m_resendOnRegister; }; diff --git a/trinity/Eve/EveVolumeObject_Blue.cpp b/trinity/Eve/EveVolumeObject_Blue.cpp index 09e3c59cf..30d359c3f 100644 --- a/trinity/Eve/EveVolumeObject_Blue.cpp +++ b/trinity/Eve/EveVolumeObject_Blue.cpp @@ -22,16 +22,16 @@ const Be::ClassInfo* EveVolumeObject::ExposeToBlue() Be::READWRITE | Be::PERSIST ) MAP_ATTRIBUTE( - "volumes", - m_volumes, - "The volumes defining the shape. Only the first box volume is used.", - Be::READ | Be::PERSIST ) + "boxVolume", + m_boxVolume, + "The box volume defining the shape", + Be::READWRITE | Be::PERSIST ) - MAP_ATTRIBUTE( + MAP_PROPERTY_PERSISTED( "consumer", - m_consumer, - "The object that receives the shape of this volume, attached by the scene file", - Be::READWRITE | Be::PERSIST ) + GetConsumer, + SetConsumer, + "The object that receives the shape of this volume, attached by the scene file" ) MAP_ATTRIBUTE( "enabled", From e27e69d4764e1eaa514c9fc040434880666bacf5 Mon Sep 17 00:00:00 2001 From: Phevos Rinis Date: Tue, 13 Oct 2026 11:59:32 +0000 Subject: [PATCH 14/15] bump carbon-trinityaudioapi to 3.1.0 --- vcpkg.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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", From dde018f02b5aa8ef924f391d71fed3829ad50913 Mon Sep 17 00:00:00 2001 From: Phevos Rinis Date: Wed, 14 Oct 2026 13:02:07 +0000 Subject: [PATCH 15/15] persist consumer through a PERSISTONLY attribute --- trinity/Eve/EveVolumeObject_Blue.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/trinity/Eve/EveVolumeObject_Blue.cpp b/trinity/Eve/EveVolumeObject_Blue.cpp index 30d359c3f..d914dd30d 100644 --- a/trinity/Eve/EveVolumeObject_Blue.cpp +++ b/trinity/Eve/EveVolumeObject_Blue.cpp @@ -27,7 +27,12 @@ const Be::ClassInfo* EveVolumeObject::ExposeToBlue() "The box volume defining the shape", Be::READWRITE | Be::PERSIST ) - MAP_PROPERTY_PERSISTED( + MAP_ATTRIBUTE( + "consumer", + m_consumer, + "", + Be::PERSISTONLY ) + MAP_PROPERTY( "consumer", GetConsumer, SetConsumer,