浮动原点移动后,逻辑坐标为什么不能跟着清零
角色走到世界坐标 (100012.25, 3.50, 100006.75),表现层为了避免单精度抖动,把场景原点移动到 (100000, 0, 100000)。如果这次操作顺便把角色的权威坐标改成 (12.25, 3.50, 6.75),近处画面会立刻稳定,但存档、服务器快照、任务触发区和相邻分块已经不再描述同一个世界位置。
浮动原点需要改变的是渲染场景采用的参考系,不是玩法世界本身。麻烦在于一次重基准通常要更新角色、相机、地形块、特效和挂接物;只要其中一部分先进入新参考系,一帧画面就会同时出现两套原点。正确性边界因此不能停在一次坐标减法,还要覆盖整批对象何时共同生效。

世界位置与场景姿态不是同一份状态
探针把 EntityWorldPosition 保持为双精度世界状态,把单精度 ScenePose 收进只读的 RebaseSnapshot。两者的转换只有一个公式:
local = world - origin
(100012.25, 3.50, 100006.75) - (100000, 0, 100000)
= (12.25, 3.50, 6.75)
这不是把误差消灭,而是让提交给引擎 Transform 的数值重新靠近零。逻辑层继续用原来的世界坐标参与分块查询、网络同步与持久化;表现层只知道当前活动原点和由它投影出的局部姿态。若反过来让每个节点自行累加平移,舍入顺序、漏更新对象和重复应用偏移都会进入可见状态。
重基准阈值也不应写成脱离内容规模的固定常量。它至少要同时约束允许的单精度位置误差、相机周围必须共存的最大局部半径,以及一次候选必须覆盖的对象集合。阈值过大时局部坐标再次失去精度;阈值过小则会频繁建立候选并增加安全帧点压力。当前探针验证提交语义,没有测量对象规模与搬移成本,因此只能把这些量列为生产系统的输入合同,不能据此给出通用距离。
核心实现位于 .tmp/daily-labs/2026-08-30-1930/Program.cs 的 FloatingOriginCoordinator。Prepare 先构造候选,TryCommit 再一次检查修订、有限值和实体集合:
sealed class FloatingOriginCoordinator
{
private readonly HashSet<int> expectedEntities;
private RebaseSnapshot active;
public FloatingOriginCoordinator(
IEnumerable<EntityWorldPosition> entities,
Double3 initialOrigin)
{
var poses = Project(entities, initialOrigin);
expectedEntities = poses.Select(x => x.EntityId).ToHashSet();
active = new RebaseSnapshot(0, initialOrigin, poses);
}
public RebaseSnapshot Active => active;
public RebaseCandidate Prepare(
IReadOnlyList<EntityWorldPosition> entities,
Double3 nextOrigin) =>
new(active.Revision, nextOrigin, Project(entities, nextOrigin));
public CommitResult TryCommit(RebaseCandidate candidate)
{
if (candidate.BaseRevision != active.Revision)
return CommitResult.StaleRevision;
if (!candidate.Origin.IsFinite)
return CommitResult.InvalidOrigin;
if (candidate.Poses.Any(x => !x.LocalPosition.IsFinite))
return CommitResult.InvalidLocalPosition;
if (!candidate.Poses.Select(x => x.EntityId).ToHashSet()
.SetEquals(expectedEntities))
return CommitResult.IncompleteEntitySet;
active = new RebaseSnapshot(
active.Revision + 1,
candidate.Origin,
candidate.Poses.ToArray());
return CommitResult.Committed;
}
private static IReadOnlyList<ScenePose> Project(
IEnumerable<EntityWorldPosition> entities,
Double3 origin) =>
entities.Select(entity =>
{
var local = entity.Value - origin;
return new ScenePose(entity.EntityId,
new Float3((float)local.X, (float)local.Y, (float)local.Z));
}).ToArray();
}
active 是表现层唯一可读的提交点。候选列表没有逐项写进活动 Transform,也没有在验证中途替换原点;所以失败只产生一个分类结果,不会留下半套坐标。

迟到候选不能把场景搬回旧参考系
重基准可能与分块加载并行。修订 0 的候选生成完毕后,若另一请求已经提交修订 1,旧候选即使包含合法数字也失去了发布资格。探针故意把同一个候选提交两次:第一次得到 Committed,第二次得到 StaleRevision,活动修订保持 1。修订号约束的是“这批局部姿态依据哪一份活动场景生成”,不能只比较目标原点是否相等。
实体集合检查处理另一种更隐蔽的破坏:某个地形块或挂接物没有进入候选。若允许其余对象先提交,缺失对象仍在旧参考系;屏幕上的巨大跳变并非精度误差,而是同一帧混用了两套坐标。当前探针要求候选 ID 集合与活动集合完全一致,缺一项便返回 IncompleteEntitySet。动态出生和销毁需要更完整的场景修订协议,不能靠放宽集合比较解决。
非有限值也必须在类型转换之后检查。双精度差值转为 float 时可能溢出为无穷;NaN 进入 Transform 后还会污染 Bounds、剔除和物理查询。探针分别覆盖非法原点与非法局部姿态,两条失败路径都保持原活动快照引用和原点不变。
固定结果证明了提交边界
当天验证命令直接运行无外部依赖的 .NET 9 探针:
cd $WORK_DIR/.tmp/daily-labs/2026-08-30-1930
dotnet restore FloatingOriginLab.csproj
dotnet build FloatingOriginLab.csproj -c Release --no-restore
dotnet run --project FloatingOriginLab.csproj -c Release --no-build
决定性输出为:
normal result=Committed revision=1 player_local=12.25,3.50,6.75
stale result=StaleRevision revision_before=1 revision_after=1
tests pass=8 fail=0 skip=0 duration_ms=19.07
恢复、构建与运行退出码均为 0;墙钟耗时分别为 2.90、7.61 和 2.32 秒,Release 构建为 0 警告、0 错误。耗时只说明本次验证实际完成,不构成重基准性能结论。八项检查覆盖完整提交、世界坐标保持、迟到拒绝、非法原点、非法局部值、实体缺失、失败后活动快照保持,以及修订只推进一次。
这个纵向切片没有模拟物理引擎、导航网格、GPU 粒子缓冲和多线程 Transform 写入。接入 Unity、Godot 或自研引擎时,还需要在安全帧点暂停相关消费者,按引擎约束批量应用 Active.Poses;GPU 常驻数据则可能需要独立的原点常量,而不是逐粒子搬移。那些机制扩大的是提交参与者,不会改变状态归属。
开篇的角色最终仍位于逻辑世界的 (100012.25, 3.50, 100006.75),只有当前渲染快照把它表达为 (12.25, 3.50, 6.75)。当原点、完整局部姿态和场景修订组成一个不可拆分的提交包,浮动原点才只是参考系切换;否则它会悄悄变成一次没有事务边界的全场景状态迁移。