Windows下OSG+osgEarth预编译3rdParty库CMake避坑

发布时间:2026/10/28 10:36:04
Windows下OSG+osgEarth预编译3rdParty库CMake避坑

简介:面向在 Windows 平台使用 Visual Studio 2019 编译 OSG3.6.5 与 OSGEarth2.10 的 C++ 开发者,这份预编译 64 位三方库包用于补齐编译链路中的依赖项,减少从源码逐个构建第三方组件的重复工作。压缩包共约 2000 个文件,大小 137.76MB,包含大量 h 头文件、lib 静态/导入库、dll 动态库,以及 cmake 配置、inc、c/cpp、hpp、inl、proto 等源码与构建相关文件,并附带 html 文档和时区数据等辅助内容,便于在工程中配置包含目录、库目录与链接项。已有 626 人学习下载,适合需要快速搭建 OSG/OSGEarth 开发环境、排查依赖缺失与链接错误的中高级图形开发者参考使用。

1. 为什么 Windows 上搭 OSG + osgEarth,总是死在第三方库这一步

做过三维地球、或者基于 OSG 做渲染的,大概都试过在 Windows 上从源码编一次 OSG3.6.5 加 osgEarth。编 OSG 本身还算温和,真正让人卡住的是它脚下那堆三方库:GDAL、CURL、PROJ、libzip、SQLite3,各自带着一套 CMake 选项和版本约束。自己逐个编,快则一天慢则一周,还未必能把 ABI 对齐。这份预编译好的 3rdParty.zip 干的就是这件事——按 VS2019(v142)工具集编好,include、lib、bin 一把打包。往下讲它怎么接进 CMake、参数怎么设、哪里最容易翻车,适合已经装好 VS2019、想跳过依赖环节直接写业务代码的人。

2. 3rdParty 包的目录与 ABI:先看清 v142 这三个字意味着什么

拿到压缩包先别急着解压到 C 盘根目录。很多人第一步就踩坑:解到一个带空格、带中文的路径下,比如D:\我的项目\osg 依赖\3rdParty,然后 CMake 里各种NOTFOUND,回头查半天以为是库不全,其实是路径里的空格把find_library的参数截断了。我一般解到D:\osg\3rdParty这种短、纯英文、无空格的路径,后面所有命令都基于它。

2.1 include / lib / bin 三段式布局与真正常被忽略的 share

OSG 系预编译三方库基本都遵循同一套布局,解压后第一层目录应该是这样:

D:\osg\3rdParty\ ├── include\ # 所有依赖的头文件平铺在一起(gdal.h、curl\curl.h、proj.h…) ├── lib\ # 导入库 .lib,Debug 版有的带 d 后缀 ├── bin\ # 运行时 DLL,外加 osgdb_gdal.dll 这类插件依赖 └── share\ # GDAL_DATA、PROJ_DATA 之类的数据目录,容易被当成垃圾删掉

include和lib是编译期用的,bin是运行期用的,这两层的分工大家都清楚。真正容易被忽略的是share——GDAL 做坐标转换要读gdalvrt.xsd、data\下的 EPSG 字典,PROJ 要读proj.db。这两个目录没有出现在include/lib/bin三段式里,很多人第一次清理磁盘时顺手删了,结果程序能编能跑,就是坐标偏出几百米,或者osgEarth加载影像时投影报错。我一般会在解压后先做一次校验:

# 在 Git Bash 里跑,确认三类关键文件都在 ls D:/osg/3rdParty/lib | grep -iE "gdal|proj|curl|sqlite3|zip" | head -20 ls D:/osg/3rdParty/bin | grep -iE "gdal.*\.dll|proj.*\.dll" | head -20 ls D:/osg/3rdParty/share 2>/dev/null || echo "share 目录缺失,PROJ/GDAL 数据需要单独补"

