Files
osmWorkflow/.trellis/spec/blender/asset-generation.md

306 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 资产生成
> 适用:改动 Blender 侧的几何构建、材质、实例化,或往场景里加新资产。
> 贯穿全篇的约束是两条:**压低对象数**GLB 要能在浏览器里跑)和
> **构建必须确定性**parity 校验的前提)。
---
## MeshBatch几何构建的主力
`mesh.py:1-9` 说明了它为什么存在:场景的绝大部分是平面多边形和拉伸棱柱,
**把它们批进一个 mesh datablock 能同时压低 Blender 对象数和导出 glTF 的节点数**
用法固定为「累积 → 一次 `finish()`」:
```python
batch = MeshBatch("Lake Surface", water_c, water_mat)
batch.add_polygon(ring, 0.10)
batch.finish() # water.py:12-14
```
### 三条内建行为
1. **自动去掉重复的闭合点**`mesh.py:41-42, 55-56`)。传闭合环或开放环都行,
`geom.py` 的宽容度一致
2. **退化输入静默返回**`len(ring) < 3` 直接 return不抛
3. **空批次 `finish()` 返回 `None`**`mesh.py:66-67`),不产生空对象
### 名字前缀是承重的
```python
# Foliage reads as blobby volume, so it wants smooth normals; the built
# environment wants its facets. The name prefix is the discriminator.
if self.name.startswith("Tree_") or self.name.startswith("Scrub_"):
for polygon in mesh.polygons:
polygon.use_smooth = True # mesh.py:72-77
```
**改植被对象的命名前缀会静默改变着色**。加新的植被类资产时要么沿用
`Tree_` / `Scrub_` 前缀,要么显式扩展这个判断。
### 单材质约束
一个 `MeshBatch` 只挂一个材质(`mesh.py:64`)。需要多材质就开多个 batch——
这也正是[材质顺序决定 GLB 索引](../pipeline/layer-registry.md#顺序是承重的)的地方。
### 便捷包装
`make_prism` / `add_roof``mesh.py:83, 89`)是「单个形体」的一次性包装,内部就是
`MeshBatch` + `finish()`。只放一个形体时用它们,批量累积时直接用 `MeshBatch`
---
## 材质:声明与构建分离
```
catalog.MATERIALS 声明「是什么」 纯 Python无 bpy
▼ materials.from_spec(spec) materials.py:198
真实的 bpy.types.Material 只在 Blender 内
```
这个拆分让 catalog 能被任何不启动 Blender 的工具读取(`materials.py:3-7`)。
### `kind` 选构建器
| `kind` | 走哪条路 | 必填字段 |
|---|---|---|
| `solid` | `make_material()` | `name``color` |
| `textured` | `make_textured_material()` | `name``diffuse``normal``scale` |
`solid` 可以再叠 `procedural`(噪声驱动的基色和凹凸,`from_spec` 里判断)。
可选字段一律 `spec.get(key, 默认值)`——**加新的可选字段不要改已有条目**。
### 加一种材质
1. `catalog.MATERIALS` **末尾追加**一个条目(顺序决定 GLB 材质索引)
2.`kind``textured`,贴图放 `assets/textures/``materials.py:14`
`TEXTURE_ROOT`
3. `generate_scene.py` 里用 `material_from_spec(catalog.MATERIALS["<key>"])`
4. **若这个材质在 Cesium 里需要调色,把 `cesium` 子契约写在同一个
`MATERIALS` 条目里**。`materials.from_spec()` 会把它序列化到
`material["cesium_export"]`,导出器优先读这个属性
### 颜色是线性 RGB
`catalog` 里的 `color` 是 Blender 的线性值,**不是** sRGB hex也**不**从
`scripts/lib/scene-layers.js` 换算。两套配色独立调过,理由见
[图层表](../pipeline/layer-registry.md#为什么颜色刻意不同步)。
---
## 实例化:树
`tree.py:1-8` 的模式——**import 一次 → bake 朝向 → 每棵树只 link 一个轻对象复用
同一个 datablock**
> Nothing is duplicated per tree, so the .blend and the exported GLB carry each
> mesh and each texture exactly once no matter how many trees are planted.
`TreeVariant``(meshes, height, base_z)` 三元组(`tree.py:62-65`
- `height` — 变体自身的高度,目标高度除以它得到缩放系数
- `base_z` — 变体自身的地面线,**取负乘以缩放**就能把树干落到 `z=0`
不管源文件把原点放在哪(`tree.py:301-303`
`assemble()` 返回种植数量,**0 表示模型缺失或 style 未知**,调用方据此回退到程序化
树。
### Shapespark 是当前模型树样式
当前唯一模型树 style 是 `shapespark`,资产来自
`assets/models/custom/shapespark_plants/tree-*/model.gltf`。该目录是从
Shapespark low-poly plants kit 拆分出的单植物 glTF/bin贴图共享在
`assets/models/custom/shapespark_plants/textures/`
`tree.py` 加载 12 个 tree 变体,每个变体 import 一次、bake 成共享 mesh datablock
OSM 树实例只 link 这些 mesh。变体选择必须确定性当前用 stable instance index 经过
golden-turn 序列选变体:
```python
variant = variants[int(((index * GOLDEN_TURN) % 1.0) * len(variants))]
```
拆分 glTF 会让 Blender 生成 `branch-01.001``branch-01.002` 这类重复材质名;
`tree.py` 必须按 base name 去重材质槽,否则 `export_cesium.py` 会为同一张贴图重复
bake Cesium 材质。
### 已删除的模型树 style 有记录
`tree.py:18-25` 记着 `polyhaven` style 被删的原因LOD1 对象不是整棵树,是给几何节点
散布用的树枝和叶簇,直接种出来是一地树枝,连同 78MB 资产一起删了)。
`apple``fattree` style 已在 Shapespark 方案被用户接受后删除apple 需要强抠图、
法线和 Cesium 曝光补偿,远近观感不如 Shapespark 稳定fattree 视觉风格不匹配。
对应资产目录 `assets/models/speedtree/apple_low/``assets/models/lyrog/fattree/`
也已删除。
**这类"试过、不行、为什么"的记录要保留。** 删掉它,下一个人会重新引入同一个资产。
## 灌木边缘实例有性能预算
`generate_scene.py` 的 scrub 不是只有贴地面:`natural=scrub` 会先生成
`Scrub_<way_id>` 地面覆盖,再沿边界实例化
`assets/models/custom/shapespark_plants/bush-03/model.gltf`
`add_scrub_edge_bushes()`),并在大 scrub 面内部稀疏补树。
这些 bush 是共享 mesh 的多 node 实例。共享 mesh 能压 GLB 体积,但 Cesium 近景 Follow
仍要处理每个 node / draw。`nantaizi-lake-innovation-valley` 曾经用
`SCRUB_BUSH_SPACING = 0.82``SCRUB_BUSH_LIMIT_PER_PATCH = 180`,结果一个场景有约
`707` 个 bush nodes车辆 Follow 到灌木密集区域时明显卡顿。当前预算是:
```python
SCRUB_BUSH_SPACING = 1.8
SCRUB_BUSH_LIMIT_PER_PATCH = 60
```
在 nantaizi 上约 `266` 个 bush nodesscrub 地面覆盖仍保留。旧高模 bush 和
rejected `bush_low` 资产已删除:前者太重,后者几何从
`63,762` vertices / `67,536` indices 降到
`4,044` / `4,050`,但用户实测视觉效果不理想,不能作为默认方向。
不要只为了“更满”把间距调回 1m 以下;那是在把预览流畅度换成近景装饰密度。若确实
需要更密的灌木,先做一个视觉可接受的低模 bush或做显式 LOD / instancing 方案,再
跑 nantaizi 的 Blender/Cesium/preview 验证。继续压 GLB 总体体积应考虑贴图尺寸/KTX2
而不是只减面。
---
## 确定性:用无理数周期代替 RNG
这是全仓最容易被无意破坏的约定。`tree.py:290-292`
```python
# Irrational periods stand in for an RNG: no repeat over any realistic
# tree count, and a pure function of the index, so rebuilding an area
# plants the identical forest.
scale_wobble = 1.0 + SCALE_JITTER * math.sin(index * 2.399963)
yaw = ((index * GOLDEN_TURN) % 1.0) * math.tau
tilt_x = TILT_JITTER * math.sin(index * 1.114517)
tilt_y = TILT_JITTER * math.cos(index * 0.927295)
```
黄金角 `GOLDEN_TURN``tree.py:47-49`)让相邻的树朝向永不重复也永不成规律——
一排树看起来像种的,不像盖章盖的。
**规则**:需要"随机"外观时,用 `index` 的纯函数(无理数周期 / 黄金角),
或者收一个显式 `seed``geom.sample_polygon_interior` 就是这么做的)。
**绝不要**用无种子的 `random` 或任何时间相关的量——
重建同一片区域必须得到逐字节相同的结构,否则
[parity 校验](../guides/artifact-parity-guide.md)永久性地红。
---
## Cesium 导出:一层独立的调色
`export_cesium.py:1-9`:创作用的场景刻意使用了一些 Blender 专有节点
(草地 tint、程序化树冠变化而 glTF 的材质词汇小得多。所以导出器
**新建临时的、仅供导出的 PBR 材质**,展 UV把引用到的图片全部内嵌进 GLB。
导出材质带 `EXPORT_PREFIX = "Cesium "` 前缀(`export_cesium.py:30`
这样第二遍扫到实例化网格的共享材质槽时能认出自己的产物、跳过不重复处理。
模型保持在**局部 ENU 坐标系**X 东、Y 北、Z 上),靠伴生 JSON 配合
`Cesium.Transforms.eastNorthUpToFixedFrame` 摆放。
### Cesium contract
新生成场景的 Cesium 导出调色写在 `catalog.MATERIALS[*]["cesium"]`,由
`materials.from_spec()` 保存成材质自定义属性 `material["cesium_export"]`。导出器打开
`.blend` 后优先读这个属性,不 import `catalog`
`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` 兼容回退。新材质不要只写旧表。
### 交通信号倒计时字体
`assets/fonts/7LED-1.ttf` 是项目纳入版本管理的倒计时字体。它的字形是反向轮廓:可见
的 LED 段是字体轮廓里的孔,而不是普通实心文字。因此 Blender 侧不能直接把文字曲线
转成普通填充面(会得到“发光背景+黑色数字”),也不能依赖曲线描边。正确做法是在
`blender/osmassets/traffic_signals.py` 中采样负 Bezier 轮廓,构造带前后盖面的挤出棱柱,
使 LED 段成为实心发光几何。数字 mesh 必须先在 Blender 中单独渲染确认,再进入 Cesium
导出导出器出现“Could not calculate tangents”只表示这些无 UV 的纯色网格没有切线,
不等同于倒计时集合为空或几何失败。
### 共享与拆分动态资产
倒计时数字按 phase group 共享 20 个数字 mesh0-19不要按信号灯复制网格。Cesium
阶段必须生成三个动态 GLB`traffic-signals-dynamic.glb` 只含灯珠,
`traffic-signals-countdown-0.glb``traffic-signals-countdown-1.glb` 分别含两个相位组的
倒计时节点。两个倒计时模型与灯珠模型使用同一个 `modelMatrix`,浏览器只切换当前数字
节点,并给整个倒计时模型设置 `color` + `ColorBlendMode.REPLACE`,从而让字色跟随当前
红/黄/绿相位且不增加每个灯的材质/几何副本。
导出器按完整材质名包含 `Countdown Group 0` / `Countdown Group 1` 判断分组;不能用
集合名的精确相等比较,否则实际材质名 `Traffic Signal Countdown Group 0` 会被误判为
空集合。
### 为什么新资产总是"发黑"
`export_cesium.py:38-54` 记录了这个反复出现的问题:
> Cesium 的默认光照偏白,**场景里每一个材质都被手工提亮过**——草往亮绿混 72%、
> 带肋墙面往白混 86%、建筑自发光 0.18。一个没调过的新资产是唯一如实渲染的东西,
> 放在旁边就显得发黑。
所以**加新资产时,"它在 Blender 里看着对"不代表在 Cesium 里对**,必须在
`catalog.MATERIALS[*]["cesium"]` 里给它配一份调校。
抠图植被走的是另一套(`FOLIAGE_ALBEDO_GAIN = 2.1` + `FOLIAGE_SATURATION = 1.75`
`:55, 66`),用**增益**而不是 tint——因为那是一张同时装着叶片、树皮、果实的图集
往绿色混会把树干也染绿。增益保留色相关系,只把整体曝光抬到和邻居一致。
`FOLIAGE_EMISSION = 0.25` 的职责只是给背光面兜底,**不是主要提亮手段**
`:32-36`)。想让植被更亮就调增益,别调自发光。
---
## 反模式
| 反模式 | 后果 |
|---|---|
| 每个形体建一个对象而不用 `MeshBatch` | 对象数与 glTF 节点数爆炸 |
| 改 `Tree_` / `Scrub_` 命名前缀 | 平滑着色静默失效 |
| 用无种子 `random` 或时间量做抖动 | parity 校验永久红 |
| 每棵树复制一份 mesh/贴图 | .blend 与 GLB 体积按棵数线性膨胀 |
| 直接 append vendored 资产的材质 | alpha-clip 缺失,树冠渲染成一块 |
| 删掉"试过不行"的注释 | 下一个人重新踩同一个坑 |
| 从 `scene-layers.js` 的 hex 换算 Blender 颜色 | 抹掉独立调过的配色 |
| 加新资产不配 Cesium 调色 | Cesium 里显得发黑 |
| 靠调 `FOLIAGE_EMISSION` 提亮植被 | 用错了旋钮,该调 albedo gain |
| 在 `MATERIALS` 中间插入条目 | GLB 材质索引整体平移 |
| 直接用 Cesium `Model.getMaterial().setValue()` 改普通 glTF PBR 材质 | 运行时数字仍保持原色,不能实现相位字色 |
| 每个信号灯各自生成 0-19 全套倒计时 mesh | 节点和几何按信号数量线性膨胀;应按两个 phase group 共享 |
## 第三方资产导入的源文件边界
第三方 Blend 只属于一次性入库工具的输入,不能成为 `build:area` 或 Cesium 预览的运行时依赖。
完成拆分后,仓库必须包含可直接消费的 glTF/bin、共享贴图、manifest 和人工预览;原始下载文件可
以删除。导入工具应将源文件路径作为显式 `--source` 参数manifest 最多保留源文件名作溯源,
不能写死用户 Downloads 目录。
删除原始文件前必须确认入库资产已通过结构验证和人工预览;删除后若需重新导入,必须重新取得
同一 Blend 与其外部贴图。`blender/tools/split_lowpoly_cars.py` 是这一约定的实例,不参与区域
构建阶段。
---
## 相关
- [模块结构](./module-structure.md):往哪儿放新代码
- [测试](./testing.md):纯几何部分怎么测
- [图层表](../pipeline/layer-registry.md):道路九层的材质从哪来
- [产物一致性指南](../guides/artifact-parity-guide.md):改完怎么验证产物没变