# 资产生成 > 适用:改动 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 里需要调色,把 `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_` 地面覆盖,再沿边界实例化 `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 nodes,scrub 地面覆盖仍保留。旧高模 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` 兼容回退。新材质不要只写旧表。 ### 为什么新资产总是"发黑" `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 材质索引整体平移 | ## 第三方资产导入的源文件边界 第三方 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):改完怎么验证产物没变