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

View File

@@ -13,7 +13,7 @@
| 新建模块、挪代码、加一种 OSM 要素 | [模块结构](./module-structure.md) ← **先确认放在哪一层** | | 新建模块、挪代码、加一种 OSM 要素 | [模块结构](./module-structure.md) ← **先确认放在哪一层** |
| 改几何构建、材质、实例化、Cesium 调色 | [资产生成](./asset-generation.md) | | 改几何构建、材质、实例化、Cesium 调色 | [资产生成](./asset-generation.md) |
| 改 `geom.py` / `osm.py` 或加纯函数 | [测试](./testing.md) | | 改 `geom.py` / `osm.py` 或加纯函数 | [测试](./testing.md) |
| 改材质名、动 `MATERIALS` 顺序 | [模块结构 · 材质名对接](./module-structure.md#已知现状两个入口靠材质名字符串对接) | | 改材质名、动 `MATERIALS` 顺序 | [模块结构 · 材质名对接](./module-structure.md#材质名对接新场景靠自定义属性旧场景靠回退表) |
| 声称"纯重构,产物不变" | [产物一致性指南](../guides/artifact-parity-guide.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_TINTS` | `export_cesium.py` |
| `EXPORT_METALLIC_OVERRIDES` | `:80` | | `EXPORT_METALLIC_OVERRIDES` | `export_cesium.py` |
| `EXPORT_BASE_COLOR_OVERRIDES` | `:86` | | `EXPORT_BASE_COLOR_OVERRIDES` | `export_cesium.py` |
| `EXPORT_EMISSION_OVERRIDES` | `:95` | | `EXPORT_EMISSION_OVERRIDES` | `export_cesium.py` |
后果 约束
- ** `catalog.MATERIALS` 里的 `name` 会静默断开这些覆盖**。没有任何校验, - **新材质的 Cesium 调色写在 `catalog.MATERIALS[*]["cesium"]`,不要只加到四张回退表**。
材质只是悄悄退回未调过的样子 否则新生成的 `.blend` 不会自带契约
- `catalog.CESIUM_EXPORT``catalog.py:142`**是死代码**——定义了但全仓无人引用。 - `catalog.py` 仍属纯 Python 层,不能 import `bpy`;序列化发生在 `materials.py`
它是一次未完成的迁移,不要以为改它会生效
- `"Office White Metal Facade"` 在四张表里都有,但 `catalog` 里**已无此材质** - `"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 | | 删掉 `sys.path.insert` 样板或 `# noqa: E402` | Blender 里 import 不到 osmassets |
| 要素模块假设 ring 已裁剪 | 越界几何进场景 | | 要素模块假设 ring 已裁剪 | 越界几何进场景 |
| 要素模块对退化输入抛异常 | 一个坏多边形中断整片区域 | | 要素模块对退化输入抛异常 | 一个坏多边形中断整片区域 |
| 改材质名只改一处 | Cesium 侧调色静默失效 | | 改材质名只改一处 |`.blend` Cesium 回退调色可能静默失效 |
| 以为改 `catalog.CESIUM_EXPORT` 会影响导出 | 它是死代码 | | 只改四张旧回退表,不改 `MATERIALS[*]["cesium"]` | 新 `.blend` 不会携带 Cesium 导出契约 |
--- ---

View File

@@ -122,7 +122,7 @@ capturedAt / durationMs / label
|---|---| |---|---|
| 挪函数、拆模块、改导入 | **必须**——纯重构的定义就是产物不变 | | 挪函数、拆模块、改导入 | **必须**——纯重构的定义就是产物不变 |
| 调整 `ROAD_LAYERS` / `MATERIALS` 的**顺序** | **必须**——会平移 GLB 材质索引 | | 调整 `ROAD_LAYERS` / `MATERIALS` 的**顺序** | **必须**——会平移 GLB 材质索引 |
| 改材质名 | **必须**——可能静默断开 Cesium 侧的四张覆盖表 | | 改材质名 | **必须**——可能影响新 `.blend``cesium_export` 契约或旧 `.blend` 的回退表 |
| 改几何构建、采样、实例化逻辑 | **必须** | | 改几何构建、采样、实例化逻辑 | **必须** |
| 改 stage 的 stdout 打印 | **必须**——标记本身是契约 | | 改 stage 的 stdout 打印 | **必须**——标记本身是契约 |
| 改 `.trellis/` 下的文档 | 不用 | | 改 `.trellis/` 下的文档 | 不用 |
@@ -164,13 +164,13 @@ capturedAt / durationMs / label
| P0 | 抽纯函数到 `osmassets/{osm,geom}.py` | ✅ 已完成 | | P0 | 抽纯函数到 `osmassets/{osm,geom}.py` | ✅ 已完成 |
| P1 | `catalog.py` 单一定义源 + `check_layers` | ✅ 已完成 | | P1 | `catalog.py` 单一定义源 + `check_layers` | ✅ 已完成 |
| P2 | 要素注册表 | ⚠️ **部分**——`water/grass/scrub/tree.py` 已拆出,但**没有 `features/` 注册表**`building` / `fountain` / `roads` 仍在 `generate_scene.py` 里 | | 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#为什么颜色刻意不同步) | | D2 | `scene-layers.js` vs `catalog.py` | 同一批图层的颜色两侧各自手调,无一致性保证(**这是刻意的**,见[图层表](../pipeline/layer-registry.md#为什么颜色刻意不同步) |
| D3 | `generate_scene.py` `tuft_density_wave` | 注释仍在跟已删除的 hedge banding 作对比 | | 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`)。 Blender 内材质声明集中在 `catalog.MATERIALS``catalog.py:56`)。
真实 `bpy.types.Material``materials.from_spec()``materials.py:198`)构建。 真实 `bpy.types.Material``materials.from_spec()``materials.py:198`)构建。
注意当前有一个未完成迁移:`export_cesium.py:68``:80``:86``:95` 的四张表 Cesium 导出调色也属于同一个材质声明:新场景把 `catalog.MATERIALS[*]["cesium"]`
仍按材质名字符串匹配。改材质名时不能只改 `catalog`;必须全仓 grep 材质名。 序列化到 `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` | 两层配置边界失效 | | 低层脚本直接读 `config/areas/*.json` | 两层配置边界失效 |
| 只改一份 `parseArgs` 的语义 | 三个入口行为分裂 | | 只改一份 `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 运行时 ogr2ogr / ogrinfo / QGIS Python GDAL/QGIS 运行时
↓ ④ GeoJSON / GeoPackage 文件 ↓ ④ GeoJSON / GeoPackage 文件
generate_scene.py Blender 内嵌 Python generate_scene.py Blender 内嵌 Python
↓ ⑤ .blend 文件 + 材质名字符串 ↓ ⑤ .blend 文件 + 材质自定义属性
export_cesium.py Blender 内嵌 Python export_cesium.py Blender 内嵌 Python
↓ ⑥ GLB + JSON ↓ ⑥ GLB + JSON
cesium-preview.js 浏览器 cesium-preview.js 浏览器
@@ -30,7 +30,7 @@ cesium-preview.js 浏览器
| ② | 两层配置 | 低层脚本读错配置源 | | ② | 两层配置 | 低层脚本读错配置源 |
| ③ | Node → 外部进程 | 环境变量缺失、退出码与信号、0 字节产物 | | ③ | Node → 外部进程 | 环境变量缺失、退出码与信号、0 字节产物 |
| ④ | 文件交换 | 图层集合/顺序漂移、精度丢失 | | ④ | 文件交换 | 图层集合/顺序漂移、精度丢失 |
| ⑤ | Python → Python | **材质名字符串**,无校验 | | ⑤ | Python → Python | 新场景靠 `material["cesium_export"]`,旧场景靠材质名回退表 |
| ⑥ | Blender → 浏览器 | 坐标系约定、材质在两种光照下的差异 | | ⑥ | Blender → 浏览器 | 坐标系约定、材质在两种光照下的差异 |
--- ---
@@ -65,7 +65,7 @@ cesium-preview.js 浏览器
| 边界 | 靠什么连接 | 有没有校验 | | 边界 | 靠什么连接 | 有没有校验 |
|---|---|---| |---|---|---|
| `scene-layers.js``catalog.py` | 图层 `id` 的集合与顺序 | ✅ `check_layers()`warn | | `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 图层是否齐全) | | GeoJSON 文件名 ↔ 图层 id | `layerFile()``<id>.geojson` | 部分reimport 会检查 gpkg 图层是否齐全) |
| stage stdout ↔ `parity.js` | `SCENE_DONE` / `CESIUM_EXPORT_DONE` 字面量 | ❌ 无 | | stage stdout ↔ `parity.js` | `SCENE_DONE` / `CESIUM_EXPORT_DONE` 字面量 | ❌ 无 |
| GLB 材质索引 ↔ `MATERIALS` 顺序 | 隐式的创建顺序 | ❌ 无(靠 parity 事后发现) | | GLB 材质索引 ↔ `MATERIALS` 顺序 | 隐式的创建顺序 | ❌ 无(靠 parity 事后发现) |
@@ -118,13 +118,17 @@ cesium-preview.js 浏览器
→ [资产生成](../blender/asset-generation.md#为什么新资产总是发黑) → [资产生成](../blender/asset-generation.md#为什么新资产总是发黑)
### 坑 5两个 Python 脚本靠字符串对接 ### 坑 5新旧 `.blend` 的材质导出契约不同
`export_cesium.py` 不 import `catalog`,靠材质名字符串匹配四张覆盖表。 新生成场景把 Cesium 导出契约写进材质自定义属性 `material["cesium_export"]`
改个材质名Cesium 侧的调色**静默失效**。`catalog.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 材质索引) - [ ] `catalog.MATERIALS` **末尾**追加(中间插入会平移 GLB 材质索引)
- [ ] 若在 Cesium 里需要调色, `export_cesium.py` 的四张表加 - [ ] 若在 Cesium 里需要调色, `catalog.MATERIALS[*]["cesium"]`,不要只改旧回退表
- [ ] 若要兼容旧 `.blend` 的同名材质,再审查 `export_cesium.py` 四张回退表
- [ ] 跑 parity - [ ] 跑 parity
### 加一个配置字段 ### 加一个配置字段

View File

@@ -21,7 +21,8 @@
- [ ]`scripts/lib/scene-layers.js:15``SCENE_LAYERS` - [ ]`scripts/lib/scene-layers.js:15``SCENE_LAYERS`
- [ ]`blender/osmassets/catalog.py:28``ROAD_LAYERS``catalog.py:56``MATERIALS` - [ ]`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` - [ ]`build-area.js:74``normalizeAreaConfig()``config/examples/template.json`
- [ ] 改任何 `execFileSync` / `spawnSync` 调起的脚本或参数 - [ ] 改任何 `execFileSync` / `spawnSync` 调起的脚本或参数
- [ ]`SCENE_DONE` / `CESIUM_EXPORT_DONE` 的 stdout 标记 - [ ]`SCENE_DONE` / `CESIUM_EXPORT_DONE` 的 stdout 标记
@@ -54,7 +55,9 @@ grep -rn "要改的值" scripts blender config
本仓库跨 JS、Blender Python、浏览器 JS 和 JSON很多连接靠字符串或文件名约定。 本仓库跨 JS、Blender Python、浏览器 JS 和 JSON很多连接靠字符串或文件名约定。
例如 `scene-layers.js``catalog.py` 只靠 `id` 集合和顺序对账; 例如 `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": {}
}

View File

@@ -22,7 +22,7 @@ _HERE = os.path.dirname(os.path.abspath(__file__))
if _HERE not in sys.path: if _HERE not in sys.path:
sys.path.insert(0, _HERE) 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 # 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 # 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. # 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 # 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 # at true albedo. Every other material goes through Cesium export contracts
# 72% toward a bright green, the ribbed facade 86% toward white) and # (grass mixes 72% toward a bright green, the ribbed facade 86% toward white,
# EXPORT_EMISSION_OVERRIDES (0.18 on the buildings), all hand-tuned against # 0.18 emission on the buildings), all hand-tuned against Cesium's washed-out
# Cesium's washed-out default lighting. A new asset dropped in untuned is the # default lighting. A new asset dropped in untuned is the one thing rendering
# one thing rendering honestly, and next to the rest it reads as black. # 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 # 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, # 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. # force on the trunk — bark just becomes a warmer brown, which it should be.
FOLIAGE_SATURATION = 1.75 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 = { EXPORT_TINTS = {
"Grass": ((0.12, 0.48, 0.08), 0.72), "Grass": ((0.12, 0.48, 0.08), 0.72),
"Tree Crown Dark": ((0.06, 0.22, 0.05), 0.18), "Tree Crown Dark": ((0.06, 0.22, 0.05), 0.18),
@@ -142,6 +145,18 @@ def source_color(material):
return color 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): def principled_bsdf(material):
if not material.use_nodes: if not material.use_nodes:
return None return None
@@ -225,8 +240,7 @@ def tinted_image(source, name, tint, factor):
return result return result
def cesium_tinted_image(material, source): def cesium_tinted_image(material, source, tint):
tint = EXPORT_TINTS.get(material.name)
if not tint or not source: if not tint or not source:
return source return source
color, factor = tint color, factor = tint
@@ -328,8 +342,11 @@ def tree_crown_image():
def make_export_material(material): def make_export_material(material):
contract = cesium_contract(material)
result = material.copy() result = material.copy()
result.name = EXPORT_PREFIX + material.name result.name = EXPORT_PREFIX + material.name
if CESIUM_EXPORT_PROPERTY in result:
del result[CESIUM_EXPORT_PROPERTY]
result.use_nodes = True result.use_nodes = True
nodes = result.node_tree.nodes nodes = result.node_tree.nodes
links = result.node_tree.links links = result.node_tree.links
@@ -339,14 +356,20 @@ def make_export_material(material):
output.location = (520, 0) output.location = (520, 0)
bsdf = nodes.new("ShaderNodeBsdfPrincipled") bsdf = nodes.new("ShaderNodeBsdfPrincipled")
bsdf.location = (250, 0) bsdf.location = (250, 0)
base_color = EXPORT_BASE_COLOR_OVERRIDES.get( has_base_color_override = (
material.name, source_color(material)) "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["Base Color"].default_value = (*base_color, 1.0)
bsdf.inputs["Roughness"].default_value = source_principled_value( bsdf.inputs["Roughness"].default_value = source_principled_value(
material, "Roughness", 0.8) material, "Roughness", 0.8)
bsdf.inputs["Metallic"].default_value = EXPORT_METALLIC_OVERRIDES.get( bsdf.inputs["Metallic"].default_value = contract.get(
material.name, source_principled_value(material, "Metallic", 0.0)) "metallic", EXPORT_METALLIC_OVERRIDES.get(
emission = EXPORT_EMISSION_OVERRIDES.get(material.name) material.name, source_principled_value(material, "Metallic", 0.0)))
emission = contract.get(
"emission", EXPORT_EMISSION_OVERRIDES.get(material.name))
if emission: if emission:
emission_color, emission_strength = emission emission_color, emission_strength = emission
if "Emission Color" in bsdf.inputs: if "Emission Color" in bsdf.inputs:
@@ -365,13 +388,14 @@ def make_export_material(material):
if material.name == "Tree Crown": if material.name == "Tree Crown":
diffuse = tree_crown_image() diffuse = tree_crown_image()
else: 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: if alpha_clipped and diffuse is not None:
safe_name = material.name.replace(" ", "_") safe_name = material.name.replace(" ", "_")
diffuse = alpha_dilated_image( diffuse = alpha_dilated_image(
diffuse, f"{EXPORT_PREFIX}{safe_name} Dilated", diffuse, f"{EXPORT_PREFIX}{safe_name} Dilated",
gain=FOLIAGE_ALBEDO_GAIN, saturation=FOLIAGE_SATURATION) gain=FOLIAGE_ALBEDO_GAIN, saturation=FOLIAGE_SATURATION)
if material.name in EXPORT_BASE_COLOR_OVERRIDES: if has_base_color_override:
diffuse = None diffuse = None
normal = None normal = None
mapping = None mapping = None
@@ -416,10 +440,9 @@ def make_export_material(material):
# Lift the crown out of Cesium's ambient. The preview configures no # 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 # environment map, so anything the sun does not hit directly falls to a
# weak default spherical-harmonic term — which is why every other # weak default spherical-harmonic term — which is why every other
# material here carries an EXPORT_EMISSION_OVERRIDES entry. A crown is # material here carries an emission override. A crown is mostly
# mostly self-shadowed leaf cards facing away from the sun, so at # self-shadowed leaf cards facing away from the sun, so at distance it
# distance it collapses into one dark mass while a sunlit close-up # collapses into one dark mass while a sunlit close-up still reads fine.
# still reads fine.
# #
# Feed the diffuse back in as the emissive texture rather than using a # 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 # flat colour: a constant would wash the bark with leaf green, whereas

View File

@@ -61,12 +61,14 @@ MATERIALS = {
"diffuse": "leafy_grass_diff_1k.jpg", "diffuse": "leafy_grass_diff_1k.jpg",
"normal": "leafy_grass_nor_gl_1k.jpg", "normal": "leafy_grass_nor_gl_1k.jpg",
"roughness": 0.92, "scale": 7.0, "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", "scrub": {"kind": "textured", "name": "Scrub Ground Cover",
"diffuse": "leafy_grass_diff_1k.jpg", "diffuse": "leafy_grass_diff_1k.jpg",
"normal": "leafy_grass_nor_gl_1k.jpg", "normal": "leafy_grass_nor_gl_1k.jpg",
"roughness": 0.96, "scale": 15.0, "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", "fountain_stone": {"kind": "solid", "name": "Fountain Stone",
"color": (0.42, 0.45, 0.43), "roughness": 0.72}, "color": (0.42, 0.45, 0.43), "roughness": 0.72},
@@ -81,25 +83,37 @@ MATERIALS = {
"diffuse": "white_plaster_02_diff_1k.jpg", "diffuse": "white_plaster_02_diff_1k.jpg",
"normal": "white_plaster_02_nor_gl_1k.jpg", "normal": "white_plaster_02_nor_gl_1k.jpg",
"roughness": 0.82, "scale": 4.2, "metallic": 0.0, "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", "building_industrial": {"kind": "textured",
"name": "Industrial White Ribbed Facade", "name": "Industrial White Ribbed Facade",
"diffuse": "corrugated_iron_03_diff_1k.jpg", "diffuse": "corrugated_iron_03_diff_1k.jpg",
"normal": "corrugated_iron_03_nor_gl_1k.jpg", "normal": "corrugated_iron_03_nor_gl_1k.jpg",
"roughness": 0.56, "scale": 2.4, "metallic": 0.16, "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", "building_office_roof": {"kind": "textured", "name": "Office Light Flat Roof",
"diffuse": "concrete_floor_02_diff_1k.jpg", "diffuse": "concrete_floor_02_diff_1k.jpg",
"normal": "concrete_floor_02_bump_1k.jpg", "normal": "concrete_floor_02_bump_1k.jpg",
"roughness": 0.84, "scale": 5.0, "roughness": 0.84, "scale": 5.0,
"normal_is_bump": True, "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", "building_industrial_roof": {"kind": "textured",
"name": "Factory Blue Metal Roof", "name": "Factory Blue Metal Roof",
"diffuse": "blue_metal_plate_diff_1k.jpg", "diffuse": "blue_metal_plate_diff_1k.jpg",
"normal": "blue_metal_plate_nor_gl_1k.jpg", "normal": "blue_metal_plate_nor_gl_1k.jpg",
"roughness": 0.48, "scale": 3.4, "metallic": 0.28, "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", "building_glass": {"kind": "solid", "name": "Office Blue Gray Glass",
"color": (0.12, 0.20, 0.24), "roughness": 0.22, "color": (0.12, 0.20, 0.24), "roughness": 0.22,
"metallic": 0.10}, "metallic": 0.10},
@@ -117,14 +131,18 @@ MATERIALS = {
(0.12, 0.36, 0.08)), (0.12, 0.36, 0.08)),
"scale": 3.2, "detail": 3.8, "scale": 3.2, "detail": 3.8,
"bump_strength": 0.08}, "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", "tree_crown_light": {"kind": "solid", "name": "Tree Crown Light",
"color": (0.13, 0.42, 0.09), "roughness": 0.88, "color": (0.13, 0.42, 0.09), "roughness": 0.88,
"procedural": {"colors": ((0.07, 0.25, 0.05), "procedural": {"colors": ((0.07, 0.25, 0.05),
(0.22, 0.56, 0.13)), (0.22, 0.56, 0.13)),
"scale": 3.6, "detail": 3.4, "scale": 3.6, "detail": 3.4,
"bump_strength": 0.07}, "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", "tree_crown": {"kind": "solid", "name": "Tree Crown",
"color": (0.10, 0.36, 0.08), "roughness": 0.88, "color": (0.10, 0.36, 0.08), "roughness": 0.88,
"procedural": {"colors": ((0.04, 0.18, 0.04), "procedural": {"colors": ((0.04, 0.18, 0.04),
@@ -136,22 +154,6 @@ MATERIALS = {
"emission": ((0.04, 0.11, 0.035), 0.02)}}, "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(): def road_material_specs():
"""Road layer materials as MATERIALS-shaped specs, in draw order.""" """Road layer materials as MATERIALS-shaped specs, in draw order."""

View File

@@ -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. the scene's material definitions without launching Blender.
""" """
import json
import os import os
import bpy import bpy
CESIUM_EXPORT_PROPERTY = "cesium_export"
TEXTURE_ROOT = os.path.abspath(os.path.join( TEXTURE_ROOT = os.path.abspath(os.path.join(
os.path.dirname(os.path.abspath(__file__)), "..", "..", os.path.dirname(os.path.abspath(__file__)), "..", "..",
"assets", "textures", "polyhaven" "assets", "textures", "polyhaven"
@@ -195,15 +198,26 @@ def link_alpha_clip(material, alpha_output, bsdf, cutoff=0.5):
material.use_backface_culling = False 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): def from_spec(spec):
"""Build a material from a `catalog.MATERIALS` entry.""" """Build a material from a `catalog.MATERIALS` entry."""
if spec["kind"] == "textured": if spec["kind"] == "textured":
return make_textured_material( material = make_textured_material(
spec["name"], spec["diffuse"], spec["normal"], spec["name"], spec["diffuse"], spec["normal"],
roughness=spec.get("roughness", 0.8), scale=spec["scale"], roughness=spec.get("roughness", 0.8), scale=spec["scale"],
normal_is_bump=spec.get("normal_is_bump", False), normal_is_bump=spec.get("normal_is_bump", False),
metallic=spec.get("metallic", 0.0), metallic=spec.get("metallic", 0.0),
tint=spec.get("tint"), tint_factor=spec.get("tint_factor", 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"], material = make_material(spec["name"], spec["color"],
spec.get("roughness", 0.8), spec.get("roughness", 0.8),
@@ -215,4 +229,4 @@ def from_spec(spec):
detail=procedural["detail"], detail=procedural["detail"],
bump_strength=procedural["bump_strength"], bump_strength=procedural["bump_strength"],
object_space=procedural.get("object_space", False)) object_space=procedural.get("object_space", False))
return material return apply_cesium_contract(material, spec)