UE5 中接入 GDAL:从vcpkg编译到插件打包

做 UE 数字孪生时,Cesium for Unreal 基本解决了数字地球、地形、影像和 3D Tiles 的加载问题。但实际项目里的数据,并不是拿来就能直接放进 Cesium。

比如 CGCS2000 高斯-克吕格 3°带坐标点、TIF、SHP,或者气象业务中的 NetCDF 数据。这些文件在 QGIS 里可以直接打开,到了 UE 里就需要另外处理。

如果分别处理,往往需要单独实现坐标转换、TIF 加载、SHP 解析和 NetCDF 读取,接口也不统一。GDAL 提供了统一的数据访问方式,所以我们最终选择在插件里接入 GDAL。

这篇文章记录目前 Win64 下的完整接入过程,包括 GDAL 的编译、ThirdParty 目录组织、Build.cs 配置、运行时 DLL 加载,以及最后怎么确认 GDAL 和 PROJ 确实工作正常。

本文使用的实际环境:

bash 复制代码
- Unreal Engine 5.1
- Windows 10 / 11 x64
- Visual Studio
- vcpkg
- GDAL 3.12.4
- PROJ 9.9.0

其他 UE5 版本的整体方式基本一致,但 GDAL、vcpkg 和 UE 的具体版本不同,依赖库和打包结果可能会有差异。


GDAL 在 UE 项目里解决什么问题

GDAL,全称 Geospatial Data Abstraction Library,是 GIS 领域很常用的一套底层数据读写库。

如果平时做过音视频,可以把它大致理解成 GIS 里的 FFmpeg:上层业务不需要分别实现 GeoTIFF、Shapefile、GeoJSON、NetCDF 等格式的解析,而是通过 GDAL 提供的一套统一接口来访问。

常用能力大致分为三类:

GDAL 中的 Driver 可以理解为"格式驱动"。每个 Driver 负责识别和读写一种或一类数据格式,例如 GTiff 负责 GeoTIFF,GeoJSON 负责 GeoJSON。调用 GDALAllRegister() 后,程序会注册当前 GDAL 编译版本中可用的 Driver。检测到 GTiff,表示当前环境具备处理 GeoTIFF 的能力。

例如读取一个 GeoTIFF,可以拿到:

bash 复制代码
- 图像宽高
- 波段数量
- 像素数据
- GeoTransform
- 投影坐标系
- 地理范围

换成其他 GDAL 支持的栅格格式,整体接口基本不用变。

这也是我们把 GDAL 接到 UE 里的主要原因:补充 Cesium for Unreal 缺少的 GIS 数据处理能力。

Cesium 和 GDAL 的分工

两者可以按下面的方式分工:

例如一个 CGCS2000 高斯投影的 GeoTIFF,我们会先通过 GDAL 识别坐标系、读取 GeoTransform,需要时转成 WGS84,然后再交给 Cesium 这一侧完成地球上的定位和显示。

获取并编译 GDAL库

获取 GDAL 主要有三种方式:

  1. 下载源码自行编译:可以控制 GDAL 版本、功能和依赖,但需要自己配置 CMake、PROJ、GEOS、TIFF 等依赖。

  2. 通过 vcpkg 安装:vcpkg 会下载源码、解析依赖并完成编译,生成的库也会遵循指定的 triplet。本文采用这种方式。

  3. 使用预编译包:安装较快,但需要确认 CPU 架构、MSVC 版本、CRT、Debug / Release 配置和依赖库是否与 UE 工程一致。

GDAL 官方项目主要发布源码和容器。Windows 预编译包通常由 OSGeo4W、GISInternals、Conda Forge、NuGet 等渠道提供。直接用于 UE,可能会有兼容性问题。

GDAL 本身依赖的库很多。我们实际编译出来的版本里,除了 GDAL 本体,还有 PROJ、GEOS、TIFF、GeoTIFF、SQLite、zlib、curl 等一系列依赖。如果从源码开始逐个准备这些库,会花很多时间处理版本和编译参数。

