# Blender:Python 场景生成层 > 覆盖 `blender/**/*.py`。 > 运行时:**两个**——Blender 内嵌 Python(bpy 层)和系统 Python(纯 Python 层)。 > 这条内部边界是本层最重要的结构约束。 --- ## 先读哪一篇 | 你要做的事 | 读 | |---|---| | 新建模块、挪代码、加一种 OSM 要素 | [模块结构](./module-structure.md) ← **先确认放在哪一层** | | 改几何构建、材质、实例化、Cesium 调色 | [资产生成](./asset-generation.md) | | 改 `geom.py` / `osm.py` 或加纯函数 | [测试](./testing.md) | | 改材质名、动 `MATERIALS` 顺序 | [模块结构 · 材质名对接](./module-structure.md#材质名对接新场景靠自定义属性旧场景靠回退表) | | 声称"纯重构,产物不变" | [产物一致性指南](../guides/artifact-parity-guide.md) | --- ## 依赖分层 ``` ┌─────────────────────────────────────────────────────────┐ │ 纯 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/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` | |---|---|---| | 行数 | 999 | 624 | | 调用 | `--background --factory-startup --python` | `--background --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 契约](../guides/artifact-parity-guide.md)的一部分 (`scripts/parity.js:121` 解析它们),**改动打印格式等于改动契约**。 ### `--factory-startup` 只在 generate 阶段用 它屏蔽本机 Blender 的 preferences 和 addon,保证场景生成不受用户配置影响。 副作用是脚本自己的目录不在 `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` | 低模喷泉水池 | --- ## 三条贯穿全层的约定 1. **构建必须确定性** 用 `index` 的纯函数(无理数周期 / 黄金角)或显式 `seed` 代替 RNG, 绝不用无种子 `random` 或时间量。重建同一片区域必须得到相同结构, 否则 parity 校验永久红。见[资产生成](./asset-generation.md#确定性用无理数周期代替-rng)。 2. **压低对象数** `MeshBatch` 批处理 + 树实例化共享 datablock。GLB 要在浏览器里跑, 节点数和贴图数都是硬成本。 3. **退化输入返回空,不抛异常** 一个坏多边形不该中断整片区域的构建。要素模块 `len(ring) < 3` 直接返回 0, `MeshBatch` 静默 return,`geom.py` 的函数返回 `[]`。 --- ## 文件速查 | 文件 | 行数 | 层 | |---|---|---| | `generate_scene.py` | 869 | 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/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**,零依赖跑得起来