大地图浮动原点:绝对分区与渲染坐标的提交契约

开放世界相机越过数千个分区后,直接把实体保存在单一世界浮点坐标中,会让渲染精度与流送生命周期纠缠。本章把位置拆成权威的 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 先验证邻域并捕获 expectedVersioncommitRebase 再原子更新原点和版本。

// .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),因此结果可以从输入复算,而不是一次不可解释的场景截图。

失败保持

失败必须发生在提交之前。第一条回归只提供一个已加载分区,要求缺失邻域时 originCellversion 保持原值;第二条重复提交同一计划,要求版本校验拒绝旧计划。两者共同阻止“原点已移动、流送未完成”和并发计划覆盖新状态。

// .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 快照,而不是缩小阈值掩盖成本。

架构判断是:绝对分区坐标负责“实体在哪里”,浮动原点只负责“这一帧怎样画”。加载屏障保证新参考系可用,版本校验保证计划不越权;这三项合同比对所有对象执行一次减法更重要。