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

223 lines
9.0 KiB
C#

using System;
using System.Collections.Generic;
namespace T3.Core.Animation;
internal static class SplineInterpolator
{
internal static void UpdateTangents(SortedList<double, VDefinition> table)
{
var count = table.Count;
if (count <= 1)
return;
var keys = table.Keys;
var values = table.Values;
// First key: start tangent
var curKey = keys[0];
var curDef = values[0];
var nextKey = keys[1];
var nextDef = values[1];
curDef.OutTangentAngle = CalcStartTangent(curKey, curDef, nextKey, nextDef);
curDef.InTangentAngle = curDef.OutTangentAngle - Math.PI;
// Middle keys
for (int i = 1; i < count - 1; ++i)
{
var prevKey = keys[i - 1];
var prevDef = values[i - 1];
curKey = keys[i];
curDef = values[i];
nextKey = keys[i + 1];
nextDef = values[i + 1];
if (NeedsTangentComputation(curDef))
{
curDef.InTangentAngle = CalcInTangent(prevKey, prevDef, curKey, curDef, nextKey, nextDef);
curDef.OutTangentAngle = CalcOutTangent(prevKey, prevDef, curKey, curDef, nextKey, nextDef);
}
}
// Last key: end tangent
var prevLastKey = keys[count - 2];
var prevLastDef = values[count - 2];
var lastKey = keys[count - 1];
var lastDef = values[count - 1];
lastDef.InTangentAngle = CalcEndTangent(prevLastKey, prevLastDef, lastKey, lastDef);
lastDef.OutTangentAngle = lastDef.InTangentAngle - Math.PI;
}
private static bool NeedsTangentComputation(VDefinition def)
{
return def.InInterpolation != VDefinition.KeyInterpolation.Constant
|| def.OutInterpolation != VDefinition.KeyInterpolation.Constant;
}
/// <summary>
/// Cubic Hermite spline interpolation between two keys.
/// See http://en.wikipedia.org/wiki/Monotone_cubic_interpolation
/// </summary>
public static double Interpolate(KeyValuePair<double, VDefinition> a, KeyValuePair<double, VDefinition> b, double u)
{
double t = (u - a.Key) / (b.Key - a.Key);
double tangentLength = b.Key - a.Key;
var p0 = a.Value.Value;
var m0 = SlopFromAngle(a.Value.OutTangentAngle) * tangentLength * a.Value.TensionOut;
var p1 = b.Value.Value;
var m1 = SlopFromAngle(b.Value.InTangentAngle) * tangentLength * b.Value.TensionIn;
var t2 = t * t;
var t3 = t2 * t;
return (2 * t3 - 3 * t2 + 1) * p0 + (t3 - 2 * t2 + t) * m0 + (-2 * t3 + 3 * t2) * p1 + (t3 - t2) * m1;
}
/// <summary>
/// Computes tan(angle) with precision guard: snaps near-zero results to exactly 0.
/// Avoids floating-point noise from tan(PI) ≈ -1.22e-16 which causes visible
/// wobble on flat curves when amplified by tangent length and min/max normalization.
/// </summary>
private static double SlopFromAngle(double angle)
{
var slope = Math.Tan(angle);
return Math.Abs(slope) < 1e-10 ? 0.0 : slope;
}
private static double CalcStartTangent(double aKey, VDefinition aDef, double bKey, VDefinition bDef)
{
switch (aDef.OutInterpolation)
{
case VDefinition.KeyInterpolation.Tangent:
return aDef.OutTangentAngle;
case VDefinition.KeyInterpolation.Linear:
case VDefinition.KeyInterpolation.Smooth:
case VDefinition.KeyInterpolation.Cubic:
return Math.PI / 2 - Math.Atan2(aKey - bKey, aDef.Value - bDef.Value);
case VDefinition.KeyInterpolation.Horizontal:
default:
return Math.PI;
}
}
private static double CalcEndTangent(double aKey, VDefinition aDef, double bKey, VDefinition bDef)
{
switch (bDef.InInterpolation)
{
case VDefinition.KeyInterpolation.Tangent:
return bDef.InTangentAngle;
case VDefinition.KeyInterpolation.Linear:
case VDefinition.KeyInterpolation.Smooth:
case VDefinition.KeyInterpolation.Cubic:
return Math.PI / 2 - Math.Atan2(bKey - aKey, bDef.Value - aDef.Value);
case VDefinition.KeyInterpolation.Horizontal:
default:
return 0;
}
}
private const double TANGENT_CLAMP_RATIO = 1.5;
private static double CalcInTangent(double prevKey, VDefinition prevDef,
double curKey, VDefinition curDef,
double nextKey, VDefinition nextDef)
{
switch (curDef.InInterpolation)
{
case VDefinition.KeyInterpolation.Tangent:
return curDef.InTangentAngle;
case VDefinition.KeyInterpolation.Smooth:
var angle = Math.PI / 2 - Math.Atan2(nextKey - prevKey, nextDef.Value - prevDef.Value);
double thirdToPrev = (prevKey - curKey) / TANGENT_CLAMP_RATIO;
double thirdToNext = (nextKey - curKey) / TANGENT_CLAMP_RATIO;
// Avoid overshooting toward next keyframe
if (prevDef.Value > nextDef.Value && (curDef.Value + Math.Tan(angle) * thirdToNext) < nextDef.Value)
{
angle = Math.PI + Math.PI / 2 - Math.Atan2(-thirdToNext, Math.Max(0, curDef.Value - nextDef.Value));
}
else if (prevDef.Value < nextDef.Value && (curDef.Value + Math.Tan(angle) * thirdToNext) > nextDef.Value)
{
angle = Math.PI + Math.PI / 2 - Math.Atan2(-thirdToNext, Math.Min(0, curDef.Value - nextDef.Value));
}
// Avoid overshooting toward previous keyframe
else if (prevDef.Value > nextDef.Value && (curDef.Value + Math.Tan(angle) * thirdToPrev) > prevDef.Value)
{
angle = Math.PI + Math.PI / 2 - Math.Atan2(thirdToPrev, Math.Max(0, -curDef.Value + prevDef.Value));
}
else if (prevDef.Value < nextDef.Value && (curDef.Value + Math.Tan(angle) * thirdToPrev) < prevDef.Value)
{
angle = Math.PI + Math.PI / 2 - Math.Atan2(thirdToPrev, Math.Min(0, -curDef.Value + prevDef.Value));
}
return angle;
case VDefinition.KeyInterpolation.Cubic:
return Math.PI / 2 - Math.Atan2(nextKey - prevKey, nextDef.Value - prevDef.Value);
case VDefinition.KeyInterpolation.Linear:
return Math.PI / 2 - Math.Atan2(curKey - prevKey, curDef.Value - prevDef.Value);
case VDefinition.KeyInterpolation.Horizontal:
default:
return 0;
}
}
private static double CalcOutTangent(double prevKey, VDefinition prevDef,
double curKey, VDefinition curDef,
double nextKey, VDefinition nextDef)
{
switch (curDef.OutInterpolation)
{
case VDefinition.KeyInterpolation.Tangent:
return curDef.OutTangentAngle;
case VDefinition.KeyInterpolation.Smooth:
double thirdToNext = (nextKey - curKey) / TANGENT_CLAMP_RATIO;
double thirdToPrev = (prevKey - curKey) / TANGENT_CLAMP_RATIO;
var angle = Math.PI / 2 - Math.Atan2(prevKey - nextKey, prevDef.Value - nextDef.Value);
// Avoid overshoot toward next keyframe
if (prevDef.Value > nextDef.Value && (curDef.Value + Math.Tan(angle) * thirdToNext) < nextDef.Value)
{
angle = Math.PI / 2 - Math.Atan2(-thirdToNext, Math.Max(0, curDef.Value - nextDef.Value));
}
else if (prevDef.Value < nextDef.Value && (curDef.Value + Math.Tan(angle) * thirdToNext) > nextDef.Value)
{
angle = Math.PI / 2 - Math.Atan2(-thirdToNext, Math.Min(0, curDef.Value - nextDef.Value));
}
// Avoid overshooting toward prev keyframe
else if (prevDef.Value > nextDef.Value && (curDef.Value + Math.Tan(angle) * thirdToPrev) > prevDef.Value)
{
angle = Math.PI / 2 - Math.Atan2(thirdToPrev, Math.Max(0, -curDef.Value + prevDef.Value));
}
else if (prevDef.Value < nextDef.Value && (curDef.Value + Math.Tan(angle) * thirdToPrev) < prevDef.Value)
{
angle = Math.PI / 2 - Math.Atan2(thirdToPrev, Math.Min(0, -curDef.Value + prevDef.Value));
}
return angle;
case VDefinition.KeyInterpolation.Cubic:
return Math.PI / 2 - Math.Atan2(prevKey - nextKey, prevDef.Value - nextDef.Value);
case VDefinition.KeyInterpolation.Linear:
return Math.PI / 2 - Math.Atan2(curKey - nextKey, curDef.Value - nextDef.Value);
case VDefinition.KeyInterpolation.Horizontal:
default:
return Math.PI;
}
}
}