generate_scene.py 从 1271 → 816 行 (-455)
P0: 纯函数搬家
- osmassets/osm.py: parse_osm / Projector / parse_height
- osmassets/geom.py: clip_polygon / point_in_polygon / distance_to_ring / sample_tree_row 等
- blender/tests/test_pure.py: 42 个 unittest (脱离 bpy 运行)
P1: 单一定义源
- osmassets/catalog.py: ROAD_LAYERS + MATERIALS (含 cesium 导出参数)
- 对接 osm2streets_scene_style.json 做图层一致性 warning
- 干掉 road_mats / layer_z / 材质参数三份副本
P2: 要素注册表
- osmassets/{water,grass,scrub}.py: 每个要素一个 assemble() 函数
- build() 中的 if/elif 链收缩为注册表调用
- 计数器集中到 counts 字典
P3: 材质契约化
- catalog.py 扩展 CESIUM_EXPORT 段 (tint/metallic/emission)
- 标记已发现的死条目 Office White Metal Facade (四表各一组)
校验:
- parity.js + scene_digest.py + glb-digest.js 三位一体
- control-1 vs p0/p1/p2a/p2b/p3-counts: 两区域全 PARITY OK
- 42 个纯 Python 测试全部通过
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
6.4 KiB
重构施工计划:osmassets 包化(P0–P3)
临时工作文档。P3 收尾后把结论并入
docs/changelog.md,本文件删除。
目标
把「从 OSM 生成 Blender / Cesium 资产」的逻辑从两个单体脚本里拆成可复用的库,使得:
- 新增一种 OSM 要素 = 新增一个
features/*.py+ 注册一行,不改build() - 道路图层表、材质规格只有一份定义,JS 侧与 Python 侧不再各存一份
export_cesium.py不再靠材质名字符串跟generate_scene.py对接- 纯几何 / 解析逻辑脱离
bpy,可用系统 python 直接测
硬约束:严格产物一致
P0–P3 全程 不改变任何输出。每期结束必须通过 parity 校验,任何差异都要么消除、要么在本文件里逐条记录原因。
已知缺陷(本轮只记录、不修):
| # | 位置 | 现象 |
|---|---|---|
| D1 | export_cesium.py:26,34,40,52 |
"Office White Metal Facade" 四张表里都有,generate_scene.py 里已无此材质——死条目 |
| D2 | scene-layers.js:15 vs generate_scene.py:1017 |
同一批图层的颜色两侧各自手调,无一致性保证 |
| D3 | generate_scene.py:788 |
tuft_density_wave 注释仍在跟已删除的 hedge banding 作对比 |
Parity 工具与基线
outputs/ 已在 .gitignore 中,基线快照放 outputs/_refactor-baseline/,不入库。
| 工具 | 位置 | 作用 |
|---|---|---|
| 场景摘要 | blender/tools/scene_digest.py |
在 Blender 内打开 .blend,输出稳定 JSON:对象名/顶点数/面数/材质槽/自定义属性、材质参数、场景属性 |
| GLB 摘要 | scripts/glb-digest.js |
纯 Node 读 GLB 的 JSON chunk,输出 node/mesh/material 清单与 PBR 参数,附 buffer 字节长度 |
| 驱动 | scripts/parity.sh <label> |
跑 blender+cesium 阶段 → 收集 SCENE_DONE / CESIUM_EXPORT_DONE / 两份摘要 / 渲染 PNG 到 outputs/_refactor-baseline/<label>/ |
先做对照实验(control):用未改动的代码连跑两次,diff 两份摘要。这一步确定哪些字段天然不确定,把这些字段列入忽略名单。没做这步的 parity 校验是假的。
已完成,结论如下(control-1 vs control-2,两区域):
.blend结构摘要两次完全一致 —— 这是主校验信号,可信.blend文件 sha256 不一致:内嵌绝对路径 + 图片打包顺序随哈希表走- 渲染 PNG sha256 不一致:EEVEE 非位级可复现
- GLB 结构(node / mesh / primitive / material / image)两次完全一致,但 accessor 数 399 vs 398、buffer 差 720 字节:glTF 导出器会去重相同 accessor,而
smart_project的 UV 带浮点噪声,一次能去重一次不能
忽略名单(写在 scripts/parity.js:IGNORED_PATHS,附原因):files.{blend,glb,render}.sha256、files.{glb,render}.bytes、glbDigest.{fileBytes,buffers,counts.accessors}。
保留比对的即真正的契约:SCENE_DONE / CESIUM_EXPORT_DONE 标记、.blend 全量结构摘要、GLB 的 node/mesh/material/image 结构、<area>.json 元数据。加上 control-1、control-2 两份基线已落盘。
样本区域:
nantaizi-lake-innovation-valley— 主样本,OSM + osm2streets GeoJSON 齐全hanyang-block— 次样本,只有intermediates产物,需先补跑一次 blender 阶段生成基线
分期
P0 — 抽纯函数(行为零变化)
新建 blender/osmassets/,只搬运、不改逻辑:
| 目标文件 | 从 generate_scene.py 搬入 |
依赖 |
|---|---|---|
osm.py |
tags (94)、parse_osm (99)、Projector (140)、parse_height (506) |
无 bpy |
geom.py |
geometry_rings (406)、feature_in_bounds (418)、clip_polygon (426)、sample_tree_row (513)、polygon_area (688)、point_in_polygon (697)、distance_to_ring (712) |
无 bpy |
generate_scene.py顶部加sys.path引导(--factory-startup下blender/不在sys.path),改为from osmassets import ...- 新增
blender/tests/test_geom.py、test_osm.py,unittest标准库,系统python3直接跑(本机 3.9,避免 3.10+ 语法) - 验收:
python3 -m unittest discover blender/tests通过 + parity 全绿
P1 — 单一定义源
新建 blender/osmassets/catalog.py:
ROAD_LAYERS:id/blender_z/material_name/color,替换generate_scene.py:1017的road_mats与1122的layer_z两份副本MATERIAL_SPECS:目前散在build()里的全部make_material/make_textured_material调用参数- 新增一致性检查:读输出目录里已存在的
osm2streets_scene_style.json,比对图层 id 集合与顺序,不一致则打 warning(不改颜色,改了就破坏 parity → 见 D2)
验收:parity 全绿;手动删一个图层 id 验证 warning 生效。
P2 — 要素注册表
新建 blender/osmassets/features/,每种要素一个模块,导出 SPEC:
water.py natural=water / water=lake
grass.py landuse=grass(含 tuft 散布)
scrub.py natural=scrub
tree.py natural=tree 节点 + natural=tree_row + 两种树风格
building.py building=*(含 roof / windows)
fountain.py amenity=fountain
roads.py osm2streets GeoJSON 图层 + highway 折线回退
scene.py::assemble()遍历注册表;build()收缩为「解析 → assemble → 灯光相机 → 存盘渲染」- 计数器改由注册表汇总,但
SCENE_DONE与scene[...]的键名、顺序保持逐字不变 - if/elif 的匹配顺序是语义的一部分(
building分支在最后),注册表必须保序
验收:parity 全绿 —— 这期风险最高,逐要素分次提交,每次单独跑 parity。
P3 — 材质契约化
catalog.py的材质规格扩展出 cesium 段:tint/metallic/base_color/emissiongenerate_scene.py把规格写进材质自定义属性material["cesium_export"] = json.dumps(spec)export_cesium.py优先读自定义属性;读不到时回落到现有四张名字表(原样保留,含 D1 死条目),保证旧.blend仍能导出且 parity 成立Tree Crown的程序化贴图特例保持不变
验收:parity 全绿;另外用重构前生成的旧 .blend 跑一次导出,确认回落路径可用。
不在本轮范围
- 输出目标可插拔(整场景 / 每要素单独 GLB)——原 P4
build-area.js里 390 行内联 HTML 与手写 glTF 的拆分——原 P4- 上表 D1–D3 的修复