所以 Win64 下我们直接使用 vcpkg。vcpkg 是微软维护的开源 C/C++ 包管理器,可以自动处理 GDAL 及其依赖库的下载和编译。

安装 vcpkg

开始前,确认 Windows 已安装 Visual Studio ,并勾选"使用 C++ 的桌面开发"和 Windows SDK。

否则无法完成无法完成GDAL的编译。

在 G:\GDAL_SDK\vcpkg 下安装 vcpkg,进入 PowerShell 执行:

bash 复制代码
git clone https://github.com/microsoft/vcpkg.git G:\GDAL_SDK\vcpkg
cd G:\GDAL_SDK\vcpkg
.\bootstrap-vcpkg.bat -disableMetrics

目录下生成 vcpkg.exe 后即可使用。

安装 GDAL 并编译

执行:

bash 复制代码
.\vcpkg.exe install gdal:x64-windows

GDAL 的依赖库比较多,vcpkg 会在安装过程中自动下载并编译。首次执行通常需要 50~60 分钟,具体时间取决于网络和机器性能。

这里用的是 x64-windows,也就是 Win64 动态库版本。没有用 x64-windows-static:UE 工程本身使用动态 CRT,第三方库如果换成 /MT 的静态 CRT,很容易引出运行库不一致的问题。当前配置直接使用 x64-windows。

GDAL 的 feature 会随着 vcpkg port 变化。如果项目需要 NetCDF、LibKML 等额外能力,先看当前 GDAL port 提供了哪些 feature,再决定是否额外指定。例如:

bash 复制代码
.\vcpkg.exe install gdal[netcdf,libkml]:x64-windows

不要直接照搬 feature 列表,最好以自己当前使用的 vcpkg 版本为准。

编译完成效果:

截图中的耗时来自当前机器和缓存状态,首次完整安装仍以实际网络和编译环境为准。

本次实际编译结果:GDAL 3.12.4、PROJ 9.9.0,运行时 DLL 共 54 个。

编译完成后,可以在 vcpkg 目录中执行下面的命令,查看当前安装的 GDAL 和 PROJ 版本:

bash 复制代码
cd G:\GDAL_SDK\vcpkg
.\vcpkg.exe list | Select-String '^(gdal|proj):'

编译完成后,主要文件位于:

把 GDAL 放进 UE 插件

接下来需要将 GDAL 库以第三方库的形式引入 UE。

创建一个C++插件,插件目录可以按下面的方式组织:

bash 复制代码
YourGeoPlugin/
├─ YourGeoPlugin.uplugin
└─ Source/
   ├─ YourGeoPlugin/
   │  ├─ YourGeoPlugin.Build.cs
   │  └─ ...
   │
   └─ ThirdParty/
      └─ GDAL/
         ├─ include/
         ├─ lib/
         │  └─ gdal.lib
         ├─ bin/
         │  ├─ gdal.dll
         │  ├─ proj_*.dll
         │  ├─ geos*.dll
         │  └─ ...
         └─ share/
            ├─ gdal/
            └─ proj/
               └─ proj.db

不需要把 installed/x64-windows 整个复制进插件。按下面的清单整理到 Source/ThirdParty/GDAL:

  • include:复制整个目录

  • lib:只复制 gdal.lib

  • bin:只复制所有 .dll,不复制 .pdb

  • share:只复制 gdal 和 proj 两个目录

整理后的 GDAL ThirdParty 目录约 100 MB。

Build.cs 配置

接下来在模块的 Build.cs 中配置 GDAL 的头文件路径和 gdal.lib,并声明打包时需要携带的 DLL 和数据文件。

YourGeoPlugin.Build.cs 中相关部分如下。

