14 KiB
资产生成
适用:改动 Blender 侧的几何构建、材质、实例化,或往场景里加新资产。 贯穿全篇的约束是两条:压低对象数(GLB 要能在浏览器里跑)和 构建必须确定性(parity 校验的前提)。
MeshBatch:几何构建的主力
mesh.py:1-9 说明了它为什么存在:场景的绝大部分是平面多边形和拉伸棱柱,
把它们批进一个 mesh datablock 能同时压低 Blender 对象数和导出 glTF 的节点数。
用法固定为「累积 → 一次 finish()」:
batch = MeshBatch("Lake Surface", water_c, water_mat)
batch.add_polygon(ring, 0.10)
batch.finish() # water.py:12-14
三条内建行为
- 自动去掉重复的闭合点(
mesh.py:41-42, 55-56)。传闭合环或开放环都行, 与geom.py的宽容度一致 - 退化输入静默返回:
len(ring) < 3直接 return,不抛 - 空批次
finish()返回None(mesh.py:66-67),不产生空对象
名字前缀是承重的
# 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 索引的地方。
便捷包装
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, 默认值)——加新的可选字段不要改已有条目。
加一种材质
catalog.MATERIALS末尾追加一个条目(顺序决定 GLB 材质索引)- 若
kind是textured,贴图放assets/textures/(materials.py:14的TEXTURE_ROOT) generate_scene.py里用material_from_spec(catalog.MATERIALS["<key>"])取- 若这个材质在 Cesium 里需要调色,把
cesium子契约写在同一个MATERIALS条目里。materials.from_spec()会把它序列化到material["cesium_export"],导出器优先读这个属性
颜色是线性 RGB
catalog 里的 color 是 Blender 的线性值,不是 sRGB hex,也不从
scripts/lib/scene-layers.js 换算。两套配色独立调过,理由见
图层表。
实例化:树
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 序列选变体:
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 到灌木密集区域时明显卡顿。当前预算是:
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:
# 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 校验永久性地红。
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 个数字 mesh(0-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 是这一约定的实例,不参与区域
构建阶段。