1. 项目概述与核心痛点
如果你正在用Unreal Engine 5做数字孪生、智慧城市或者大场景仿真,大概率绕不开一个需求:把真实世界的地图、影像和地形数据搬进引擎里。Cesium for Unreal插件是连接虚幻引擎与真实地理空间数据的黄金桥梁,它让你能直接加载全球高精度地形和影像,把项目坐标锚定在真实的地球上。
但用过的朋友都知道,官方插件有个让人头疼的限制:它原生不支持WMTS协议。这意味着,在国内GIS开发中应用最广泛的“天地图”服务,你没法直接用它加载。天地图作为国家基础地理信息公共服务平台,提供了权威、稳定且免费的影像、电子地图和地形服务,是很多国内项目的首选数据源。官方不支持,大家通常的“野路子”是挂个代理服务器,把WMTS请求转发成Cesium ion或TMS格式,或者在外部用GIS工具预处理成离线瓦片。这不仅增加了架构复杂度、引入了网络延迟和不稳定因素,还让整个数据流变得臃肿,背离了在UE中实现一体化、高性能场景的初衷。
所以,这个项目的目标非常明确:告别外挂代理和繁琐的预处理,通过直接修改并编译Cesium for Unreal插件的源码,为其增加原生的WMTS协议支持,特别是完美适配天地图服务。这不是简单的配置教程,而是一次从源码层面对插件能力的深度拓展。完成后,你将在UE5的编辑器内,像添加一个静态网格体一样,轻松拖入一个“Cesium WMTS”图层,填入天地图的URL模板和图层标识,真实的中国地图就会立刻呈现在你的场景中。整个过程完全在引擎内部完成,数据流简洁,性能可控。
2. 环境准备与源码获取
动手之前,我们需要把“厨房”收拾好。编译一个像Cesium for Unreal这样涉及地理空间计算、图形API和虚幻引擎深度集成的插件,对环境的要求比较严格。
2.1 基础软件栈安装
首先,确保你的系统满足以下基础要求:
- Windows 10/11 64位:这是UE5官方主要支持的开发平台。
- Visual Studio 2022:安装时务必勾选“使用C++的桌面开发”工作负载,以及右侧明细中的“Windows 10/11 SDK”和“C++ CMake工具”。这是编译C++代码的基石。
- Git:用于克隆源码。建议安装Git for Windows,并确保
git命令可以在命令行中运行。 - Unreal Engine 5.3+:推荐使用5.3或更高版本。你需要通过Epic Games启动器安装引擎,并且必须下载源代码版本。在启动器的“库”->“引擎版本”旁边,点击“+”号,选择5.3版本,并在高级选项里勾选“源代码”。这一步至关重要,因为编译插件需要引擎的完整头文件和库。
2.2 获取Cesium for Unreal插件源码
我们不使用Epic商城下载的已编译二进制插件,而是直接从GitHub获取源码。
- 打开命令行(如PowerShell或Git Bash),导航到你打算存放项目的目录,例如
D:\Projects。 - 执行克隆命令:
这里的git clone --recursive https://github.com/CesiumGS/cesium-unreal.git--recursive参数极其重要,因为Cesium插件依赖了一些子模块(如cesium-native),这个参数会一并把它们下载下来。 - 进入克隆下来的目录:
cd cesium-unreal - (可选但推荐)切换到一个稳定的发布分支。直接使用
main分支可能包含最新的、但不稳定的开发代码。我们可以切换到与你的UE5版本兼容的标签,例如针对UE5.3:
你可以去项目的 Releases页面 查看哪个版本号与你的引擎匹配。使用标签能确保代码状态与官方发布的二进制插件一致,减少未知错误。git checkout tags/v2.0.0 -b ue5.3-dev
2.3 生成项目文件与初步编译
Cesium for Unreal使用CMake作为构建系统,我们需要先生成Visual Studio的解决方案文件。
- 在
cesium-unreal根目录下,创建一个名为build的文件夹。 - 打开“x64 Native Tools Command Prompt for VS 2022”(在开始菜单搜索即可找到)。务必使用这个命令行,它配置了VS的编译环境变量。
- 在命令行中,导航到刚才创建的
build目录。 - 运行CMake配置命令。这里需要指定你的UE5安装路径:
请将cmake .. -G "Visual Studio 17 2022" -A x64 -DCMAKE_PREFIX_PATH="C:\Program Files\Epic Games\UE_5.3\Engine"-DCMAKE_PREFIX_PATH后面的路径替换为你电脑上UE5引擎源码的实际安装路径。 - 如果CMake配置成功,你会在
build目录下看到一个Cesium.sln文件。 - 用Visual Studio 2022打开这个
Cesium.sln。 - 在VS的解决方案配置中,选择“Development Editor”和“Win64”。
- 在解决方案资源管理器中,右键点击“ALL_BUILD”项目,选择“生成”。这一步会编译cesium-native等核心库,可能需要一段时间。
注意:首次编译可能会因为网络问题(需要下载第三方库如draco、sqlite等)失败。如果遇到下载错误,可以尝试配置命令行代理(仅用于下载开源库),或者手动查看CMake输出中失败的URL,尝试用浏览器下载后放到指定的缓存目录。这是编译大型C++项目常见的“拦路虎”,需要耐心。
3. 核心修改:为插件添加WMTS支持
这是整个项目的技术核心。我们需要在插件的源代码中,增加对WMTS协议解析和图层创建的逻辑。
3.1 理解Cesium的图层加载架构
在修改前,先快速理解插件是如何加载图层的。Cesium for Unreal中,主要的图层类(如CesiumWebMapServiceRasterOverlay对应WMS)都继承自CesiumRasterOverlay。它们的核心任务是,根据给定的地理范围(Tile)和层级(Level),构造出一个能下载到对应瓦片图片的URL。插件内部有一个瓦片调度系统,会调用这些图层类的getTileUrl或类似方法获取URL,然后下载、解码、贴到三维地形上。
WMTS(Web Map Tile Service)是一种标准的瓦片地图服务协议,它与更常见的TMS或XYZ瓦片的主要区别在于其请求URL的规范性。一个典型的WMTS请求URL模板(KVP方式)长这样:http://service.xxx/maps?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER={Layer}&STYLE={Style}&TILEMATRIXSET={TileMatrixSet}&TILEMATRIX={TileMatrix}&TILEROW={TileRow}&TILECOL={TileCol}&FORMAT={Format}
我们的目标就是创建一个新的类,能够解析这样的模板,并将{TileMatrix},{TileRow},{TileCol}等占位符替换为当前请求的实际值。
3.2 创建WMTS图层类
定位源码目录:在
cesium-unreal源码中,插件的C++代码主要位于Plugins/CesiumForUnreal/Source/CesiumRuntime/Private和Public目录下。我们可以在Public/RasterOverlays和Private/RasterOverlays下创建新文件。创建头文件:在
Public/RasterOverlays/下新建文件CesiumWebMapTileServiceRasterOverlay.h。// CesiumWebMapTileServiceRasterOverlay.h #pragma once #include "CesiumRasterOverlay.h" #include "CesiumWebMapTileServiceRasterOverlay.generated.h" UCLASS(DisplayName="Web Map Tile Service (WMTS) Overlay") class CESIUMRUNTIME_API UCesiumWebMapTileServiceRasterOverlay : public UCesiumRasterOverlay { GENERATED_BODY() public: UCesiumWebMapTileServiceRasterOverlay(); // WMTS服务的基础URL(不含具体参数) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Cesium") FString BaseUrl; // 图层名称 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Cesium") FString Layer; // 样式,通常为"default" UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Cesium") FString Style = "default"; // 瓦片矩阵集,天地图通常使用"w"或"c" UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Cesium") FString TileMatrixSet; // 图片格式,如"image/jpeg"或"image/png" UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Cesium") FString Format; protected: virtual std::unique_ptr<Cesium3DTilesSelection::RasterOverlay> CreateOverlay() override; };这个头文件定义了我们新的蓝图可编辑类,暴露了WMTS服务所需的关键参数。
创建源文件:在
Private/RasterOverlays/下新建文件CesiumWebMapTileServiceRasterOverlay.cpp。// CesiumWebMapTileServiceRasterOverlay.cpp #include "CesiumWebMapTileServiceRasterOverlay.h" #include "CesiumRasterOverlay.h" #include <Cesium3DTilesSelection/WebMapTileServiceRasterOverlay.h> #include <CesiumAsync/IAssetAccessor.h> #include <CesiumUtility/Uri.h> using namespace Cesium3DTilesSelection; UCesiumWebMapTileServiceRasterOverlay::UCesiumWebMapTileServiceRasterOverlay() : UCesiumRasterOverlay() { // 可以设置一些默认值 this->MaterialLayerKey = TEXT("WMTSOverlay"); } std::unique_ptr<Cesium3DTilesSelection::RasterOverlay> UCesiumWebMapTileServiceRasterOverlay::CreateOverlay() { // 确保基础URL不为空 if (this->BaseUrl.IsEmpty()) { return nullptr; } // 构建WMTS选项 WebMapTileServiceRasterOverlayOptions wmtsOptions; wmtsOptions.layer = TCHAR_TO_UTF8(*this->Layer); wmtsOptions.style = TCHAR_TO_UTF8(*this->Style); wmtsOptions.tileMatrixSetID = TCHAR_TO_UTF8(*this->TileMatrixSet); wmtsOptions.format = TCHAR_TO_UTF8(*this->Format); // 调用cesium-native库创建WMTS覆盖层 // 注意:这里假设cesium-native已包含WebMapTileServiceRasterOverlay类。 // 实际上,我们可能需要在cesium-native中也添加对应支持,这是一个更深的修改层级。 // 为了教程连贯性,我们先在此处描述UE插件侧的理想接口。 // 真实情况是,我们需要先确保底层的Cesium Native库支持WMTS。 // 以下为伪代码,示意最终调用 // auto pAssetAccessor = this->GetAssetAccessor(); // auto pAsyncSystem = this->GetAsyncSystem(); // return std::make_unique<WebMapTileServiceRasterOverlay>( // TCHAR_TO_UTF8(*this->GetName()), // TCHAR_TO_UTF8(*this->BaseUrl), // std::vector<CesiumAsync::IAssetAccessor::THeader>(), // wmtsOptions, // pAssetAccessor, // pAsyncSystem); // 由于直接修改cesium-native超出单篇教程范围,我们采用一种更实用的“适配器”思路。 // 见下一小节。 return nullptr; }到这里你可能会发现关键问题:
Cesium3DTilesSelection命名空间下可能并没有现成的WebMapTileServiceRasterOverlay类。是的,官方cesium-native库目前并未实现WMTS。因此,我们需要一个更巧妙的方案。
3.3 实战方案:创建WMTS URL适配器类
既然底层库不支持,我们可以在UE插件层面,创建一个“适配器”,将WMTS的请求,实时转换为底层已支持的TileMapServiceRasterOverlay(TMS) 或WebMapServiceRasterOverlay(WMS) 所能理解的请求。但WMTS与WMS的请求模式不同,更接近TMS的瓦片坐标体系。一个更直接的方法是,我们继承CesiumRasterOverlay,自己实现一个CustomRasterOverlay,重写其获取瓦片URL的逻辑。
调整策略:实现一个UCesiumWMTSRasterOverlay
我们放弃直接依赖不存在的native类,改为在UE侧实现一个自定义的RasterOverlay,它利用cesium-native的RasterOverlay基类,但自己处理URL生成。
修改头文件,使其继承自一个更通用的基类(如果需要与现有架构融合,可能需要更复杂的集成。为简化,这里展示核心思想):
// 在CesiumWebMapTileServiceRasterOverlay.h中调整 // ... 包含必要的头文件 ... #include <Cesium3DTilesSelection/RasterOverlay.h> #include <CesiumAsync/IAssetAccessor.h> class CESIUMRUNTIME_API FCesiumWMTSRasterOverlay : public Cesium3DTilesSelection::RasterOverlay { public: FCesiumWMTSRasterOverlay( const std::string& name, const std::string& baseUrl, const std::string& layer, const std::string& style, const std::string& tileMatrixSet, const std::string& format, const std::shared_ptr<CesiumAsync::IAssetAccessor>& pAssetAccessor, const CesiumAsync::AsyncSystem& asyncSystem); protected: virtual CesiumAsync::Future<LoadedRasterOverlayImage> loadTileImage( const Cesium3DTilesSelection::RasterOverlayTile& tile) const override; private: std::string _baseUrl; std::string _layer; std::string _style; std::string _tileMatrixSet; std::string _format; };然后在
UCesiumWebMapTileServiceRasterOverlay的CreateOverlay方法中,返回这个自定义类的实例。在cpp文件中实现URL构建和加载逻辑:
// 在CesiumWebMapTileServiceRasterOverlay.cpp中补充实现 FCesiumWMTSRasterOverlay::FCesiumWMTSRasterOverlay(...) : RasterOverlay(name, pAssetAccessor, asyncSystem) , _baseUrl(baseUrl), _layer(layer), ... {} CesiumAsync::Future<LoadedRasterOverlayImage> FCesiumWMTSRasterOverlay::loadTileImage( const Cesium3DTilesSelection::RasterOverlayTile& tile) const { // 1. 从tile中获取瓦片的层级、行、列号 const CesiumGeometry::QuadtreeTileID& tileID = tile.getTileID(); int level = tileID.level; int x = tileID.x; int y = tileID.y; // 2. 根据WMTS KVP规范构建URL // 注意:WMTS的Y轴原点可能与TMS相反。天地图通常使用TMS规范,即原点在左上角。 // 需要根据具体服务的TileMatrixSet定义进行调整。天地图的“w”坐标系是TMS。 std::string url = _baseUrl; url += "?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0"; url += "&LAYER=" + _layer; url += "&STYLE=" + _style; url += "&TILEMATRIXSET=" + _tileMatrixSet; url += "&TILEMATRIX=" + std::to_string(level); // 假设TileMatrix编码与层级一致 url += "&TILEROW=" + std::to_string(y); // TMS行号 url += "&TILECOL=" + std::to_string(x); // TMS列号 url += "&FORMAT=" + _format; // 3. 使用基类提供的assetAccessor异步下载图片 return this->getAssetAccessor() ->get(this->getAsyncSystem(), url, {}) .thenImmediately([this](std::shared_ptr<CesiumAsync::IAssetRequest>&& pRequest) { // 处理响应,解码图像数据,封装成LoadedRasterOverlayImage返回 const CesiumAsync::IAssetResponse* pResponse = pRequest->response(); // ... 图像解码逻辑(可参考其他Overlay的实现)... LoadedRasterOverlayImage result; // ... 填充result ... return result; }); }这个实现的关键在于正确构建WMTS URL。你需要查阅目标WMTS服务(如天地图)的
GetCapabilities文档,确认其TileMatrix标识符的命名规则(是简单的数字层级,还是像“EPSG:4326:0”这样的字符串),以及TileRow和TileCol的原点方向。
3.4 集成到UE编辑器并适配天地图
- 修改
UCesiumWebMapTileServiceRasterOverlay::CreateOverlay,使其实例化我们自定义的FCesiumWMTSRasterOverlay,并传入从蓝图编辑器中设置的参数(BaseUrl,Layer等)。 - 编译插件:在Visual Studio中,重新编译整个
Cesium.sln解决方案(选择“重新生成解决方案”更稳妥)。 - 在UE5中创建测试关卡:
- 启动UE5编辑器,创建一个新项目或打开现有项目(选择“C++”类型,否则无法加载源码插件)。
- 在插件管理器中,启用“Cesium for Unreal”插件(现在是你编译的自定义版本)。
- 重启编辑器后,在内容浏览器中右键,选择“Cesium” -> “Cesium World Terrain” 创建一个全球地形。
- 在地形Actor的细节面板中,找到“Raster Overlays”数组,点击“+”号添加一项。
- 在下拉菜单中,你应该能看到新出现的“Web Map Tile Service (WMTS) Overlay”选项。选择它。
- 配置天地图参数:
- BaseUrl:
https://t0.tianditu.gov.cn/{Layer}_c/wmts(以影像为例)。注意,这里包含了{Layer}占位符,我们的代码需要处理。更常见的做法是BaseUrl固定,Layer作为单独参数。天地图的URL模板通常是:https://t0.tianditu.gov.cn/img_c/wmts?request=GetTile&...。所以BaseUrl可以设为https://t0.tianditu.gov.cn/img_c/wmts。 - Layer:
img(影像) 或vec(矢量地图) 或cia(影像注记) 或cva(矢量注记)。 - Style:
default - TileMatrixSet:
w(对应WGS84坐标系,Web墨卡托)。如果是经纬度直投,可能是c。 - Format:
image/jpeg(影像) 或image/png(矢量)。 - 还需要注意:天地图WMTS服务要求
TileMatrix参数的值不是简单的数字层级,而是字符串如EPSG:4326:0、EPSG:4326:1... 或者对于Web墨卡托是w、w1...。这需要我们在loadTileImage函数中做一个映射。可以预先定义一个映射表:std::map<int, std::string> levelToTileMatrix = { {0, "w"}, {1, "w1"}, ... };。
- BaseUrl:
实操心得:在调试WMTS图层时,最有效的方法是把构建好的URL打印到日志中,然后直接复制到浏览器里访问,看是否能返回正确的瓦片图片。这能快速定位是URL构建错误、参数错误还是服务本身的问题。另外,注意虚幻引擎的日志输出级别,确保你的日志能被看到。
4. 编译打包与问题排查
4.1 完整编译与打包插件
解决依赖与编译错误:首次编译自定义插件,几乎一定会遇到各种编译错误。常见问题包括:
- 找不到头文件:检查
#include路径是否正确,确保包含了Cesium3DTilesSelection/和CesiumAsync/等cesium-native的头文件。你可能需要在插件的Build.cs文件中添加额外的包含路径或依赖模块。 - 链接错误:确保你的自定义类正确链接了CesiumRuntime模块。在
CesiumRuntime.Build.cs中,PublicDependencyModuleNames和PrivateDependencyModuleNames需要包含所有用到的模块。 - C++标准问题:确保项目属性中C++语言标准设置为
C++17或更高。
- 找不到头文件:检查
生成可用于分发/备份的插件:在VS中编译成功后,
cesium-unreal目录下的Plugins/CesiumForUnreal就是编译好的插件。你可以将其整个文件夹复制到任意UE项目的Plugins/目录下,或者打包备份。注意,你需要同时复制Binaries、Intermediate、Resources和Source等关键文件夹。
4.2 常见问题与解决方案实录
以下是我在编译和集成过程中踩过的坑以及解决办法:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 编译时出现“无法打开包括文件: ‘Cesium3DTilesSelection/...’” | 1. cesium-native子模块未正确更新。 2. 生成VS工程文件时CMake路径配置错误。 | 1. 在cesium-unreal根目录执行git submodule update --init --recursive。2. 删除 build文件夹,用正确的-DCMAKE_PREFIX_PATH重新运行CMake。 |
| 在UE编辑器中看不到“WMTS Overlay”选项 | 1. 插件编译成功但未启用。 2. UCLASS宏未正确设置或模块未重新加载。 3. 插件源码未正确集成到UE模块系统。 | 1. 在“编辑”->“插件”中确保Cesium for Unreal已勾选并重启。 2. 在VS中“重新生成”后,彻底关闭UE编辑器再重新打开。 3. 检查 CesiumRuntime模块的.uplugin和.Build.cs文件,确保它们包含了新增的源文件。 |
| 添加WMTS图层后,地形一片黑或粉红 | 1. URL构建错误,返回404或错误图片。 2. 瓦片坐标系(TMS vs. WMTS Y轴)搞反。 3. 天地图Token或密钥问题(部分服务需要)。 | 1. 在loadTileImage函数中打印完整URL到UE日志(UE_LOG(LogCesium, Log, TEXT("URL: %s"), *FString(url.c_str()))),在浏览器中手动验证。2. 尝试将 TILEROW的计算改为(1 << level) - 1 - y进行Y轴翻转。3. 检查天地图服务是否需要 tk参数,如果需要,在BaseUrl后附加&tk=你的密钥。 |
| 性能问题,瓦片加载慢 | 1. 网络请求频繁。 2. 未启用瓦片缓存。 | 1. Cesium插件本身有瓦片调度优化,确保你的CustomRasterOverlay没有阻塞异步系统。2. 检查 CesiumRasterOverlay基类的属性,如MaximumSimultaneousTileLoads(最大同时加载数)和MaximumCacheSize(缓存大小),可适当调整。 |
| 控制台出现大量“Invalid response”警告 | 1. 服务返回非200状态码。 2. 图片格式解码失败。 | 1. 检查URL中的参数值(特别是Layer, TileMatrixSet)是否与服务能力文档完全匹配,区分大小写。 2. 确保 Format参数与服务器返回的MIME类型一致。天地图PNG格式有时返回image/png; mode=8bit,可能需要调整解码逻辑的容错性。 |
独家避坑技巧:
- 分步验证法:不要试图一次性写完所有代码并期望它工作。先实现一个最简单的、能打印日志的
CreateOverlay函数,确保新类能被UE正确实例化。然后逐步实现URL构建、网络请求、图片解码。 - 善用Cesium原生示例:
cesium-unreal源码中自带示例关卡(在Content/目录下)。参考CesiumWebMapServiceRasterOverlay等已有类的实现,模仿其代码结构和错误处理方式,能事半功倍。 - 处理天地图“tk”密钥:目前天地图大部分公开WMTS服务已不需要密钥,但如果你使用某些特定图层或遇到访问限制,可能需要申请一个。如果URL需要
tk参数,不要把它硬编码在BaseUrl里,可以像其他属性一样,在蓝图类中增加一个FString AccessToken属性,然后在构建URL时附加上去。 - 坐标系对齐:确保你的
CesiumWorldTerrain或CesiumGeoreference设置的坐标系与WMTS图层的坐标系(TileMatrixSet)匹配。天地图w对应Web墨卡托(EPSG:3857),c对应经纬度(EPSG:4326)。如果坐标系不匹配,会导致瓦片位置错乱。
5. 进阶优化与扩展思路
当基本的WMTS加载功能跑通后,你可以考虑以下优化和扩展,让插件更加健壮和易用。
5.1 自动获取Capabilities文档
一个专业的WMTS客户端应该能解析服务的GetCapabilitiesXML文档,自动填充可用的图层(Layer)、样式(Style)、瓦片矩阵集(TileMatrixSet)和格式(Format)列表。你可以:
- 在
UCesiumWebMapTileServiceRasterOverlay类中添加一个LoadCapabilities的蓝图调用函数。 - 该函数向
BaseUrl?service=WMTS&request=GetCapabilities发起请求。 - 使用UE的XML解析模块(如
FXmlFile)或第三方库(如pugixml,需集成)解析返回的XML。 - 将解析出的列表更新到蓝图属性的下拉选项中,实现可视化选择,避免手动输入错误。
5.2 支持RESTful风格WMTS
除了KVP(键值对URL)方式,WMTS还支持RESTful风格的URL模板,如{TileMatrix}/{TileRow}/{TileCol}.png。你可以扩展你的类,增加一个UrlTemplate属性,让用户可以选择模式并填写对应的模板。这需要更复杂的字符串替换逻辑,但能兼容更多样的WMTS服务。
5.3 集成到Cesium离子工作流
虽然我们绕过了Cesium ion,但你可以考虑将自定义的WMTS图层配置(包括URL、图层名、密钥等)保存为一个资产文件。然后编写一个小的编辑器工具,允许用户导入这个资产文件,自动创建对应的WMTS图层Actor。这对于团队协作和项目配置管理很有帮助。
5.4 性能分析与调试工具
可以继承UCesiumRasterOverlay的调试功能,为你的WMTS图层添加更详细的调试信息,例如在屏幕上显示当前视口正在加载的瓦片URL、加载状态、缓存命中率等。这对于优化大规模场景的性能瓶颈至关重要。
编译并修改Cesium for Unreal插件的过程,本质上是一次对虚幻引擎插件架构和地理空间数据加载流程的深度探索。它可能充满挑战,但成功后的收益是巨大的:你获得了一个完全受控、深度定制、且与你的项目需求完美契合的地理数据加载方案。从此,无论是天地图、ArcGIS Server的WMTS,还是任何符合OGC标准的内网瓦片服务,你都能在UE5中轻松调用,为你的数字孪生世界注入鲜活、精准的真实地理基底。