bash 复制代码
// IPluginManager 位于 Projects 模块
PrivateDependencyModuleNames.Add("Projects");
if (Target.Platform == UnrealTargetPlatform.Win64)
{
    string GdalRoot = Path.GetFullPath(
        Path.Combine(ModuleDirectory, "../ThirdParty/GDAL"));
    string BinDir = Path.Combine(GdalRoot, "bin");
    // Header
    PublicIncludePaths.Add(
        Path.Combine(GdalRoot, "include"));
    // Import library
    PublicAdditionalLibraries.Add(
        Path.Combine(GdalRoot, "lib", "gdal.lib"));
    // gdal.dll 由模块主动加载
    PublicDelayLoadDLLs.Add("gdal.dll");
    // 所有 Runtime DLL 都需要随包带走
    foreach (string DllPath in Directory.GetFiles(BinDir, "*.dll"))
    {
        RuntimeDependencies.Add(
            DllPath,
            StagedFileType.NonUFS);
    }
    // GDAL runtime data
    string GdalDataDir =
        Path.Combine(GdalRoot, "share", "gdal");
if (Directory.Exists(GdalDataDir))
    {
        foreach (string FilePath in Directory.GetFiles(
            GdalDataDir,
"*",
            SearchOption.AllDirectories))
        {
            RuntimeDependencies.Add(
                FilePath,
                StagedFileType.NonUFS);
        }
    }
    // PROJ database
    string ProjDataDir =
        Path.Combine(GdalRoot, "share", "proj");
if (Directory.Exists(ProjDataDir))
    {
        foreach (string FilePath in Directory.GetFiles(
            ProjDataDir,
"*",
            SearchOption.AllDirectories))
        {
            RuntimeDependencies.Add(
                FilePath,
                StagedFileType.NonUFS);
        }
    }
} 

这里几项配置的作用分别是:

这里只对 gdal.dll 做 Delay Load。其他 DLL 虽然同样要通过 RuntimeDependencies 带进安装包,但 UE 模块并没有直接链接这些库,它们是在 Windows 加载 gdal.dll 时作为二级依赖加载的。

为什么使用 Delay Load

如果不做延迟加载,Windows 会在加载插件模块时查找 gdal.dll。此时 StartupModule() 还没有执行,GDAL 的 bin 目录也没有加入 DLL 搜索路径,模块可能会加载失败。

配置 Delay Load 后,先在 StartupModule() 中调用 PushDllDirectory() 设置搜索路径,再通过 GetDllHandle() 加载 gdal.dll。加载成功后才能调用 GDAL API。

RuntimeDependencies 为什么使用 NonUFS

Windows 需要从实际路径加载 DLL,PROJ 也需要直接读取 proj.db。这些文件不能只放在 UE 的 Pak 虚拟文件系统中,因此使用 StagedFileType.NonUFS。

不同 UE 版本和打包配置生成的目录可能不同,打包后要实际检查一次。如果需要固定输出位置,可以使用 RuntimeDependencies.Add(TargetPath, SourcePath) 明确指定目标路径。

修改 Build.cs 后,先关闭 UE 编辑器,重新生成 Visual Studio 工程文件,避免 GDAL 头文件在编辑器中显示红线。

StartupModule 中加载 GDAL

完成 Build.cs 配置后,还需要在模块启动时加载 GDAL DLL,并设置 GDAL 和 PROJ 的数据目录。

先在插件模块的实现文件 YourGeoPluginModule.cpp 顶部补充所需头文件,并定义当前文件使用的日志分类:

bash 复制代码
#include "HAL/PlatformProcess.h"
#include "Interfaces/IPluginManager.h"
#include "Misc/Paths.h"
THIRD_PARTY_INCLUDES_START
#include "cpl_conv.h"
#include "gdal.h"
THIRD_PARTY_INCLUDES_END
DEFINE_LOG_CATEGORY_STATIC(LogYourGeoPlugin, Log, All);

