Files
2026-07-13 13:13:17 +08:00

211 lines
7.0 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using T3.Core.DataTypes;
namespace T3.Core.Animation;
public class CurveOptimizer
{
public CurveOptimizer(List<Curve> curves)
{
_curves = curves;
InitializePositions();
InitializeCurveSampleBuffers();
ResampleCurve(0, _positionsWithImpact.Count - 1);
}
private void InitializePositions()
{
// Gather all positions from the curves in a dictionary...
var uniquePositions = new HashSet<double>();
foreach (var k in _curves.SelectMany(curve => curve.GetVDefinitions()))
{
uniquePositions.Add(k.U);
}
_positionsWithImpact = new List<PositionWithImpact>(); ;
foreach (var position in uniquePositions.OrderBy(o => o))
{
_positionsWithImpact.Add(new PositionWithImpact() { Position = position, Impact = double.NaN });
}
}
/**
* Sampling the curve is expensive. So we sample the state before the next optimization
* for faster comparision. These curves are stored for each curve in an array with the
* necessary size for the samplerate between min and max postiion.
*
* After removing keys these sections of the curve have to resampled.
*/
private void InitializeCurveSampleBuffers()
{
_sampleStartPosition = _positionsWithImpact.First().Position;
_sampleCount = (int)(Math.Abs(_positionsWithImpact.Last().Position - _sampleStartPosition) / SAMPLE_RATE);
if (_sampleCount < 2)
return;
_samplesForCurves = new double[_curves.Count, _sampleCount + 1];
}
public void ResampleCurve(int startIndex, int endIndex)
{
for (var curveIndex = 0; curveIndex < _curves.Count; ++curveIndex)
{
var curve = _curves[curveIndex];
for (var sampleIndex = startIndex; sampleIndex < _sampleCount && sampleIndex <= endIndex; ++sampleIndex)
{
var samplePos = _sampleStartPosition + sampleIndex * SAMPLE_RATE;
_samplesForCurves[curveIndex, sampleIndex] = curve.GetSampledValue(samplePos);
}
}
}
public void OptimizeCurves(int maxPositionCount)
{
_maxPositionCount = maxPositionCount;
while (_positionsWithImpact.Count > _maxPositionCount)
{
UpdatePositionImpacts();
_leastImpactedPositionIndex = FindIndexWithMinimumImpact();
var minImpactPosition = _positionsWithImpact[_leastImpactedPositionIndex].Position;
// Remove keys from curve
foreach (var curve in _curves)
{
if (curve.HasVAt(minImpactPosition))
curve.RemoveKeyframeAt(minImpactPosition);
}
// Remove position from list and invalidate neighbours
var startSampleIndex = 0;
var endSampleIndex = 0;
if (_leastImpactedPositionIndex > 0)
{
_positionsWithImpact[_leastImpactedPositionIndex - 1].Impact = double.NaN;
startSampleIndex = _leastImpactedPositionIndex - 1;
}
if (_leastImpactedPositionIndex > 1)
{
startSampleIndex = _leastImpactedPositionIndex - 2;
}
if (_leastImpactedPositionIndex + 2 < _positionsWithImpact.Count)
{
_positionsWithImpact[_leastImpactedPositionIndex + 1].Impact = double.NaN;
endSampleIndex = _leastImpactedPositionIndex + 1;
}
if (_leastImpactedPositionIndex + 3 < _positionsWithImpact.Count)
{
endSampleIndex = _leastImpactedPositionIndex + 2;
}
_positionsWithImpact.RemoveAt(_leastImpactedPositionIndex);
// Resample section of curve...
ResampleCurve(IndexFromU(_positionsWithImpact[startSampleIndex].Position),
IndexFromU(_positionsWithImpact[endSampleIndex].Position));
}
}
class PositionWithImpact
{
public double Position;
public double Impact;
}
private int FindIndexWithMinimumImpact()
{
var minImpact = double.PositiveInfinity;
var minImpactIndex = 0;
var index = -1;
foreach (var p in _positionsWithImpact)
{
index++;
if (!(p.Impact < minImpact))
continue;
minImpact = p.Impact;
minImpactIndex = index;
}
return minImpactIndex;
}
/**
* Go through the current list of positions and update if necessary.
* Invalidated positions are marked with an impact of double.NaN.
*/
public void UpdatePositionImpacts()
{
if (_positionsWithImpact.Count <= 2)
return;
double u, nextU, previousU;
// Yes, these are 3 nested loops. Don't ask.
for (var keyIndex = 1; keyIndex < _positionsWithImpact.Count - 1; ++keyIndex)
{
var p = _positionsWithImpact[keyIndex];
u = p.Position;
nextU = _positionsWithImpact[keyIndex + 1].Position;
previousU = _positionsWithImpact[keyIndex - 1].Position;
var accumulatedImpact = 0.0;
if (!double.IsNaN(p.Impact))
return;
for (var curveIndex = 0; curveIndex < _curves.Count; ++curveIndex)
{
var curve = _curves[curveIndex];
var vDef = curve.GetV(u);
if (vDef == null)
continue;
curve.RemoveKeyframeAt(u);
var startSampleIndex = IndexFromU(previousU);
var endSampleIndex = IndexFromU(nextU) + 1;
for (var sampleIndex = startSampleIndex; sampleIndex <= endSampleIndex && sampleIndex < _sampleCount; ++sampleIndex)
{
var sampleU = _sampleStartPosition + sampleIndex * SAMPLE_RATE;
var originalValue = _samplesForCurves[curveIndex, sampleIndex];
var newValue = curve.GetSampledValue(sampleU);
accumulatedImpact += Math.Abs(originalValue - newValue);
}
curve.AddOrUpdateV(u, vDef);
}
p.Impact = accumulatedImpact;
}
}
private int IndexFromU(double u)
{
var samplePosition = (SAMPLE_RATE * (int)(u / SAMPLE_RATE));
return (int)((samplePosition - _sampleStartPosition) / SAMPLE_RATE);
}
private double _sampleStartPosition;
private int _sampleCount;
private List<Curve> _curves;
private int _maxPositionCount;
private List<PositionWithImpact> _positionsWithImpact = new();
const double SAMPLE_RATE = 1 / 60.0;
private double[,] _samplesForCurves;
private int _leastImpactedPositionIndex = 0;
}