# 资产生成 > 适用:改动 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[""])` 取 4. **若这个材质在 Cesium 里需要调色,去 `export_cesium.py` 的四张表加**—— 见下文,那边按材质名字符串匹配 ### 颜色是线性 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 未知**,调用方据此回退到程序化 树(`tree.py:277-279`)。 ### 材质在本地重建,不沿用源文件 `tree.py:12-16`:两个 vendored 模型的材质都不能直接用。apple 的贴图 57% 是透明的 (那是叶片卡),没有 alpha-clip 设置的话整个树冠会渲染成一块。 **vendored 资产的材质一律重建**,不要 `append` 源文件的材质。 ### 已删除的第三种 style 有记录 `tree.py:18-25` 记着 `polyhaven` style 被删的原因(LOD1 对象不是整棵树,是给几何节点 散布用的树枝和叶簇,直接种出来是一地树枝,连同 78MB 资产一起删了)。 **这类"试过、不行、为什么"的记录要保留。** 删掉它,下一个人会重新引入同一个资产。 --- ## 确定性:用无理数周期代替 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` 摆放。 ### 四张覆盖表(按材质名字符串) | 表 | 位置 | 作用 | |---|---|---| | `EXPORT_TINTS` | `:68` | 往某个颜色混合 | | `EXPORT_METALLIC_OVERRIDES` | `:80` | 平铺的金属度覆盖 | | `EXPORT_BASE_COLOR_OVERRIDES` | `:86` | 直接替换基色 | | `EXPORT_EMISSION_OVERRIDES` | `:95` | 自发光兜底 | ⚠️ `export_cesium.py` **不 import `catalog`**,靠材质名字符串匹配。 `catalog.CESIUM_EXPORT` 是**死代码**。改材质名前先读 [模块结构](./module-structure.md#已知现状两个入口靠材质名字符串对接)。 ### 为什么新资产总是"发黑" `export_cesium.py:38-54` 记录了这个反复出现的问题: > Cesium 的默认光照偏白,**场景里每一个材质都被手工提亮过**——草往亮绿混 72%、 > 带肋墙面往白混 86%、建筑自发光 0.18。一个没调过的新资产是唯一如实渲染的东西, > 放在旁边就显得发黑。 所以**加新资产时,"它在 Blender 里看着对"不代表在 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 材质索引整体平移 | --- ## 相关 - [模块结构](./module-structure.md):往哪儿放新代码 - [测试](./testing.md):纯几何部分怎么测 - [图层表](../pipeline/layer-registry.md):道路九层的材质从哪来 - [产物一致性指南](../guides/artifact-parity-guide.md):改完怎么验证产物没变