ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

osgEarth三维场景配置实战:高程、影像与SHP数据统一管理

osgEarth三维场景配置实战:高程、影像与SHP数据统一管理 在三维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 TILEDYES -co COMPRESSLZW -co BIGTIFFYES 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 nameMyScene 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 namedem drivergdal urldem.tif/url /heightfield而driver很像是一个插件名称gdal表示通过GDAL驱动读取栅格。osgEarth支持的driver有很多gdal、tms、wms、tilecache、xyz、arcgis等。如果数据是本地栅格就用gdal如果数据是网络瓦片就用tms或xyz。影像层也类似image nameimg drivergdal urlimage.tif/url /image有人会问高程和影像是不是必须成对出现不是。你可以只有高程没有影像也可以只有影像没有高程甚至可以只有矢量。不过在实际的三维地形可视化里通常是高程影像一起出现影像负责“画皮”高程负责“撑骨”。这里还有一个比较高级的细节多个影像源可以叠在一起osgEarth会按照图层顺序从上往下叠色。这就适合“影像底图本地矢量注记底图”叠加的场景。图层顺序是由earth文件里的书写顺序决定的排在前面的在上层。3.3 vectordata与featureSHP加载的关键逻辑SHP数据在earth文件里是通过vector标签加载的。但注意矢量数据在osgEarth里不是直接画到屏幕上的而是进入“feature”管线经过样式规则style转换后变成图形绘制出来。加载SHP的基础写法vector nameboundary driverogr urlboundary.shp/url /vector如果只是这样写界面上可能什么都看不到。因为osgEarth不知道你打算怎么画这条边界——是画线还是画填充面宽度多少颜色多少。你需要在矢量数据内部设置styles告诉osgEarth怎么去表达这条边界。vector nameboundary driverogr urlboundary.shp/url styles style typetext/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 namedemo_scene srsEPSG:3857 options !-- 缓存目录第一次加载后生成瓦片缓存二次加载明显变快 -- cache typefilesystem path./osgearth_cache/path /cache /options !-- 高程数据 -- heightfield nameelevation drivergdal urldem.tif/url /heightfield !-- 影像数据 -- image nameimagery drivergdal urlimage.tif/url /image !-- SHP 边界数据 -- vector nameboundaries driverogr urlcounty.shp/url styles style typetext/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经纬度那把srsEPSG: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_ptrosgEarth::Map map new osgEarth::Map(); osg::ref_ptrosgEarth::MapNodeOptions mapNodeOptions new osgEarth::MapNodeOptions(); osg::ref_ptrosgEarth::MapNode mapNode new osgEarth::MapNode(map, *mapNodeOptions); osg::ref_ptrosgEarth::MapNodeOptions::ReaderWriter rw new osgEarth::MapNodeOptions::ReaderWriter(); mapNodeOptions rw-read(demo.earth); osg::ref_ptrosgViewer::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|GEOGCRSSHP则用ogrinfoogrinfo 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 nameimagery drivergdal urlimage.tif/url max_level18/max_level min_level5/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最给我省时间的特性之一。
返回列表