Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
4 changes: 4 additions & 0 deletions trinity/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ set(_SOURCES
Curves/Tr2CurveScalar.cpp
Curves/Tr2CurveScalar.h
Curves/Tr2CurveScalar_Blue.cpp
Curves/Tr2CurveSegmentSearch.h
Curves/Tr2CurveScalarExpression.cpp
Curves/Tr2CurveScalarExpression.h
Curves/Tr2CurveScalarExpression_Blue.cpp
Expand Down Expand Up @@ -236,8 +237,11 @@ set(_SOURCES
Curves/Tr2TranslationAdapter.cpp
Curves/Tr2TranslationAdapter.h
Curves/Tr2TranslationAdapter_Blue.cpp
Curves/TriCurveSampler.cpp
Curves/TriCurveSampler.h
Curves/TriCurveSet.cpp
Curves/TriCurveSet.h
Curves/TriCurveSetTime.h
Curves/TriCurveSet_Blue.cpp
Curves/TriEventCurve.cpp
Curves/TriEventCurve.h
Expand Down
73 changes: 17 additions & 56 deletions trinity/Curves/Tr2CurveQuaternion.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include "StdAfx.h"
#include "Tr2CurveQuaternion.h"
#include "Tr2CurveSegmentSearch.h"

extern Be::VarChooser Tr2CurveInterpolationChooser[];

Expand Down Expand Up @@ -35,34 +36,34 @@ Tr2CurveQuaternion::Tr2CurveQuaternion( IRoot* lockobj ) :
// --------------------------------------------------------------------------------
void Tr2CurveQuaternion::UpdateValue( double time )
{
m_currentValue = GetValue( time );
m_currentValue = SampleValue( time, m_lastSegment );
}

// --------------------------------------------------------------------------------
Quaternion* Tr2CurveQuaternion::Update( Quaternion* in, Be::Time time )
{
*in = m_currentValue = GetValue( TimeAsDouble( time ) );
*in = m_currentValue = SampleValue( TimeAsDouble( time ), m_lastSegment );
return in;
}

// --------------------------------------------------------------------------------
Quaternion* Tr2CurveQuaternion::Update( Quaternion* in, double time )
{
*in = m_currentValue = GetValue( time );
*in = m_currentValue = SampleValue( time, m_lastSegment );
return in;
}

// --------------------------------------------------------------------------------
Quaternion* Tr2CurveQuaternion::GetValueAt( Quaternion* in, Be::Time time )
{
*in = GetValue( TimeAsDouble( time ) );
*in = SampleValue( TimeAsDouble( time ), m_lastSegment );
return in;
}

// --------------------------------------------------------------------------------
Quaternion* Tr2CurveQuaternion::GetValueAt( Quaternion* in, double time )
{
*in = GetValue( time );
*in = SampleValue( time, m_lastSegment );
return in;
}

Expand Down Expand Up @@ -106,6 +107,12 @@ float Tr2CurveQuaternion::Length()

// --------------------------------------------------------------------------------
Quaternion Tr2CurveQuaternion::GetValue( double time ) const
{
return SampleValue( time, m_lastSegment );
}

// --------------------------------------------------------------------------------
Quaternion Tr2CurveQuaternion::SampleValue( double time, size_t& segmentCache ) const
{
if( m_keys.empty() )
{
Expand All @@ -130,59 +137,13 @@ Quaternion Tr2CurveQuaternion::GetValue( double time ) const
}

float t = GetLocalTime( time );

if( m_lastSegment + 1 < count )
{
// try cached last segment first
auto& k0 = m_keys[m_lastSegment];
auto& k1 = m_keys[m_lastSegment + 1];

if( t >= k0.m_time && t < k1.m_time )
{
return GetSegmentValue( t, k0, k1 );
}

if( m_lastSegment + 2 < count )
{
// try a segment to the right of the cached one
auto& k0 = m_keys[m_lastSegment + 1];
auto& k1 = m_keys[m_lastSegment + 2];

if( t >= k0.m_time && t < k1.m_time )
{
++m_lastSegment;
return GetSegmentValue( t, k0, k1 );
}
}

if( m_lastSegment > 1 )
{
// try a segment to the left of the cached one
auto& k0 = m_keys[m_lastSegment - 1];
auto& k1 = m_keys[m_lastSegment];

if( t >= k0.m_time && t < k1.m_time )
{
--m_lastSegment;
return GetSegmentValue( t, k0, k1 );
}
}
}

// cache miss: try all segments
for( size_t i = 0; i + 1 < count; ++i )
// Most frames land in the cached segment: check it here, without a call
size_t i = segmentCache;
if( i + 1 >= count || t < m_keys[i].m_time || t >= m_keys[i + 1].m_time )
{
auto& k0 = m_keys[i];
auto& k1 = m_keys[i + 1];

if( t >= k0.m_time && t < k1.m_time )
{
m_lastSegment = int32_t( i );
return GetSegmentValue( t, k0, k1 );
}
i = FindCurveSegment( m_keys, t, segmentCache );
}
m_lastSegment = count - 2;
return GetSegmentValue( t, m_keys[count - 2], m_keys[count - 1] );
return GetSegmentValue( t, m_keys[i], m_keys[i + 1] );
}

// --------------------------------------------------------------------------------
Expand Down
5 changes: 5 additions & 0 deletions trinity/Curves/Tr2CurveQuaternion.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ BLUE_CLASS( Tr2CurveQuaternion ) :
virtual float Length();

Quaternion GetValue( double time ) const;
// The value at time, like GetValue, but looking keys up from the caller's
// segment cache rather than the curve's own. For callers that sample the
// curve at times of their own (bindings with a time offset), so they and the
// curve's own UpdateValue don't keep moving each other's cache.
Quaternion SampleValue( double time, size_t& segmentCache ) const;
Quaternion GetCurrentValue() const;

void OnKeysChanged();
Expand Down
73 changes: 17 additions & 56 deletions trinity/Curves/Tr2CurveScalar.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include "StdAfx.h"
#include "Tr2CurveScalar.h"
#include "Tr2CurveSegmentSearch.h"

extern Be::VarChooser Tr2CurveInterpolationChooser[];
extern Be::VarChooser Tr2CurveTangentTypeChooser[];
Expand Down Expand Up @@ -96,25 +97,25 @@ void Tr2CurveScalar::SetName( const char* name )
// --------------------------------------------------------------------------------
void Tr2CurveScalar::UpdateValue( double time )
{
m_currentValue = GetValue( time );
m_currentValue = SampleValue( time, m_lastSegment );
}

// --------------------------------------------------------------------------------
float Tr2CurveScalar::Update( Be::Time time )
{
return m_currentValue = GetValue( TimeAsDouble( time ) );
return m_currentValue = SampleValue( TimeAsDouble( time ), m_lastSegment );
}

// --------------------------------------------------------------------------------
float Tr2CurveScalar::Update( double time )
{
return m_currentValue = GetValue( time );
return m_currentValue = SampleValue( time, m_lastSegment );
}

// --------------------------------------------------------------------------------
float Tr2CurveScalar::GetValueAt( Be::Time time )
{
return GetValue( TimeAsDouble( time ) );
return SampleValue( TimeAsDouble( time ), m_lastSegment );
}

// --------------------------------------------------------------------------------
Expand All @@ -126,11 +127,17 @@ void Tr2CurveScalar::ScaleTime( float s )
// --------------------------------------------------------------------------------
float Tr2CurveScalar::GetValueAt( double time )
{
return GetValue( time );
return SampleValue( time, m_lastSegment );
}

// --------------------------------------------------------------------------------
float Tr2CurveScalar::GetValue( double time ) const
{
return SampleValue( time, m_lastSegment );
}

// --------------------------------------------------------------------------------
float Tr2CurveScalar::SampleValue( double time, size_t& segmentCache ) const
{
if( m_keys.empty() )
{
Expand Down Expand Up @@ -169,59 +176,13 @@ float Tr2CurveScalar::GetValue( double time ) const
}

float t = GetLocalTime( time );

if( m_lastSegment + 1 < count )
// Most frames land in the cached segment: check it here, without a call
size_t i = segmentCache;
if( i + 1 >= count || t < m_keys[i].m_time || t >= m_keys[i + 1].m_time )
{
// try cached last segment first
auto& k0 = m_keys[m_lastSegment];
auto& k1 = m_keys[m_lastSegment + 1];

if( t >= k0.m_time && t < k1.m_time )
{
return GetSegmentValue( t, k0, k1 );
}

if( m_lastSegment + 2 < count )
{
// try a segment to the right of the cached one
auto& k0 = m_keys[m_lastSegment + 1];
auto& k1 = m_keys[m_lastSegment + 2];

if( t >= k0.m_time && t < k1.m_time )
{
++m_lastSegment;
return GetSegmentValue( t, k0, k1 );
}
}

if( m_lastSegment > 1 )
{
// try a segment to the left of the cached one
auto& k0 = m_keys[m_lastSegment - 1];
auto& k1 = m_keys[m_lastSegment];

if( t >= k0.m_time && t < k1.m_time )
{
--m_lastSegment;
return GetSegmentValue( t, k0, k1 );
}
}
}

// cache miss: try all segments
for( size_t i = 0; i + 1 < count; ++i )
{
auto& k0 = m_keys[i];
auto& k1 = m_keys[i + 1];

if( t >= k0.m_time && t < k1.m_time )
{
m_lastSegment = int32_t( i );
return GetSegmentValue( t, k0, k1 );
}
i = FindCurveSegment( m_keys, t, segmentCache );
}
m_lastSegment = count - 2;
return GetSegmentValue( t, m_keys[count - 2], m_keys[count - 1] );
return GetSegmentValue( t, m_keys[i], m_keys[i + 1] );
}

// --------------------------------------------------------------------------------
Expand Down
5 changes: 5 additions & 0 deletions trinity/Curves/Tr2CurveScalar.h
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@ BLUE_CLASS( Tr2CurveScalar ) :
void SetName( const char* name );

float GetValue( double time ) const;
// The value at time, like GetValue, but looking keys up from the caller's
// segment cache rather than the curve's own. For callers that sample the
// curve at times of their own (bindings with a time offset), so they and the
// curve's own UpdateValue don't keep moving each other's cache.
float SampleValue( double time, size_t& segmentCache ) const;
float GetTangent( double time ) const;
float GetCurrentValue() const;

Expand Down
62 changes: 62 additions & 0 deletions trinity/Curves/Tr2CurveSegmentSearch.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright © 2026 CCP ehf.

#pragma once
#ifndef Tr2CurveSegmentSearch_h
#define Tr2CurveSegmentSearch_h

#include <algorithm>

// Index i of the key segment [keys[i], keys[i + 1]) that contains t, for a curve
// with at least two keys. Falls back to the last segment when none contains t.
//
// cachedSegment is a hint, updated to the segment found: curves sampled at
// steadily moving times find their segment at or next to the cached one. Each
// caller sampling at its own times - a curve's own UpdateValue, or a binding
// with a time offset - should keep its own hint, or they keep moving each
// other's and every lookup has to search.
template <typename Keys>
size_t FindCurveSegment( const Keys& keys, float t, size_t& cachedSegment )
{
// This runs for every curve evaluation, so the cache checks are written out
// rather than going through helpers that unoptimised builds would call.
const size_t count = keys.size();
const size_t cached = cachedSegment;

if( cached + 1 < count )
{
const float start = keys[cached].m_time;
const float end = keys[cached + 1].m_time;
if( t >= start && t < end )
{
return cached;
}
if( t >= end && cached + 2 < count && t < keys[cached + 2].m_time )
{
return cachedSegment = cached + 1;
}
if( t < start && cached > 0 && t >= keys[cached - 1].m_time )
{
return cachedSegment = cached - 1;
}
}

// Keys are kept sorted by time, so binary search for the segment.
const auto next = std::upper_bound( keys.begin(), keys.end(), t, []( float time, const auto& key ) { return time < key.m_time; } );
const size_t found = size_t( next - keys.begin() );
if( found > 0 && found < count && t >= keys[found - 1].m_time && t < keys[found].m_time )
{
return cachedSegment = found - 1;
}

// Keys edited out of order without being re-sorted: try every segment.
for( size_t i = 0; i + 1 < count; ++i )
{
if( t >= keys[i].m_time && t < keys[i + 1].m_time )
{
return cachedSegment = i;
}
}
return cachedSegment = count - 2;
}

#endif // Tr2CurveSegmentSearch_h
Loading