Refactor Cesium material export contract
This commit is contained in:
@@ -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."}
|
||||
171
.trellis/tasks/08-03-material-export-contract/design.md
Normal file
171
.trellis/tasks/08-03-material-export-contract/design.md
Normal 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 override;alpha-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 更新随代码回滚。
|
||||
@@ -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."}
|
||||
114
.trellis/tasks/08-03-material-export-contract/implement.md
Normal file
114
.trellis/tasks/08-03-material-export-contract/implement.md
Normal 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`。
|
||||
82
.trellis/tasks/08-03-material-export-contract/prd.md
Normal file
82
.trellis/tasks/08-03-material-export-contract/prd.md
Normal 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`。
|
||||
33
.trellis/tasks/08-03-material-export-contract/task.json
Normal file
33
.trellis/tasks/08-03-material-export-contract/task.json
Normal 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": {}
|
||||
}
|
||||
Reference in New Issue
Block a user