原版静态碰撞四叉树构造

本批交付

补齐 CQuadtreeCollision/CQuadtreeCollisionNode 的根节点构造及逐面插入,再连接已有候选收集和几何扫掠。

  • 846 组三精度原生构造参考,合计 23,238 个节点。
  • 根/子节点边界、拓扑、逐节点面顺序、数组容量及构造对象数一致。
  • 在生成的真实树上运行 2,892 次候选查询,顺序逐项一致。
  • 核心累计 1,412 条断言、238 项相关 Python 回归通过。
  • Godot C# 构建零警告、零错误,coresimlint 通过。
  • Godot 资源探针更新为实际构树,共 22 项检查通过,不再给岩石手填单叶节点。

武器可构造覆盖仍为 1,053/1,419,完整地图世界接入和全武器实机验收没有据此标成完成。

来源函数

原函数作用
5EFE10CQuadtreeCollision:保存原始 bounds、网格及最小宽度,扩展根节点 X/Z 范围
5EF7D0CQuadtreeCollisionNode:记录 bounds/网格,清空子节点和面向量
5EF920逐面插入,按当前面决定分裂和下沉
5EFC80已实现的候选收集,保留树顺序与重复面 ID
468840/7FCA70面数组追加、容量增长与重分配
5F7766–5F77B6关卡装载中的最小节点宽度计算片段

IDA 的 5EF920/5EFE10 伪代码里 Vector3 构造参数明显错位;实际参数和浮点存储顺序以原始汇编及机器码参考为准。

最小节点宽度

5F6250 在 5F73C0 将整数阈值初始化为 1,使用已有关卡 AABB 的 max-min(原 Ogre 向量减法,分量存 f32),随后在 5F7766 计算:

text
extent = f32(bounds.max - bounds.min)
scaled = Ogre.length(extent) * 0.001f   // 保留 x87 精度
cell_integer = scaled > 1 ? trunc(scaled) : 1
minimum_cell_width = f32(uint32(cell_integer))

length 是完整三维长度,包含 Y,不是只看 X/Z,也不是固定 1。

原片段在转换时暂时切换 x87 rounding 为 toward-zero,执行 int64 截断,再使用低 32 位;调用构造器前以 unsigned 规则转换回 float。当前实现明确支持不超出 uint32 的有效输入域,溢出显式诊断,不把低字回绕成零后伪装成有效分裂阈值。

原生参考直接执行这段原指令,到其后续边界返回;不是用 C/Python 重写公式产生预期值。边界输入包含 1000、2000 附近的相邻 float32,以及高 Y、零平面宽度等情况。

根节点范围:5EFE10

外层 wrapper 保存的原始 min/max 不变;根节点用调整后的副本:

text
width = abs(max.x - min.x)
depth = abs(max.z - min.z)
edge = max(width, depth)

if width < edge:
    pad = (edge - width) * 0.5
    min.x = f32(min.x - pad)
    max.x = f32(max.x + pad)

if depth < edge:
    pad = (edge - depth) * 0.5
    min.z = f32(min.z - pad)
    max.z = f32(max.z + pad)

中间量保持原 x87 精度,端点按原位置存 f32;Y 完全不变。普通有序 bounds 因此按 X/Z 正方形范围扩展,最终浮点存储仍可能留下舍入差异。原版不会先交换倒置 min/max,本实现也没有自动“修正”它们。

构造时总会建立一个空根节点,网格没有面也一样。

插入不是按面数分桶

每次 5EF920 插入一个面:

  1. 计算当前节点的 X 宽度 max.x-min.x。
  2. 若尚未分裂、X 宽度严格大于 minimumCellWidth,且当前节点完整包含新面的 X/Z bounds,则分裂。
  3. 分裂时立即建立全部四个子节点。
  4. 依次检查四个子节点,选择第一个完整包含该面 X/Z bounds 的子节点递归插入。
  5. 若没有子节点包含,或当前节点没有分裂,把面 ID 追加到当前节点。

关键点:

  • 没有“达到 N 个面才分裂”的门。
  • 判断是完整包含,不是相交。
  • Y 不参与插入分区。
  • 宽度恰好等于阈值不分裂。
  • 已存储在当前节点的面不会重新分发,即使后来发生分裂。
  • 重复 ID 不去重,输入顺序不排序。
  • 面跨越子节点边界时留在父节点,不复制进多个子节点。
  • 新面不在根内时仍可能保留在根上,不自动扩根、不重建整棵树。

因此不能以一般“桶容量限制 + 自动重平衡”的四叉树替代。

子节点顺序和坐标

先保存:

text
half = f32((max.x - min.x) * 0.5)
midX = f32(min.x + half)
midZ = f32(min.z + half)