第一行确认导入库存在,第三行确认share是否随包附带。有些精简版 3rdParty 会把share去掉以减小体积,这种情况下必须自己从对应上游版本把proj.db和 GDAL 的data目录拷进来,否则后面坐标转换会静默出错——它不崩,只是结果不对,这种问题最难查。

2.2 为什么 mingw 或 v143 编出来的库不能直接塞进来

MSVC 的 ABI 不是铁板一块。VS2015 之后微软承诺了 v140 到 v143 的二进制兼容,但这条承诺有几个前提:C++ 运行时库版本要能对上、_ITERATOR_DEBUG_LEVEL要一致、RuntimeLibrary(/MD 还是 /MT)要一致。预编译包最大的价值就在于它替你把这一串约束锁死了。

具体到 VS2019,工具集是 v142,_MSC_VER是 1929(16.11 是 1929,16.0 是 1920)。如果你拿一份 MinGW-w64 编的 GDAL 导入库往项目里塞,链接器会直接报符号找不到,因为 MinGW 的 name mangling 和 MSVC 完全是两套体系,__imp_gdal_open和_gdal_open@8根本对不上。更隐蔽的是用 VS2022 的 v143 编的库:链接能过,运行时偶尔崩,因为 v143 的 STL 内部实现有几处布局微调,跨版本传std::string、std::vector这种带堆内存的对象时,一侧的_Container_base12和另一侧对不上,析构时就是一次野指针访问。

所以这份包的关键不是“有 GDAL”,而是“GDAL 是用 v142 编的”。判断方法很简单,用 dumpbin 看导入库或者 DLL 的运行时依赖:

# 用 VS2019 自带的 dumpbin,路径以本机实际安装为准 "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30133\bin\Hostx64\x64\dumpbin.exe" ^ /dependents D:\osg\3rdParty\bin\gdal*.dll

输出里如果出现VCRUNTIME140.dll、MSVCP140.dll,说明是 MSVC 运行时体系;如果出现libgcc_s_seh-1.dll、libstdc++-6.dll,那就是 MinGW 编的,直接不能用。这一步花三十秒,能省掉后面半天的链接报错排查。

2.3 版本对齐表:OSG 3.6.5 吃哪些库、osgEarth 又多吃哪些

OSG 3.6.5 本体对三方库的依赖其实很克制,主要是图像编解码和网络加载;osgEarth 才是真正的依赖大户,它把 GDAL 当几何与栅格引擎、把 PROJ 当坐标转换引擎、把 CURL 当瓦片下载通道。两者对同一份 3rdParty 的消费方式不一样:

依赖OSG 3.6.5 是否直接使用osgEarth 是否直接使用缺失后的表现
zlib是(png/tiff 的底层)间接编译期找不到zlib.h
libpng / libjpeg / libtiff是(osgdb_png 等插件)间接插件编不出来,读图返回空
freetype是(osgText)是文字渲染段错误
curl是(osgdb_curl 插件)是(网络瓦片)远程瓦片 404 或超时
GDAL否是.earth配置里 GDAL 驱动全部失效
PROJ否是坐标转换结果偏移,且不报错
SQLite3否是(feature 缓存)缓存无法写入,日志里刷 warning
libzip否是(kmz 读取).kmz打开失败

这张表的意义在于定位:当链接报unresolved external symbol时,先看符号属于哪一列,就知道该去查 OSG 的 Find 脚本还是 osgEarth 的。OSG 侧找不到库通常是ACTUAL_3RDPARTY_DIR没生效,osgEarth 侧找不到则是CMAKE_PREFIX_PATH或者依赖的传递版本不匹配——尤其是 GDAL 3.x 要求 PROJ 6 以上,版本一旦错配,GDAL 自己编得出来,但调proj_normalize_for_visualization时会返回空。

