本文属于系列「游戏中的数学:从坐标到可同步运动」第 8 / 15 篇

游戏中的数学 08:曲线参数化与弧长重参数化

上一章将 t 定义为插值参数,本章进一步区分曲线参数与实际路程。路径资源给出位置函数,运动系统消费的则是累计距离。三次 Bezier 的 t 仅为混合权重;控制点分布不均时,相同参数增量不会产生相同世界距离。验证使用四个固定控制点建立 4,001 项弧长表,并比较 100 个等参数段与 100 个等弧长段,以量化速度波动。

// $WORK_DIR/.tmp/game-math-lab/Program.cs · Chapter08
Vector2 p0 = new(0, 0);
Vector2 p1 = new(2, 6);
Vector2 p2 = new(8, -2);
Vector2 p3 = new(10, 4);

var table = BuildArcTable(p0, p1, p2, p3, 4000);
var uniformT = Enumerable.Range(0, 101)
    .Select(i => Bezier(p0, p1, p2, p3, i / 100f))
    .ToArray();
var uniformS = Enumerable.Range(0, 101)
    .Select(i => Bezier(p0, p1, p2, p3,
        (float)ArcToT(table, i / 100.0)))
    .ToArray();

曲线定义

三次 Bezier 是四个点的加权和:

[ B(t)=(1-t)^3P_0+3(1-t)^2tP_1+3(1-t)t^2P_2+t^3P_3 ]

该定义保证端点与曲线连续性,但不保证 |B'(t)| 恒定。路径资源持有控制点和参数域,运动系统持有米每秒与目标路程;弧长表负责连接两种语义。弧长表仅依赖曲线几何,适合离线构建或在资源变更时重建。

// Program.cs · Bezier
static Vector2 Bezier(
    Vector2 p0, Vector2 p1,
    Vector2 p2, Vector2 p3, float t)
{
    var u = 1 - t;
    return u * u * u * p0
         + 3 * u * u * t * p1
         + 3 * u * t * t * p2
         + t * t * t * p3;
}

Bezier 控制点、切线与采样点

弧长离散化

三次 Bezier 的解析弧长不适合作为通用运行时求值方法。离散方法使用分段折线累计距离并记录 (t,s);反查时通过二分定位相邻表项,再在局部线性区间求参数。每次查询复杂度为 O(log N),当前表固定为 4,001 项。同一曲线被多个实体复用时,弧长表随资源共享,而不复制到每个跟随器。

// Program.cs · BuildArcTable
static List<Arc> BuildArcTable(
    Vector2 p0, Vector2 p1,
    Vector2 p2, Vector2 p3, int segments)
{
    var result = new List<Arc> { new(0, 0) };
    var previous = p0;
    double sum = 0;

    for (var i = 1; i <= segments; i++)
    {
        var t = i / (double)segments;
        var point = Bezier(p0, p1, p2, p3, (float)t);
        sum += Vector2.Distance(previous, point);
        result.Add(new Arc(t, sum));
        previous = point;
    }
    return result;
}

弧长反查

二分查找确定目标所在区间,区间内按累计距离插值。输入 fraction=0.5 表示走过总长的一半,而非直接取 t=0.5。固定曲线的近似总长为 12.433628 m。表分辨率属于误差预算;控制点变化后,旧表整体失效。

// Program.cs · ArcToT
static double ArcToT(IReadOnlyList<Arc> table, double fraction)
{
    if (table.Count < 2 || table[^1].S < 1e-9)
        return 0;
    var target = table[^1].S * Math.Clamp(fraction, 0, 1);
    var lo = 0;
    var hi = table.Count - 1;

    while (lo + 1 < hi)
    {
        var mid = (lo + hi) / 2;
        if (table[mid].S < target) lo = mid;
        else hi = mid;
    }

    var a = table[lo];
    var b = table[hi];
    var span = b.S - a.S;
    if (span < 1e-9) return a.T;
    var f = (target - a.S) / span;
    return a.T + (b.T - a.T) * f;
}

匀速误差

等参数采样的段长标准差为 0.019488 m,对应移动体沿曲线的周期性速度变化;等弧长反查后降至 0.00000527 m。回归门槛要求后者小于前者的 5%。四个控制点均为 (1,1) 时,零长表使 ArcToT(table,0.5) 返回起点参数 0,避免零分母生成 NaN。

反查函数还满足单调性:目标路程增加时,返回参数不得减小。累计表若因并发更新、精度截断或资源损坏而失序,二分查找仍可能返回数值结果,但不再对应正确区间。加载阶段验证首项为零、累计距离非递减、末项等于总长,可避免跟随器在运行时处理结构异常。

// Program.cs · Chapter08:统计与断言
var dt = SegmentLengths(uniformT);
var ds = SegmentLengths(uniformS);
var degenerate = BuildArcTable(
    Vector2.One, Vector2.One,
    Vector2.One, Vector2.One, 8);

Check(
    Std(ds) < Std(dt) * .05,
    "arc-length lookup equalizes segment lengths");
Check(ArcToT(degenerate, .5) == 0,
    "zero-length curve maps to start parameter");

WriteData(8, new
{
    samples = 101,
    lookup_samples = 4001,
    parameter_mean = dt.Average(),
    parameter_std = Std(dt),
    arclength_mean = ds.Average(),
    arclength_std = Std(ds),
    total_length = table[^1].S,
    degenerate_length = degenerate[^1].S,
    degenerate_t = 0
});

等参数与等弧长的段长对照

实验结果

$ cd $WORK_DIR
$ dotnet run --project .tmp/game-math-lab/GameMathLab.csproj -- --out .tmp/game-math-lab/out

CH08 PASS segments=100 t_std=0.019488m arc_std=0.000005m length=12.433628m
TOTAL pass=15 fail=0 skip=0 chapters=15 elapsed_ms=148.7
exit_code=0
{
  "samples": 101,
  "lookup_samples": 4001,
  "parameter_mean": 0.12433057606220245,
  "parameter_std": 0.019488149363260005,
  "arclength_mean": 0.12433023974299431,
  "arclength_std": 0.000005271764799195839,
  "total_length": 12.433628018479794,
  "degenerate_length": 0,
  "degenerate_t": 0
}

资源一致性

更密的表减少折线近似误差,同时增加构建与存储成本。4,000 段构成当前可复算基线,资源管线可依据允许的最大段长误差执行自适应细分。仅在控制点实时变化且查询次数较少时,逐帧重建才具有合理性;常规规模共享不可变表,路径跟随器仅保存当前路程。

表版本与曲线资源绑定。编辑器移动控制点后若只更新可视曲线,运行时会得到几何正确但速度错误的路径。资源管线在同一次构建中产出控制点、弧长表和误差摘要,并按内容版本整体替换;读取者不应观察到新控制点与旧表组合的中间状态。

所有者 保存状态 失效条件
路径资源 控制点、弧长表 控制点变化
跟随器 当前路程 路径切换
运动系统 米每秒 玩法输入变化

导数接口

弧长采样器交付连续位置 p(t)。下一章在同一时间域中估计速度与加速度,并将差分步长纳入误差合同;曲线参数、路程和时间由此保持相互独立。