拿到“2.boston.earth”这个文件名时,很多刚接触osgEarth的朋友会以为它只是某个三维场景的启动配置,点开就完事了。但实际上,这个案例文件背后是一条完整的链路:从shapefile矢量数据的读取、坐标参考系的换算、符号规则的编写,到最终把建筑轮廓、道路中心线这些二维数据“立起来”变成三维模型。这篇文章就拿boston这个案例当引子,把osgEarth里矢量转三维模型这件事从头到尾讲透,让你看完自己也能写出来。
内容适合三类人:一是刚上手osgEarth、想搞明白earth文件到底怎么写的初学者;二是手里有arcgis导出的shapefile数据、正愁怎么在三维场景里可视化展示的GIS开发;三是想给项目做技术选型、判断osgEarth能不能承接“矢量数据批量建模”需求的人。全程按实际踩坑的经历来讲,不绕弯子。
1. 2.boston.earth到底是什么:案例背后的技术闭环
1.1 从文件名拆解:earth文件在osgEarth里的角色
osgEarth是一个基于OpenSceneGraph的三维地形渲染引擎,它的一个核心设计思路是“用文本描述场景”。所谓earth文件,本质上就是一个XML格式的场景描述文档,文件里定义了加载哪些影像、哪些高程、哪些矢量数据、如何符号化、使用哪种坐标参考系。osgEarth启动后读取这个文件,按照里面的声明去调度数据,最终渲染出一个三维地球或局部地形场景。
“2.boston.earth”这个命名其实是沿用了osgEarth早期示例的习惯,“2”代表这是面向osgEarth 2.x接口的示例文件,boston表示场景区域是美国波士顿。里面通常包含三类图层:影像层提供底图纹理,高程层提供起伏地形,模型层或矢量层负责把建筑、道路等shapefile数据加载进来。波士顿这个区域的案例在GIS圈里很常见,因为它有公开的建筑轮廓数据和相对规整的街道网络,非常适合演示矢量拉伸建模。
1.2 从shapefile到三维模型:必须跨过的三道坎
直接用shapefile生成三维模型,看起来就是把二维多边形加一个高度值,但实际操作中要跨过三道坎。第一道坎是坐标参考系。shapefile通常是平面投影坐标,比如UTM或者各地方坐标系,而osgEarth场景默认跑在WGS84经纬度或者地心坐标系里,两套坐标系不统一,数据加载进来就会飞出地球或者堆在原点。第二道坎是“转成什么”的问题。矢量数据本身只是数学意义上的点线面,在三维场景里可以做成贴在地面上的彩色多边形,也可以沿垂直方向拉伸成体块,还可以用别的三维模型文件去做实例替换。第三道坎是符号化参数的映射。shapefile属性表里的字段,比如建筑层数、高度、名称、类型,需要想办法映射到三维渲染的视觉参数上,这就要靠style表达式来解决。
boston这个案例最有价值的一点,就是它把这三道坎都演示了一遍:影像数据如何配准、建筑shp如何按属性拉伸、道路和地块如何贴地显示。把这个案例拆开看懂了,其它场景只是换个数据源的事。
2. shapefile进入osgEarth之前:数据准备与坐标系校验
2.1 shapefile文件家族:不止.shp一个文件
在arcgis里导出一份shapefile,你会看到同名的一堆文件,常见的有.shp、.shx、.dbf、.prj、.sbn、.sbx等。很多新手只把.shp文件拷走,结果加载时报错,其实就是文件不完整。.shp存几何图形,.shx是索引,.dbf是属性表,这三者是基础三件套,缺一个都打不开。.prj存坐标系描述,这个文件太容易被忽略了,但恰恰是它决定了osgEarth能不能正确解析数据位置。
我给一个建议:在做任何三维可视化之前,先用QGIS或者arcmap把shapefile打开看一眼,确认几何类型(点、线、面)、属性字段和坐标系。尤其是确认要素是Polygon还是MultiPolygon,这会影响后面拉伸建模的配置方式。属性表里的高度字段如果是字符串类型,在后面写expression时会被当成文本处理,导致拉伸失效,这也是常见坑。
2.2 坐标系校验:为什么矢量数据飞到了非洲海岸
osgEarth里坐标参考系的基础配置在map节点上。如果你写的是type="geocentric",那是地心坐标系,单位是米,一个经纬度坐标可能直接被解释成地心直角坐标,数据位置自然完全错乱。如果写的是type="geodetic",则是经纬度坐标,但如果shapefile本身是投影坐标,直接加载也会错位。
判断一个shapefile的坐标系最直接的方式是用GDAL的命令行工具gdalinfo,它会读取.prj内容并输出一个完整的WKT描述。对于没有.prj文件的shapefile,需要先根据数据来源确定投影参数,比如数据是某城市规划局提供的,通常是当地的城市坐标系或者国家2000投影坐标系,这种情况下需要先通过arcgis的投影工具转换到WGS84,再交给osgEarth使用。
2.3 高度与单位陷阱:建筑高度字段不能盲目使用
shapefile属性表里的高度字段,单位不一定是米。有的数据源用的是英尺(foot),波士顿的数据尤其容易出现这个问题,因为美国很多公开建筑数据习惯用英尺记录高度。在配置拉伸高度时,如果直接把这个值当米用,你会发现建筑高度变成实际的三倍多,整个城市跟积木堆起来一样。
解决办法很简单:先看属性表里字段的备注说明,如果单位是英尺,就在expression里乘以0.3048换算成米。同样地,很多属性表里只有建筑层数而没有建筑高度,这时候需要估算层高,普通住宅按3米每层,写字楼按3.8米到4.2米每层,也能得到一个能用于演示的效果。
3. 写出可用的earth文件:矢量拉伸三维模型关键配置拆解
3.1 earth文件整体骨架:map节点与options
先给一个我实际验证过可以跑的earth文件骨架,这个文件逻辑完整,不像默认模板那么简陋。假设本地有一个boston_buildings.shp和一个dem.tif,用这个配置可以直接把建筑拉伸成三维体块。
<map name="boston_demo" type="geocentric" version="2"> <options> <lighting>false</lighting> </options> <image layer driver="gdal" name="boston_image"> <url>./data/boston_imagery.tif</url> </image> <elevation layer driver="gdal" name="boston_dem"> <url>./data/boston_dem.tif</url> </elevation> <model layer driver="feature_model" name="boston_buildings"> <features driver="ogr"> <url>./data/boston_buildings.shp</url> </features> <styles> <style type="text/css"> building { extrusion: height; extrusion-height: [height_m]; extrusion-flatten: true; fill-color: #b0b0b0; stroke-color: #404040; stroke-width: 1.5; } </style> </styles> </model> </map>这个配置里,image layer负责底图,elevation layer负责地形起伏,model layer就是矢量建模的核心。注意model layer的driver是feature_model,意思是“把矢量要素渲染成几何模型”。features子节点里的driver="ogr"告诉osgEarth用GDAL的OGR接口去读shapefile,这是最常用的方式,比单独写driver="shp"要通用,因为它还能读GeoJSON、PostGIS等。
3.2 矢量样式语言:CSS风格的规则系统
osgEarth的矢量样式语言参考了CSS的写法,核心是把要素的视觉表现拆成一个个规则。上面的例子里,building就是样式选择器,表示这个规则应用于名称为building的要素。你也可以用属性过滤,比如写[height_m>30]作为选择器,只拉伸高度大于30米的建筑。
关键属性有三个。第一个是extrusion,它决定了拉伸方式,常用的值是height,意思是“按要素属性值沿垂直方向拉伸”。第二个是extrusion-height,指定具体的拉伸高度值或表达式,这里我写的是[height_m],表示读取属性字段height_m的值。第三个是extrusion-flatten,这个很多人不知道,它表示是否在拉伸前把多边形压平,默认值是false,如果地形起伏大,不压平的话建筑的底面会随着地形扭曲,所以建筑通常要设成true。
3.3 表达式与属性映射:让数据驱动模型形态
如果你只是把所有建筑都拉伸成同样的灰色方块,那这个三维模型没有太多信息量。更好的做法是通过表达式把属性表中的信息映射成颜色、高度、透明度等视觉变量。
举个例子,把建筑按高度染色,可以用下面这种样式配置:
<style type="text/css"> building { extrusion: height; extrusion-height: [height_m]; fill-color: expression("color = uvec3(1.0 - [height_m]/100.0, [height_m]/100.0, 0.4)"); } </style>expression支持osgEarth内置的表达式语法,这里uvec3表示一个三维向量,用来构造RGB颜色。这个表达式的意思是:高度越高,红色分量越低,绿色分量越高,形成一个从红到绿的渐变。这种把属性映射到视觉变量的方式,在数据可视化里非常实用,比如用颜色区分地类、用高度表示人口密度,都能直接在样式里写完。
3.4 拉伸高度单位与地形基准:让建筑稳稳站在地面上
在geocentric场景里,拉伸高度默认的单位是米,但这只是“相对高度”,建筑的底部在哪个高程基准上则取决于是否开启clamp。如果建筑数据本身没有高程信息,只靠拉伸是无法贴合地形的,底部可能陷入地里或者悬空。
地形基准的处理有三种常见模式。一是靠elevation layer提供真实地形,然后让建筑底部贴到地形表面,也就是clamp到地形;二是完全忽略地形,把建筑底面放在海拔0米,适合平坦场地或飞行漫游场景;三是用extrusion-flatten把压平,再用extrusion-height作相对高度,让建筑随着地形整体起伏。实际项目里,城市级场景通常选第一种,但要注意建筑密度高的时候,逐要素clamp会导致布置和地形采样计算量成倍增长,大数据量时需要考虑性能优化。
3.5 点、线、面不同几何类型的建模思路
shapefile不只是面要素,道路是线要素,路灯、树木是点要素,它们在osgEarth里的建模方式完全不同。面要素最适合拉伸成体块,这是最直观的三维模型生成方式。线要素一般做贴地处理或管线建模,比如道路可以贴在地形表面形成路网,也可以用extrusion做墙体式效果,或者是做管线时把线偏移一个半径变成圆柱。点要素适合做模型替换,也就是所谓的instance,给每个点位置放一个gltf模型文件。
模型替换的配置方法是在样式里指定模型源,比如:
<model layer driver="feature_model" name="trees"> <features driver="ogr"> <url>./data/trees.shp</url> </features> <styles> <style type="text/css"> tree { model: "./models/oak.gltf"; scale: 1.0; } </style> </styles> </model>这种部署方式的优势在于,几十万个点要素加载的是同一个模型文件,GPU实例化渲染,不会因为模型数量多而卡顿。
4. 实操过程与核心环节实现:从零搭一个波士顿街区
4.1 数据准备环节:公开数据的获取与预处理
要复现这个案例,第一步是准备数据。波士顿地区的建筑轮廓数据可以从MassGIS(马萨诸塞州地理信息平台)下载,数据格式是shapefile,坐标系通常已经是WGS84或者Massachusetts State Plane投影。如果是State Plane投影,需要用arcgis或QGIS先做一个投影转换。
我的习惯是先建一个固定目录结构,把原始数据放data_raw,处理后的数据放data,earth文件和模型文件放根目录下。整个流程里最影响成败的是两个操作:一是统一坐标系,在QGIS里把原始shp另存为WGS84地理坐标系;二是用窗口裁剪数据范围,避免加载整个州的数据导致场景卡顿。用一个矩形框切出波士顿市中心范围,一般就能满足演示需求。
4.2 高程数据准备:没有DEM时让建筑看起来更真实
很多演示案例只做建筑拉伸,没有高程数据,出来的效果就像一堆盒子平铺在平地上,虽然能用,但少了地面的起伏感。加一层高程数据之后,地形的不规则起伏配合建筑拉伸,立体感立刻强很多。
DEM数据可以用SRTM或者ASTER GDEM的公开数据下载,范围覆盖全球,分辨率在30米左右,做城市级场景基本够用。下载下来的是tif格式,直接给osgEarth读就行。要注意的是,SRTM原始数据可能带了NoData值,加载到osgEarth里会出现坑洞,建议在QGIS里先用填洼工具或者重新采样把NoData处理掉,再导出成GeoTIFF。
4.3 编写earth配置文件:从空文件到完整场景
实际写earth文件时,我习惯分三步走。第一步只写map节点和影像层,加载后确认底图位置正确,经纬度对得上。第二步加高程层,看地形起伏是否正常。第三步加vector model图层,逐个检查矢量数据的加载效果。
在第一步确认位置的时候,可以直接用osgEarth附带的osgearth_qt工具打开earth文件,鼠标左键旋转、滚轮缩放,判断相机视角是否正确。如果影像层加载完场景一片白或黑,多半是影像数据范围与map节点的坐标系不匹配,或者数据源URL路径写错了。
第三步里最容易出问题的是style选择器名称的匹配。如果在shapefile的属性表里有一个字段叫NAME,值是"BuildingA",那么在CSS里可以写[NAME='BuildingA']作为精确匹配选择器。很多时候模型不显示,就是因为选择器名称和属性值对不上。
4.4 进阶优化:多密度LOD与批量瓦片
当数据量变大时,直接把几十万个要素全部拉伸成模型,无论内存还是帧率都顶不住。解决方案是用osgEarth的LOD机制对矢量数据进行分层,在远距离显示简化版,近距离显示完整模型。
我用的一个比较实用的策略是把矢量图层拆成两个model layer:一个加载经过多边形简化算法处理过的低精度shp,用于远距离显示;另一个加载原始高精度shp,用于近距离显示。然后通过max_range和min_range属性控制显示距离。
<model layer driver="feature_model" name="buildings_lod2"> <features driver="ogr"> <url>./data/boston_buildings_simplify.shp</url> </features> <max_range>3000</max_range> <styles> <style type="text/css"> building { extrusion: height; extrusion-height: [height_m]; fill-color: #909090; } </style> </styles> </model> <model layer driver="feature_model" name="buildings_lod1"> <features driver="ogr"> <url>./data/boston_buildings.shp</url> </features> <min_range>0</min_range> <max_range>1500</max_range> <styles> <style type="text/css"> building { extrusion: height; extrusion-height: [height_m]; fill-color: #b0b0b0; } </style> </styles> </model>这样的分层配置不会增加地图文件复杂度,但场景流畅度提升非常明显,算是投入产出比最高的一种优化手段。
4.5 在场景中叠加注记和辅助要素
除了三维模型,形状数据还能用来生成注记。osgEarth的annotation功能可以把点、线、面的属性文本直接贴到场景里。例如给建筑加一个名称标签,配置方式是在model layer之外再加一个annotation layer。
<annotation layer driver="ogr" name="labels"> <features driver="ogr"> <url>./data/boston_buildings_labels.shp</url> </features> <styles> <style type="text/css"> text { text: [NAME]; fill-color: #ffffff; declutter: true; } </style> </styles> </annotation>declutter属性很实用,它开启文字避让,密集的建筑不会出现标签互相遮挡的情况。不过在三维场景里,标签的朝向和大小需要跟着视角实时调整,这个设置在osgEarth 2.x里面还不够灵活,很多项目会选择在渲染后处理阶段或者用UI组件叠加,而不是直接用annotation。
5. 常见问题与排查技巧实录:按报错场景逐一解决
5.1 矢量数据加载不上:OGR读取失败的原因
报错信息一般在控制台输出,常见的有“Unable to open datasource”和“Failed to open shapefile”。前者要检查路径、文件名拼写,尤其是Windows系统下路径里的反斜杠要改成双反斜杠或正斜杠。后者要检查三件套文件是否齐全,尤其是.dbf缺失会导致属性读取失败。
还有一个非常隐蔽的问题是:shapefile文件名包含了中文或特殊字符。GDAL/OGR对文件路径编码的处理在不同操作系统上并不一致,遇到中文路径容易直接打不开。我的经验是项目路径尽量用纯英文,数据文件名也规范成字母加下划线的格式。
5.2 模型显示不出来:样式规则没有命中
配置写了一大段,结果场景里一个模型都没有,这种情况八成是style选择器没有匹配到任何要素。osgEarth的样式规则是按顺序匹配的,第一个匹配的规则生效,后面同名规则不会覆盖。
排查步骤是先去掉样式选择器里的属性过滤,写一个通配规则比如“*”,确认要素本身的几何图形能不能加载;如果能加载,再逐步加回属性过滤条件。还有一个辅助手段,在命令行里加上OSGEARTH_NOTIFY_LEVEL=DEBUG,能看到每条矢量要素被哪些样式规则处理。
5.3 模型整体偏移:坐标系基准不一致
整体偏移的特征是模型不在影像对应的位置上,而是整体移动了一段距离。通常原因是shapefile是投影坐标,但没有被转成WGS84,或者map节点用了geocentric而数据是经纬度。
解决思路是用gdaltransform或者QGIS重新投影,而不是在osgEarth里手动加偏移。手动加偏移只能平移一个固定量,但只要数据范围大一点或者地形有旋转,正确转投影之后才不会有形变误差。对于精度要求高的场景,比如城市规划项目,建议用七参数转换,不能简单用WGS84的经纬度直接替换。
5.4 建筑悬浮或陷地:高程基准和拉伸基准不一致
这个问题在城市级项目中特别明显,尤其是在起伏地形上。建筑浮在半空,是因为拉伸后的体块底部还是参照海拔0米,而地形本身海拔已经几十米了。解决办法是给拉伸的样式开启clamp,让几何体统一贴到地形表面。
配置里需要在style块中加一行:
building { clamp-to-terrain: true; }如果用了clamp-to-terrain建筑依然陷地,常见原因是DEM数据本身精度不够,在陡峭地形上的采样点高程有偏差,导致建筑被嵌入坡面。这时候可以配合extrusion-flatten,让建筑底面纯水平,再把底部做一个小幅度的抬高,视觉上就稳了。
5.5 大数据量场景卡顿:性能优化方向
当要素数量超过十万级,而且每个要素都带拉伸,帧率会明显下降。性能瓶颈主要在几何体生成和渲染提交两个环节。优化方向有三个。
第一个是简化几何,在QGIS或者arcgis里用简化算法减少多边形顶点数,对建筑这种规则轮廓,顶点数减一半几乎看不出差别。第二个是分段显示,前面提到的LOD方式,远距离用低精度数据甚至用图片替代。第三个是开启缓存和批处理,osgEarth支持在加载时对矢量进行瓦片化,生成缓存到本地,第二次运行时速度会有数量级提升,配置方式是加一个缓存目录:
<map> <options> <cache type="filesystem"> <path>./cache</path> </cache> </options> </map>第四个方向是把静态建筑批量导出成b3dm或者gltf瓦片,交给专门的瓦片加载模块,这个适合做最终交付,因为运行时不需要再读shp、再执行样式解析,而是直接加载已烘焙好的三维模型瓦片,性能是最佳的。
写在最后:一点个人经验
我在实际项目里用osgEarth处理过不少类似boston的建筑矢量数据,最大的体会是,这个框架把“数据到模型的转换”压缩到了配置层面,你不需要写复杂的C++代码,但你对坐标、单位、样式属性映射的理解必须足够扎实,否则一个很小的单位换算错误,整个城市模型的高度和位置就会全线崩溃。所以如果你刚开始学,建议别急着追求炫酷效果,先从一份简单的建筑shp跑通整个流程,再去叠加复杂属性、做色彩映射和LOD优化。等你把这一套跑顺了,从shapefile到三维模型的整个链路就彻底成为你自己的东西了。