阶段 2:资产合同 —— 旧 NPZ 导入、坐标/单位、绑定、包容器与工具 - What: shared/scene3d 新增四个模块与两个工具的真实现。digest.py:规范化内容摘要(C 连续小端字节 + 排序 JSON;DIGEST_SCHEMA 版本化),geometry_id(只看 part 角色/顶点/三角形,故意不含法线材质与 LOD 名——改颜色不得让标定失效)、binding_id、field_domain_id(形状、flip_v、source_scale、原点/索引约定、映射内容)、asset_hash(条目摘要 + 去自引用字段的 manifest)。legacy.py:LegacyFingerImporter 按"先单位、后刚体"两步转换(meters_per_source_unit 必填且不得猜、validate_rigid_transform 拒绝含缩放/镜像/非仿射的 4x4);split_at_uv_seams 按 (源顶点, 量化 UV) 拆点,接缝处不平均 UV;vertex_normals 面积加权并给无面顶点有限回退;uv_to_pixel 采用实测约定 x=u*(W-1)、y=(1-v)*(H-1)(flip_v 时);build_binding 生成四点双线性采样表(域内裁剪、零权 tap 丢弃并重分配、未用项 -1/0);uv_correspondence_residual 用 Procrustes 残差把"UV 是否对得上 world_xyz"变成可断言的事实;estimate_meters_per_source_unit 只报告不应用。glb.py:glTF 2.0 子集读取(GLB 头/chunk 校验、accessor/bufferView/stride、节点 TRS 与矩阵烘焙进顶点、法线用逆转置再归一化、缺法线则计算),明确拒绝 Draco/meshopt/量化扩展、稀疏 accessor、外部 buffer、骨骼、形态目标、非 TRIANGLES、越界索引、非有限坐标与超预算三角形;with_housing 把外壳作为独立 part 加入且不动感知几何 id。package.py:.oryscene(zip:envelope.json + manifest.json + sensing/housing *.npy,无 pickle 无代码执行),写入原子(先写 .partial 再替换)、读取逐一校验 magic/envelope 版本/payload 状态/条目路径/manifest 自哈希/逐条目 sha256/dtype/shape/**从包内数组重算 geometry_id 与 binding_id**,加密封套明确报"本版本只开明文包"。tools/scene_assets/validate_scene.py 与 compile_scene.py 真实现(含 --json 报告与退出码:0 通过、2 检查失败、1 参数/文件问题;未给单位时停在 needs_units 并打印实测候选尺度)。测试:shared/tests/test_scene3d_assets.py 33 项(真实 1032 导出与 1005 对比资产的输入样例、接缝/法线/绑定/单位/变换/摘要的失败样例、GLB 往返与拒绝样例)、test_scene3d_package.py 24 项(往返、外壳条目、以及篡改条目/篡改 manifest 哈希/自洽但错误的几何/非有限坐标/多余条目/缺条目/路径穿越/dtype 与 shape 不符/未来版本/加密封套/key provider 的拒绝)。docs/architecture/studio-3d-stages.md 阶段 2 记为完成并附验收结果。 - Why: 阶段 2 要回答的是"这份几何到底是什么、单位是什么、它和哪套 UV 域配套、以及这份包有没有被动过",这些一旦错了会以"三维看起来有点怪"的形式出现且事后无法追溯。因此把 UV 约定做成可测量的事实(真实资产残差 0.0064 vs 错误翻转 0.59)、把单位做成必填参数而不是默认值、把 identity 做成内容摘要并**在读取时重算**,同时用真实资产当输入样例、用篡改后的包当失败样例。 - Verification: 真实资产导入:1032 顶点/1974 三角形/域 300×280/尺寸 15.81×15.0×7.5 mm,validate_assets 通过;UV 残差 0.0064(错翻转 0.59);两套资产混用被拒(geometry_mismatch);缩放/镜像/非仿射变换被拒;compile→validate 端到端 ok(包内条目哈希与 manifest 自哈希全部通过);shared 全量 244 passed;studio 118 passed;ui_check 554 项 0 失败;.repro/verify_assets.py 22 项 0 失败。过程中修掉 5 个真实缺陷:np.load 需要真实文件对象(手写内存 stub 抛空消息异常)、包可以"自洽却错误"(改数组+改哈希后无人重算几何 id,现已重算并比对)、binding 行数不匹配被误报为域错误、np.ptp 平铺比较无意义(换算/不变性断言改为按轴)、以及合成 PoC 场景自己用字符串当 geometry_id(所有人都会抄它,等于绕过检查)。 - Not in this change: 签名与 AES-GCM(阶段 5);LOD 简化;calibration.py 侧的资产哈希登记;三维视图仍未被应用导入(阶段 4 接线)。

This commit is contained in:
ykcheungab
2026-09-14 15:08:29 +08:00
parent cad795c890
commit d0dcac743f
11 changed files with 3065 additions and 58 deletions
+32 -12
View File
@@ -81,23 +81,43 @@ python tools/scene_assets/probe_gl.py --profile desktop33 --output gl-report.jso
---
## 阶段 2:资产合同(几何与坐标)
## 阶段 2:资产合同(几何与坐标)—— 已完成
**做**:`LegacyFingerImporter`(旧 NPZ:`mesh.npz`/`unroll_map_uv_s0.4.npz`/`camera_intrinsics.npz`)、
坐标与单位统一(`meters_per_source_unit` → 刚体 `transform_sensor_from_source`)、
`SurfaceBinding` 的生成与自检、`validate_scene.py`、一份简化外壳 GLB 的编译路径、manifest/schema。
**做**:`legacy.py`(旧 NPZ 导入:接缝拆点、面积加权法线、UV→域绑定、单位与刚体变换、UV 对应性自检)、
`digest.py`(几何/域/资产的内容摘要)、`glb.py`(glTF 2.0 子集读取:烘焙节点变换、法线用逆转置、
拒绝 Draco/骨骼/形态目标/非三角形/外部缓冲)、`package.py`(`.oryscene` zip 容器:manifest + 逐条目
sha256 + 原子写入 + 读取时先验签再相信)、两个工具(`validate_scene.py`、`compile_scene.py`)真实现。
**验收**:
**验收(已通过)**:`shared/tests/test_scene3d_assets.py` 33 项、`test_scene3d_package.py` 24 项,
其中需要真实资产的用例在缺资产时自动 skip。要点:
| 用例 | 通过标准 |
| 用例 | 结果 |
|---|---|
| 拓扑不混用 | 1032 与 1005 两套网格分别加载;错配(binding 顶点数 ≠ 感知面顶点数)被拒 |
| UV 域 | 300×280 与 500×500 两个域的 `shape_hw`/scale/flip_v 各不相同,错配被拒 |
| 采样表 | 每顶点权重和为 1;未用项 `-1`/0;越界下标被拒;接缝不跨域插值 |
| 单位 | 三个已知非共线点在表面与外壳中对齐;米/毫米换算与法线方向正确 |
| 身份 | 改 manifest 任一哈希、改 binding、改 geometry_id 均被拒并给出 `geometry_mismatch`/`field_domain_mismatch` |
| 真实 1032 顶点导出导入并校验 | 1032 顶点 / 1974 三角形 / 域 300×280 / 尺寸 15.81×15.0×7.5 mm |
| UV 约定是**测**出来的而不是猜的 | `x=u·(W−1)`、`y=(1−v)·(H−1)`(flip_v=1)相对残差 **0.0064**;错的翻转 **0.59** |
| 两套资产不得混用 | 1032/300×280 与 1005/1024×1024 分别导入;混用被拒(`geometry_mismatch`,提示 "belongs to a different mesh") |
| 采样表 | 权重和 = 1;未用项 −1/0;像素正中只用一个 tap;边界外的 tap 被丢弃且权重重分配 |
| 单位与变换是两件事 | 缩放/镜像/非仿射变换被拒;刚体平移逐顶点同向量且各轴跨度不变 |
| 身份 | 同一输入同一哈希;改 manifest 哈希、改条目字节、多/少条目、路径穿越、dtype/shape 不符、加密封套**全部被拒**且诊断码正确 |
| **重新计算几何 id** | 换掉顶点数组并把所有哈希改成自洽后,仍被拒——manifest 的 geometry_id 必须等于**包内数组**的摘要 |
| 外壳 GLB | 节点变换烘焙进顶点、法线用逆转置并归一化、缺法线则计算、三角形预算超限报 `gpu_budget_exceeded` |
**产出样例**:一张 `finger` 场景包 + 一份 `validate_scene.py --json` 报告。
`compile_scene.py` → `validate_scene.py` 端到端在本机跑通(见 `.repro` 之外的仓库内工具;
产物示例:1032 顶点包,`geometry_id` 与 `asset_hash` 见提交信息)。
**这一阶段抓到的**:
1. **`np.load` 需要真正的文件对象**:手写的内存 stub 让它抛出一个空消息的异常,换 `io.BytesIO`。
2. **包可以自我一致却是错的**:改数组 + 改哈希后无人重算几何 id,于是"把别的网格贴上你的
geometry_id"能通过全部哈希校验。现在加载器从包内数组重算 sensing 几何 id 与 binding id 并比对。
3. **`np.ptp()` 平铺比较没有意义**(跨轴 max−min):换算与刚体不变性的断言必须按轴比较。
4. **合成场景自己违反了合同**:`poc_scene()` 之前用字符串 `"poc-geometry"` 当 id——所有人都会抄它,
于是那个检查形同虚设;现在 PoC 场景的身份也是内容摘要。
5. **binding 行数不匹配被报成域错误**:1032 行的绑定配 1005 顶点的网格是 *几何* 不一致,
按规格改为 `geometry_mismatch`。
**明确未做**:签名与加密(阶段 5);LOD 简化(第一版用全分辨率绑定);`housing` 的 GLB 目前只支持
单文件、无扩展的子集(按规格如此)。
---