提示:不要试图用 vcpkg 再装一份 GDAL 去补这个包缺的部分。两套 GDAL 混在一个链接单元里,符号重复定义只是表象,真正的问题是两份proj.db版本不同、坐标轴的轴序约定不同,排查成本远高于重新找一份完整的 3rdParty。

3. 接进 CMake:OSG 3.6.5 从配置到 install 的完整链路

库齐了,下一步是让 OSG 的 CMake 认出它们。OSG 的构建脚本写得比较“老派”,它不依赖find_package的现代模式,而是自己在CMakeModules\Find3rdPartyDependencies.cmake里逐个find_path+find_library,搜索根目录来自一个叫ACTUAL_3RDPARTY_DIR的变量。理解这一点,后面所有奇怪行为都能解释。

3.1 两个路径变量:ACTUAL_3RDPARTY_DIR 与 CMAKE_PREFIX_PATH

ACTUAL_3RDPARTY_DIR是 OSG 自己的私有变量,只对 OSG 的 CMake 生效。它的作用是告诉Find3rdPartyDependencies.cmake:去这个目录下的include找头文件、lib找.lib、bin找 DLL。设了它,OSG 会拼出类似D:/osg/3rdParty/lib/osg160-osg.lib的完整路径去探测。

CMAKE_PREFIX_PATH是 CMake 的通用机制,走的是<prefix>/include、<prefix>/lib、<prefix>/bin这套约定。它对 osgEarth 更关键,因为 osgEarth 用的是标准find_package(GDAL)、find_package(CURL),这些脚本认CMAKE_PREFIX_PATH而不认 OSG 的私有变量。

稳妥的做法是两个都设,指向同一份 3rdParty:

# 这两个变量最好同时存在,覆盖 OSG 私有查找和标准 find_package 两条路径 set(ACTUAL_3RDPARTY_DIR "D:/osg/3rdParty" CACHE PATH "OSG 3rdParty 根目录") set(CMAKE_PREFIX_PATH "D:/osg/3rdParty" CACHE PATH "标准 find_package 搜索根")

参数说明:CACHE PATH表示这个值会写进CMakeCache.txt,后续重新配置沿用缓存;如果换了 3rdParty 路径,必须把CMakeCache.txt删掉重新生成,否则旧路径会被缓存锁死,改命令行参数也不生效——这是新手最常见的“我明明改了啊”的来源。

3.2 一条能直接复制的生成命令

假设 OSG 源码在D:\osg\OpenSceneGraph-3.6.5,安装目标在D:\osg\install,在源码目录下开一个 cmd 窗口:

cmake -S . -B build-vs2019 -G "Visual Studio 16 2019" -A x64 ^ -DACTUAL_3RDPARTY_DIR=D:/osg/3rdParty ^ -DCMAKE_PREFIX_PATH=D:/osg/3rdParty ^ -DCMAKE_INSTALL_PREFIX=D:/osg/install ^ -DBUILD_OSG_APPLICATIONS=ON ^ -DBUILD_DOCUMENTATION=OFF ^ -DCMAKE_CONFIGURATION_TYPES="Debug;Release"

逐项说清楚:-G "Visual Studio 16 2019" -A x64指定生成器和目标架构,架构必须和 3rdParty 的位数一致,包是 x64 的就必须-A x64,混用 32 位会得到一堆LNK1112: 模块计算机类型冲突;BUILD_OSG_APPLICATIONS=ON决定要不要连带编osgviewer、osgconv这些示例程序,对验证很有用,建议开着;BUILD_DOCUMENTATION=OFF关掉文档生成,纯粹省时间;CMAKE_CONFIGURATION_TYPES限定为 Debug 和 Release 两套,避免出现 MinSizeRel 之类没人维护的配置。

生成完成后,不要急着打开 sln 手动点编译,用命令行一次跑完:

# 先编 Release 并安装 cmake --build build-vs2019 --config Release --target INSTALL -- /m # 再编 Debug 并安装到同一前缀下的 debug 子目录(后面会解释为什么) cmake --build build-vs2019 --config Debug --target INSTALL -- /m