THIRD_PARTY_INCLUDES_START / END 建议保留。GDAL 的 Header 不受 UE 控制,直接包含时可能触发 UE 的 Warning、宏或编译选项冲突。UE 自己也为这种第三方 Header 提供了这组宏。

DLL 句柄和搜索目录需要一直保留到模块卸载,因此不能定义成 StartupModule() 的局部变量:

bash 复制代码
namespace
{
    FString GdalBinDirectory;
    void* GdalDllHandle = nullptr;
}
void FYourGeoPluginModule::StartupModule()
{
    const TSharedPtr<IPlugin> Plugin =
        IPluginManager::Get().FindPlugin(TEXT("YourGeoPlugin"));
if (!Plugin.IsValid())
    {
        UE_LOG(
            LogYourGeoPlugin,
            Error,
            TEXT("YourGeoPlugin not found."));
return;
    }
    const FString GdalRoot = FPaths::Combine(
        Plugin->GetBaseDir(),
        TEXT("Source/ThirdParty/GDAL"));
    GdalBinDirectory =
        FPaths::Combine(GdalRoot, TEXT("bin"));
    // GDAL 的二级 DLL 也都在这个目录
    FPlatformProcess::PushDllDirectory(*GdalBinDirectory);
    GdalDllHandle =
        FPlatformProcess::GetDllHandle(
            *FPaths::Combine(
                GdalBinDirectory,
                TEXT("gdal.dll")));
if (!GdalDllHandle)
    {
        UE_LOG(
            LogYourGeoPlugin,
            Error,
            TEXT("Failed to load gdal.dll"));
        FPlatformProcess::PopDllDirectory(*GdalBinDirectory);
        GdalBinDirectory.Reset();
return;
    }
    const FString GdalData =
        FPaths::Combine(
            GdalRoot,
            TEXT("share/gdal"));
    const FString ProjData =
        FPaths::Combine(
            GdalRoot,
            TEXT("share/proj"));
    CPLSetConfigOption(
"GDAL_DATA",
        TCHAR_TO_UTF8(*GdalData));
    CPLSetConfigOption(
"PROJ_DATA",
        TCHAR_TO_UTF8(*ProjData));
    // 兼容旧版本 PROJ
    CPLSetConfigOption(
"PROJ_LIB",
        TCHAR_TO_UTF8(*ProjData));
    GDALAllRegister();
    UE_LOG(
        LogYourGeoPlugin,
        Log,
        TEXT("GDAL loaded: %hs"),
        GDALVersionInfo("RELEASE_NAME"));
}

CPLSetConfigOption() 本身由 gdal.dll 导出,必须等 GetDllHandle() 成功后再调用。

DLL 搜索路径和二级依赖

GetDllHandle() 加载 gdal.dll 时,Windows Loader 会继续解析它的导入表,并加载 PROJ、GEOS、TIFF、SQLite、zlib、curl 等二级依赖。调用 PushDllDirectory() 后,这些 DLL 才能从同一个 bin 目录中被找到。

如果加载失败,先确认 gdal.dll 是否存在,再根据日志排查缺失的二级依赖。失败分支还要调用 PopDllDirectory(),避免修改进程后续的 DLL 搜索行为。

GDAL 加载成功后再设置数据目录。GDAL_DATA 指向 share/gdal;当前 PROJ 使用 PROJ_DATA 指向 share/proj。同时设置 PROJ_LIB,用于兼容仍读取旧变量名的 PROJ 版本。

模块卸载

卸载模块前,要先停止 GDAL 相关任务,并释放所有 GDALDataset、坐标转换对象和其他 GDAL 资源。确认没有线程继续调用 GDAL 后,再释放 DLL:

bash 复制代码
void FYourGeoPluginModule::ShutdownModule()
{
    // 先停止任务并释放所有 GDAL 对象。
if (GdalDllHandle)
    {
        FPlatformProcess::FreeDllHandle(GdalDllHandle);
        GdalDllHandle = nullptr;
    }
if (!GdalBinDirectory.IsEmpty())
    {
        FPlatformProcess::PopDllDirectory(*GdalBinDirectory);
        GdalBinDirectory.Reset();
    }
}

