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

游戏中的数学 08:曲线参数为什么不等于路程

路径系统给移动体的是位置函数,运动系统需要的却是“走了多少米”。三次 Bezier 的 t 只是混合权重;控制点分布不均时,每增加相同的 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 控制点、切线与采样点

弧长表

解析弧长一般不值得塞进运行时。实验用分段折线累计距离,记录 (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=112.8
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)。下一章不再直接改位置,而是从同一函数用差分估计速度与加速度,并把采样步长写入误差合同。