--target INSTALL是 CMake 的伪目标,等价于在 VS 里右键 INSTALL 工程生成;-- /m把后面的参数透传给 MSBuild,/m表示多核并行编译,能把 OSG 的编译时间从四十分钟压到十分钟以内——但第一次编建议先不加/m,让报错信息顺序清晰,确认没问题后再开并行。

3.3 编译产物长什么样、Debug 和 Release 必须分开装

编完之后去D:\osg\install看,目录结构是这样的:

D:\osg\install\ ├── bin\ # osg160-osg.dll、osg160-osgDB.dll、osgviewer.exe 等 ├── include\osg\ # 头文件 ├── lib\ # osg160-osg.lib(Release 导入库) └── lib\osgPlugins-3.6.5\ # 插件 DLL:osgdb_png.dll、osgdb_gdal.dll、osgdb_curl.dll…

osgPlugins-3.6.5这个目录名里的版本号是硬编码在 OSG 的 CMake 里的,程序运行时会在bin的同级或者OSG_LIBRARY_PATH指定的位置找它。如果把 DLL 从bin拷到别处、却不带上这个插件目录,结果就是readNodeFile对.png、.tif一律返回空指针,日志里只有一句Warning: Could not find plugin to read objects from file。

Debug 和 Release 必须分开装,原因是 OSG 的导入库命名:Release 是osg160-osg.lib,Debug 在默认配置下会带d后缀变成osg160-osgd.lib,但如果你在同一个build目录里先后生成两种配置,install 到同一个前缀会互相覆盖——最后lib目录里只剩最后编的那个配置,另一种配置的项目引用时找不到对应.lib。我的做法是生成两个 build 目录:

# Debug 单独一个 build 目录,装到独立前缀 cmake -S . -B build-vs2019-debug -G "Visual Studio 16 2019" -A x64 ^ -DACTUAL_3RDPARTY_DIR=D:/osg/3rdParty ^ -DCMAKE_PREFIX_PATH=D:/osg/3rdParty ^ -DCMAKE_INSTALL_PREFIX=D:/osg/install-debug ^ -DCMAKE_DEBUG_POSTFIX=d cmake --build build-vs2019-debug --config Debug --target INSTALL -- /m

CMAKE_DEBUG_POSTFIX=d让 Debug 的产物统一带d后缀,这样即使不小心装到同一目录,DLL 名也不会冲突。代价是项目里引用时要注意:Debug 配置链接osg160-osgd.lib,Release 链接osg160-osg.lib,用 CMake 的debug/optimized关键字区分:

target_link_libraries(myapp PRIVATE debug D:/osg/install-debug/lib/osg160-osgd.lib optimized D:/osg/install/lib/osg160-osg.lib)

4. 编 osgEarth:依赖匹配、选项开关与插件目录

OSG 编完只是走了一半。osgEarth 在它之上,且对 GDAL、PROJ 的版本比 OSG 苛刻得多。真正让 osgEarth 编译失败的原因,九成不是 OSG 没编好,而是 GDAL 的版本和 PROJ 的版本对不上,或者 CURL 的静态库/动态库选错了。

4.1 osgEarth 的 CMake 选项里哪几个真该动

osgEarth 的CMakeLists.txt会吐出一大串OSGEARTH_*选项,但真正需要手动干预的只有四五个。其余保持默认即可,乱改反而更麻烦:

选项默认建议值为什么
BUILD_OSGEARTH_EXAMPLESONON生成osgearth_viewer,是后面自检的工具
OSGEARTH_USE_QT视环境OFF没装 Qt 时开着会卡在find_package(Qt5)报错
BUILD_SHARED_LIBSONON静态链接会把 GDAL 的依赖链全部拖进来
OSGEARTH_ENABLE_PROFILINGOFFOFF开着会在每帧插统计代码,调试没问题,出包记得关
CMAKE_INSTALL_PREFIX空与 OSG 同前缀头文件能一套include找齐,省得配两遍路径

