大地图浮动原点:绝对分区与渲染坐标的提交契约
开放世界相机越过数千个分区后,直接把实体保存在单一世界浮点坐标中,会让渲染精度与流送生命周期纠缠。本章把位置拆成权威的 cell + offset,把浮动原点限定为渲染派生状态;切换不是逐对象修补,而是一次带加载屏障和版本号的提交。
// .tmp/daily-labs/2026-07-30-1930/world-origin.js
// symbols: WorldPosition, OriginCoordinator.constructor
export class WorldPosition {
constructor(cellX, cellZ, offsetX, offsetZ) {
this.cellX = cellX;
this.cellZ = cellZ;
this.offsetX = offsetX;
this.offsetZ = offsetZ;
Object.freeze(this);
}
}
export class OriginCoordinator {
constructor({ cellSize, thresholdCells, loadedCells }) {
this.cellSize = cellSize;
this.thresholdCells = thresholdCells;
this.loadedCells = new Set(loadedCells);
this.originCell = Object.freeze({ x: 0, z: 0 });
this.version = 0;
}
}

坐标契约
WorldPosition 属于模拟与流送系统,分区索引不因相机移动而变化;渲染器只读取相对当前 originCell 的坐标。本文固定分区边长为 1024 单位,触发阈值为两个分区。两个不变量由此成立:重定原点不得改写权威位置;同一版本内,所有渲染坐标必须由同一个原点推导。
// .tmp/daily-labs/2026-07-30-1930/world-origin.js
// symbol: OriginCoordinator.toRenderSpace
toRenderSpace(position) {
return {
x: (position.cellX - this.originCell.x)
* this.cellSize + position.offsetX,
z: (position.cellZ - this.originCell.z)
* this.cellSize + position.offsetZ,
originVersion: this.version,
};
}
把每个实体的绝对浮点坐标整体减去偏移,看似更直接,却会同时修改模拟、物理、导航与网络快照。任何一个系统漏改或错序,都会产生跨系统分叉。分区位置保持不动,则原点切换只影响可重建的表现数据,回放和服务器状态也不需要理解客户端相机。
提交屏障
相机到达目标分区并不表示原点可以立即切换。新原点周围的 3×3 分区必须全部进入已加载集合,否则渲染坐标已切换而碰撞或地形仍缺失。planRebase 先验证邻域并捕获 expectedVersion,commitRebase 再原子更新原点和版本。
// .tmp/daily-labs/2026-07-30-1930/world-origin.js
// symbols: planRebase, commitRebase
planRebase(observer) {
const dx = observer.cellX - this.originCell.x;
const dz = observer.cellZ - this.originCell.z;
if (Math.max(Math.abs(dx), Math.abs(dz))
< this.thresholdCells) return null;
const target = { x: observer.cellX, z: observer.cellZ };
const required = this.#requiredNeighborhood(target);
const missing = required.filter(
(key) => !this.loadedCells.has(key),
);
if (missing.length > 0) {
throw new Error(`rebase blocked: missing ${missing.join(",")}`);
}
return Object.freeze({
target,
required,
expectedVersion: this.version,
});
}
commitRebase(plan) {
if (plan.expectedVersion !== this.version) {
throw new Error("rebase blocked: stale plan");
}
this.originCell = Object.freeze({ ...plan.target });
this.version += 1;
return { originCell: this.originCell, version: this.version };
}
这里选择完整邻域屏障,代价是相机可能短暂停在旧参考系;收益是切换后可见区的地形、碰撞和实体集合具有同一准备边界。允许边加载边提交会缩短等待,却把缺块处理扩散到渲染、物理和玩法查询,不适合当前单一相机与九格活跃区。
正常时序
观察者位于 (4096,-2048)+(12.5,31.25),实体位于相邻分区。计划检查九格后把原点版本从 0 提交为 1。实体的权威四元组未变,只有渲染派生值从百万量级收敛到 (1124.5,1224.25)。
{
"cellSize": 1024,
"thresholdCells": 2,
"entity": {
"cellX": 4097,
"cellZ": -2047,
"offsetX": 100.5,
"offsetZ": 200.25
},
"before": {
"x": 4195428.5,
"z": -2095927.75,
"originVersion": 0
},
"after": {
"x": 1124.5,
"z": 1224.25,
"originVersion": 1
}
}

正常链路是“观察者越界—邻域就绪—提交版本—派生渲染坐标”。坐标差恰好为 (4096×1024,-2048×1024),因此结果可以从输入复算,而不是一次不可解释的场景截图。
失败保持
失败必须发生在提交之前。第一条回归只提供一个已加载分区,要求缺失邻域时 originCell 与 version 保持原值;第二条重复提交同一计划,要求版本校验拒绝旧计划。两者共同阻止“原点已移动、流送未完成”和并发计划覆盖新状态。
// .tmp/daily-labs/2026-07-30-1930/world-origin.test.js
// symbols: missing neighborhood..., stale plan...
test("missing neighborhood blocks rebase without changing origin", () => {
const coordinator = new OriginCoordinator({
cellSize: 1024,
thresholdCells: 2,
loadedCells: ["8:8"],
});
const observer = new WorldPosition(8, 8, 0, 0);
assert.throws(() => coordinator.planRebase(observer), /missing/);
assert.deepEqual(coordinator.originCell, { x: 0, z: 0 });
assert.equal(coordinator.version, 0);
});
test("stale plan cannot overwrite a newer origin version", () => {
const coordinator = new OriginCoordinator({
cellSize: 1024,
thresholdCells: 2,
loadedCells: neighborhood(4, 4),
});
const plan = coordinator.planRebase(
new WorldPosition(4, 4, 0, 0),
);
coordinator.commitRebase(plan);
assert.throws(() => coordinator.commitRebase(plan), /stale plan/);
assert.deepEqual(coordinator.originCell, { x: 4, z: 4 });
assert.equal(coordinator.version, 1);
});
失败后继续使用旧原点是显式退化策略:渲染坐标可能再次增大,但权威状态仍一致,系统可以等待流送重试。回滚半数对象的坐标既更复杂,也无法证明所有派生缓存已恢复,因此不采用。
运行证据
探针使用 Node.js 内置测试器,无新增依赖。三项测试覆盖正常提交、缺失邻域和过期计划;退出码为 0,通过 3、失败 0、跳过 0,总耗时 149.381125ms。行为探针独立退出 0,并输出上述固定坐标变化。
$ cd $WORK_DIR/.tmp/daily-labs/2026-07-30-1930
$ node --test
✔ rebase preserves absolute position and bounds render coordinates
✔ missing neighborhood blocks rebase without changing origin
✔ stale plan cannot overwrite a newer origin version
ℹ tests 3
ℹ suites 0
ℹ pass 3
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 149.381125
exit_code=0
$ node probe.js
before=(4195428.5,-2095927.75,version=0)
after=(1124.5,1224.25,version=1)
render_shift=(4194304,-2097152)
exit_code=0
演进边界
当前查询邻域的时间与活跃格数量线性相关,本例固定为九次集合查询;没有性能实测,因此只声明该复杂度。单相机、单协调器时,完整屏障最容易审查。出现多相机分屏、服务器跨分区物理、活跃区超过数百格,或一次切换无法在帧边界内重建全部派生缓存时,应演进为按世界分区维护的多参考系和显式 originVersion 快照,而不是缩小阈值掩盖成本。
架构判断是:绝对分区坐标负责“实体在哪里”,浮动原点只负责“这一帧怎样画”。加载屏障保证新参考系可用,版本校验保证计划不越权;这三项合同比对所有对象执行一次减法更重要。