大世界原点漂移后,运动矢量必须把上一帧带到同一坐标系

角色在世界里只向前移动了 1 个单位,屏幕上的运动矢量却突然指向反方向,长度达到 23。这个错误通常出现在大世界系统执行原点漂移的那一帧:对象的绝对位置从 1002 变为 1003,渲染原点同时从 1000 移到 1024。两次变化各自都正确,直接相减却得到完全错误的时间信息。

大世界原点漂移与连续运动矢量

运动矢量描述的是同一个对象跨帧的物理位移。参与相减的两个位置因此必须属于同一坐标系。原点漂移系统若只更新当前帧的相对位置,上一帧缓存仍以旧原点为基准;此时减法比较的是两个不同的坐标系,原点偏移会被误认为对象运动。

相对坐标本身没有错,跨帧相减的前提错了

固定样本可以把问题拆得很清楚。上一帧相对位置是 1002 - 1000 = 2,当前帧相对位置是 1003 - 1024 = -21。朴素结果为 -21 - 2 = -23,恰好等于真实位移 +1 减去原点位移 24。因此,错误不是浮点精度噪声,也不是速度单位不一致,而是原点状态进入时间差分时没有保持一致。

OriginMotion.cs 中的 FrameSample 明确保存两帧绝对位置与两帧原点。关键实现没有试图修改真实运动,而是先把上一帧相对位置迁移到当前原点,再与当前相对位置相减:

using System.Numerics;

namespace OriginMotionLab;

public readonly record struct FrameSample(
    Vector3 PreviousAbsolute,
    Vector3 CurrentAbsolute,
    Vector3 PreviousOrigin,
    Vector3 CurrentOrigin);

public static class OriginMotion
{
    public static Vector3 NaiveRelativeDelta(FrameSample sample)
    {
        var previousRelative = sample.PreviousAbsolute - sample.PreviousOrigin;
        var currentRelative = sample.CurrentAbsolute - sample.CurrentOrigin;
        return currentRelative - previousRelative;
    }

    public static Vector3 RebasedRelativeDelta(FrameSample sample)
    {
        var previousRelative = sample.PreviousAbsolute - sample.PreviousOrigin;
        var originDelta = sample.CurrentOrigin - sample.PreviousOrigin;
        var previousInCurrentOrigin = previousRelative - originDelta;
        var currentRelative = sample.CurrentAbsolute - sample.CurrentOrigin;
        return currentRelative - previousInCurrentOrigin;
    }
}

这段代码也给出了状态所有权的边界:大世界系统拥有 PreviousOriginCurrentOrigin,时间渲染阶段拥有上一帧对象变换缓存。提交运动矢量之前,两者必须在同一个帧快照中汇合。若渲染线程只收到当前原点,它无法从旧的相对位置恢复正确基准;把修正推迟到着色器,也仍然需要把原点差作为显式帧常量传入。

原点切换还必须是一次帧边界事务。若场景节点已经应用新原点,而可见性列表或上一帧矩阵仍在旧原点下被异步写入,同一批绘制命令内部也会出现混合基准。可靠的提交顺序是先冻结本帧可见对象,记录原点差,再生成统一基准下的当前与历史变换,最后开放下一帧更新。多线程渲染器可以复制快照,不应让渲染任务直接读取仍在变化的全局原点。

保留绝对位置并不意味着所有渲染计算都要使用大数值。绝对坐标适合充当跨帧身份和重建依据,GPU 侧仍可消费靠近零点的相对坐标。二者之间的转换必须发生在有明确帧编号的边界上;否则某个矩阵看似数值合理,却无法判断它属于哪一个原点版本。

原点漂移前后运动矢量计算对照

原点变化为零时,两条路径应完全重合

修正不能破坏普通帧。Program.cs 同时覆盖无漂移、正负方向漂移、静止对象、多轴漂移和小数位移。对每个样本,RebasedRelativeDelta 都必须等于绝对位置差;对于原点不同的样本,测试还要求朴素算法稳定暴露偏差。当天运行命令与决定性结果如下:

$ cd $WORK_DIR/.tmp/daily-labs/origin-motion && dotnet build -c Release --no-restore
Build succeeded. 0 Warning(s), 0 Error(s). Exit code: 0

$ dotnet run -c Release --no-build
passed=12 failed=0 skipped=0 elapsedMs=1.1492
expectedNaiveFailures=7
fixed: expected=(1,0,0) naive=(-23,0,0) corrected=(1,0,0)
Exit code: 0

无漂移时 originDelta 为零,修正路径退化为普通的相对位置差;有漂移时,上一帧位置先减去原点差,才进入时间差分。这个退化性质很重要,因为它允许渲染管线始终走同一接口,不必为漂移帧切换材质或临时清空全部历史。

何时应该丢弃历史,而不是继续修正

坐标重映射只解决刚体世界变换的基准变化。对象若发生传送、网格拓扑替换、蒙皮姿态缓存缺失,或者相机投影参数产生不连续跳变,即使两帧已经处于同一原点,历史仍可能无效。此时应由对象或视图状态显式给出 historyValid=false,让 TAA、运动模糊与时域超分辨率拒绝该样本,而不是依赖一个速度阈值猜测。

相反,单纯的原点漂移不应自动清空全屏历史。它是坐标表示改变,场景的物理连续性仍然存在。全屏失效虽然能隐藏一帧错误矢量,却会同时丢掉所有稳定像素的时间积累;把上一帧变换带到当前原点,才能保留这种连续性。

当前探针验证到世界空间位移为止,没有覆盖相机抖动、裁剪空间投影、蒙皮顶点和程序化变形。接入真实渲染器时,仍需用当前与上一帧的视图投影矩阵把这份一致的世界空间状态送入裁剪空间。可以保留的架构接口很明确:时间渲染数据必须携带两帧原点,或者保证所有历史变换在缓存提交时已统一到同一原点;只保存一个无语义的相对矩阵,无法独立支撑原点漂移帧的正确运动矢量。