OSGEARTH_USE_QT值得单独说:很多机器上装了 Qt 但只装了 MinGW 版本,osgEarth 的find_package(Qt5)会找到它,然后链接时因为 Qt 是 MinGW 编的、而主体是 MSVC,报一堆LNK2019。与其去修,不如直接-DOSGEARTH_USE_QT=OFF,等真正需要 Qt 界面时再针对性地配一套 MSVC 版 Qt。

4.2 GDAL / PROJ / CURL / SQLite 的对接口子

osgEarth 的FindGDAL.cmake走的是标准流程:先找gdal.h,再找gdal.lib,最后尝试用gdal_version.h读版本号。这条链在 3rdParty 布局正确时一次就过,出问题基本集中在两个地方。

第一是 GDAL 的库文件名。预编译包里的导入库常见形如gdal.lib,但也有的包带版本号gdal305.lib。osgEarth 的查找脚本只认gdal这个名字,遇到带版本号的就找不到。解决办法是在 CMake 命令行里显式指定:

cmake -S . -B build-osgearth -G "Visual Studio 16 2019" -A x64 ^ -DCMAKE_PREFIX_PATH="D:/osg/install;D:/osg/3rdParty" ^ -DGDAL_INCLUDE_DIR=D:/osg/3rdParty/include ^ -DGDAL_LIBRARY=D:/osg/3rdParty/lib/gdal.lib ^ -DPROJ_INCLUDE_DIR=D:/osg/3rdParty/include ^ -DPROJ_LIBRARY=D:/osg/3rdParty/lib/proj.lib ^ -DCURL_INCLUDE_DIR=D:/osg/3rdParty/include ^ -DCURL_LIBRARY=D:/osg/3rdParty/lib/libcurl.lib ^ -DOSGEARTH_USE_QT=OFF ^ -DCMAKE_INSTALL_PREFIX=D:/osg/install

注意CMAKE_PREFIX_PATH这里用了分号分隔两个路径:D:/osg/install是刚编好的 OSG,D:/osg/3rdParty是三方库。Windows 下 CMake 的列表分隔符就是分号,写成逗号或者空格都会失效。

第二是 CURL 的静态/动态之争。3rdParty 的lib目录里可能同时有libcurl.lib(静态)和libcurl_imp.lib(动态导入),前者要求链接时再补一堆ws2_32、crypt32、wldap32的系统库,后者不需要。指向libcurl_imp.lib更省事;如果只有静态库,就得在target_link_libraries里补系统库,否则报一堆unresolved external symbol __imp_WSAStartup。

CURL 还有一个常被忽略的点:CURL_STATICLIB宏。静态链接 libcurl 时,头文件里必须定义这个宏,否则curl.h会按动态导入的方式声明函数,链接时符号对不上。osgEarth 的 CMake 不会自动加,需要手动:

add_definitions(-DCURL_STATICLIB)

PROJ 的坑更隐蔽:GDAL 3.x 内部依赖 PROJ,如果 3rdParty 里 GDAL 是 3.x、PROJ 却是 5.x,GDAL 自己编译时不会报错,但运行到坐标转换就会返回空指针。判断方法是看share目录下的proj.db——PROJ 6 之后坐标系统定义全部搬进了这个 SQLite 文件,PROJ 5 及以前是没有的。有proj.db就说明是 6 以上,和 GDAL 3.x 匹配。

4.3 插件目录与 OSG_LIBRARY_PATH 的真实优先级

osgEarth 编译完之后,bin里会多出osg160-osgEarth.dll、osg160-osgEarthAnnotation.dll、osg160-osgEarthSymbology.dll这一批,插件目录osgPlugins-3.6.5里会多出osgdb_earth.dll、osgdb_kml.dll(如果 GDAL/OGR 编进来了)。

