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

15 KiB
Raw Blame History

资产生成

适用:改动 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

三条内建行为

  1. 自动去掉重复的闭合点mesh.py:41-42, 55-56)。传闭合环或开放环都行, 与 geom.py 的宽容度一致
  2. 退化输入静默返回len(ring) < 3 直接 return不抛
  3. 空批次 finish() 返回 Nonemesh.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_roofmesh.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() namecolor
textured make_textured_material() namediffusenormalscale

solid 可以再叠 procedural(噪声驱动的基色和凹凸,from_spec 里判断)。 可选字段一律 spec.get(key, 默认值)——加新的可选字段不要改已有条目

加一种材质

  1. catalog.MATERIALS 末尾追加一个条目(顺序决定 GLB 材质索引)
  2. kindtextured,贴图放 assets/textures/materials.py:14TEXTURE_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 hexscripts/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.001branch-01.002 这类重复材质名; tree.py 必须按 base name 去重材质槽,否则 export_cesium.py 会为同一张贴图重复 bake Cesium 材质。

已删除的模型树 style 有记录

tree.py:18-25 记着 polyhaven style 被删的原因LOD1 对象不是整棵树,是给几何节点 散布用的树枝和叶簇,直接种出来是一地树枝,连同 78MB 资产一起删了)。

applefattree 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.82SCRUB_BUSH_LIMIT_PER_PATCH = 180,结果一个场景有约 707 个 bush nodes车辆 Follow 到灌木密集区域时明显卡顿。当前预算是:

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

# 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_TURNtree.py:47-49)让相邻的树朝向永不重复也永不成规律—— 一排树看起来像种的,不像盖章盖的。

规则:需要"随机"外观时,用 index 的纯函数(无理数周期 / 黄金角), 或者收一个显式 seedgeom.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 摆放。

WGS84 ENU 坐标契约

Blender 中所有经纬度几何必须通过 osmassets.osm.Projector 转换。该转换必须与 Cesium 的 eastNorthUpToFixedFrame(anchor) 使用同一个 WGS84 椭球语义:先将经纬度转换为 ECEF再将相对锚点的向量投影到 East/North 轴。禁止用固定 111320 m/deg 的 equirectangular等距圆柱近似生成场景坐标。

固定米/度近似只会在锚点附近碰巧重合;纬向比例与 WGS84 实际比例不同,误差会随离锚点 距离增长。表现为 Cesium Entity 路线在部分道路居中、在其他道路相对整个 GLB 路面同向 平移。只验证 route 与 GeoJSON 自洽无法发现此问题,必须重新生成 blender,cesium,preview 并在最终 Cesium 画面中核对。

修改 Projector 后至少执行:

python3 -m unittest blender.tests.test_pure
npm run build:area -- --config config/areas/<area>.json --stages blender,cesium,preview

blender.tests.test_pure.ProjectorTest 必须断言锚点为原点、East/North 方向正确,以及局部 经纬度增量符合 WGS84 椭球曲率半径。

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_TINTSEXPORT_METALLIC_OVERRIDESEXPORT_BASE_COLOR_OVERRIDESEXPORT_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 阶段必须生成三个动态 GLBtraffic-signals-dynamic.glb 只含灯珠, traffic-signals-countdown-0.glbtraffic-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 颜色 抹掉独立调过的配色
用固定米/度比例投影经纬度 GLB 与 Cesium Entity 随离锚点距离产生位置漂移
加新资产不配 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 是这一约定的实例,不参与区域 构建阶段。


相关