
做 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 主要有三种方式:
-
下载源码自行编译:可以控制 GDAL 版本、功能和依赖,但需要自己配置 CMake、PROJ、GEOS、TIFF 等依赖。
-
通过 vcpkg 安装:vcpkg 会下载源码、解析依赖并完成编译,生成的库也会遵循指定的 triplet。本文采用这种方式。
-
使用预编译包:安装较快,但需要确认 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 的加载结果。
打包后检查下面三项:
-
输出目录中包含 GDAL 的
bin、share/gdal和share/proj,其中bin内包含gdal.dll及其依赖,share/proj内包含proj.db -
打包生成的 EXE 能正常启动
-
程序启动后,版本日志正常输出,并且蓝图测试函数返回正确的 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 和数据文件已经正确进入打包目录。
官方资料与延伸阅读
-
GDAL 官方网站与文档:https://gdal.org/en/stable/
-
GDAL 下载说明:https://gdal.org/en/stable/download.html
-
GDAL GitHub 仓库:https://github.com/OSGeo/gdal
-
从源码编译 GDAL:https://gdal.org/en/stable/development/building_from_source.html
-
Raster API 教程:https://gdal.org/en/stable/tutorials/raster_api_tut.html
-
Vector API 教程:https://gdal.org/en/stable/tutorials/vector_api_tut.html
-
PROJ 官方文档:https://proj.org/en/stable/
-
vcpkg 官方文档:https://learn.microsoft.com/zh-cn/vcpkg/