运行时,OSG 查找插件的顺序是这样的:先看环境变量OSG_LIBRARY_PATH里的路径,再看可执行文件所在目录,再看编译时硬编码的OSG_PLUGINS路径。这个顺序意味着——如果你把 exe 拷到别处测试,而OSG_LIBRARY_PATH还指向原来那台机器的安装目录,程序会加载旧插件,表现出来就是“明明新编了 GDAL 驱动,行为还是老的”。

调试期我一般这么设:

# 只在当前 cmd 会话生效,避免污染系统环境变量 set OSG_LIBRARY_PATH=D:\osg\install\lib\osgPlugins-3.6.5 set PATH=D:\osg\install\bin;D:\osg\3rdParty\bin;%PATH% set GDAL_DATA=D:\osg\3rdParty\share\gdal set PROJ_DATA=D:\osg\3rdParty\share\proj

PROJ_DATA在旧版本里叫PROJ_LIB,PROJ 8 之后改成PROJ_DATA,两个都设上不冲突,老代码新代码都能跑。GDAL_DATA指向的目录里应该有gdalvrt.xsd、pcs.csv、gcs.csv这几类文件,缺了的话GDALAllRegister之后读栅格会报Unable to open EPSG support file。

5. 避坑与排查:五条踩出来的记录

下面这几条都是我在不同机器上真实撞过的,按“现象 → 原因 → 解决”写,遇到类似的直接对号入座。

5.1 链接期:LNK2038 与 RuntimeLibrary 值不匹配

现象:链接 osgEarth 时报LNK2038: 检测到“RuntimeLibrary”的不匹配项: 值“MT_StaticRelease”不匹配值“MD_DynamicRelease”,或者反过来。

原因:3rdParty 里的某个库是用/MT(静态 CRT)编的,而 OSG 或 osgEarth 的 CMake 默认用/MD(动态 CRT)。这两套不能混,混了就是两套堆管理器,跨模块new/delete立刻出问题。

解决:先确认 3rdParty 用的是哪套。用 dumpbin 看 DLL 依赖,有VCRUNTIME140.dll说明是/MD,没有则是/MT。确认后在自己的 CMake 里对齐:

# 和 3rdParty 统一为动态 CRT;如果包是 /MT 就改成 MultiThreaded set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")

这行必须在project()之前设,否则不生效。改完记得删CMakeCache.txt重新生成,缓存会锁住旧的运行时设置。

5.2 运行期:双击闪退,缺的是什么 dll

现象:osgearth_viewer.exe双击后窗口一闪就没了,命令行里跑也没有任何输出。

原因:缺 DLL。Windows 加载器在找不到依赖时会直接终止进程,连main都进不去,所以不会有任何日志。最常见的是gdal30x.dll、proj_8_2.dll、libcurl.dll这几个——它们既不在bin里,也没进PATH。

解决:用 dumpbin 递归查依赖,或者用 Dependencies 工具看。我习惯用命令行快速定位:

# 递归列出所有找不到的依赖,路径换成实际 exe "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30133\bin\Hostx64\x64\dumpbin.exe" ^ /dependents D:\osg\install\bin\osgearth_viewer.exe | findstr /i ".dll"

把列出的一级依赖对照D:\osg\install\bin和D:\osg\3rdParty\bin逐个核,缺哪个把所在目录加进PATH再跑。确认没问题后,再把这些 DLL 拷到 exe 同级目录,做成便携形式。

5.3 加载期:.earth 文件报插件缺失

现象:osgearth_viewer test.earth报Warning: Could not find plugin to read objects from file "test.earth",但文件路径明明是对的。

原因:osgdb_earth.dll插件没被加载。可能有两种:一是插件目录里根本没有这个 DLL(osgEarth 编译时没生成),二是OSG_LIBRARY_PATH指向了别的地方,OSG 去那里找不到。

