Appearance
QGIS 图纸到 glTF 模型的数据链路
个人技术实验记录,全部产物自制。
三维场景最难的不是渲染,是几何从哪来。手绘一晚上也就十几栋,而一个场地几十上百栋。我的思路是把建模拆成"取数 → 清洗 → 参数化 → 导出"四段,中间全部脚本化,QGIS 只负责取数和检查。
一、取数:局部坐标系比经纬度好用
场景是几百米尺度,用经纬度算距离、铺网格会一直被投影变形烦。我把原点定在场地一角,全程用局部平面坐标(米),只在最后接影像时才换算一次经纬度。
元数据里保留三样东西,后面所有脚本都依赖它们:
| 字段 | 含义 | 用途 |
|---|---|---|
origin | 局部坐标原点对应的经纬度 | 接影像、接地理引擎时做一次换算 |
bbox_deg | 场地外包框(经纬度) | 描边、相机定位、瓦片取图范围 |
pixel_size_m | 影像像素地面分辨率 | 配准误差判断、贴图细节估算 |
具体带号投影(比如 CGCS2000 的 3 度带)在 QGIS 工程里定义,导出时已经是本地米。注意:精确坐标属于敏感地理信息,公开发布时一律降精度或直接删掉,所以这篇里只写字段含义、不贴真实数值。
二、清洗:轮廓必须是四边形
体块生成对轮廓的要求很朴素:四个角点、按绕向排好。真实图纸里常见的问题:
- 多边形顶点冗余(一条直线上插了十几个点)→ 用道格拉斯-普克抽稀
- 建筑被拆成多个碎块 → 按外接矩形合并
- 绕向不一致 → 统一成逆时针,法向计算才不会翻面
抽稀阈值我按 0.5 m 试的,再大就会把小体量建筑削成三角形。
三、参数化:三个字段决定一栋楼
每栋楼只保留最小信息集(下面是脱敏后的结构示例,坐标为场地内部局部米制,没有原点无法换算回实地):
json
{
"id": "B01",
"area_m2": 2192.5,
"height_m": 10.15,
"floors": 1,
"aspect": 1.56,
"gable": false,
"points": [[-158.3, 78.05], [-168.03, 103.77], [-93.45, 131.96], [-83.73, 106.24]]
}aspect(长宽比)用来判断是不是狭长体,狭长的更适合单坡gable决定要不要起屋脊height_m由层数 × 层高推,单层大跨度直接按净高给
屋顶起坡高度是常量(我用了墙高 8.8 m、屋脊抬 1.6 m 这一组),先全局统一,之后要精细化再按栋覆盖。先跑通再好看,这条管线一开始就想做细节肯定写不完。
四、导出:Blender 无头 + glTF
Blender 用命令行无头模式跑,脚本里 bmesh 直接建顶点面,比调用建模 UI 操作稳得多,而且能进版本控制:
bash
blender --background --python scripts/build_model_boxes.py关键约定:
- 轴向:Blender X=东、Y=-北、Z=上;导出 glTF 后 X=东、Z=北。跨引擎时这一条不写死必然翻车。
- 贴图在 Blender 内部用 numpy 生成后
pack()进 glb,不引外部素材文件,产物单文件可分发。 - 材质按法向分配:顶面(
normal.z > 0.9)给屋顶材质,其余给墙面材质。 - UV 用盒式投影(按面法向选投影平面),世界坐标除以重复尺度,这样贴图密度和真实尺寸挂钩,不会每栋楼贴图大小不一。
py
def add_uv(obj, scale=6.0, up_scale=0.5):
for poly in mesh.polygons:
n = poly.normal
if abs(n.z) > 0.7: u, t = v.x / scale, v.y / (scale * up_scale)
elif abs(n.x) > abs(n.y): u, t = v.y / scale, v.z / scale
else: u, t = v.x / scale, v.z / scale五、检查:中间产物一定要能画图看
每一步都输出 PNG 预览(轮廓叠加、体块正交视图、配准对比)。没有中间可视化时,几何错误要等到渲染完才暴露,排查成本翻几倍。这也是为什么效果图页里全是半成品图——那些才是真正有用的调试产物。
六、链路小结
矢量图纸 / 影像
→ QGIS 抽稀、取角点、局部坐标化
→ JSON(每栋一栋:轮廓 + 高度 + 坡向)
→ Blender 无头脚本(bmesh 建体块 + 程序化贴图 + 盒式 UV)
→ glTF(单文件,贴图内嵌)
→ 浏览器 Three.js 实时渲染换场地时只需要重跑前两步,后面的脚本不动——这是我判断一条管线算不算"搭好了"的标准。