diff --git a/.trellis/spec/blender/asset-generation.md b/.trellis/spec/blender/asset-generation.md index 2017503..3fa9f5d 100644 --- a/.trellis/spec/blender/asset-generation.md +++ b/.trellis/spec/blender/asset-generation.md @@ -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[""])` 取 -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——因为那是一张同时装着叶片、树皮、果实的图集, diff --git a/.trellis/spec/blender/index.md b/.trellis/spec/blender/index.md index a96de0d..1d9cb9b 100644 --- a/.trellis/spec/blender/index.md +++ b/.trellis/spec/blender/index.md @@ -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) | --- diff --git a/.trellis/spec/blender/module-structure.md b/.trellis/spec/blender/module-structure.md index ef52969..0ea978c 100644 --- a/.trellis/spec/blender/module-structure.md +++ b/.trellis/spec/blender/module-structure.md @@ -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 字符串 + ↓ +.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 导出契约 | --- diff --git a/.trellis/spec/guides/artifact-parity-guide.md b/.trellis/spec/guides/artifact-parity-guide.md index 55fb112..519c68e 100644 --- a/.trellis/spec/guides/artifact-parity-guide.md +++ b/.trellis/spec/guides/artifact-parity-guide.md @@ -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 作对比 | diff --git a/.trellis/spec/guides/code-reuse-thinking-guide.md b/.trellis/spec/guides/code-reuse-thinking-guide.md index 4aa219e..90bd946 100644 --- a/.trellis/spec/guides/code-reuse-thinking-guide.md +++ b/.trellis/spec/guides/code-reuse-thinking-guide.md @@ -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 导出契约 | diff --git a/.trellis/spec/guides/cross-layer-thinking-guide.md b/.trellis/spec/guides/cross-layer-thinking-guide.md index c630c48..774b5b4 100644 --- a/.trellis/spec/guides/cross-layer-thinking-guide.md +++ b/.trellis/spec/guides/cross-layer-thinking-guide.md @@ -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()` 拼 `.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 ### 加一个配置字段 diff --git a/.trellis/spec/guides/index.md b/.trellis/spec/guides/index.md index 7614be5..ec2372a 100644 --- a/.trellis/spec/guides/index.md +++ b/.trellis/spec/guides/index.md @@ -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` +的材质名回退表。 --- diff --git a/.trellis/tasks/08-03-material-export-contract/check.jsonl b/.trellis/tasks/08-03-material-export-contract/check.jsonl new file mode 100644 index 0000000..f219a45 --- /dev/null +++ b/.trellis/tasks/08-03-material-export-contract/check.jsonl @@ -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."} diff --git a/.trellis/tasks/08-03-material-export-contract/design.md b/.trellis/tasks/08-03-material-export-contract/design.md new file mode 100644 index 0000000..8baca91 --- /dev/null +++ b/.trellis/tasks/08-03-material-export-contract/design.md @@ -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"] + │ + ▼ +.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 更新随代码回滚。 diff --git a/.trellis/tasks/08-03-material-export-contract/implement.jsonl b/.trellis/tasks/08-03-material-export-contract/implement.jsonl new file mode 100644 index 0000000..549c8b6 --- /dev/null +++ b/.trellis/tasks/08-03-material-export-contract/implement.jsonl @@ -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."} diff --git a/.trellis/tasks/08-03-material-export-contract/implement.md b/.trellis/tasks/08-03-material-export-contract/implement.md new file mode 100644 index 0000000..d57472f --- /dev/null +++ b/.trellis/tasks/08-03-material-export-contract/implement.md @@ -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`。 diff --git a/.trellis/tasks/08-03-material-export-contract/prd.md b/.trellis/tasks/08-03-material-export-contract/prd.md new file mode 100644 index 0000000..7f67365 --- /dev/null +++ b/.trellis/tasks/08-03-material-export-contract/prd.md @@ -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`。 diff --git a/.trellis/tasks/08-03-material-export-contract/task.json b/.trellis/tasks/08-03-material-export-contract/task.json new file mode 100644 index 0000000..7977d77 --- /dev/null +++ b/.trellis/tasks/08-03-material-export-contract/task.json @@ -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": {} +} \ No newline at end of file diff --git a/blender/export_cesium.py b/blender/export_cesium.py index 485e013..53deadf 100644 --- a/blender/export_cesium.py +++ b/blender/export_cesium.py @@ -22,7 +22,7 @@ _HERE = os.path.dirname(os.path.abspath(__file__)) if _HERE not in sys.path: sys.path.insert(0, _HERE) -from osmassets.materials import link_alpha_clip # noqa: E402 +from osmassets.materials import CESIUM_EXPORT_PROPERTY, link_alpha_clip # noqa: E402 # Marks a material this exporter produced, so a second pass over an instanced @@ -41,11 +41,11 @@ FOLIAGE_EMISSION = 0.25 # exists because the apple atlas is genuinely dark: its green texels average # sRGB (0.249, 0.35, 0.12), a deep forest green, and the bark is darker still. # Rendered at true albedo that is correct — but nothing else in this scene is -# at true albedo. Every other material goes through EXPORT_TINTS (grass mixes -# 72% toward a bright green, the ribbed facade 86% toward white) and -# EXPORT_EMISSION_OVERRIDES (0.18 on the buildings), all hand-tuned against -# Cesium's washed-out default lighting. A new asset dropped in untuned is the -# one thing rendering honestly, and next to the rest it reads as black. +# at true albedo. Every other material goes through Cesium export contracts +# (grass mixes 72% toward a bright green, the ribbed facade 86% toward white, +# 0.18 emission on the buildings), all hand-tuned against Cesium's washed-out +# default lighting. A new asset dropped in untuned is the one thing rendering +# honestly, and next to the rest it reads as black. # # A gain rather than a tint, because a tint is what the other materials use and # it is wrong here: they are single-surface, this is an atlas holding leaves, @@ -65,6 +65,9 @@ FOLIAGE_ALBEDO_GAIN = 2.1 # force on the trunk — bark just becomes a warmer brown, which it should be. FOLIAGE_SATURATION = 1.75 +# Legacy fallback for .blend files created before materials carried their own +# `cesium_export` custom property. New scenes should get these values from +# catalog.MATERIALS[*]["cesium"], serialized by osmassets.materials.from_spec(). EXPORT_TINTS = { "Grass": ((0.12, 0.48, 0.08), 0.72), "Tree Crown Dark": ((0.06, 0.22, 0.05), 0.18), @@ -142,6 +145,18 @@ def source_color(material): return color +def cesium_contract(material): + payload = material.get(CESIUM_EXPORT_PROPERTY) + if not payload: + return {} + try: + if isinstance(payload, str): + payload = json.loads(payload) + except (TypeError, ValueError): + return {} + return payload if isinstance(payload, dict) else {} + + def principled_bsdf(material): if not material.use_nodes: return None @@ -225,8 +240,7 @@ def tinted_image(source, name, tint, factor): return result -def cesium_tinted_image(material, source): - tint = EXPORT_TINTS.get(material.name) +def cesium_tinted_image(material, source, tint): if not tint or not source: return source color, factor = tint @@ -328,8 +342,11 @@ def tree_crown_image(): def make_export_material(material): + contract = cesium_contract(material) result = material.copy() result.name = EXPORT_PREFIX + material.name + if CESIUM_EXPORT_PROPERTY in result: + del result[CESIUM_EXPORT_PROPERTY] result.use_nodes = True nodes = result.node_tree.nodes links = result.node_tree.links @@ -339,14 +356,20 @@ def make_export_material(material): output.location = (520, 0) bsdf = nodes.new("ShaderNodeBsdfPrincipled") bsdf.location = (250, 0) - base_color = EXPORT_BASE_COLOR_OVERRIDES.get( - material.name, source_color(material)) + has_base_color_override = ( + "base_color" in contract or + material.name in EXPORT_BASE_COLOR_OVERRIDES + ) + base_color = contract.get("base_color", EXPORT_BASE_COLOR_OVERRIDES.get( + material.name, source_color(material))) bsdf.inputs["Base Color"].default_value = (*base_color, 1.0) bsdf.inputs["Roughness"].default_value = source_principled_value( material, "Roughness", 0.8) - bsdf.inputs["Metallic"].default_value = EXPORT_METALLIC_OVERRIDES.get( - material.name, source_principled_value(material, "Metallic", 0.0)) - emission = EXPORT_EMISSION_OVERRIDES.get(material.name) + bsdf.inputs["Metallic"].default_value = contract.get( + "metallic", EXPORT_METALLIC_OVERRIDES.get( + material.name, source_principled_value(material, "Metallic", 0.0))) + emission = contract.get( + "emission", EXPORT_EMISSION_OVERRIDES.get(material.name)) if emission: emission_color, emission_strength = emission if "Emission Color" in bsdf.inputs: @@ -365,13 +388,14 @@ def make_export_material(material): if material.name == "Tree Crown": diffuse = tree_crown_image() else: - diffuse = cesium_tinted_image(material, diffuse) + tint = contract.get("tint", EXPORT_TINTS.get(material.name)) + diffuse = cesium_tinted_image(material, diffuse, tint) if alpha_clipped and diffuse is not None: safe_name = material.name.replace(" ", "_") diffuse = alpha_dilated_image( diffuse, f"{EXPORT_PREFIX}{safe_name} Dilated", gain=FOLIAGE_ALBEDO_GAIN, saturation=FOLIAGE_SATURATION) - if material.name in EXPORT_BASE_COLOR_OVERRIDES: + if has_base_color_override: diffuse = None normal = None mapping = None @@ -416,10 +440,9 @@ def make_export_material(material): # Lift the crown out of Cesium's ambient. The preview configures no # environment map, so anything the sun does not hit directly falls to a # weak default spherical-harmonic term — which is why every other - # material here carries an EXPORT_EMISSION_OVERRIDES entry. A crown is - # mostly self-shadowed leaf cards facing away from the sun, so at - # distance it collapses into one dark mass while a sunlit close-up - # still reads fine. + # material here carries an emission override. A crown is mostly + # self-shadowed leaf cards facing away from the sun, so at distance it + # collapses into one dark mass while a sunlit close-up still reads fine. # # Feed the diffuse back in as the emissive texture rather than using a # flat colour: a constant would wash the bark with leaf green, whereas diff --git a/blender/osmassets/catalog.py b/blender/osmassets/catalog.py index 050ca6c..9e70d59 100644 --- a/blender/osmassets/catalog.py +++ b/blender/osmassets/catalog.py @@ -61,12 +61,14 @@ MATERIALS = { "diffuse": "leafy_grass_diff_1k.jpg", "normal": "leafy_grass_nor_gl_1k.jpg", "roughness": 0.92, "scale": 7.0, - "tint": (0.12, 0.48, 0.08), "tint_factor": 0.72}, + "tint": (0.12, 0.48, 0.08), "tint_factor": 0.72, + "cesium": {"tint": ((0.12, 0.48, 0.08), 0.72)}}, "scrub": {"kind": "textured", "name": "Scrub Ground Cover", "diffuse": "leafy_grass_diff_1k.jpg", "normal": "leafy_grass_nor_gl_1k.jpg", "roughness": 0.96, "scale": 15.0, - "tint": (0.085, 0.30, 0.065), "tint_factor": 0.46}, + "tint": (0.085, 0.30, 0.065), "tint_factor": 0.46, + "cesium": {"tint": ((0.08, 0.28, 0.07), 0.28)}}, "fountain_stone": {"kind": "solid", "name": "Fountain Stone", "color": (0.42, 0.45, 0.43), "roughness": 0.72}, @@ -81,25 +83,37 @@ MATERIALS = { "diffuse": "white_plaster_02_diff_1k.jpg", "normal": "white_plaster_02_nor_gl_1k.jpg", "roughness": 0.82, "scale": 4.2, "metallic": 0.0, - "tint": (0.92, 0.94, 0.92), "tint_factor": 0.38}, + "tint": (0.92, 0.94, 0.92), "tint_factor": 0.38, + "cesium": {"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)}}, "building_industrial": {"kind": "textured", "name": "Industrial White Ribbed Facade", "diffuse": "corrugated_iron_03_diff_1k.jpg", "normal": "corrugated_iron_03_nor_gl_1k.jpg", "roughness": 0.56, "scale": 2.4, "metallic": 0.16, - "tint": (0.86, 0.92, 0.94), "tint_factor": 0.68}, + "tint": (0.86, 0.92, 0.94), "tint_factor": 0.68, + "cesium": {"tint": ((0.90, 0.93, 0.91), 0.86), + "metallic": 0.08, + "emission": ((0.90, 0.93, 0.91), 0.18)}}, "building_office_roof": {"kind": "textured", "name": "Office Light Flat Roof", "diffuse": "concrete_floor_02_diff_1k.jpg", "normal": "concrete_floor_02_bump_1k.jpg", "roughness": 0.84, "scale": 5.0, "normal_is_bump": True, - "tint": (0.82, 0.86, 0.88), "tint_factor": 0.35}, + "tint": (0.82, 0.86, 0.88), "tint_factor": 0.35, + "cesium": {"tint": ((0.82, 0.86, 0.88), 0.35), + "base_color": (0.88, 0.90, 0.88), + "emission": ((0.88, 0.90, 0.88), 0.14)}}, "building_industrial_roof": {"kind": "textured", "name": "Factory Blue Metal Roof", "diffuse": "blue_metal_plate_diff_1k.jpg", "normal": "blue_metal_plate_nor_gl_1k.jpg", "roughness": 0.48, "scale": 3.4, "metallic": 0.28, - "tint": (0.03, 0.42, 0.78), "tint_factor": 0.45}, + "tint": (0.03, 0.42, 0.78), "tint_factor": 0.45, + "cesium": {"tint": ((0.08, 0.50, 0.88), 0.58), + "emission": ((0.08, 0.50, 0.88), 0.12)}}, "building_glass": {"kind": "solid", "name": "Office Blue Gray Glass", "color": (0.12, 0.20, 0.24), "roughness": 0.22, "metallic": 0.10}, @@ -117,14 +131,18 @@ MATERIALS = { (0.12, 0.36, 0.08)), "scale": 3.2, "detail": 3.8, "bump_strength": 0.08}, - "cesium": {"tint": ((0.06, 0.22, 0.05), 0.18)}}, + "cesium": {"tint": ((0.06, 0.22, 0.05), 0.18), + "base_color": (0.065, 0.24, 0.055), + "emission": ((0.025, 0.07, 0.02), 0.015)}}, "tree_crown_light": {"kind": "solid", "name": "Tree Crown Light", "color": (0.13, 0.42, 0.09), "roughness": 0.88, "procedural": {"colors": ((0.07, 0.25, 0.05), (0.22, 0.56, 0.13)), "scale": 3.6, "detail": 3.4, "bump_strength": 0.07}, - "cesium": {"tint": ((0.16, 0.42, 0.09), 0.16)}}, + "cesium": {"tint": ((0.16, 0.42, 0.09), 0.16), + "base_color": (0.14, 0.40, 0.085), + "emission": ((0.045, 0.12, 0.03), 0.015)}}, "tree_crown": {"kind": "solid", "name": "Tree Crown", "color": (0.10, 0.36, 0.08), "roughness": 0.88, "procedural": {"colors": ((0.04, 0.18, 0.04), @@ -136,22 +154,6 @@ MATERIALS = { "emission": ((0.04, 0.11, 0.035), 0.02)}}, } -# Cesium-specific overrides that don't have a home in the material system yet: -# metallic overrides (flat values, not materials) and emission overrides for -# colours that export_cesium.py hand-tuned separately. -CESIUM_EXPORT = { - "metallic_overrides": { - "Office White Plaster Facade": 0.0, - "Industrial White Ribbed Facade": 0.08, - }, - "emission_overrides": { - "Office White Plaster Facade": ((0.93, 0.94, 0.91), 0.18), - "Office Light Flat Roof": ((0.88, 0.90, 0.88), 0.14), - "Industrial White Ribbed Facade": ((0.90, 0.93, 0.91), 0.18), - "Factory Blue Metal Roof": ((0.08, 0.50, 0.88), 0.12), - }, -} - def road_material_specs(): """Road layer materials as MATERIALS-shaped specs, in draw order.""" diff --git a/blender/osmassets/materials.py b/blender/osmassets/materials.py index 4af3045..e613160 100644 --- a/blender/osmassets/materials.py +++ b/blender/osmassets/materials.py @@ -6,11 +6,14 @@ the catalog importable by plain Python, and by anything else that wants to read the scene's material definitions without launching Blender. """ +import json import os import bpy +CESIUM_EXPORT_PROPERTY = "cesium_export" + TEXTURE_ROOT = os.path.abspath(os.path.join( os.path.dirname(os.path.abspath(__file__)), "..", "..", "assets", "textures", "polyhaven" @@ -195,15 +198,26 @@ def link_alpha_clip(material, alpha_output, bsdf, cutoff=0.5): material.use_backface_culling = False +def apply_cesium_contract(material, spec): + contract = spec.get("cesium") + if contract is None: + if CESIUM_EXPORT_PROPERTY in material: + del material[CESIUM_EXPORT_PROPERTY] + return material + material[CESIUM_EXPORT_PROPERTY] = json.dumps(contract, sort_keys=True) + return material + + def from_spec(spec): """Build a material from a `catalog.MATERIALS` entry.""" if spec["kind"] == "textured": - return make_textured_material( + material = make_textured_material( spec["name"], spec["diffuse"], spec["normal"], roughness=spec.get("roughness", 0.8), scale=spec["scale"], normal_is_bump=spec.get("normal_is_bump", False), metallic=spec.get("metallic", 0.0), tint=spec.get("tint"), tint_factor=spec.get("tint_factor", 0.0)) + return apply_cesium_contract(material, spec) material = make_material(spec["name"], spec["color"], spec.get("roughness", 0.8), @@ -215,4 +229,4 @@ def from_spec(spec): detail=procedural["detail"], bump_strength=procedural["bump_strength"], object_space=procedural.get("object_space", False)) - return material + return apply_cesium_contract(material, spec)