解决:先dir看插件目录里有没有osgdb_earth.dll。有的话,检查环境变量:

# 确认 OSG 实际在哪个路径下找插件 echo %OSG_LIBRARY_PATH% # 如果输出是空或者别的路径,直接在当前会话覆盖 set OSG_LIBRARY_PATH=D:\osg\install\lib\osgPlugins-3.6.5

还有一种情况:插件 DLL 存在,但因为它的依赖(比如gdal30x.dll)没找到,加载时LoadLibrary返回失败,OSG 只报“找不到插件”,不报“依赖缺失”——这就是典型的黑匣子场景。用 dumpbin 查这个插件 DLL 的依赖,比盯着 osgEarth 的日志有用得多。

5.4 结果层:坐标偏了、地形发白

现象:影像和矢量叠在一起,位置对不上,偏了几百米;或者地形加载出来一片白,没有纹理。

原因:偏坐标是PROJ_DATA没设,程序找不到proj.db,坐标转换退化成恒等变换——不报错,只是算错。地形发白通常是GDAL_DATA缺失导致 GDAL 无法识别影像的坐标系,或者osgdb_gdal.dll插件没加载。

解决:两个环境变量都补上,然后写个小程序验证坐标转换:

// crs_check.cpp —— 验证 PROJ 数据可用 #include <proj.h> #include <cstdio> int main() { // 4326(EPSG:4326) 转 3857(EPSG:3857),源点取经度 116、纬度 39 PJ_CONTEXT* ctx = proj_context_create(); PJ* trans = proj_create_crs_to_crs(ctx, "EPSG:4326", "EPSG:3857", nullptr); if (!trans) { std::printf("proj_create_crs_to_crs 失败,检查 PROJ_DATA\n"); return 1; } PJ_COORD in = proj_coord(116.0, 39.0, 0, 0); PJ_COORD out = proj_trans(trans, PJ_FWD, in); // 正常结果约 x≈12913060, y≈4711000,若等于输入说明转换退化了 std::printf("x=%.2f y=%.2f\n", out.xy.x, out.xy.y); proj_destroy(trans); proj_context_destroy(ctx); return 0; }

判定标准很直接:输出接近12913060和4711000就是对的;如果输出还是116、39,说明 PROJ 找不到数据,退化成了恒等变换。这一步能帮你把“坐标偏了”从玄学变成可测的数值。

6. 部署与自检:让 exe 在没有 VS 的机器上跑起来

开发机上能跑不算完,真正交付时对方机器上既没装 VS 也没配PATH,所有依赖都得跟着 exe 走。这一节讲两件事:怎么把 DLL 收齐,以及怎么在不启动界面的情况下验证依赖链完整。

6.1 用 CMake 的 GET_RUNTIME_DEPENDENCIES 收 DLL

手动一个个拷 DLL 是最容易漏的方式,因为依赖是传递的:osgearth_viewer.exe依赖osg160-osgEarth.dll,后者又依赖gdal30x.dll,再往下还有proj_8_2.dll、sqlite3.dll。CMake 3.16 之后提供了file(GET_RUNTIME_DEPENDENCIES),能一次性把整条依赖链扒出来。

写一个独立的deploy.cmake,用cmake -P调用,不参与主构建:

# deploy.cmake —— 收集可执行文件的运行时依赖到统一目录 set(_exe "D:/osg/install/bin/osgearth_viewer.exe") set(_search "D:/osg/install/bin" "D:/osg/3rdParty/bin") # 依赖搜索目录 set(_out "D:/osg/deploy") # 输出目录 file(MAKE_DIRECTORY "${_out}") file(GET_RUNTIME_DEPENDENCIES EXECUTABLES "${_exe}" DIRECTORIES ${_search} RESOLVED_DEPENDENCIES_VAR _resolved PRE_EXCLUDE_REGEXES "api-ms-.*" "ext-ms-.*" # 系统 API 集转发 dll 不拷 POST_EXCLUDE_REGEXES ".*[Ss]ystem32.*" # 系统目录里的不拷 ) foreach(_dep IN LISTS _resolved) message(STATUS "collect: ${_dep}") file(COPY "${_dep}" DESTINATION "${_out}") endforeach()

