
热词列表里那条“blender行政高程数据建立3d模型”我一看就知道说的是哪类案例——拿着真实地理数据在Blender里重建立体地形。oeasy这套Blender教程的第20集正好是一个完整的“数据变模型”实战案例。这种完整案例和碎片教程最大的区别是它逼着你走完从数据准备到最终导出的每一环中间任何一步断掉模型都出不来。对于刚接触Blender建模的人来说跟着这种完整案例做一遍比看几十个单独讲修改器或快捷键的视频都更有用因为它能把零散的操作串成一条生产链路。我按这个思路完整跑了一遍流程先解决地形数据从哪来、怎么变成Blender能用的灰度图再做地形网格把行政区边界叠加上去最后导出成多种格式。这篇文章把我实际踩过的坑、推荐的做法、还有那些热搜词里反复出现的“导出json”“FBX插件”“optix不显示”之类的问题一次性说清楚。1. 案例拆解一个完整案例到底“完整”在哪先别急着上手操作把案例的框架看明白后面每一步你就知道自己在做什么了。包含行政边界和高程数据的建模本质上做的是“把真实世界的坐标数据映射到三维软件的空间里”。1.1 为什么拿真实地理数据当素材很多人学Blender都是拿立方体、猴头练手练完就忘。用真实地理数据做案例有个好处它自带一套坐标系统和属性信息你做出来的模型是能被验证的——对比卫星图、对比实际地形的起伏模型对不对一眼就能看出来。真实地理数据建模一般分两类高程数据DEM描述地表海拔高度的栅格数据每个像素记录一个高度值用它来生成地形起伏。行政区划数据描述省、市、区县边界的矢量数据用它来给地形“画区域”。把两者叠到同一个三维空间里就得到了一张带立体起伏的行政区划图。这种模型在城市规划展示、文化遗产数字化、游戏地形底图、数据可视化大屏这些场景里非常常见。我见过不少团队做智慧城市项目时第一步就是把城市地形和行政边界做出三维底图再往上叠加业务数据。用真实数据驱动建模还有一个好处它能强迫你养成“先理解数据再动手建模”的习惯。比如拿到一份高程TIFF文件你不能直接拖进Blender就指望它变成山你得知道它的坐标系、分辨率、无效值怎么处理。这套思路放到任何数据建模场景都通用。1.2 案例覆盖的完整流程整个案例可以拆成五个环节每个环节都有独立的产出物环节输入处理方式产出物数据获取公开的DEM数据、行政区划GeoJSON下载、裁剪、格式检查合格的高程TIFF和边界文件数据预处理DEM TIFF文件坐标归一化、灰度映射、空值处理灰度图PNG或顶点坐标数组地形生成灰度图或坐标数组置换修改器或脚本生成网格带起伏的地形模型边界叠加行政区划坐标坐标换算、曲线创建、挤出、贴图遮罩带边界的完整三维模型渲染与导出三维模型材质、灯光、相机、格式转换最终图像、FBX/OBJ/GLTF/JSON文件这五个环节里最容易卡住的不是Blender操作本身而是第二和第四步——数据怎么进Blender、边界怎么和地形对齐。后面两章我详细拆。2. 地形数据准备从数字高程模型到灰度图先说清楚一个底层逻辑Blender本身不理解“海拔高度”这个概念它只认识物体的坐标、顶点的位置、材质贴图的颜色。所以要把海拔数据变成三维地形最常见的方式是利用置换修改器——它读一张灰度图把图像中每个像素的亮度值当成顶点位移量。白色代表高、黑色代表低中间灰阶是过渡高度。2.1 数据源的取舍公开可用的DEM数据源主要有几个体系SRTM、ALOS、ASTER GDEM。这些数据集覆盖全球分辨率通常是30米左右。对城市级地形建模来说30米分辨率完全够用但如果你想做一个小片区的精细地形比如一座园区、一段河道的微地形这个分辨率就不够了需要找更高精度的测绘数据。选数据源的标准就一条模型覆盖范围和所需细节的比值。我个人的经验是覆盖一个城市几十公里范围30米分辨率足够覆盖一个区县十几公里范围30米分辨率够用但局部细节会显得“糊”覆盖几公里的景区或园区需要找LiDAR或更高精度的数据源或者接受地形平滑化下载数据后第一时间要做的是检查无效值。公开DEM数据常有Nodata区域比如水体、云层遮挡这些位置的像素值可能是-9999之类的大数。如果不处理灰度映射的时候会把整个色彩范围拉爆地形图变成一块白斑加一块黑斑。处理方式是把无效值统一改成0或者直接置为NaN。2.2 用Python把DEM转成灰度图的实操我习惯在进Blender之前先用Python把DEM文件预处理成一张16位灰度PNG。这一步的核心是“归一化映射”也就是把DEM里的实际海拔范围可能从0米到3000米线性映射到灰度区间0到65535这样置换修改器读取时才能获得完整的位移精度。下面这段代码可以直接跑依赖rasterio和PILimport rasterio import numpy as np from PIL import Image with rasterio.open(city_dem.tif) as src: dem src.read(1) nodata src.nodata dem np.where(dem nodata, 0, dem) # 把无效值和异常值剔除后再算范围 valid dem[dem 0] dem_min float(np.min(valid)) dem_max float(np.max(valid)) # 归一化到16bit灰度 norm (dem - dem_min) / (dem_max - dem_min) * 65535 norm np.clip(norm, 0, 65535).astype(np.uint16) Image.fromarray(norm).save(city_dem_16bit.png) print(f海拔范围: {dem_min:.1f}m ~ {dem_max:.1f}m)这里有个细节值得注意为什么要用16位而不是8位因为8位灰度只有256个亮度等级如果你手头的地形海拔高差很大——比如一座山从50米到2000米——分配到每个亮度级就有接近8米的高度步进做出来的地形会呈现一层层的“台阶”断层。16位灰度有65536个等级同样场景下每个等级只有约0.03米完全看不出阶梯。提示如果置换出来的地形起伏过于夸张不要改灰度图直接在置换修改器里调低强度数值就可以这比反复重新生成灰度图高效得多。2.3 另一个思路直接用坐标点生成地形灰度图方案适合“有完整栅格文件”的情况。但有时候你手里的数据不是TIFF而是一堆经纬度和高度记录比如GPS采集点、点云数据、无人机测绘成果。这时候没必要非得转成灰度图再进Blender直接让Python脚本在Blender里按坐标点生成网格更省事。这个方法我在下一章展开因为行政边界叠加的时候也会用到同样的坐标换算思路。3. 把灰度图变成起伏地形两种建模路径地形数据进Blender有两条主流路径各有适用场景。一条适合新手快速出效果另一条适合精确控制模型结构。3.1 路径一细分平面加置换修改器这是最稳妥、对新手上手最友好的方案。操作逻辑很简单Blender里新建一个平面把它细分到足够多的面数再叠加置换修改器读取灰度图让顶点高度跟着图像亮度变化。具体步骤在Blender中新建平面默认尺寸2米按S放大我这里习惯放到1000到2000之间具体数值取决于你模型要表现的地理范围。进入编辑模式全选所有顶点右键选择“细分”——这里建议用“细分”而不是“简单细分”前者会做平滑插值后者会让网格产生明显的块状感。细分级数根据电脑性能来。注意每次细分面数乘以4初始平面2个三角形区域细分10次会到上百万面很多老电脑到这一部就开始卡了。稳妥做法是先细分7到8次大概几万到几十万面既保留地形细节又不会卡死。给平面添加“置换”修改器在纹理设置里新建一个图像纹理加载刚才生成的灰度图。调整置换强度。强度值就是最大位移高度。如果城市海拔高差300米强度填300Blender默认单位是米。如果发现地形反过来——山谷变成了山脊——把强度改为负数即可这是法线方向不同导致的。置换修改器里有个关键参数容易忽略纹理坐标方式。默认是“局部坐标”如果平面有旋转或缩放图像映射会跟着扭曲导致地形形状对不上。我通常先在修改器面板把纹理坐标改成“生成”模式再调整平面的缩放这样图像能按物体的原始空间正确铺开。3.2 路径二脚本直接生成顶点网格如果你手里的数据本身是规则网格采样点比如每个经纬度点对应一个高度值我更推荐用Python脚本直接生成网格。这样做的好处是每一个顶点的位置都由数据精确决定不存在置换修改器那种“按图像亮度驱动”的间接误差。转换逻辑是地理坐标到三维坐标的映射以数据左下角为原点经度差乘以每度经度对应的米数得到X方向偏移注意要乘cos纬度修正因为经线在高纬度会收缩纬度差乘以每度纬度对应的米数得到Y方向偏移高度值直接作为Z轴一个简化的脚本长这样import bpy def create_terrain_from_grid(grid_data, cols, rows, lon0, lat0, lat_ref): import math verts [] faces [] # 每度纬度约111320米 meter_per_deg_lat 111320.0 meter_per_deg_lon 111320.0 * math.cos(math.radians(lat_ref)) for i in range(rows): for j in range(cols): lon, lat, h grid_data[i * cols j] x (lon - lon0) * meter_per_deg_lon y (lat - lat0) * meter_per_deg_lat z h verts.append((x, y, z)) for i in range(rows - 1): for j in range(cols - 1): a i * cols j b a 1 c a cols 1 d a cols faces.append((a, b, c, d)) mesh bpy.data.meshes.new(terrain_from_data) mesh.from_pydata(verts, [], faces) mesh.update() obj bpy.data.objects.new(terrain, mesh) bpy.context.collection.objects.link(obj) return obj用脚本建网格的好处还在于它直接把行政边界的坐标换算逻辑也统一了。只要数据来源的经纬度范围一致换算公式一致地形网格和边界曲线放进场景后就是天然对齐的不需要手动调整位置。3.3 两条路径怎么选方案精度性能开销操作难度适用场景置换修改器受图像分辨率和细分面数限制中低低快速出效果、DEM灰度图现成脚本生成网格高直接由数据精度决定取决于顶点数中高精确建模、需要配合其他坐标数据我的建议是第一次做先用置换修改器跑通全局理解整个流程。等到需要精细控制模型结构时比如要把建筑底面贴合地形、要做喷绘材质分类再切换到脚本方案。4. 行政区划数据叠加让地形带上“属性”地形有了起伏但还只是一座“秃山”。行政边界的作用是把这座山切开让每个区域拥有名字、颜色、独立的属性。这一步做完模型才从“地形沙盘”变成“行政区划三维地图”。4.1 边界线进入Blender的两条路行政边界数据一般是GeoJSON或SHP格式里面存的是一组组的经纬度坐标点。把边界数据弄进Blender有两条路径第一条是直接导入。Blender原生不支持SHP需要借助GIS类插件比如blender-osm它可以从在线地图源获取边界和建筑物数据。用这类插件的优势是省事缺点是依赖外部数据源数据格式不确定且边界和地形的对齐不一定精准。第二条是手动转换。用Python读取GeoJSON把经纬度列表转成三维坐标再生成曲线对象。虽然要写脚本但整个过程完全可控而且和第3.2节的地形生成共用同一套坐标换算结果不会出现错位。我个人的选择是第二条。不是排斥插件而是数据建模项目里可控性往往比省事更重要。ORDER数据出问题的时候你至少知道问题出在哪个环节。4.2 手动坐标换算并生成边界的实操下面这段代码演示了如何读取GeoJSON并生成Blender曲线import json import bpy import math from mathutils import Vector # 读取GeoJSON with open(districts.geojson, r, encodingutf-8) as f: data json.load(f) lat0 data[features][0][geometry][coordinates][0][0][1] lon0 data[features][0][geometry][coordinates][0][0][0] meters_per_deg_lat 111320.0 meters_per_deg_lon 111320.0 * math.cos(math.radians(lat0)) def geo_to_xy(lon, lat): x (lon - lon0) * meters_per_deg_lon y (lat - lat0) * meters_per_deg_lat return (x, y) # 对每个区域创建一条闭合曲线 for feature in data[features]: name feature[properties].get(name, district) coords feature[geometry][coordinates][0] # 可能是多边形外环 polyline_points [geo_to_xy(lon, lat) for lon, lat in coords] # 创建曲线 curve_data bpy.data.curves.new(name, typeCURVE) curve_data.dimensions 3D spline curve_data.splines.new(POLY) spline.points.add(len(polyline_points) - 1) for i, (x, y) in enumerate(polyline_points): spline.points[i].co (x, y, 0, 1) # 留Z为0后面再抬高 obj bpy.data.objects.new(name, curve_data) bpy.context.collection.objects.link(obj)生成的曲线是在地形表面平面高度Z0进入编辑模式后把所有控制点选中沿Z轴微调抬高一点比如0.5米再给曲线一个挤出值让它变成薄片就能看到“贴”在地形上的区域块。4.3 给不同区域分配颜色的实操区域块的着色有两种方式简单粗暴的和通用性强的。简单粗暴每个区域做成一个独立的挤出网格分别赋予不同的材质。这种做法的好处是每个区域都是单独的物体后续做点击交互、数据标注都方便坏处是区域多了材质管理很麻烦。通用性强在材质编辑器里用“网格坐标”向量接入ColorRamp把一张已经按区域着色的图像比如在GIS软件里导出的分区色块图作为蒙版映射到地形表面。这种方式适合“只看颜色分区”的场景因为不产生额外几何体渲染压力小。我做这类模型时通常两种结合地形主体用一张整体色块贴图重点区域单独生成挤出体并配独立材质。这样既有整体效果又有局部可交互性。5. 拿到模型之后导出格式与跨工具交付模型建完了画面上看起来也像模像样了剩下的问题就是怎么把它交出去。热搜词里那一串“blender导出json”“blender导出sketchup文件”“FBX插件”说明大量人卡在导出这一步。这里有坑而且不少。5.1 导出格式按场景选格式适用场景注意事项FBX游戏引擎Unity/Unreal、动画传递注意轴方向Blender是Z轴向上其他软件可能是Y轴向上OBJ通用静态模型、大部分DCC软件不带骨骼动画带材质需要配套MTL文件GLTF/GLBWeb3D、AR/VR、Three.js材质PBR兼容性最好推荐Web场景使用STL3D打印只保留网格材质信息全部丢弃SKPSketchUp场景衔接需要第三方插件导出效果受插件免费版限制JSON数据可视化、自定义程序通常通过Python脚本自定义导出属性不同项目交付的格式差异很大。给游戏团队我一般导FBX导之前确认一次轴向给Web前端团队我导GLB给SketchUp用户导出SKP时我提前跟他们说明免费插件导出的文件可能会出现材质丢失需要他们在SketchUp里重新赋一次材质。5.2 把地形连同属性导出成JSON热搜词里“blender如何导出json”出现的频率不低。其实Blender内置的导出格式没有JSON但数据可视化项目比如echarts、three.js加载经常需要。这时候直接用Python脚本自动生成JSON就很方便而且可以自定义要输出的属性。import bpy import json obj bpy.context.active_object mesh obj.data # 如果地形被应用了修改器先复制一份并应用 applied_obj obj.copy() applied_obj.data obj.data.copy() bpy.context.collection.objects.link(applied_obj) # 读取世界坐标系下的顶点坐标 verts [] for v in applied_obj.data.vertices: world_co applied_obj.matrix_world v.co verts.append([round(world_co.x, 2), round(world_co.y, 2), round(world_co.z, 2)]) faces [] for p in applied_obj.data.polygons: faces.append([idx for idx in p.vertices]) # 如果有自定义属性一并输出 custom_props {k: v for k, v in obj.items() if not k.startswith(_)} out_data { name: obj.name, vertices: verts, faces: faces, properties: custom_props } with open(terrain_export.json, w, encodingutf-8) as f: json.dump(out_data, f, ensure_asciiFalse, indent2) print(f导出完成{len(verts)} 个顶点{len(faces)} 个面)这个脚本输出的JSON是前端友好型的顶点数组和面索引数组可以直接喂给Three.js的BufferGeometry也可以在echarts的custom series里做三维地图底图。如果需要更小的体积可以把indent去掉顶点坐标保留一位小数。5.3 那些绕不开的插件与格式兼容问题热搜词里有一条“blender导入导出fbx模型插件better fbx importer exporter v6.3.5”说明很多人对Blender原生FBX模块不满意。说实话如果你只是导出一个静态地形网格原生FBX导出完全够用。但如果涉及骨骼动画、多个UV通道、特定游戏引擎的命名规则第三方插件确实更稳。用第三方插件的时候最烦的就是版本匹配。Blender 3.x和4.x的API有差异老插件直接装进新版Blender可能整个Addon都加载失败。装插件之前先看支持的Blender版本号这比文档里任何设置都重要。插件的功能和设置反而不用调太细默认选项能应对大多数场景。“blender不显示optix”这条热搜我也顺便说一句。OptiX是NVIDIA显卡的硬件光追加速技术Blender用它做渲染加速。模型没问题但渲染面板里不显示OptiX通常三个原因显卡驱动太老Blender版本和驱动不匹配或者在Blender偏好设置里没有勾选对应的计算设备。排查顺序建议先更新显卡驱动再检查偏好设置里的Cycles渲染设备和GPU勾选最后看Blender版本日志。6. 整套跑下来最值得记录的三个雷数据建模和纯三维建模不太一样它的问题往往不是操作不会而是数据本身或者数据与软件的衔接出了岔子。我跑这套案例时遇到三个比较典型的雷记录下来给大家做个参考。6.1 地形“反了”或“变平了”的根因第一次用置换修改器加载灰度图地形完全没变化或者整片模型像被压平了一样。排查顺序很重要先看置换强度数值是不是为0或者太小——我有一次就是强度填了0.01地形起伏只有零点几的位移肉眼根本看不出来再看灰度图的位深有些8位灰度图压缩后细节丢失严重大块区域亮度一样地形就会平得像镜面最后看纹理坐标映射模式如果平面有旋转图像可能被映射到错误的轴向地形看着就是歪的。如果地形方向反了强度改成负数即可。这不是什么玄学就是你的平面法线方向默认朝上置换修改器按法线方向往外推负数就是反方向推。6.2 面数失控导致操作卡顿置换修改器要生效必须给平面足够多的细分面。但很多新手不知道细分级数是指数级增长一个平面细分1次变4面细分5次变1024面细分10次已经超过100万面。到100万面的时候转动视口都开始掉帧。我的建议是先用低细分跑通流程比如细分7次约16万面观察效果。确认地形方向和强度没问题后再局部增加细分或者使用多级精度修改器。还有一个优化技巧在微调阶段把置换修改器临时隐藏不渲染只在你需要确认效果的时候开启预览。这样操作流畅度能提升不少。如果模型最终要放到游戏引擎里导出前必须减面。用“Decimate”修改器里的“塌陷”模式配合“平面度”参数可以在地形主体保持基本形态的前提下把面数砍到原来的三分之一甚至更少。6.3 导出FBX/GLTF后贴图丢失模型在Blender里渲染得好好的导出FBX到Unity或者GLB到网页后贴图全部丢失变成一片灰白。这个问题分两种情况一种是你根本没把纹理嵌入到导出文件里另一种是贴图路径暴露了你本机的绝对路径。GLB格式本身支持嵌入纹理导出时面板下方有个“储存”选项选“打包”就能把图片嵌入文件。FBX导出时虽然有嵌入贴图选项但有些DCC软件不认更稳妥的做法是把模型使用的贴图文件和FBX放在同一个文件夹里用相对路径引用。导出后我还习惯用文本编辑器打开FBX看一眼贴图路径字段确认没有出现“C:\Users\xxx”这样的绝对路径。第三方插件导出SKP时贴图丢失更常见免费版插件对材质支持很有限基本只导出颜色不导出纹理映射。所以给用SketchUp的同事交付时我一边发SKP文件一边连同贴图文件夹打包并提醒他材质需要重赋。我个人的习惯是每次跑完这套流程工程文件按版本号存三份原始数据版、模型建模版、导出发布版。原始数据版指的是还没进Blender的数据文件和处理脚本模型建模版是带完整修改器和材质节点的Blend文件导出发布版是已经应用修改器、减面、整理好命名、准备导出的文件。这样每次改完一个环节不会因为误操作毁掉整个工程。做数据建模留后路往往比一步到位更重要。