Refactor Cesium material export contract

This commit is contained in:
2026-08-03 11:52:38 +08:00
parent 50dc5f4e5a
commit 0635c09458
16 changed files with 583 additions and 95 deletions

View File

@@ -78,8 +78,9 @@ catalog.MATERIALS 声明「是什么」 纯 Python无 bpy
2.`kind``textured`,贴图放 `assets/textures/``materials.py:14`
`TEXTURE_ROOT`
3. `generate_scene.py` 里用 `material_from_spec(catalog.MATERIALS["<key>"])`
4. **若这个材质在 Cesium 里需要调色,去 `export_cesium.py` 的四张表加**——
见下文,那边按材质名字符串匹配
4. **若这个材质在 Cesium 里需要调色,`cesium` 子契约写在同一个
`MATERIALS` 条目里**。`materials.from_spec()` 会把它序列化到
`material["cesium_export"]`,导出器优先读这个属性
### 颜色是线性 RGB
@@ -160,18 +161,24 @@ tilt_y = TILT_JITTER * math.cos(index * 0.927295)
模型保持在**局部 ENU 坐标系**X 东、Y 北、Z 上),靠伴生 JSON 配合
`Cesium.Transforms.eastNorthUpToFixedFrame` 摆放。
### 四张覆盖表(按材质名字符串)
### Cesium contract
| 表 | 位置 | 作用 |
|---|---|---|
| `EXPORT_TINTS` | `:68` | 往某个颜色混合 |
| `EXPORT_METALLIC_OVERRIDES` | `:80` | 平铺的金属度覆盖 |
| `EXPORT_BASE_COLOR_OVERRIDES` | `:86` | 直接替换基色 |
| `EXPORT_EMISSION_OVERRIDES` | `:95` | 自发光兜底 |
新生成场景的 Cesium 导出调色写在 `catalog.MATERIALS[*]["cesium"]`,由
`materials.from_spec()` 保存成材质自定义属性 `material["cesium_export"]`。导出器打开
`.blend` 后优先读这个属性,不 import `catalog`
⚠️ `export_cesium.py` **不 import `catalog`**,靠材质名字符串匹配。
`catalog.CESIUM_EXPORT` 是**死代码**。改材质名前先读
[模块结构](./module-structure.md#已知现状两个入口靠材质名字符串对接)。
`cesium` 子契约字段:
| 字段 | 作用 |
|---|---|
| `tint` | diffuse 贴图导出前往目标颜色混合 |
| `metallic` | 覆盖导出 PBR metallic |
| `base_color` | 直接替换导出基色,并禁用 diffuse/normal 图 |
| `emission` | 自发光兜底 |
`export_cesium.py` 仍保留 `EXPORT_TINTS``EXPORT_METALLIC_OVERRIDES`
`EXPORT_BASE_COLOR_OVERRIDES``EXPORT_EMISSION_OVERRIDES` 四张按材质名字符串匹配的表,
但它们只是旧 `.blend` 兼容回退。新材质不要只写旧表。
### 为什么新资产总是"发黑"
@@ -181,8 +188,8 @@ tilt_y = TILT_JITTER * math.cos(index * 0.927295)
> 带肋墙面往白混 86%、建筑自发光 0.18。一个没调过的新资产是唯一如实渲染的东西,
> 放在旁边就显得发黑。
所以**加新资产时,"它在 Blender 里看着对"不代表在 Cesium 里对**,必须去四张表里
给它配一份调校。
所以**加新资产时,"它在 Blender 里看着对"不代表在 Cesium 里对**,必须
`catalog.MATERIALS[*]["cesium"]`给它配一份调校。
抠图植被走的是另一套(`FOLIAGE_ALBEDO_GAIN = 2.1` + `FOLIAGE_SATURATION = 1.75`
`:55, 66`),用**增益**而不是 tint——因为那是一张同时装着叶片、树皮、果实的图集

View File

@@ -13,7 +13,7 @@
| 新建模块、挪代码、加一种 OSM 要素 | [模块结构](./module-structure.md) ← **先确认放在哪一层** |
| 改几何构建、材质、实例化、Cesium 调色 | [资产生成](./asset-generation.md) |
| 改 `geom.py` / `osm.py` 或加纯函数 | [测试](./testing.md) |
| 改材质名、动 `MATERIALS` 顺序 | [模块结构 · 材质名对接](./module-structure.md#已知现状两个入口靠材质名字符串对接) |
| 改材质名、动 `MATERIALS` 顺序 | [模块结构 · 材质名对接](./module-structure.md#材质名对接新场景靠自定义属性旧场景靠回退表) |
| 声称"纯重构,产物不变" | [产物一致性指南](../guides/artifact-parity-guide.md) |
---

View File

@@ -135,31 +135,52 @@ argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
---
## ⚠️ 已知现状:两个入口靠材质名字符串对接
## 材质名对接:新场景靠自定义属性,旧场景靠回退表
**这是当前实际状态,不是设计目标。改动材质名之前必读。**
`export_cesium.py` **不 import `catalog`**,这是刻意边界:导出器消费 `.blend`
里保存的材质事实,而不是用当前源码里的 catalog 按材质名反查。
`export_cesium.py` **不 import `catalog`**(它只 import
`from osmassets.materials import link_alpha_clip`)。它自己维护四张以**材质名字符串**
为键的覆盖表:
新生成场景的数据流是:
```
catalog.MATERIALS[*]["cesium"] # 纯 Python 数据,无 bpy
materials.from_spec() # 写 material["cesium_export"] JSON 字符串
<area>.blend # 契约随场景文件保存
export_cesium.py # 优先读 material custom property
```
`cesium` 子契约可携带:
| 字段 | 作用 |
|---|---|
| `tint` | diffuse 贴图导出前往目标颜色混合 |
| `metallic` | 覆盖导出 PBR metallic |
| `base_color` | 直接覆盖导出材质 base color并禁用 diffuse/normal 图 |
| `emission` | 设置导出材质自发光 |
为了兼容旧 `.blend``export_cesium.py` 仍保留四张以**材质名字符串**为键的回退表:
| 表 | 位置 |
|---|---|
| `EXPORT_TINTS` | `export_cesium.py:68` |
| `EXPORT_METALLIC_OVERRIDES` | `:80` |
| `EXPORT_BASE_COLOR_OVERRIDES` | `:86` |
| `EXPORT_EMISSION_OVERRIDES` | `:95` |
| `EXPORT_TINTS` | `export_cesium.py` |
| `EXPORT_METALLIC_OVERRIDES` | `export_cesium.py` |
| `EXPORT_BASE_COLOR_OVERRIDES` | `export_cesium.py` |
| `EXPORT_EMISSION_OVERRIDES` | `export_cesium.py` |
后果
约束
- ** `catalog.MATERIALS` 里的 `name` 会静默断开这些覆盖**。没有任何校验,
材质只是悄悄退回未调过的样子
- `catalog.CESIUM_EXPORT``catalog.py:142`**是死代码**——定义了但全仓无人引用。
它是一次未完成的迁移,不要以为改它会生效
- **新材质的 Cesium 调色写在 `catalog.MATERIALS[*]["cesium"]`,不要只加到四张回退表**。
否则新生成的 `.blend` 不会自带契约
- `catalog.py` 仍属纯 Python 层,不能 import `bpy`;序列化发生在 `materials.py`
- `"Office White Metal Facade"` 在四张表里都有,但 `catalog` 里**已无此材质**
`docs/refactor-plan.md` 记为缺陷 D1,本轮只记录不修)
`docs/refactor-plan.md` 记为缺陷 D1)。它只能作为旧 `.blend` 回退兼容存在,
不要迁回新契约源
**改材质名时**四张表 + `catalog.MATERIALS` + `catalog.ROAD_LAYERS` 全部 grep 一遍。
**改材质名时**`catalog.MATERIALS` + `catalog.ROAD_LAYERS` + 四张旧回退表全部
grep 一遍;新生成场景靠 `cesium_export` 属性,旧场景仍靠回退表。
---
@@ -172,8 +193,8 @@ argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
| 删掉 `sys.path.insert` 样板或 `# noqa: E402` | Blender 里 import 不到 osmassets |
| 要素模块假设 ring 已裁剪 | 越界几何进场景 |
| 要素模块对退化输入抛异常 | 一个坏多边形中断整片区域 |
| 改材质名只改一处 | Cesium 侧调色静默失效 |
| 以为改 `catalog.CESIUM_EXPORT` 会影响导出 | 它是死代码 |
| 改材质名只改一处 |`.blend` Cesium 回退调色可能静默失效 |
| 只改四张旧回退表,不改 `MATERIALS[*]["cesium"]` | 新 `.blend` 不会携带 Cesium 导出契约 |
---

View File

@@ -122,7 +122,7 @@ capturedAt / durationMs / label
|---|---|
| 挪函数、拆模块、改导入 | **必须**——纯重构的定义就是产物不变 |
| 调整 `ROAD_LAYERS` / `MATERIALS` 的**顺序** | **必须**——会平移 GLB 材质索引 |
| 改材质名 | **必须**——可能静默断开 Cesium 侧的四张覆盖表 |
| 改材质名 | **必须**——可能影响新 `.blend``cesium_export` 契约或旧 `.blend` 的回退表 |
| 改几何构建、采样、实例化逻辑 | **必须** |
| 改 stage 的 stdout 打印 | **必须**——标记本身是契约 |
| 改 `.trellis/` 下的文档 | 不用 |
@@ -164,13 +164,13 @@ capturedAt / durationMs / label
| P0 | 抽纯函数到 `osmassets/{osm,geom}.py` | ✅ 已完成 |
| P1 | `catalog.py` 单一定义源 + `check_layers` | ✅ 已完成 |
| P2 | 要素注册表 | ⚠️ **部分**——`water/grass/scrub/tree.py` 已拆出,但**没有 `features/` 注册表**`building` / `fountain` / `roads` 仍在 `generate_scene.py` 里 |
| P3 | 材质契约化(自定义属性传递 spec | **未做**——`export_cesium.py` 仍不 import `catalog`,靠四张材质名表;`catalog.CESIUM_EXPORT` 是死代码 |
| P3 | 材质契约化(自定义属性传递 spec | ✅ 已完成——`catalog.MATERIALS[*]["cesium"]``materials.from_spec()` 写入 `material["cesium_export"]``export_cesium.py` 优先读该属性;四张材质名表仅作旧 `.blend` 回退 |
### 已知缺陷(记录在案,本轮不修)
| # | 位置 | 现象 |
|---|---|---|
| D1 | `export_cesium.py:74,82,91,100` | `"Office White Metal Facade"` 四张表里都有,但 `catalog` 里已无此材质——死条目 |
| D1 | `export_cesium.py` 旧回退表 | `"Office White Metal Facade"` 四张表里都有,但 `catalog` 里已无此材质——只作为旧 `.blend` 回退兼容保留 |
| D2 | `scene-layers.js` vs `catalog.py` | 同一批图层的颜色两侧各自手调,无一致性保证(**这是刻意的**,见[图层表](../pipeline/layer-registry.md#为什么颜色刻意不同步) |
| D3 | `generate_scene.py` `tuft_density_wave` | 注释仍在跟已删除的 hedge banding 作对比 |

View File

@@ -55,8 +55,11 @@ Blender 高度与线性颜色。两侧靠 `catalog.check_layers()` 对账集合
Blender 内材质声明集中在 `catalog.MATERIALS``catalog.py:56`)。
真实 `bpy.types.Material``materials.from_spec()``materials.py:198`)构建。
注意当前有一个未完成迁移:`export_cesium.py:68``:80``:86``:95` 的四张表
仍按材质名字符串匹配。改材质名时不能只改 `catalog`;必须全仓 grep 材质名。
Cesium 导出调色也属于同一个材质声明:新场景把 `catalog.MATERIALS[*]["cesium"]`
序列化到 `material["cesium_export"]``export_cesium.py` 优先读这个属性。四张
`export_cesium.py` 材质名表仍存在,但只作为旧 `.blend` 的兼容回退。
改材质名时不能只改 `catalog`;必须全仓 grep 材质名,确认新契约和旧回退路径都合理。
---
@@ -155,4 +158,4 @@ Blender 内材质声明集中在 `catalog.MATERIALS``catalog.py:56`)。
| 低层脚本直接读 `config/areas/*.json` | 两层配置边界失效 |
| 只改一份 `parseArgs` 的语义 | 三个入口行为分裂 |
| 把要素模块裁剪逻辑挪到调用方 | 不同要素的越界处理开始漂移 |
| 只 `catalog.CESIUM_EXPORT` 加导出覆盖 | 当前不会生效;导出器没读它 |
| 只 `export_cesium.py` 的旧回退表,不写 `MATERIALS[*]["cesium"]` | 新 `.blend` 不会携带 Cesium 导出契约 |

View File

@@ -18,7 +18,7 @@ build-osm2streets-qgis.js Node + osm2streets WASM
ogr2ogr / ogrinfo / QGIS Python GDAL/QGIS 运行时
↓ ④ GeoJSON / GeoPackage 文件
generate_scene.py Blender 内嵌 Python
↓ ⑤ .blend 文件 + 材质名字符串
↓ ⑤ .blend 文件 + 材质自定义属性
export_cesium.py Blender 内嵌 Python
↓ ⑥ GLB + JSON
cesium-preview.js 浏览器
@@ -30,7 +30,7 @@ cesium-preview.js 浏览器
| ② | 两层配置 | 低层脚本读错配置源 |
| ③ | Node → 外部进程 | 环境变量缺失、退出码与信号、0 字节产物 |
| ④ | 文件交换 | 图层集合/顺序漂移、精度丢失 |
| ⑤ | Python → Python | **材质名字符串**,无校验 |
| ⑤ | Python → Python | 新场景靠 `material["cesium_export"]`,旧场景靠材质名回退表 |
| ⑥ | Blender → 浏览器 | 坐标系约定、材质在两种光照下的差异 |
---
@@ -65,7 +65,7 @@ cesium-preview.js 浏览器
| 边界 | 靠什么连接 | 有没有校验 |
|---|---|---|
| `scene-layers.js``catalog.py` | 图层 `id` 的集合与顺序 | ✅ `check_layers()`warn |
| `generate_scene.py``export_cesium.py` | **材质名字符串** | ❌ **无** |
| `generate_scene.py``export_cesium.py` | `.blend` 材质自定义属性 `cesium_export` | 部分(旧 `.blend` 仍靠材质名回退表) |
| GeoJSON 文件名 ↔ 图层 id | `layerFile()``<id>.geojson` | 部分reimport 会检查 gpkg 图层是否齐全) |
| stage stdout ↔ `parity.js` | `SCENE_DONE` / `CESIUM_EXPORT_DONE` 字面量 | ❌ 无 |
| GLB 材质索引 ↔ `MATERIALS` 顺序 | 隐式的创建顺序 | ❌ 无(靠 parity 事后发现) |
@@ -118,13 +118,17 @@ cesium-preview.js 浏览器
→ [资产生成](../blender/asset-generation.md#为什么新资产总是发黑)
### 坑 5两个 Python 脚本靠字符串对接
### 坑 5新旧 `.blend` 的材质导出契约不同
`export_cesium.py` 不 import `catalog`,靠材质名字符串匹配四张覆盖表。
改个材质名Cesium 侧的调色**静默失效**。`catalog.CESIUM_EXPORT` 想解决这个问题,
但迁移没做完,它现在是死代码。
新生成场景把 Cesium 导出契约写进材质自定义属性 `material["cesium_export"]`
`catalog.MATERIALS[*]["cesium"]``materials.from_spec()``.blend`
`export_cesium.py`。导出器仍不 import `catalog`,这是为了让契约跟着 `.blend`
走,而不是用当前源码按材质名反查。
**教训****字符串键的跨模块耦合必须配一个对账机制**,否则重命名就是定时炸弹。
`.blend` 没有这个属性,所以 `export_cesium.py` 仍保留四张材质名回退表。改材质名时,
新场景和旧场景两条路都要想清楚。
**教训****跨阶段契约必须随产物保存;兼容旧产物的字符串回退也要被审查**。
---
@@ -141,7 +145,8 @@ cesium-preview.js 浏览器
### 加一个材质
- [ ] `catalog.MATERIALS` **末尾**追加(中间插入会平移 GLB 材质索引)
- [ ] 若在 Cesium 里需要调色, `export_cesium.py` 的四张表加
- [ ] 若在 Cesium 里需要调色, `catalog.MATERIALS[*]["cesium"]`,不要只改旧回退表
- [ ] 若要兼容旧 `.blend` 的同名材质,再审查 `export_cesium.py` 四张回退表
- [ ] 跑 parity
### 加一个配置字段

View File

@@ -21,7 +21,8 @@
- [ ]`scripts/lib/scene-layers.js:15``SCENE_LAYERS`
- [ ]`blender/osmassets/catalog.py:28``ROAD_LAYERS``catalog.py:56``MATERIALS`
- [ ]`blender/export_cesium.py:68` 等四张按材质名字符串匹配的覆盖表
- [ ]`catalog.MATERIALS[*]["cesium"]``material["cesium_export"]`
`export_cesium.py` 的旧材质名回退表
- [ ]`build-area.js:74``normalizeAreaConfig()``config/examples/template.json`
- [ ] 改任何 `execFileSync` / `spawnSync` 调起的脚本或参数
- [ ]`SCENE_DONE` / `CESIUM_EXPORT_DONE` 的 stdout 标记
@@ -54,7 +55,9 @@ grep -rn "要改的值" scripts blender config
本仓库跨 JS、Blender Python、浏览器 JS 和 JSON很多连接靠字符串或文件名约定。
例如 `scene-layers.js``catalog.py` 只靠 `id` 集合和顺序对账;
`generate_scene.py``export_cesium.py` 的材质覆盖目前靠材质名字符串,没有自动校验。
`generate_scene.py``export_cesium.py`材质导出契约靠 `.blend` 里的
`material["cesium_export"]` 自定义属性,旧 `.blend` 仍靠 `export_cesium.py`
的材质名回退表。
---

View File

@@ -0,0 +1,5 @@
{"file": ".trellis/spec/blender/index.md", "reason": "Quality check must verify Blender package constraints and runtime boundary compliance."}
{"file": ".trellis/spec/blender/module-structure.md", "reason": "Quality check must verify catalog remains pure Python and material export coupling is correctly updated."}
{"file": ".trellis/spec/blender/asset-generation.md", "reason": "Quality check must verify Cesium material export behavior and foliage special-case preservation."}
{"file": ".trellis/spec/blender/testing.md", "reason": "Quality check must run and interpret the pure Python unittest suite."}
{"file": ".trellis/spec/guides/artifact-parity-guide.md", "reason": "Quality check must evaluate parity evidence and identify unacceptable output drift."}

View File

@@ -0,0 +1,171 @@
# 技术设计Cesium 材质导出契约化
> 对应 `prd.md`。本文件定义边界、数据流、兼容策略和风险控制。
---
## 1. 当前边界
`catalog.py` 是纯 Python 层,只声明材质事实,不能 import `bpy``materials.py`
属于 bpy 层,负责把 catalog spec 变成真实 `bpy.types.Material``export_cesium.py`
打开 `.blend` 后只应消费场景里已有的材质信息。
因此正确边界是:
```
catalog.MATERIALS[*]["cesium"] # 纯数据
materials.from_spec() # bpy 层写 material["cesium_export"]
<area>.blend # 契约随场景文件保存
export_cesium.py # 优先读 material custom property
```
不采用:
```
export_cesium.py -> import catalog -> 按 material.name 反查
```
原因:这仍然依赖材质名字符串,而且旧 `.blend` 应该能在没有新 catalog contract 的情况下导出。
## 2. Contract Shape
材质自定义属性名固定为:
```python
CESIUM_EXPORT_PROPERTY = "cesium_export"
```
JSON payload 建议 shape
```json
{
"tint": [[0.92, 0.94, 0.92], 0.38],
"metallic": 0.0,
"base_color": [0.93, 0.94, 0.91],
"emission": [[0.93, 0.94, 0.91], 0.18]
}
```
字段语义:
| 字段 | 类型 | 语义 | 旧来源 |
|---|---|---|---|
| `tint` | `[[r,g,b], factor]` | diffuse 贴图导出前往目标颜色混合 | `EXPORT_TINTS` |
| `metallic` | number | 覆盖导出 PBR metallic | `EXPORT_METALLIC_OVERRIDES` |
| `base_color` | `[r,g,b]` | 直接覆盖导出材质 base color并禁用 diffuse/normal 图 | `EXPORT_BASE_COLOR_OVERRIDES` |
| `emission` | `[[r,g,b], strength]` | 设置导出材质自发光 | `EXPORT_EMISSION_OVERRIDES` |
缺失字段表示沿用源材质当前值或当前特殊路径。
## 3. Catalog Migration
把四张旧表中的有效条目迁入 `catalog.MATERIALS` 对应项:
- `Grass` -> `MATERIALS["grass"]["cesium"]`
- `Scrub Ground Cover` -> `MATERIALS["scrub"]["cesium"]`
- `Tree Crown*` -> 现有 `tree_crown*["cesium"]` 扩展到完整字段
- `Office White Plaster Facade` -> `building_default`
- `Office Light Flat Roof` -> `building_office_roof`
- `Industrial White Ribbed Facade` -> `building_industrial`
- `Factory Blue Metal Roof` -> `building_industrial_roof`
`Office White Metal Facade` 当前没有 catalog 材质,不迁入新契约。旧表可保留该 key 作为旧
`.blend` 回退兼容。
`catalog.CESIUM_EXPORT` 应消失或变成真实消费路径。优先方案:删除全局 `CESIUM_EXPORT`
把其中仍有效的内容迁入各自 `MATERIALS[*]["cesium"]`
## 4. Material Construction
`materials.py`
1. import `json`
2. 定义同名常量 `CESIUM_EXPORT_PROPERTY = "cesium_export"`,或一个私有 helper
3. `from_spec(spec)` 构造材质后调用:
```python
def apply_cesium_contract(material, spec):
contract = spec.get("cesium")
if contract is None:
material.pop(CESIUM_EXPORT_PROPERTY, None)
return
material[CESIUM_EXPORT_PROPERTY] = json.dumps(contract, sort_keys=True)
```
注意:
- `sort_keys=True``.blend` 摘要更稳定。
- 没有 `cesium` 的材质应清掉旧属性,避免同名材质复用时残留。
- `catalog.py` 仍不 import `bpy`
## 5. Export Consumption
`export_cesium.py`
1. 增加 `cesium_contract(material)`,读取并 `json.loads(material.get("cesium_export", "{}"))`
2. JSON 解析失败时返回 `{}` 并继续旧回退,避免坏旧文件直接崩溃
3. `make_export_material(material)` 开头读取一次 contract
```python
contract = cesium_contract(material)
```
4. 每个覆盖点改成 “contract 优先,旧表回退”:
```python
base_color = contract.get("base_color", EXPORT_BASE_COLOR_OVERRIDES.get(material.name, source_color(material)))
metallic = contract.get("metallic", EXPORT_METALLIC_OVERRIDES.get(...))
emission = contract.get("emission", EXPORT_EMISSION_OVERRIDES.get(material.name))
tint = contract.get("tint", EXPORT_TINTS.get(material.name))
```
5. `cesium_tinted_image()` 可以改为接收 `tint` 参数,避免函数内部再查旧表。
## 6. Foliage Special Case
Alpha cut-out foliage 不在本次 contract 化范围内,原因是它不只是普通材质覆盖,还包含:
- `source_alpha_clipped(material)`
- `alpha_dilated_image(..., gain=FOLIAGE_ALBEDO_GAIN, saturation=FOLIAGE_SATURATION)`
- diffuse texture 连接到 emission color
- `FOLIAGE_EMISSION`
这些逻辑保持原样。普通 contract 的 `emission` 只处理非 alpha-clipped 或程序化材质的平铺
emission overridealpha-clipped 分支仍使用 diffuse-as-emissive texture。
## 7. Validation Strategy
快速检查:
```bash
python3 -m unittest discover blender/tests
```
结构验证:
```bash
node scripts/parity.js capture material-contract-before
node scripts/parity.js capture material-contract-after
node scripts/parity.js compare material-contract-before material-contract-after
```
如果 before capture 已存在于实现前after 在实现后跑。若 compare 报差异:
- `.blend` 摘要多出材质自定义属性 `cesium_export` 是预期差异,但需要确认没有对象、网格、
材质槽顺序、GLB material PBR 参数等非预期变化。
- GLB 摘要应尽量全绿;若发生 PBR 参数变化,优先修实现而不是接受差异。
## 8. Rollback
回滚点明确:
- `catalog.py` 还原 `MATERIALS["cesium"]` 迁移和 `CESIUM_EXPORT` 处理。
- `materials.py` 删除 custom property 写入。
- `export_cesium.py` 恢复旧四张表直接读取。
- spec 更新随代码回滚。

View File

@@ -0,0 +1,5 @@
{"file": ".trellis/spec/blender/index.md", "reason": "Blender layer routing, runtime boundaries, and stage marker constraints for material export changes."}
{"file": ".trellis/spec/blender/module-structure.md", "reason": "Defines catalog/materials/export_cesium ownership and the current material-name string coupling to replace."}
{"file": ".trellis/spec/blender/asset-generation.md", "reason": "Documents material construction, Cesium export behavior, foliage special cases, and parity-sensitive generation rules."}
{"file": ".trellis/spec/guides/artifact-parity-guide.md", "reason": "Required validation guidance for pure refactor changes to .blend/.glb outputs."}
{"file": ".trellis/spec/guides/cross-layer-thinking-guide.md", "reason": "Covers material-name cross-layer coupling and checks for Blender-to-Cesium data contracts."}

View File

@@ -0,0 +1,114 @@
# 执行计划Material export contract
> 对应 `prd.md` / `design.md`。按序执行,避免把视觉调参和契约迁移混在一起。
---
## 前置检查
```bash
python3 ./.trellis/scripts/get_context.py --mode packages
python3 -m unittest discover blender/tests
git status --porcelain
```
可选但推荐:若本机 Blender / 输出产物可用,先采集 before parity
```bash
node scripts/parity.js capture material-contract-before
```
## Step 1 — 迁移 catalog 契约
- [x]`blender/osmassets/catalog.py` 中把有效 Cesium 覆盖迁入对应
`MATERIALS[*]["cesium"]`
- [x] 不把 `"Office White Metal Facade"` 迁入新契约源
- [x] 删除或消解 `catalog.CESIUM_EXPORT` 死代码状态
- [x] 保持 `MATERIALS` 顺序不变,不在中间插入新材质
检查:
```bash
grep -n "CESIUM_EXPORT\\|Office White Metal Facade\\|cesium" blender/osmassets/catalog.py
```
## Step 2 — 材质创建时写自定义属性
- [x]`blender/osmassets/materials.py` 增加 `cesium_export` 写入 helper
- [x] `from_spec()` 对每个 material 应用 helper
- [x]`cesium` 的材质清理旧属性,避免复用同名材质时残留
检查点:
```bash
python3 -m unittest discover blender/tests
```
## Step 3 — 导出器优先读 contract
- [x]`blender/export_cesium.py` 增加 `cesium_contract(material)` helper
- [x] `make_export_material()` 使用 contract 优先、旧表回退
- [x] `cesium_tinted_image()` 改为由调用方传入 tint
- [x] 旧四张表保留为旧 `.blend` 兼容层,并注释说明
- [x] alpha-clipped foliage 分支不做行为重构
检查:
```bash
grep -n "EXPORT_TINTS\\|EXPORT_METALLIC_OVERRIDES\\|EXPORT_BASE_COLOR_OVERRIDES\\|EXPORT_EMISSION_OVERRIDES\\|cesium_contract" blender/export_cesium.py
```
## Step 4 — 验证新 `.blend` 带 contract
用一个区域跑 `blender` 阶段或 parity capture 后,使用 Blender/Python 摘要确认材质自定义属性存在。
可接受的验证方式:
-`blender/tools/scene_digest.py` 输出里看材质 custom property
- 或用 Blender `--background --python-expr` 检查若干材质的 `cesium_export`
目标材质至少覆盖:
- `Grass`
- `Office White Plaster Facade`
- `Industrial White Ribbed Facade`
- `Factory Blue Metal Roof`
- `Tree Crown`
## Step 5 — Spec 更新
- [x] 更新 `.trellis/spec/blender/module-structure.md`
- [x] 更新 `.trellis/spec/blender/asset-generation.md`
- [x] 必要时更新 `.trellis/spec/guides/artifact-parity-guide.md` 的 P3 状态
移除或改写“`catalog.CESIUM_EXPORT` 是死代码 / 改它不会生效 / P3 未做”的当前事实。
## Step 6 — 全量验收
```bash
python3 -m unittest discover blender/tests
node scripts/parity.js capture material-contract-after
node scripts/parity.js compare material-contract-before material-contract-after
git status --porcelain
```
若没有可用 before capture则先在未改动代码上 capture before如果实现已经完成但 before 缺失,
不要伪造 parity 结论,明确记录“未运行 before/after parity”的缺口。
## 回滚点
- Step 1 后如果纯测试失败,回滚 `catalog.py` 迁移。
- Step 3 后如果旧 `.blend` 导出行为不清晰,保留旧表优先,重新收窄 contract 读取范围。
- Parity 若出现非自定义属性以外的 `.blend` / GLB 结构差异,先修实现,不扩大忽略名单。
## 实施记录
- 2026-08-03实现完成。`node scripts/parity.js capture material-contract-before`
`material-contract-after` 均成功。
- `node scripts/parity.js compare material-contract-before material-contract-after`
返回差异 15 项:仅 `.blend` 材质 `props.cesium_export` 新增和 `.blend` bytes 变化;
未出现对象、网格、材质 PBR、GLB 结构或 metadata 差异。
- after snapshot 确认 `Grass``Office White Plaster Facade`
`Industrial White Ribbed Facade``Factory Blue Metal Roof``cesium_export`
`hanyang-block` 还确认 `Tree Crown Dark` / `Tree Crown Light`。默认样本不创建
`Tree Crown`,另用 Blender `--python-expr` 构造 `catalog.MATERIALS["tree_crown"]`
确认该材质也写入 `cesium_export`

View File

@@ -0,0 +1,82 @@
# Material export contract
**类型**refactor · **范围**`blender` · **创建**2026-08-03
---
## Goal
把 Cesium 导出调色/覆盖规则从 `blender/export_cesium.py` 里的材质名字符串表迁移到
`blender/osmassets/catalog.py` 的材质声明中,让 Blender 场景材质自己携带 Cesium
导出契约,降低改材质名时静默断开导出效果的风险。
用户价值:后续新增或重命名材质时,只需要维护 `catalog.MATERIALS` 附近的材质定义;
Cesium 导出器消费 `.blend` 中保存的契约,而不是靠另一份易漂移的字符串表猜测。
## Background
当前实际状态由代码和 spec 共同确认:
- `blender/osmassets/catalog.py:56``MATERIALS` 是材质声明源;`materials.py:198`
`from_spec()` 把 spec 构造成真实 Blender 材质。
- `blender/export_cesium.py:68,80,86,95` 仍维护四张以 `material.name` 为 key 的导出覆盖表:
`EXPORT_TINTS``EXPORT_METALLIC_OVERRIDES``EXPORT_BASE_COLOR_OVERRIDES`
`EXPORT_EMISSION_OVERRIDES`
- `blender/osmassets/catalog.py:142` 已有 `CESIUM_EXPORT`,但 `.trellis/spec/blender/module-structure.md`
记录它是死代码,当前导出器不会读取。
- `"Office White Metal Facade"` 仍出现在 `export_cesium.py:74,82,91,100`,但当前
`catalog.MATERIALS` 已无对应材质;这是 `docs/refactor-plan.md` / spec 记录的 D1。
- `.trellis/spec/guides/artifact-parity-guide.md` 记录 P3 未做:材质契约化仍缺失。
## Requirements
1. `catalog.MATERIALS` 中需要 Cesium 特殊导出的材质必须就地携带 `cesium` 子契约。
该契约覆盖现有四张表的有效行为,包括 `tint``metallic``base_color``emission`
2. `materials.from_spec()` 必须在创建材质时把 `spec["cesium"]` 序列化写入材质自定义属性
`material["cesium_export"]`,使 `.blend` 能保存该契约。
3. `export_cesium.py` 必须优先读取 `material["cesium_export"]` 并据此导出;只有旧 `.blend`
或缺失属性的材质才回退到现有四张表。
4.`.blend` 兼容性必须保留:没有 `cesium_export` 自定义属性时,导出行为不应比当前更少。
5. alpha cut-out foliage 的特殊路径保持现状:`FOLIAGE_ALBEDO_GAIN``FOLIAGE_SATURATION`
`FOLIAGE_EMISSION``alpha_dilated_image()` 和 diffuse-as-emissive texture 逻辑不在本任务重构。
6. 本任务是契约迁移,不是视觉调参;目标是新生成产物的结构摘要尽量与迁移前一致。
7. 本任务不得把 `bpy` 引入 `catalog.py`,不得把 `export_cesium.py` 改成直接 import
`catalog` 再按材质名反查。
8. 任务完成后需要更新 `.trellis/spec/blender/` 中关于当前材质名字符串对接、`catalog.CESIUM_EXPORT`
死代码、P3 未完成状态的说明。
## Out of Scope
- 不修 `docs/refactor-plan.md` 的 D2`scene-layers.js``catalog.py` 颜色不同步已经被 spec
定性为刻意设计。
- 不清理 `generate_scene.py``tuft_density_wave` 注释 D3。
- 不做 P2 features 注册表重构。
- 不改变道路图层顺序、`MATERIALS` 创建顺序、stage stdout 标记或 Cesium preview 运行时。
- 不新增依赖、lint/type 工具链或 CI。
## Acceptance Criteria
- [ ] 新生成的 `.blend` 材质中,已有 Cesium 覆盖规则的 catalog 材质带有
`cesium_export` 自定义属性。
- [ ] `export_cesium.py` 对带 `cesium_export` 的材质优先使用该属性;对缺失属性的旧材质仍走
旧四张表回退。
- [ ] `"Office White Metal Facade"` 不再出现在新契约源中;若旧表保留它,只能作为旧 `.blend`
回退兼容存在,并在代码注释中说明。
- [ ] `catalog.CESIUM_EXPORT` 不再是死代码:要么被移除并迁入 `MATERIALS[*]["cesium"]`
要么被真实读取;最终 spec 与代码描述一致。
- [ ] `python3 -m unittest discover blender/tests` 通过。
- [ ] 对默认 parity 样本执行改动前/改动后 capture + compare如果结构摘要差异存在
必须逐条解释为预期差异或修复到全绿。
- [ ] `.trellis/spec/blender/module-structure.md``.trellis/spec/blender/asset-generation.md`
更新为新的材质导出契约,不再把“改 `catalog.CESIUM_EXPORT` 不会生效”描述为当前事实。
## Key Decisions
- 采用材质自定义属性 `cesium_export` 作为跨 `.blend` 的契约载体,而不是让导出器 import
`catalog`。原因:旧 `.blend` 没有当前 Python catalog 上下文也应该能导出;契约必须跟着场景文件走。
- 回退旧四张表作为兼容层保留到本任务结束,不在同一任务里删除兼容路径。
- 不触碰 foliage alpha 特殊路径,避免把高风险贴图处理与 contract 迁移混在一起。
## Open Questions
无阻塞开放问题。实现前仍需要用户确认本规划摘要并允许进入 `in_progress`

View File

@@ -0,0 +1,33 @@
{
"id": "material-export-contract",
"name": "material-export-contract",
"title": "Material export contract",
"description": "Move Cesium material export overrides from export_cesium.py name tables into catalog-backed material contracts saved on Blender materials.",
"status": "in_progress",
"dev_type": "refactor",
"scope": "blender",
"package": "blender",
"priority": "P2",
"creator": "dingkang",
"assignee": "dingkang",
"createdAt": "2026-08-03",
"completedAt": null,
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [
"blender/osmassets/catalog.py",
"blender/osmassets/materials.py",
"blender/export_cesium.py",
".trellis/spec/blender/module-structure.md",
".trellis/spec/blender/asset-generation.md",
".trellis/spec/guides/artifact-parity-guide.md"
],
"notes": "Complex task. Planning artifacts must be reviewed before task.py start; preserve old .blend export fallback and validate with unittest plus parity evidence.",
"meta": {}
}