PRE_EXCLUDE_REGEXES过滤掉api-ms-win-crt-*这类 API 集转发 DLL,它们在 Win10 上是系统自带的,拷过去反而会干扰系统加载;POST_EXCLUDE_REGEXES按路径过滤,把System32下的都排除,否则会把kernel32.dll也复制一份到输出目录——这不仅没必要,某些情况下还会导致加载异常。跑完之后D:/osg/deploy里会有一批 DLL,加上 exe 本身,再加手工拷一个osgPlugins-3.6.5目录和share下的proj.db,就是一整套可以直接分发的运行时。

还有两个容易漏的东西:VC 运行时(msvcp140.dll、vcruntime140.dll、vcruntime140_1.dll)和osgPlugins-3.6.5插件目录。前者最好让目标机装一次 VC++ 2015-2022 可再发行组件包,别手工拷,版本对不上更麻烦;后者必须整目录跟着走,插件是按文件名动态加载的,少一个就是少一种格式支持。

6.2 一个三十行的启动自检程序

界面程序出问题时最难的是“它什么也没说就挂了”。我的习惯是写一个极简的控制台自检程序,链接同一套库,在部署前先跑一遍,把问题暴露在终端里:

// selfcheck.cpp —— 依赖链快速自检,返回 0 表示可用 #include <osg/Version> #include <osg/Node> #include <osg/ref_ptr> #include <osgDB/Registry> #include <cstdio> int main() { // 1. 版本对齐:编译期版本和运行时 dll 版本应当一致 std::printf("OSG version: %s\n", osgGetVersion()); // 2. 插件目录:打印实际搜索路径,确认 OSG_LIBRARY_PATH 生效 const auto& libs = osgDB::Registry::instance()->getLibraryFilePathList(); for (const auto& p : libs) std::printf("plugin dir: %s\n", p.c_str()); // 3. 关键插件:尝试加载 osgb 读取器,缺插件时这里会返回 false bool ok = osgDB::Registry::instance()->loadLibrary( osgDB::Registry::instance()->createLibraryNameForExtension("osgb")); std::printf("osgdb_osg plugin: %s\n", ok ? "ok" : "MISSING"); // 4. 端到端:读一个空 osgb,验证插件 + 依赖 dll 全链路 osg::ref_ptr<osg::Node> n = osgDB::readNodeFile("probe.osgb"); std::printf("readNodeFile: %s\n", n.valid() ? "ok" : "FAIL"); return (ok && n.valid()) ? 0 : 1; }

对应的 CMake 只有三行:

add_executable(selfcheck selfcheck.cpp) # 与主程序链接同一套库,才能复现相同的依赖环境 target_link_libraries(selfcheck PRIVATE osg osgDB)

用法上有个小技巧:先在一台干净的虚拟机里跑selfcheck.exe,返回非 0 就说明部署包不完整,此时去看它打印的plugin dir和MISSING行,能省掉在目标机上盲猜的时间。probe.osgb不需要有任何实际内容,一个空文件就够——目的不是读数据,是触发插件加载和依赖解析这两步,任何一步断链都会在这里暴露。

从那以后我每次出包前都强制走一遍deploy.cmake加selfcheck.exe这套组合,不管多急都不跳。吃过一次亏:图省事手工拷了几个 DLL,现场演示时程序在加载地形阶段静默退出,回来查是两个小时后的事,原因是漏了libzip.dll,而它只在读特定压缩包时才被引用,本地测试根本触发不到。希望你不用经历这一遍。

本文还有配套的精品资源,点击获取