Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions trinity/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,9 @@ set(_SOURCES
Eve/AudioGameObject.cpp
Eve/AudioGameObject.h
Eve/AudioGameObject_Blue.cpp
Eve/EveTriggerVolume.cpp
Eve/EveTriggerVolume.h
Eve/EveTriggerVolume_Blue.cpp
Eve/EveEffectRoot2.cpp
Eve/EveEffectRoot2.h
Eve/EveEffectRoot2_Blue.cpp
Expand Down
1 change: 1 addition & 0 deletions trinity/Controllers/Actions/Tr2ActionBindRTPC.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ void Tr2ActionBindRTPC::StartWithController( ITr2ActionController* controller )
void Tr2ActionBindRTPC::Stop( ITr2ActionController& controller )
{
controller.UnRegisterUpdateable( *this );
m_emitter = nullptr;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is this related to the PR? I vaguely remember this change as fixing a memory leak. Should it be in a separate PR?

}

void Tr2ActionBindRTPC::StopWithController( ITr2ActionController* controller )
Expand Down
292 changes: 292 additions & 0 deletions trinity/Eve/EveTriggerVolume.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
// Copyright © 2026 CCP ehf.

#include "StdAfx.h"
#include "EveTriggerVolume.h"
#include "TriDevice.h"

namespace
{
void InvokeTriggerCallback( void* context, bool entered )
{
EveTriggerVolume* triggerVolume = reinterpret_cast<EveTriggerVolume*>( context );

triggerVolume->InvokeCallback( entered );
triggerVolume->GetRawRoot()->Unlock();
}

void TriggerEnterCallback( void* context )
{
InvokeTriggerCallback( context, true );
}

void TriggerExitCallback( void* context )
{
InvokeTriggerCallback( context, false );
}
}

EveTriggerVolume::EveTriggerVolume( IRoot* lockobj ) :
PARENTLOCK( m_volumes ),
PARENTLOCK( m_exclusionVolumes ),
PARENTLOCK( m_externalParameters ),
m_rotation( 0.0f, 0.0f, 0.0f, 1.0f ),
m_translation( 0.0f, 0.0f, 0.0f ),
m_worldTransform( IdentityMatrix() ),
m_enterThreshold( 0.5f ),
m_isInside( false ),
m_currentIntensity( 0.0f )
{
}

EveTriggerVolume::~EveTriggerVolume()
{
}

void EveTriggerVolume::RebuildBoundingSphere()
{
CCP_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 EveTriggerVolume::SetCallback( const BlueScriptCallback& callback )
{
m_callback = callback;
}

void EveTriggerVolume::InvokeCallback( bool entered )
{
BlueScriptCallback callback = m_callback;
if( !callback )
{
return;
}

callback.CallVoid( m_name.c_str(), entered ).ReportException();
}

void EveTriggerVolume::QueueCallback( bool entered )
{
if( !m_callback )
{
return;
}
// Keeps the object alive until the queued callback runs.
GetRawRoot()->Lock();

if( entered )
{
gTriDev->AddPostUpdateCallback( TriggerEnterCallback, reinterpret_cast<void*>( this ) );
}
else
{
gTriDev->AddPostUpdateCallback( TriggerExitCallback, reinterpret_cast<void*>( this ) );
}
}

void EveTriggerVolume::UpdateWorldTransform( Be::Time time )
{
Vector3 translation;

if( m_ballPosition )
{
m_ballPosition->Update( &translation, time );
}
else
{
translation = m_translation;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't it awkward that m_translation is only used until the volume is attached to the ball?

}

m_worldTransform = RotationMatrix( m_rotation ) * TranslationMatrix( translation );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you not taking ball's rotation into account?

}

// IEveSpaceObject2
void EveTriggerVolume::UpdateSyncronous( const EveUpdateContext& updateContext )
{
CCP_STATS_ZONE( __FUNCTION__ );

UpdateWorldTransform( updateContext.GetTime() );

RebuildBoundingSphere();

UpdateTriggerState( updateContext );
}

float EveTriggerVolume::GetMaxIntensity( const PIEveVolumeVector& volumes, const Vector3& position )
{
float intensity = 0.0f;
for( const auto& volume : volumes )
{
intensity = std::max( intensity, volume->GetIntensity( position ) );
if( intensity == 1.0f )
{
// early exit
break;
}
}
return intensity;
}

void EveTriggerVolume::UpdateTriggerState( const EveUpdateContext& updateContext )
{
m_currentIntensity = 0.0f;

bool inside = false;
if( m_trackedPosition && !m_volumes.empty() )
{
Vector3 trackedPosition;
m_trackedPosition->Update( &trackedPosition, updateContext.GetTime() );

Matrix inverseWorldTransform = Inverse( m_worldTransform );
Vector3 positionInObjectSpace = Transform( trackedPosition, inverseWorldTransform ).GetXYZ();

// check first if the tracked position is within the bounding sphere
if( m_boundingSphere.IsPointInside( positionInObjectSpace ) )
{
m_currentIntensity = GetMaxIntensity( m_volumes, positionInObjectSpace );

if( m_currentIntensity != 0.0f )
{
// check if the tracked position is within an exclusion volume
float negativeIntensity = GetMaxIntensity( m_exclusionVolumes, positionInObjectSpace );
m_currentIntensity = std::max( 0.0f, m_currentIntensity - negativeIntensity );
}
}

inside = m_currentIntensity >= m_enterThreshold;
}

if( inside != m_isInside )
{
m_isInside = inside;
QueueCallback( inside );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we discussed before, I don't see the reason to queue these callbacks: UpdateTriggerState is only called from UpdateSyncronous, always on the main thread. It should be safe to call Python directly in this context, or am I missing something?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I completely forgot to address this one after our discussion.

}
}

void EveTriggerVolume::UpdateAsyncronous( const EveUpdateContext& updateContext )
{
}

void EveTriggerVolume::UpdateVisibility( const EveUpdateContext& updateContext, const Matrix& parentTransform )
{
}

void EveTriggerVolume::GetRenderables( std::vector<ITr2Renderable*>& renderables, Tr2ImpostorManager* impostors )
{
}

bool EveTriggerVolume::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 EveTriggerVolume::UpdateModelCenterWorldPosition( Vector3& position, Be::Time t )
{
UpdateWorldTransform( t );
GetModelCenterWorldPosition( position );
}

void EveTriggerVolume::GetModelCenterWorldPosition( Vector3& position ) const
{
position = Transform( m_boundingSphere.center, m_worldTransform ).GetXYZ();
}

bool EveTriggerVolume::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 EveTriggerVolume::GetLocalToWorldTransform( Matrix& transform ) const
{
transform = m_worldTransform;
}

Vector3 EveTriggerVolume::GetWorldPosition()
{
return m_worldTransform.GetTranslation();
}

Quaternion EveTriggerVolume::GetWorldRotation()
{
return Normalize( RotationQuaternion( m_worldTransform ) );
}

bool EveTriggerVolume::Initialize()
{
UpdateWorldTransform( Be::Time( 0.0 ) );
RebuildBoundingSphere();
return true;
}

void EveTriggerVolume::GetDebugOptions( Tr2DebugRendererOptions& options )
{
options.insert( "Trigger Volumes" );
options.insert( "Trigger Exclusion Volumes" );
options.insert( "Trigger Bounding Sphere" );
}

void EveTriggerVolume::RenderDebugInfo( ITr2DebugRenderer2& renderer )
{
if( renderer.HasOption( GetRawRoot(), "Trigger Volumes" ) )
{
// green when the tracked position is inside, white otherwise
Color color = 0xFFFFFFFF;
if( m_isInside )
{
color = 0xFF33FF33;
}

for( const auto& volume : m_volumes )
{
volume->RenderDebugInfo( renderer, m_worldTransform, color );
}
}

if( renderer.HasOption( GetRawRoot(), "Trigger Exclusion Volumes" ) )
{
for( const auto& volume : m_exclusionVolumes )
{
volume->RenderDebugInfo( renderer, m_worldTransform, 0xFFFF3333 );
}
}

if( renderer.HasOption( GetRawRoot(), "Trigger Bounding Sphere" ) )
{
renderer.DrawSphere( this, TranslationMatrix( m_boundingSphere.center ) * m_worldTransform, m_boundingSphere.radius, 10, Tr2DebugRenderer::Wireframe, 0xff333333 );
}
}
Loading