FIELD NOTES

NOTE 01 · BUILDING THE FOREST

把森林做成一个可以被验证的东西

地形是怎么烘焙出来的,远景为什么单独处理,以及城镇为什么比战场更难做。

以下内容是开发者的个人记录,涉及内部里程碑、技术选择与失败过程。它描述的是当前状态,不是最终形态,也不构成任何发布承诺。

森林不是「做得好看」就算完成

这个项目里,世界有两种身份。

一种是你看到的森林:光束、苔藓、水声、远处的树冠。 另一种是联机权威眼里的森林:一张锁死的高度场,一个语义哈希,一组整数坐标。

这两者必须分开,否则联机就会出问题。所以森林的每一次改动,我都要先回答一个问题: 它动的是哪一个?


一、地形是烘焙出来的,不是在编辑器里摆出来的

addons/terrain_pipeline/ 是一条确定性的离线烘焙链。它的产出是固定的:

  • 一块连续的 20×20 m 地形网格;
  • 共享的材质权重与 AO 掩码;
  • 简化后的碰撞体;
  • 连通的导航网格;
  • 42 块石板、38 处森林实例、28 处遗迹实例;
  • 26/26 个 Sector MultiMesh 全部带序列化缓冲,108 个变换全部有效且空间上互不重合。

为什么要把这些数字写进文档?因为它们是我判断「这次改动有没有破坏东西」的唯一依据。 地形不是手工摆的,它是脚本跑出来的;同样的输入必须产出同样的结果,否则整条链就没有意义。

v1 的这些文件是哈希钉死、不可变的。要做 v2,就意味着新建 *_v2.gd 文件、 新的运行时开关、新的校验器、新的证据目录——旧的一份字节都不动。


二、地形语义哈希,和那道防火墙

联机权威被钉死在地形 v1 的语义哈希 c1e82575… 上,它被封在 P1 的内容包里, 和内容包哈希 09a0de4a…、森林哈希 54ccd449…、城镇哈希 411b6aaa… 一起构成联机身份。

这意味着:地形 v2 是表现层和 P0B 离线碰撞用的,它不能改变 P1 的权威。 如果哪天真要换,那是一次单独批准的内容包重建,不是「顺手升个级」。

这个哈希被钉在三个地方,而且必须保持一致:

  • p0b_combat_slice_v1.gd 的后端表;
  • p0b_combat_slice_v1_structure_test.gd
  • scripts/automation/validate_p0b_combat_slice_v1.py

三处不一致,校验器直接失败。我加这条约束是因为我确信自己以后一定会忘。


三、地图不该在边缘断掉

森林战场的合法边界是 25 m。这在玩法上没问题,但在画面上很糟: 你会看到世界在某个位置突然结束,后面什么都没有。

我做了一层纯表现的远景。战场那一层是 192×192 m 的不透明地面延续, 铺在锁死的 64×64 m 表现地形之下和之外;另外还有一道有界的透明森林地平线弧, 跟随生产相机的焦点移动,而地面延续保持世界固定。城镇那一层是一张弯曲的、 带 mipmap 的透明森林幕布,挂在既有的 TownPresentation 子树里—— 用 --p1-town-presentation=off 可以整层关掉。

这些远景网格全部关闭阴影和 GI,用各向异性 mipmap 过滤。它们不含碰撞、导航、标签、 权威状态、协议状态或任何玩法效果。

贴图是生成出来的(战场 1024×1024 不透明,城镇 2048×768 带 alpha)。 这里有一条我很在意的纪律:唯一的正向风格输入是那张带哈希的世界基准图。 运行时截图只作为相机、比例和地图边界的诊断输入被字节归档, 不是正向的风格或配色来源。这个区分听起来吹毛求疵,但它决定了风格会不会慢慢漂走。

城镇那张贴图的底边必须完全不透明(0/2048 个透明像素), 而且要通过一条高饱和洋红溢色检测——因为透明抠图很容易在边缘留下彩边, 而这条边正好会贴在玩家视线高度上。


四、城镇比战场难

战场是有边界的、可控的、一次只解决一件事。城镇不是。

城镇里同时发生的事情包括:多名旅人在场、名字要按字节原样显示(Unicode 不能被吞)、 聊天要按纯文本渲染、树冠遮挡要在角色走到树后时淡出、光照要保持可读、 性能还不能塌。

树冠遮挡淡出这一条尤其麻烦。相机遮挡淡化器和地形场景之间有一份硬合同: Modules 子树必须是 MultiMesh、TRANSFORM_3D、启用颜色、没有自定义数据、 单表面 StandardMaterial3D。地形 v2 想换实现可以,但这些节点名和形状必须保留, 否则遮挡淡出会静默失效——不报错,只是某天你发现角色被树挡住了看不见。


五、我现在怎么判断森林「没坏」

不是靠看截图。截图会骗人:DoF 会在静态捕获里没刷新、光照会因为一个不相关的默认值被改掉。 我判断的方式是跑校验器,看它有没有打出 *_OK

  • 启动检查 startup_check.sh
  • 森林战场导出校验(会绑定远景层、树遮挡、遗迹透明度这些资源的确切哈希);
  • P1 内容身份重算 build_p1_content_v1.py --check
  • 结构测试。

只有这些全绿,我才会去看画面。反过来的顺序会让我一次次为了好看的截图接受错误的状态。


森林还在长。城镇的细节、远景的层次、边缘的可读性都还在迭代中, 这一篇写的是它现在的样子。