Files
tooll3--t3/Core.Tests/HermiteBezierComparisonTests.cs
2026-07-13 13:13:17 +08:00

221 lines
8.7 KiB
C#

using System;
using System.Collections.Generic;
using T3.Core.Animation;
using T3.Core.DataTypes;
using Xunit;
using Xunit.Abstractions;
namespace Core.Tests;
/// <summary>
/// Verifies that cubic Bezier evaluation with correct conversion factors
/// produces identical results to the current Hermite evaluation.
/// This is critical for migration: existing Smooth/Cubic animations must not change shape.
/// </summary>
public class HermiteBezierComparisonTests
{
private readonly ITestOutputHelper _output;
public HermiteBezierComparisonTests(ITestOutputHelper output) => _output = output;
[Fact]
public void BezierMatchesHermite_SmoothKeys()
{
var curve = new Curve();
curve.AddOrUpdateV(0.0, new VDefinition
{
Value = 0.0,
InInterpolation = VDefinition.KeyInterpolation.Smooth,
OutInterpolation = VDefinition.KeyInterpolation.Smooth,
});
curve.AddOrUpdateV(1.0, new VDefinition
{
Value = 1.0,
InInterpolation = VDefinition.KeyInterpolation.Smooth,
OutInterpolation = VDefinition.KeyInterpolation.Smooth,
});
// Get the Hermite tangent angles after auto-computation
var keyA = curve.Table.Values[0];
var keyB = curve.Table.Values[1];
_output.WriteLine($"KeyA: OutAngle={keyA.OutTangentAngle:F6}, slope={Math.Tan(keyA.OutTangentAngle):F6}");
_output.WriteLine($"KeyB: InAngle={keyB.InTangentAngle:F6}, slope={Math.Tan(keyB.InTangentAngle):F6}");
var segmentWidth = keyB.U - keyA.U;
// Current Hermite: m = tan(angle) * segmentWidth * tension
var m0 = Math.Tan(keyA.OutTangentAngle) * segmentWidth * keyA.TensionOut;
var m1 = Math.Tan(keyB.InTangentAngle) * segmentWidth * keyB.TensionIn;
// Bezier conversion: P1 = P0 + m0/3, P2 = P3 - m1/3
// (m is the Hermite tangent, so the Bezier handle offset in value = m/3,
// and in time = segmentWidth/3)
double p0x = keyA.U, p0y = keyA.Value;
double p1x = keyA.U + segmentWidth / 3.0;
double p1y = keyA.Value + m0 / 3.0;
double p2x = keyB.U - segmentWidth / 3.0;
double p2y = keyB.Value - m1 / 3.0;
double p3x = keyB.U, p3y = keyB.Value;
_output.WriteLine($"Bezier: P0=({p0x:F4},{p0y:F4}) P1=({p1x:F4},{p1y:F4}) P2=({p2x:F4},{p2y:F4}) P3=({p3x:F4},{p3y:F4})");
var maxError = 0.0;
for (double u = 0.0; u <= 1.0; u += 0.01)
{
var hermiteValue = curve.GetSampledValue(u);
// Bezier evaluation with root finding
var t = FindBezierT(u, p0x, p1x, p2x, p3x);
var bezierValue = EvalBezier(t, p0y, p1y, p2y, p3y);
var error = Math.Abs(hermiteValue - bezierValue);
maxError = Math.Max(maxError, error);
if (error > 1e-6)
_output.WriteLine($" u={u:F2}: hermite={hermiteValue:F8}, bezier={bezierValue:F8}, error={error:E2}");
}
_output.WriteLine($"Max error: {maxError:E4}");
Assert.True(maxError < 1e-6, $"Hermite and Bezier should match. Max error: {maxError:E4}");
}
[Fact]
public void BezierMatchesHermite_AsymmetricSmooth()
{
// Three keys with different values — Smooth auto-tangents produce non-trivial slopes
var curve = new Curve();
curve.AddOrUpdateV(0.0, new VDefinition
{
Value = 0.0,
InInterpolation = VDefinition.KeyInterpolation.Smooth,
OutInterpolation = VDefinition.KeyInterpolation.Smooth,
});
curve.AddOrUpdateV(0.5, new VDefinition
{
Value = 2.0,
InInterpolation = VDefinition.KeyInterpolation.Smooth,
OutInterpolation = VDefinition.KeyInterpolation.Smooth,
});
curve.AddOrUpdateV(1.0, new VDefinition
{
Value = 0.5,
InInterpolation = VDefinition.KeyInterpolation.Smooth,
OutInterpolation = VDefinition.KeyInterpolation.Smooth,
});
// Test both segments
var maxError = CompareSegments(curve);
_output.WriteLine($"Max error across all segments: {maxError:E4}");
Assert.True(maxError < 1e-6, $"Max error: {maxError:E4}");
}
[Fact]
public void BezierMatchesHermite_ManualTangent()
{
var curve = new Curve();
curve.AddOrUpdateV(0.0, new VDefinition
{
Value = 0.0,
InInterpolation = VDefinition.KeyInterpolation.Tangent,
OutInterpolation = VDefinition.KeyInterpolation.Tangent,
OutTangentAngle = 0.6, // ~34 degrees
TensionOut = 1.0f,
});
curve.AddOrUpdateV(1.0, new VDefinition
{
Value = 1.0,
InInterpolation = VDefinition.KeyInterpolation.Tangent,
OutInterpolation = VDefinition.KeyInterpolation.Tangent,
InTangentAngle = 0.3,
TensionIn = 1.0f,
});
var maxError = CompareSegments(curve);
_output.WriteLine($"Max error: {maxError:E4}");
Assert.True(maxError < 1e-6, $"Max error: {maxError:E4}");
}
private double CompareSegments(Curve curve)
{
var maxError = 0.0;
var keys = curve.Table.Keys;
var values = curve.Table.Values;
for (var seg = 0; seg < curve.Table.Count - 1; seg++)
{
var keyA = values[seg];
var keyB = values[seg + 1];
var segmentWidth = keyB.U - keyA.U;
var slopeA = SafeTan(keyA.OutTangentAngle);
var slopeB = SafeTan(keyB.InTangentAngle);
var m0 = slopeA * segmentWidth * keyA.TensionOut;
var m1 = slopeB * segmentWidth * keyB.TensionIn;
double p0x = keyA.U, p0y = keyA.Value;
double p1x = keyA.U + segmentWidth / 3.0;
double p1y = keyA.Value + m0 / 3.0;
double p2x = keyB.U - segmentWidth / 3.0;
double p2y = keyB.Value - m1 / 3.0;
double p3x = keyB.U, p3y = keyB.Value;
for (double u = keyA.U; u <= keyB.U; u += segmentWidth * 0.01)
{
var hermiteValue = curve.GetSampledValue(u);
var t = FindBezierT(u, p0x, p1x, p2x, p3x);
var bezierValue = EvalBezier(t, p0y, p1y, p2y, p3y);
var error = Math.Abs(hermiteValue - bezierValue);
maxError = Math.Max(maxError, error);
}
}
return maxError;
}
private static double SafeTan(double angle)
{
var slope = Math.Tan(angle);
return Math.Abs(slope) < 1e-10 ? 0.0 : slope;
}
// --- Bezier math ---
private static double EvalBezier(double t, double p0, double p1, double p2, double p3)
{
var u = 1 - t;
return u * u * u * p0 + 3 * u * u * t * p1 + 3 * u * t * t * p2 + t * t * t * p3;
}
private static double EvalBezierDerivative(double t, double p0, double p1, double p2, double p3)
{
var u = 1 - t;
return 3 * u * u * (p1 - p0) + 6 * u * t * (p2 - p1) + 3 * t * t * (p3 - p2);
}
private static double FindBezierT(double targetX, double p0x, double p1x, double p2x, double p3x)
{
// Newton-Raphson
var t = (targetX - p0x) / (p3x - p0x); // Initial guess: linear
t = Math.Clamp(t, 0, 1);
for (var i = 0; i < 20; i++)
{
var x = EvalBezier(t, p0x, p1x, p2x, p3x);
var dx = EvalBezierDerivative(t, p0x, p1x, p2x, p3x);
if (Math.Abs(dx) < 1e-12)
break;
var newT = t - (x - targetX) / dx;
if (Math.Abs(newT - t) < 1e-12)
return Math.Clamp(newT, 0, 1);
t = Math.Clamp(newT, 0, 1);
}
return t;
}
}