如果后台线程仍在读取 Dataset,或者 GDAL 对象尚未销毁就调用 FreeDllHandle(),后续函数调用会跳转到已经卸载的代码地址,通常会直接导致崩溃。

编译运行测试

完成这些配置后,先编译项目并启动 UE 编辑器,在 Output Log 中搜索插件日志。正常情况下会输出:

编译通过说明头文件和 gdal.lib 配置正确;启动后出现版本日志,说明 gdal.dll 及其二级依赖已经成功加载。确认这一步正常后,再继续验证 Driver 和坐标转换。

编写一个 GDAL 测试功能

版本日志只能证明 DLL 已经加载。接下来用固定坐标做一次 EPSG 转换,同时验证 GDAL、PROJ 和 proj.db。

新建 Public/GdalTestLibrary.h,声明一个蓝图函数:

bash 复制代码
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "GdalTestLibrary.generated.h"
UCLASS()
class YOURGEOPLUGIN_API UGdalTestLibrary
    : public UBlueprintFunctionLibrary
{
    GENERATED_BODY()
public:
    UFUNCTION(
        BlueprintCallable,
        Category = "GDAL",
        meta = (DisplayName = "Run GDAL Test"))
    static bool RunGdalTest(
        FString& OutVersion,       // 当前加载的 GDAL 版本
        int32& OutDriverCount,     // 已注册的 GDAL Driver 数量
        bool& bOutHasGTiff,        // 是否包含 GeoTIFF Driver
        FString& OutWebMercator,   // 120°E、30°N 转为 EPSG:3857 后的坐标
        FString& OutError);        // 测试失败时的错误信息
};

其中 YOURGEOPLUGIN_API 要替换成插件模块实际生成的 API 宏,例如模块名为 MyGDAL 时使用 MYGDAL_API。

再新建 Private/GdalTestLibrary.cpp:

bash 复制代码
#include "GdalTestLibrary.h"
THIRD_PARTY_INCLUDES_START
#include "gdal.h"
#include "ogr_spatialref.h"
THIRD_PARTY_INCLUDES_END
bool UGdalTestLibrary::RunGdalTest(
    FString& OutVersion,
    int32& OutDriverCount,
    bool& bOutHasGTiff,
    FString& OutWebMercator,
    FString& OutError)
{
    // 读取当前加载的 GDAL 版本和 Driver 注册情况。
    OutVersion = UTF8_TO_TCHAR(
        GDALVersionInfo("RELEASE_NAME"));
    OutDriverCount = GDALGetDriverCount();
    bOutHasGTiff =
        GDALGetDriverByName("GTiff") != nullptr;
    OutWebMercator.Reset();
    OutError.Reset();
    OGRSpatialReference Source;
    OGRSpatialReference Target;
    // 创建 WGS84 经纬度和 Web Mercator 两个坐标系。
if (Source.SetFromUserInput("EPSG:4326") != OGRERR_NONE ||
        Target.SetFromUserInput("EPSG:3857") != OGRERR_NONE)
    {
        OutError = TEXT("创建 EPSG 坐标系失败,请检查 proj.db。");
returnfalse;
    }
    // 固定使用传统 GIS 轴顺序:X 为经度,Y 为纬度。
    Source.SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
    Target.SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
    // 创建从 EPSG:4326 到 EPSG:3857 的坐标转换器。
    OGRCoordinateTransformation* Transform =
        OGRCreateCoordinateTransformation(&Source, &Target);
if (!Transform)
    {
        OutError = TEXT("创建坐标转换器失败。");
returnfalse;
    }
    // 测试点:东经 120°,北纬 30°。
    double X = 120.0;
    double Y = 30.0;
    const bool bTransformSuccess =
        Transform->Transform(1, &X, &Y) != 0;
    // 转换器使用完后必须释放。
    OGRCoordinateTransformation::DestroyCT(Transform);
if (!bTransformSuccess)
    {
        OutError = TEXT("坐标转换执行失败。");
returnfalse;
    }
    // 保留 4 位小数,方便在蓝图中直接查看和对照。
    OutWebMercator = FString::Printf(
        TEXT("%.4f, %.4f"),
        X,
        Y);
    // Driver 数量和 GTiff Driver 也是本次测试的通过条件。
if (OutDriverCount <= 0 || !bOutHasGTiff)
    {
        OutError = TEXT("GDAL Driver 注册不完整。");
returnfalse;
    }
returntrue;
}