两轴都使用 X 宽度的 half,不是分别重算 X/Z 中点。

子节点X 范围Z 范围
0minX → midXmidZ → maxZ
1midX → maxXmidZ → maxZ
2minX → midXminZ → midZ
3midX → maxXminZ → midZ

四个节点保留父节点 Y 范围。包含判断允许边界相等,所以恰好位于中心的点面优先进入子节点 0(低 X、高 Z),不是随机选边。

分两阶段加入关卡几何

5F6250 的调用位置确认:

  • 5F79AF:按初始碰撞网格 face ID 顺序加入。
  • 5F8F9D:条件细节追加产生新面后,从旧面数量开始把新增面加入同一棵树

第二阶段会更新网格 bounds/法线,但此处没有重新调用根构造器。不能把追加细节后的全部面重新构一棵“等价树”;旧面所处节点及最终候选次序可能不同。

NativeCollisionTreeBuilder 支持增量 Insert,Snapshot 返回独立的查询树副本。后续插入不改变此前的快照,但没有将构造中的加载状态包装成已实现的持久化世界状态。

原数组存储

面向量容量从 0 → 1,再翻倍增长;追加时只在 count 达到 capacity 时增长。核心显式设置这一容量策略,没有采用 .NET List 的默认 4 起步。

参考执行原 468840/7FCA70,分配器及 memmove 是明确边界。逐节点容量之和与核心一致。原基础构造 405910 也实际执行,构造计数等于节点数加一个 wrapper。

分配失败、非法指针、非正阈值及浮点边界不再缩小所导致的无限递归不伪造成功;核心有明确输入/深度诊断。报告的等价范围不包括任意损坏内存或原 FPU exception/status 标志。

实际资源和 Godot 接入验证

核心以原 ROCK_03_COLLISION 42 个顶点/14 个面建立 5 节点树,再用实际树候选驱动索引 MeshSweep,14 组定向扫掠命中通过。

Godot 探针则使用真实节点的受控姿态(含负缩放),由原碰撞资源计算世界 bounds 和变换后的逐面 bounds,实际构出 9 个节点,minimumCellWidth=1。

覆盖整个 X/Z、但 Y 很远的查询返回:

text
0,1,2,4,5,6,7,8,9,10,11,12,13,3

面 3 因树位置最后访问,说明真实候选并不等于简单 face-ID 顺序。运行时保留该顺序;资源测试中的排序仅用于断言“14 个面各出现一次”,不修改传出的候选列表。

该探针仍不装载正式游戏场景逻辑、不改默认存档。材质和相机配置没有变,本批 22 项 GPU/资源检查不是新的像素一致性验收。

产物与证据边界

  • core_sim/NativeCollisionTreeBuilder.cs。
  • core_sim.tests/NativeCollisionTreeChecks.cs。
  • tools/build_native_collision_tree_oracle.py、native_collision_tree_oracle.c。
  • tests/test_native_collision_tree.py。
  • torchlight-2-poc/NativeVisualBackendProbe.cs 更新为实际构树。
  • build/native_collision_tree_oracle/results.json。
  • build/native_collision_tree_mismatches.json(空)。
  • build/native_collision_tree_core.log、native_collision_tree_python.log、native_collision_tree_godot.log、native_collision_tree_visual.log。

参考使用明确的原始 bounds/逐面 bounds/插入次序及分配存储,执行原构造、插入、数组操作和查询;没有给定预制树拓扑。

EXE SHA256: 186472c3057b38f4cdff4696959a943c396ae6166995f7418997b5ea853e8a5e

OgreMain.dll SHA256: 974c1dc77ea2818276cc7eb94a8b95b2ccf8595e28fa3f14b2cd6d516fdb67a1

下一批

后续进展:碰撞属性与角色/物品链表接线已导入 7731 份 UNIT 默认,并更正“列表类型未知”的旧状态;下面保留本篇交付时的下一步,完整地图对象生命周期仍需继续。

原四叉树构造本身不再是未实现项。接下来核实两条初始碰撞对象链的生产者、关卡 bounds 的实际来源与地图世界上下文,把构造/查询用于正式导航贡献及攻击遮挡。

正式世界注册、缺导出资产、动画和攻击型 proc、全部武器实机验收仍待完成。不能因为单资源或合成世界参考全部通过就声称全地图、全武器已齐全。

本批按 godot-master 的分层要求把树构造放在核心层,Godot 只提供真实节点与碰撞数据。main 原地工作,未提交、未 attach 原游戏,快照 v29 未改。默认存档 SHA256 保持: 9840fe858b6e311c73f8c2d4ed49912d59d71365f6278392811730e012d32a7f