7.0 KiB
Blender:Python 场景生成层
覆盖
blender/**/*.py。 运行时:两个——Blender 内嵌 Python(bpy 层)和系统 Python(纯 Python 层)。 这条内部边界是本层最重要的结构约束。
先读哪一篇
| 你要做的事 | 读 |
|---|---|
| 新建模块、挪代码、加一种 OSM 要素 | 模块结构 ← 先确认放在哪一层 |
| 改几何构建、材质、实例化、Cesium 调色 | 资产生成 |
改 geom.py / osm.py 或加纯函数 |
测试 |
改材质名、动 MATERIALS 顺序 |
模块结构 · 材质名对接 |
| 声称"纯重构,产物不变" | 产物一致性指南 |
依赖分层
┌─────────────────────────────────────────────────────────┐
│ 纯 Python 层 —— 无 bpy,系统 python 可跑可测 │
│ │
│ osmassets/osm.py OSM XML 解析 + 局部米制投影 │
│ osmassets/geom.py 平面几何(米制) │
│ osmassets/catalog.py 图层与材质的声明 │
│ │
│ ▲ blender/tests/test_pure.py 覆盖这一层(51 个用例) │
└─────────────────────────────────────────────────────────┘
▲ 只能单向依赖
┌─────────────────────────────────────────────────────────┐
│ bpy 层 —— 只在 Blender 内运行,无单元测试 │
│ │
│ osmassets/mesh.py MeshBatch 等几何构建 │
│ osmassets/materials.py 材质构建(消费 catalog 的声明) │
│ osmassets/tree.py 树实例化 │
│ osmassets/water.py grass.py scrub.py │
│ osmassets/features.py 要素分发注册 │
│ osmassets/building.py fountain.py roads.py 要素装配 │
│ │
│ generate_scene.py export_cesium.py 两个入口 │
│ tools/scene_digest.py 结构摘要工具 │
│ │
│ ▲ 回归防线是 parity 校验,不是单元测试 │
└─────────────────────────────────────────────────────────┘
在纯 Python 层里 import bpy 会静默废掉整个测试套件——它不会失败,
而是 import 阶段就崩,看起来像环境问题。
推论:能挪进纯 Python 层的逻辑就挪。一个函数只要不碰 bpy,放进 geom.py
就立刻获得被测试覆盖的资格。
两个入口
generate_scene.py |
export_cesium.py |
|
|---|---|---|
| 行数 | 895 | 647 |
| 调用 | --background --factory-startup --python |
--background --factory-startup --python |
| 输入 | --osm + --geojson(可选) |
--blend |
| 输出 | --output(.blend)、--render(.png) |
--glb、--metadata(.json) |
| 完成标记 | SCENE_DONE |
CESIUM_EXPORT_DONE |
| 由谁调起 | build-area.js 的 blender 阶段 |
build-area.js 的 cesium 阶段 |
两个 stdout 标记是 parity 契约的一部分
(scripts/parity.js:121 解析它们),改动打印格式等于改动契约。
后台阶段使用 --factory-startup
它屏蔽本机 Blender 的 preferences 和 addon,保证场景生成与 Cesium 导出不受用户配置影响。
副作用是脚本自己的目录不在 sys.path 上,所以两个入口开头都有那段
sys.path.insert 样板 + # noqa: E402——不是可以整理掉的坏味道。
场景构建的输入约定
generate_scene.py:9-12 记录了一个容易踩的坑:
范围只认 OSM 的
bounds元素,不用全部节点算包围盒。OSM 导出可能带上 请求范围之外的 relation 成员,用全部节点会得到一个大得离谱的模型。
--geojson 是可选的。给了就用 osm2streets 的精细道路面、人行道、车道标线、
斑马线;不给则回退到简单的 OSM highway 折线(generate_scene.py:14-16,
回退逻辑在 :849)。
植被映射(generate_scene.py:18-20):
| OSM 标签 | 产物 |
|---|---|
natural=tree(节点) |
单棵树 |
natural=tree_row(way) |
等距成排的树 |
landuse=grass |
绿地 + 可选草簇散布 |
natural=scrub |
低矮灌木覆盖 |
amenity=fountain |
低模喷泉水池 |
三条贯穿全层的约定
-
构建必须确定性 用
index的纯函数(无理数周期 / 黄金角)或显式seed代替 RNG, 绝不用无种子random或时间量。重建同一片区域必须得到相同结构, 否则 parity 校验永久红。见资产生成。 -
压低对象数
MeshBatch批处理 + 树实例化共享 datablock。GLB 要在浏览器里跑, 节点数和贴图数都是硬成本。 -
退化输入返回空,不抛异常 一个坏多边形不该中断整片区域的构建。要素模块
len(ring) < 3直接返回 0,MeshBatch静默 return,geom.py的函数返回[]。
文件速查
| 文件 | 行数 | 层 |
|---|---|---|
generate_scene.py |
895 | bpy · 入口 |
export_cesium.py |
647 | bpy · 入口 |
osmassets/tree.py |
318 | bpy |
osmassets/geom.py |
241 | 纯 |
osmassets/materials.py |
232 | bpy |
osmassets/catalog.py |
197 | 纯 |
osmassets/mesh.py |
126 | bpy |
osmassets/osm.py |
89 | 纯 |
osmassets/features.py |
20 | bpy · 分发 |
osmassets/building.py / fountain.py / roads.py |
56 / 49 / 40 | bpy |
osmassets/water.py / grass.py / scrub.py |
15 / 22 / 13 | bpy |
tools/scene_digest.py |
171 | bpy · 工具 |
tests/test_pure.py |
372 | 纯 · 测试 |
blender/README.md 是面向使用者的运行说明,与本目录互补——用法写那边,
改法写这边。
技术选型现状
- Blender 4.x,
bpy+mathutils;export_cesium.py额外用numpy(Blender 自带) - 纯 Python 层只用标准库(
math、json、os、xml.etree), 这是它能用系统 python 跑的前提——不要给它加第三方依赖 - 无类型标注、无 lint 配置。保持现状;引入工具链是独立决定
unittest而非 pytest,零依赖跑得起来