重新生成 Visual Studio 工程文件并编译项目。然后在蓝图中调用 Run GDAL Test,正常输出应为:

各输出项的含义:

  • Return Value:整个测试是否通过

  • Version:程序实际加载的 GDAL 版本

  • Driver Count:GDALAllRegister() 注册的 Driver 数量

  • Has GTiff:是否能使用 GeoTIFF Driver

  • Web Mercator:测试点从 EPSG:4326 转到 EPSG:3857 后的坐标

  • Error:失败原因;测试通过时为空

这组结果说明 Driver 已注册、GTiff 可用,并且 PROJ 能从 proj.db 中读取 EPSG 定义。

打包验证

Editor 中运行正常后,再打一个 Development Win64 包。Development 配置会保留日志,方便检查 GDAL 的加载结果。

打包后检查下面三项:

  1. 输出目录中包含 GDAL 的 bin、share/gdal 和 share/proj,其中 bin 内包含 gdal.dll 及其依赖,share/proj 内包含 proj.db

  2. 打包生成的 EXE 能正常启动

  3. 程序启动后,版本日志正常输出,并且蓝图测试函数返回正确的 Driver 和坐标转换结果

GDAL 插件中包含必要的 bin 和 share 目录:

程序启动后的测试结果:

三项都正常,说明 GDAL 的 DLL 和数据文件已经随程序完成打包。

几个实际遇到的问题

GDAL 头文件引起大量 Warning

用 THIRD_PARTY_INCLUDES_START / END 包住 GDAL 头文件。

编辑器启动时提示 gdal.dll 加载失败

先确认 gdal.dll 是否存在,然后检查它依赖的 proj_*.dll、geos*.dll、sqlite*.dll 等是否完整复制到 bin。GDAL 不是只复制一个 gdal.dll 就能运行。

GDAL 能加载,但 EPSG 转换失败

先看日志里是否有 Cannot find proj.db。如果有,通常是 share/proj 路径错误,或者 proj.db 没有进入安装包。

编辑器正常,打包版本失败

检查 RuntimeDependencies 和最终打包目录。Editor 环境存在额外的 DLL 搜索路径,编辑器中运行正常,不代表 DLL 和数据文件已经正确进入打包目录。

官方资料与延伸阅读

相关推荐
努力学习的小廉1 小时前
UE5入门系列(一):环境搭建.md
ue5
fanfan_hongyun4 小时前
【UE5.1 创建C++文件】
c++·ue5
努力学习的小廉7 小时前
UE5 入门系列(二):编辑器基础操作速通
ue5·编辑器
Дерек的学习记录1 天前
Unreal Engine 5:开关门实现和流程控制
开发语言·学习·ue5
远离UE42 天前
拉格朗日 动力学问题
ue5·图形渲染·图形学·动力学
远离UE47 天前
UE5 Substrate Thin Film 源码拆析
开发语言·javascript·ue5
七仔啊8 天前
UE5源码下载及编译
ue5
Zhichao_978 天前
【UE5.8 MCP】接入AI大模型
ue5
曼巴UE59 天前
UE 属性同步
网络·c++·ue5