在三维GIS项目里折腾过osgEarth的朋友,应该都体会过那种“数据堆了一桌、代码改了一夜”的滋味。我最早用osgEarth做地形可视化时,高程、影像、SHP边界是分三套代码分开加载的,每换一块测试区域,就得改路径、调参数、重新编译一遍,循环往复。后来才真正意识到,earth文件才是osgEarth这套框架的核心“剧本”——场景里该有什么数据、数据从哪里来、按什么规则叠加,都应该写在一个可配置的文本里,而不是散落在C++源码中。这篇实战教程就围绕osgearth、earth文件、高程、影像和SHP这五个关键词展开,给出一份能直接落地复用的配置方案,附完整代码和逐项说明。适合刚开始入门osgEarth、被数据加载顺序和坐标系问题折磨过的朋友,也适合项目里需要频繁切换数据源、希望把三维场景配置化的开发者。
1. 为什么非要用earth文件来管数据
很多刚接触osgEarth的人,第一时间想到的是直接在C++代码里一行一行加载数据。这没有错,但是只要你做过一个稍微正式一点的项目,就会察觉到这条路走不长。早期我做过一个小样的三维地形预览工具,功能很简单:加载一块高程、叠加一张影像、再画几个SHP多边形。我按常规方式写了三个DataNode,调试的时候一切正常,可等到换了数据源,问题就全冒出来了。路径得重新硬编码,坐标系参数要改代码,图层顺序不小心调反,整个场景就黑一半。说白了,代码和配置混在一起,是维护性最大的敌人。
1.1 多源数据管理里的真实痛点
回到实际操作场景。一个标准的三维场景,通常至少包含三类数据:高程(DEM)数据、影像(DOM)数据、矢量边界(SHP)数据。这三类数据来源不同,坐标系可能不同,分辨率差别也很大。如果不用earth文件统一管理,你大概率会遇到这几种情况:
- 改数据路径要重编译:每回换一个测试区域的DEM,就要在代码里改文件路径,改了还怕路径里有中文或特殊字符,osgEarth解析出问题。
- 坐标系五花八门:有的数据是WGS84经纬度,有的是Web Mercator,有的是地方坐标系。裸写在代码里没人会为每次改动去查参数表,一旦set层次关系不对,数据直接飘移。
- 图层顺序不直观:加载了影像、高程、矢量后,你很难一眼看出来哪个在上、哪个在下、哪个参与了地形起伏驱动。
后来我彻底换成了earth文件驱动的方式。场景里所有的数据源变成了一段段的XML描述,放在一个.earth文件里。不管是换DEM、换影像,还是调整图层顺序,全部在文本里完成,改完刷新就能看到效果。这才是项目级的组织方式。
1.2 earth文件在整个项目里的角色
说一个粗浅但准确的类比:earth文件是三维场景的“总谱”,C++程序只是负责演奏这份总谱的乐队。乐队可以换,谱子不用重写。你定义地图的范围、坐标系、高程层、影像层、矢量图层,全部通过XML标签描述,osgEarth在运行时会解析这些标签,自动完成数据读入和图层装配。
这个设计带来的最大好处,是场景制作与代码开发解耦。测绘内业的朋友可以把一个earth文件当作“工程文件”来做,开发人员只需要写一个通用的加载入口。项目里如果有很多测试点,每个测试点都可以有自己的earth文件,运行参数通过命令行传入,连代码都不用动。这也是官方示例里osgearth_viewer可以同时打开不同.earth的原因之一。
所以本文把重心放在earth文件配置本身上,而不是写一堆C++代码。理解了earth文件的写法,你等于掌握了osgEarth的枢纽。
2. 先把三样数据准备好:高程、影像、SHP的选型与预处理
在碰earth文件之前,先把手上这三类数据“洗净”。我不止一次看到有人拿着无效数据跑来问为什么加载不出来,最后发现是数据本身的问题。earth文件配置得再漂亮,也救不了一份坐标烂掉的高程。
2.1 高程数据:选什么格式,避免哪些坑
高程数据在osgEarth里通过heightfield图层读取。最常用的格式是GeoTIFF,单波段、带高程值的TIFF。一般来自SRTM、ASTER GDEM、ALOS等开源DEM数据,或者测绘部门提供的成果DEM。
高程数据最容易踩的坑有三个:
- 一个是有无效值(NoData)。SRTM原始数据里经常有空洞或填充值,如果直接丢给osgEarth,场景里会出现突兀的“针”或者黑洞。拿到数据先检查,有NoData的用GDAL工具填掉,再用,后面渲染才干净。
- 另一个是单位不统一。有的高程数据用米,有的用英尺,还有少数旧数据高程异常值。虽说不影响加载,但显示出来的地形幅度完全不对,排查起来很闹心。
- 还有一个是坐标系问题,这个我后面结合earth文件一起说。
还有一点很多人容易忽略:高程数据的范围往往比影像小,比如我只下载了一块山区的DEM,但影像覆盖范围大一圈。配置的时候,高程层缺少数据的地方,osgEarth会返回无效高度,地形默认变成平地,这个现象在初期调试时不要误判成bug。
2.2 影像数据:从GeoTIFF到影像金字塔
影像数据可以是GeoTIFF、JPEG、PNG,甚至网络瓦片(如TMS、WMS)。不过如果你的影像是什么卫星图、航飞正射影像、历史影像这种动辄几GB的大文件,我强烈建议先做金字塔,也就是影像金字塔(overview)。
为什么?因为osgEarth虽然在3D渲染时对数据进行分块调度,但如果你给它的原始文件只有一层全分辨率,没有金字塔,每次相机拉远时,它都得从原始大图里抽取、缩略,性能会非常差。用gdaladdo建几层概览,或者直接用gdal_retile生成规范的金字塔目录,加载速度能迎来质变。
我自己有一个习惯:影像进入三维场景之前,统一用GDAL处理一遍,转换到目标坐标系,同时生成金字塔。用命令行做的话,大致是这样:
gdal_translate -of GTiff -co TILED=YES -co COMPRESS=LZW -co BIGTIFF=YES source.tif target_3857.tif gdaladdo -r average target_3857.tif 2 4 8 16 32 64这两条命令做完,影像数据就适合交给osgEarth了。坐标系的转换很重要,我通常直接转到3857,也就是Web Mercator,主要是因为后续很多SHP数据都是这个坐标系,统一起来省心。
2.3 SHP数据:坐标系统一与属性字段
SHP是矢量数据里最常见的格式。加载SHP本身并不复杂,但要注意两点。一个是SHP的坐标系必须和工程坐标系匹配。另一个是SHP数据的字段和属性值,决定了你能做出什么效果——比如加载一个行政区边界,你是只画边界线,还是要填充区域,甚至根据某个字段做颜色分级,这些需要提前统一。
我有一个比较“土”但很有效的做法:拿到SHP第一件事,打开QGIS,看一眼属性表的字段名和坐标系,先用QGIS另存为需要的坐标系,投影变了,属性表里的内容不会变。这样到写earth文件时,坐标系一目了然,不用去猜。
ogr2ogr -t_srs EPSG:3857 target.shp source.shp如果你手上只有SHP边界,没有样式要求,这一步就够用了。如果还要在osgEarth里按字段渲染,那么属性字段的命名和类型,建议不要用中文。中文字段名在某些底层驱动里解析容易出幺蛾子,建议选型阶段顺手改成英文,省得后面为编码问题头痛。
3. earth文件的核心写法:从map标签到各数据源配置
现在数据准备好了,开始写earth文件。这个文件的本质是XML,后缀是.earth。看起来很简单,但每个标签的层级与属性都有讲究。刚开始不熟悉的时候,我经常因为漏了一层嵌套,导致图层没进map,或者高程层根本没参与地形生成。
3.1 map、options、srs:整个配置的基石
earth文件的根是<map>标签。官方文档给的推荐结构是:
<map name="MyScene"> <options> ... </options> <heightfield ...>... </heightfield> <image ...>... </image> <vector ...>... </vector> </map>注意,<options>不是必须的,但我每次都会写,因为里面可以设置投影坐标系和缓存目录。<map>标签上也可以直接加srs属性,定义整个地图形参考系。
先确定全局坐标系,这一点极其关键。如果全局是EPSG:4326,那么高程、影像、SHP最好都是经纬度坐标。如果全局是EPSG:3857,那么所有数据源也应该是3857。osgEarth不是不能做动态重投影,但说实话,数据在跑实时重投影的时候,既费时间又有潜在的精度损失。况且源数据千奇百怪,真正动态重投影的效果经常不如预处理好。
我在项目里的默认选择是EPSG:3857,原因很简单:很多在线瓦片服务用的就是这个坐标系,后续如果要用XYZ在线影像作为底色,可以无缝对接。如果你处理的是地方测绘数据,高程和影像都是CGCS2000/高斯克吕格投影,那就直接以你的当地坐标系作为map的srs,不要强行转到3857,因为高程驱动地形时,单位是米,用投影坐标系反而更直观。
3.2 heightfield与image:高程和影像的配置要点
高程数据源的标签是<heightfield>,影像数据源的标签是<image>。它们常见的属性包括driver、url、name等。下面是一个最简例子:
<heightfield name="dem" driver="gdal"> <url>dem.tif</url> </heightfield>而driver很像是一个插件名称,gdal表示通过GDAL驱动读取栅格。osgEarth支持的driver有很多:gdal、tms、wms、tilecache、xyz、arcgis等。如果数据是本地栅格,就用gdal;如果数据是网络瓦片,就用tms或xyz。
影像层也类似:
<image name="img" driver="gdal"> <url>image.tif</url> </image>有人会问,高程和影像是不是必须成对出现?不是。你可以只有高程没有影像,也可以只有影像没有高程,甚至可以只有矢量。不过在实际的三维地形可视化里,通常是高程+影像一起出现:影像负责“画皮”,高程负责“撑骨”。
这里还有一个比较高级的细节:多个影像源可以叠在一起,osgEarth会按照图层顺序从上往下叠色。这就适合“影像底图+本地矢量注记底图”叠加的场景。图层顺序是由earth文件里的书写顺序决定的,排在前面的在上层。
3.3 vectordata与feature:SHP加载的关键逻辑
SHP数据在earth文件里是通过<vector>标签加载的。但注意,矢量数据在osgEarth里不是直接画到屏幕上的,而是进入“feature”管线,经过样式规则(style)转换后,变成图形绘制出来。加载SHP的基础写法:
<vector name="boundary" driver="ogr"> <url>boundary.shp</url> </vector>如果只是这样写,界面上可能什么都看不到。因为osgEarth不知道你打算怎么画这条边界——是画线,还是画填充面,宽度多少,颜色多少。你需要在矢量数据内部设置<styles>,告诉osgEarth怎么去表达这条边界。
<vector name="boundary" driver="ogr"> <url>boundary.shp</url> <styles> <style type="text/css"> boundary { stroke: #ffcc00; stroke-width: 2px; } </style> </styles> </vector>解读一下这段样式:boundary是样式名称,对应SHP里的图层名;stroke是边框颜色;stroke-width是线宽。如果你要填充整个面,还需要增加fill样式属性。这里的设计思维与Web前端里的CSS非常相似——osgEarth把矢量样式从数据加载里剥离开,所以你在配置矢量时,用一段CSS风格的文本就能控制渲染效果。
可能你也发现了,SHP加载的难点其实不在“加载”,而在“样式”。我会在下面完整示例中给出一份能直接看到边界线的配置。
4. 一份拿来就能用的完整earth文件:高程+影像+SHP
前面讲了不少原理,下面给出一份完整的earth文件。这份配置我实际跑过,在osgEarth 2.10和3.x版本上都能正常加载。数据源放在相对路径下,所以只要把三个数据和这个earth文件放在同一个目录,就能直接打开。
4.1 完整earth文件代码与分段说明
<map name="demo_scene" srs="EPSG:3857"> <options> <!-- 缓存目录:第一次加载后生成瓦片缓存,二次加载明显变快 --> <cache type="filesystem"> <path>./osgearth_cache</path> </cache> </options> <!-- 高程数据 --> <heightfield name="elevation" driver="gdal"> <url>dem.tif</url> </heightfield> <!-- 影像数据 --> <image name="imagery" driver="gdal"> <url>image.tif</url> </image> <!-- SHP 边界数据 --> <vector name="boundaries" driver="ogr"> <url>county.shp</url> <styles> <style type="text/css"> county { stroke: #ffff00; stroke-width: 2.5px; fill: #ff0000; fill-opacity: 0.2; } </style> </styles> </vector> </map>逐段说一下我为什么这么写。第一步,map标签的name和srs,决定了当前场景的坐标系。这个例子用的EPSG:3857,所以三个数据源文件都建议是3857。如果你手里的三样数据都是WGS84经纬度,那把srs="EPSG:4326"改掉即可。
第二步,options里的cache。这个很重要。第一次加载大影像和高程时,osgEarth会把中间瓦片写到osgearth_cache目录里,之后再次加载时不需要重新读取原始文件,体验差距很大。如果你测试后觉得配置改动频繁,还可以在运行时临时禁用缓存,或者在代码里换缓存目录。缓存类型除了filesystem,还有sqlite、mongodb等,文件系统最直观。
第三步,高程和影像。这两个标签的结构几乎对称,不过要注意:image不能驱动地形,heightfield才是负责生成地形的。有些新手把高程也写成<image>,最后看到的是平面,原因就在这里。
第四步,vector标签。注意样式类型是text/css,里面定义的是样式规则。内容我解释一下:county是样式名字,按常理是SHP图层名。如果不确定,可以先用QGIS查看图层名,再写到这里。stroke定义边框,fill定义填充色,fill-opacity定义透明度,这让行政边界既能看清范围又不遮挡底图。
4.2 用osgearth_viewer验证加载效果
写完earth文件,不要急着写C++代码。先拿官方自带的osgearth_viewer命令验证一下:
osgearth_viewer demo.earth运行后,你可以用鼠标旋转、缩放场景。如果一切正常,你会看到地形有起伏,影像贴在地表上,行政边界线以半透明红色面域和黄色边界线的形式叠加在最上层。
这一步能验证很多事:数据源路径是否写对了、坐标系是否兼容、栅格数据是否能为osgEarth所读取。如果这里都正常,后面写代码集成自然水到渠成。
我曾经犯过一个低级错误:所有数据文件都放在data/子目录里,但earth文件写在根目录,url直接写了文件名,导致加载失败。后来我意识到,earth文件里的url路径是相对earth文件自身所在位置解析的,不是相对工作目录。你可以用绝对路径避开这个问题,但更干净的做法是保持相对路径,把earth文件和data目录组织好。
4.3 在C++里加载这个earth文件
虽然本文重点在配置,但还是要说一句代码怎么调。如果你的程序用osgEarth的API,加载earth文件实际上只需要一个MapNode:
#include <osgEarth/MapNode> #include <osgEarth/Map> #include <osgEarth/MapNodeOptions> #include <osgEarthUtil/EarthManipulator> #include <osgViewer/Viewer> int main(int argc, char** argv) { osgEarth::initialize(); osg::ref_ptr<osgEarth::Map> map = new osgEarth::Map(); osg::ref_ptr<osgEarth::MapNodeOptions> mapNodeOptions = new osgEarth::MapNodeOptions(); osg::ref_ptr<osgEarth::MapNode> mapNode = new osgEarth::MapNode(map, *mapNodeOptions); osg::ref_ptr<osgEarth::MapNodeOptions::ReaderWriter> rw = new osgEarth::MapNodeOptions::ReaderWriter(); mapNodeOptions = rw->read("demo.earth"); osg::ref_ptr<osgViewer::Viewer> viewer = new osgViewer::Viewer(); viewer->setSceneData(mapNode.get()); viewer->setCameraManipulator(new osgEarth::Util::EarthManipulator()); return viewer->run(); }准确地说,osgEarth 2.x到3.x的接口有些变化,我在这里给的写法适合2.x/3.x常见版本。关键点在于:通过osgDB::readNodeFile("demo.earth")返回的节点本身就包含了MapNode信息,你只要把它作为SceneData,配上EarthManipulator就能操作地球。这部分细节不同版本差异较大,建议去对应版本的示例工程里搜loadEarthFile。不过只要earth文件配置正确,换不同的代码入口都不会太折腾。
5. 加载过程中的高频坑与排查思路
配好一份earth文件,第一次就可能摔跟头。这里把我在实际项目里遇到的几类常见问题及其排查链路写出来。很多问题不是配置本身的问题,而是数据与数据之间坐标系、缓存、样式不对齐导致的。
5.1 数据源不显示,先别急着改代码
如果场景空白,或者某个图层完全看不到,第一步不是改代码,而是用命令行工具检查数据本身。我的排查顺序是这样的:
- 先把数据用QGIS或者Global Mapper打开确认文件完整、范围正确。
- 再用GDS命令行看坐标系是否和earth文件的srs一致。
- 再看earth文件里的url路径是否与earth文件相对路径正确。
- 最后才看样式规则,比如矢量线宽是否太小、透明度是否太低。
大多数“图层不显示”的问题,最终都指向坐标系或者路径。特别提醒:有时候投影坐标系的单位是米,而经纬度坐标系的单位是度。如果你的map是3857,给的高程却是4326的未处理数据,osgEarth虽然理论上能做重投影,但某些底层驱动遇到投影范围变化时,会出现高程拉伸或者地形扭曲。这时候最省事的办法是把高程数据先投影到3857,再用earth文件加载。
还有一个看起来不起眼但也容易踩的坑:数据文件名带空格或中文。我在Windows环境试过带中文路径的SHP,有时能加载有时不能,且报错信息还看不懂。处理方式很简单,复制数据到纯英文路径下,文件名里用下划线代替空格。
5.2 坐标系不统一的典型表现与处理
三样数据坐标系不统一,会出现两个典型怪象。一个是高程影像错位,影像贴在高程上但边界对不齐,像一张画布被硬扯到凹凸不平的模型上。另一个是矢量漂移,SHP边界画在离实际位置很远的地方,甚至出现在海对面。
我建议用gdalinfo检查每个文件的投影信息:
gdalinfo dem.tif | grep -E "Coordinate System|PROJCRS|GEOGCRS" gdalinfo image.tif | grep -E "Coordinate System|PROJCRS|GEOGCRS"SHP则用ogrinfo:
ogrinfo county.shp -so -al | grep -E "Coordinate System|PROJCRS|GEOGCRS"如果发现三样数据不一样,统一用GDAL和OGR转成目标坐标系。一般来说,推荐以影像或高程中范围更大、精度需求更高的数据为基础选坐标系,再转其他两个。比如你的底图是省卫星影像,高程是全国SRTM,边界是县界SHP,那我建议以影像的坐标系作为全场景统一坐标系。
5.3 性能问题:缓存、瓦片与内存
数据能显示了,跑起来却很卡,这是另一个常见的坑。说一个很普遍的情况:影像有5GB,没有建金字塔,osgEarth每帧都要从超大TIFF里抽取数据,CPU占用直接拉满,镜头一转就卡成PPT。
解决思路有两个方向。一个是数据侧:给影像建金字塔,方法上文的gdaladdo命令即可。另一个是参数侧:在earth文件的<image>标签里设置max_level、min_level等瓦片层级限制,防止相机拉近时加载过细的层级。比如:
<image name="imagery" driver="gdal"> <url>image.tif</url> <max_level>18</max_level> <min_level>5</min_level> </image>这里min_level不是必须的,但max_level能防止用户过度放大时,程序拼命加载超出数据实际分辨率的瓦片,造成卡顿。实际配置时可以根据数据的最高分辨率来定。如果你的影像地面分辨率是1米,max_level设为18或19基本合适;如果只是看全域轮廓,15、16已经够。
还有一点与内存有关:osgEarth默认的瓦片缓存大小有限,如果内存吃紧,可以考虑在<options>里加<osgEarth:scene>级别的缓存大小控制,或者通过代码设置资源限制。这种调优和具体业务关系很大,我一般只在实际帧率不达标时再动。
5.4 矢量SHP加载后没有样式的处理
最后单说一下SHP加载后“只显示点、不显示线面”的情况。这种问题十有八九出在样式规则上。osgEarth的样式匹配依赖于SHP图层名。如果你在<vector>内部定义的样式名和SHP里的图层名不一致,它就默认用无样式渲染,等于看不见。
排查时,先在QGIS里打开SHP图层的属性,确认图层名,然后把earth文件里<style>下的样式名改成完全一致。另外,注意CSS样式里属性的写法。stroke是矢量线条边框颜色。stroke-width的默认单位是像素,2.0~3.0肉眼看起来比较舒服。如果看不到填充区域,检查一下是否是fill-opacity设置得太低,或者图层没有形成闭合面。
还有一种情况是,SHP要素类型是Point,但样式写了stroke和fill,这种匹配不上自然也就不显示。点数据应该用marker符号,这类需求在点状地物(如基站点位、采样点)里很常见。如果你用点SHP,样式里要写marker-placement和marker-file,或者用osgEarth内置的模型符号。
6. 视角与后续扩展
我实际用这套earth文件方案做了两个小项目,一个是用省域边境SHP叠加历史影像做变化分析,另一个是拿城市规划边界叠加倾斜摄影和高程做选址预览。前者的核心收获是:一旦把数据配置写进earth文件,换不同年份的历史影像只需要改一行url,后面所有叠加分析都不用重新编译代码。后者的收获是,高程数据的好坏直接决定三维场景的“实用感”,如果DEM分辨率太低,地形就像一块块台阶,SHP边界叠上去后没法提供有效参考。
给你一个非常实用的建议:不断更新一套自己的“数据预处理+earth配置模板”。把GS转换命令、金字塔命令、earth文件基本骨架作为项目初始化模板,每次拿到新数据,先跑一遍预处理,再替换url。这样即便来了新人接手,也能快速上手,而不会因为某个OSG版本接口不同,陷进写代码的泥潭。
最后分享一个我个人的小习惯:每次配置earth文件时,打开osgearth_viewer之前,先开系统的资源监视器,看到CPU和内存占用正常后,再加载大场景。如果加载时间异常长,先看是不是首次缓存导致,不要急着下结论说配置有错。等缓存建立起来,二次打开的速度快好几倍,这是osgEarth最给我省时间的特性之一。