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

152 lines
5.5 KiB
C#

using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
namespace T3.Core.Animation;
/// <summary>
/// Evaluates a 2D cubic Bezier curve segment with root finding.
/// Used for Tangent-mode keyframes with custom tension (weighted handles).
/// For Smooth/Cubic/Horizontal modes, <see cref="SplineInterpolator"/> is used instead
/// (mathematically identical at tension=1.0, but faster without root finding).
/// </summary>
internal static class BezierInterpolator
{
/// <summary>
/// Evaluates the curve value at time <paramref name="u"/> for a segment
/// defined by two keyframes with Bezier control points.
/// </summary>
public static double Interpolate(KeyValuePair<double, VDefinition> a, KeyValuePair<double, VDefinition> b, double u)
{
var keyA = a.Value;
var keyB = b.Value;
var segmentWidth = b.Key - a.Key;
if (segmentWidth <= 0)
return keyA.Value;
// Compute Hermite tangent magnitudes (same as SplineInterpolator)
var slopeA = SlopFromAngle(keyA.OutTangentAngle);
var slopeB = SlopFromAngle(keyB.InTangentAngle);
var m0 = slopeA * segmentWidth * keyA.TensionOut;
var m1 = slopeB * segmentWidth * keyB.TensionIn;
// Convert Hermite tangents to Bezier control points
// Relationship: m = 3 * (P1 - P0) for cubic Bezier ↔ Hermite
double p0x = a.Key, p0y = keyA.Value;
double p1x = a.Key + segmentWidth * keyA.TensionOut / 3.0;
double p1y = keyA.Value + m0 / 3.0;
double p2x = b.Key - segmentWidth * keyB.TensionIn / 3.0;
double p2y = keyB.Value - m1 / 3.0;
double p3x = b.Key, p3y = keyB.Value;
// Root find: solve BezierX(t) = u for parameter t
var t = FindParameterForTime(u, p0x, p1x, p2x, p3x);
// Evaluate Y at found parameter
return EvalCubic(t, p0y, p1y, p2y, p3y);
}
/// <summary>
/// Determines whether a segment between two keyframes requires Bezier evaluation
/// (as opposed to faster Hermite). A segment needs Bezier when at least one endpoint
/// has Tangent interpolation with non-default tension (Weighted=true).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool SegmentNeedsBezier(VDefinition a, VDefinition b)
{
// Only use Bezier when explicitly weighted with non-default tension
return (a.Weighted && a.OutInterpolation == VDefinition.KeyInterpolation.Tangent
// ReSharper disable once CompareOfFloatsByEqualityOperator
&& a.TensionOut != 1.0f)
|| (b.Weighted && b.InInterpolation == VDefinition.KeyInterpolation.Tangent
// ReSharper disable once CompareOfFloatsByEqualityOperator
&& b.TensionIn != 1.0f);
}
/// <summary>
/// Finds the Bezier parameter t where BezierX(t) = targetX.
/// Uses Newton-Raphson with linear initial guess and bisection fallback.
/// </summary>
private static double FindParameterForTime(double targetX, double p0x, double p1x, double p2x, double p3x)
{
// Linear initial guess
var t = (targetX - p0x) / (p3x - p0x);
t = Math.Clamp(t, 0.0, 1.0);
// Newton-Raphson iterations
for (var i = 0; i < MaxNewtonIterations; i++)
{
var x = EvalCubic(t, p0x, p1x, p2x, p3x);
var residual = x - targetX;
if (Math.Abs(residual) < Tolerance)
return t;
var dx = EvalCubicDerivative(t, p0x, p1x, p2x, p3x);
if (Math.Abs(dx) < 1e-12)
break; // Derivative too small, fall through to bisection
var newT = t - residual / dx;
// Clamp to valid range
newT = Math.Clamp(newT, 0.0, 1.0);
if (Math.Abs(newT - t) < Tolerance)
return newT;
t = newT;
}
// Bisection fallback (robust but slower)
return BisectionFallback(targetX, p0x, p1x, p2x, p3x);
}
private static double BisectionFallback(double targetX, double p0x, double p1x, double p2x, double p3x)
{
double lo = 0, hi = 1;
for (var i = 0; i < MaxBisectionIterations; i++)
{
var mid = (lo + hi) * 0.5;
var x = EvalCubic(mid, p0x, p1x, p2x, p3x);
if (Math.Abs(x - targetX) < Tolerance)
return mid;
if (x < targetX)
lo = mid;
else
hi = mid;
}
return (lo + hi) * 0.5;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static double EvalCubic(double t, double p0, double p1, double p2, double p3)
{
var u = 1.0 - t;
return u * u * u * p0 + 3.0 * u * u * t * p1 + 3.0 * u * t * t * p2 + t * t * t * p3;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static double EvalCubicDerivative(double t, double p0, double p1, double p2, double p3)
{
var u = 1.0 - t;
return 3.0 * u * u * (p1 - p0) + 6.0 * u * t * (p2 - p1) + 3.0 * t * t * (p3 - p2);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static double SlopFromAngle(double angle)
{
var slope = Math.Tan(angle);
return Math.Abs(slope) < 1e-10 ? 0.0 : slope;
}
private const int MaxNewtonIterations = 8;
private const int MaxBisectionIterations = 30;
private const double Tolerance = 1e-8;
}