Unity接入SuperMap在线服务:瓦片加载、坐标转换与性能优化实践
做Unity项目碰到GIS底图需求SuperMap超图在线服务这块绕不开。我接过几个数字孪生项目Unity里要把影像底图、矢量边界、三维地形体一整套叠进去技术和坑都踩了一遍。这篇我把完整接入思路、代码结构、坐标转换、性能优化都整理出来尤其是一些官方文档里不会写明白的细节希望对想在Unity里接超图在线服务的人有点实际帮助。1. 先搞清楚“超图在线服务”到底能提供什么1.1 在线服务类型地图、数据、分析、三维很多人第一次接触超图以为它只是“一堆瓦片底图”。实际上SuperMap在线服务体系远不止这些。从我的项目经验看Unity接入时最常用的是这几类地图服务提供影像、矢量、地形底图通常以WMS、WMTS、XYZ瓦片接口暴露。数据服务提供要素查询、属性表读取对应REST的Data服务返回GeoJSON或SuperMap自定义JSON。分析服务包括缓冲区分析、叠加分析、路径分析等Unity里常用于做场景内的空间判断。三维服务提供倾斜摄影、BIM、点云、地形等三维切片这类服务可以从SuperMap iServer发布也可以在超图Online上托管。Unity接入在线服务的核心问题不是数据在哪而是“怎么把网络上的GIS数据翻译成Unity能用的游戏对象”。这一点决定了很多方案选型。1.2 REST架构和URL的组成规律SuperMap iServer开放的服务全部走REST风格地址规律非常清晰。一个典型的在线服务地址长这样http://192.168.1.100:8090/iserver/services/map-world/rest/maps/World拆开看就是四层host:port服务所在服务器地址和端口。/iserver/services/iServer服务根路径固定不变。map-world服务名称可能叫map-china、>http://192.168.1.100:8090/iserver/services/map-china/rest/maps/China/tile.png?x0y0scale1.35e7这里是根据切片的行列号和比例尺参数来获取图片。瓦片请求参数在不同接口里不统一有的用scale有的用z/tilematrixset所以接入前最有效的动作是先打开服务地址的metadata文档用浏览器看一下返回JSON确认参数名。1.3 在线服务的鉴权问题SuperMap在线服务通常有两种访问方式一种是局域网iServer不鉴权一种是超图Online或公有云服务需要令牌。超图Online的token是在控制台申请的一般放在URL后面用token传参。项目里千万不要把token硬编码到公开版本中。我遇到过客户把token写死在代码里然后打包发到测试环境结果token被同行抓包盗用白白烧掉不少服务配额。正确做法是内部项目和离线包可以带token对外演示版本用后端代理由服务器端转发请求给Unity只暴露一个不带敏感信息的局域网地址。2. 在Unity里选哪条接入路线2.1 路线ASuperMap官方Unity插件SuperMap有面向Unity的插件具体产品线叫SuperMap iClient3D for Unity它封装了场景加载、数据图层、相机控制等一整套接口能直接对接iServer的三维服务和地图服务。优点很明显坐标系、裁剪、LOD都不用自己管底层本身就是GIS引擎在加载倾斜摄影和地形时有天然优势。缺点是包体偏大版本和Unity版本绑定较紧我试过在Unity 2021.3下接入时遇到一些GLSL兼容问题需要手动改插件里的Shader。如果你项目里以GIS数据为主比如倾斜摄影、地形、点云都要在同一个坐标系下严格套合优先考虑这条路线。2.2 路线B手写请求加脚本加载REST/瓦片官方插件适合重GIS场景但很多Unity项目并不是纯GIS项目它只是需要一层“地图背景”。这时候手写请求加载更轻量、更容易和现有游戏框架融合。手写方案的核心思路是用UnityWebRequest请求在线瓦片图片或要素数据然后用Sprite或Texture渲染在场景里。这个方案灵活度高可以自己控制加载时机、缓存策略、坐标系转换而且和Unity UI系统、URP渲染管线都好配合。从我的项目来看手写方案维护成本其实低于官方插件因为出现问题可以逐行排查官方插件出了问题往往只能等版本更新。2.3 选型对比和环境配置对比项官方插件路线手写请求路线坐标系处理自动处理需要手动转换GIS三维数据支持完整支持只支持瓦片和简单要素包体大小较大很小版本耦合度高低适合场景数字孪生底座、倾斜摄影轻量地图、游戏背景二次开发难度中低环境配置方面Unity端需要确认网络权限。Android工程需要在Player Settings里开启Internet权限Windows平台一般不需要额外配置但要注意打包后用人和开发机之间的防火墙规则。iOS的话需要配置ATS例外因为很多超图服务用的是http而不是https否则签发证书时会拦截。3. 实操Unity请求在线瓦片并渲染成场景底图3.1 从服务端拿瓦片地址以最常见的XYZ瓦片为例。SuperMap iServer发布的在线地图服务如果开启了瓦片缓存可以在浏览器里直接拉出瓦片。比如请求一个缩放级别为10的瓦片URL可能是http://your-server/iserver/services/map-china/rest/maps/China/tile.png?z10x435y197注意这里的y轴方向从小图到谷歌地图y方向各不相同。SuperMap里不管具体切片方案最好先通过REST接口拿到bounds和tileMatrixSet再反算行列号不要想当然地用标准Web Mercator算法硬套。我自己项目里常用的办法是直接在Unity里写一个辅助窗口输入中心点经纬度和缩放级别把行列号打印出来这样调试瓦片请求时非常直观。3.2 用UnityWebRequest写一个瓦片加载器写瓦片加载器先用一个TileLoader类管理去重和缓存避免每个瓦片重复创建协程。using System.Collections; using UnityEngine; using UnityEngine.Networking; public class TileLoader : MonoBehaviour { private Dictionarystring, Texture2D _tileCache new Dictionarystring, Texture2D(); private HashSetstring _inFlight new HashSetstring(); public void LoadTile(string url, System.ActionTexture2D onDone) { if (_tileCache.TryGetValue(url, out var tex)) { onDone?.Invoke(tex); return; } if (_inFlight.Contains(url)) return; _inFlight.Add(url); StartCoroutine(DownloadTile(url, tex { _inFlight.Remove(url); if (tex ! null) { _tileCache[url] tex; } onDone?.Invoke(tex); })); } private IEnumerator DownloadTile(string url, System.ActionTexture2D onDone) { using var request UnityWebRequestTexture.GetTexture(url); request.timeout 10; yield return request.SendWebRequest(); if (request.result ! UnityWebRequest.Result.Success) { Debug.LogWarning($Tile download failed: {url}, {request.error}); onDone?.Invoke(null); yield break; } var tex DownloadHandlerTexture.GetContent(request); onDone?.Invoke(tex); } }几个细节需要注意。_tileCache用字典存储Texture2D必须控制最大数量否则加载一圈地图内存就爆了。我一般设置一个LruCache限制在128张左右超出后释放最久未用的纹理。_inFlight做请求去重防止同一个瓦片被多个物体同时请求。request.timeout建议设置不然遇到断网或服务端假死协程会卡很久。加载纹理后还要设置纹理的wrapMode为ClampfilterMode为Bilinear否则瓦片边缘会有明显接缝尤其是卫星影像底图接缝特别显眼。3.3 坐标转换经纬度与Unity世界坐标互转这是整个接入中最容易翻车的地方。GIS坐标系用的是经纬度墨卡托投影Unity用的是左手坐标系单位是米。我的通用做法是以服务器地图范围的中心点为Unity原点将其他经纬度点换算成相对中心点的偏移。public class GisCoordinateConverter { public Vector2 OriginLonLat; public float MetersPerUnit 1f; public Vector2 LonLatToUnity(Vector2 lonLat) { // 这里简化为等距投影适合小范围场景 float x (lonLat.x - OriginLonLat.x) * 111320f; float y (lonLat.y - OriginLonLat.y) * 111320f; return new Vector2(x / MetersPerUnit, y / MetersPerUnit); } public Vector2 UnityToLonLat(Vector2 unityPos) { float lon unityPos.x * MetersPerUnit / 111320f OriginLonLat.x; float lat unityPos.y * MetersPerUnit / 111320f OriginLonLat.y; return new Vector2(lon, lat); } }这段代码只是一个基础版本用了简化的等距投影公式纬度在30度以上时误差会明显上升中高纬度地区至少要用WGS84的Meractor投影或直接引用SuperMap的投影转换库。我在一个江苏的项目里直接用等距公式结果南北方向偏移了几百米后来换成标准Mercator投影误差才降到米级。如果你的Unity场景里还需要叠加无人机倾斜摄影模型那么坐标系的基准必须是和GIS服务完全一致的CGCS2000或者WGS84不能自己玩简化版。3.4 性能缓存与多线程避坑Unity主线程不适合处理大量网络请求和纹理解码。Texture2D下载完成后必须回到主线程使用但瓦片图片的裁剪、坐标计算、JSON解析可以放到后台线程。我建议这样分工主线程协程发起请求、创建GameObject、设置材质纹理。后台线程计算瓦片行列号、解析服务返回的GeoJSON、顶点坐标批量转换。缓存层用LRU缓存管理Texture和要素数据。还要特别注意Unity默认纹理的最大尺寸限制。如果瓦片是8192x8192的大图直接加载会被Unity压缩或降采样显示模糊。需要把纹理的maxTextureSize调大或者干脆把大瓦片在服务端切好Unity端只加载512或256的小瓦片。另外同一个帧里不要同时发起太多请求我遇到过连续请求上百张瓦片导致Unity冻结几秒的情况。后来加了并发数量限制每次最多同时6个请求配合队列调度。4. 进阶在Unity里叠加SuperMap矢量要素和三维数据4.1 请求要素服务并绘制边界很多业务场景需要底图上叠加“地块边界”“行政区划”这类矢量要素。SuperMap要素服务通常通过REST的queryResult接口拿数据。请求一段地址http://your-server/iserver/services/data-plots/rest/data/plots/RegionShp/queryResult.json?queryParameter.attributeFilterSMID%3C100returnContenttrue响应是GeoJSON或超图JSON。Unity端解析后把经纬度坐标通过转换器映射成Unity坐标然后构建LineRenderer或者Polygon网格。绘制地块边界时我踩过最大的坑是顶点数量过多。一个完整的行政区边界可能有几万个顶点直接生成Mesh会导致场景瞬间卡死。解决方法有两种使用SuperMap服务的抽稀功能返回前设置queryParameter.maxFeatures或geometrySimplify参数。客户端做Douglas-Peucker抽稀阈值调到肉眼看不出来明显变形就行。4.2 三维场景服务的切片加载思路超图三维切片服务和普通地形瓦片不太一样。SuperMap iServer发布倾斜摄影或BIM数据后会生成一套三维瓦片树Unity端需要按照视野距离和包围盒来决定加载哪个层级的瓦片。手写太复杂建议这里直接用官方插件。如果实在要用纯Unity方案可以基于超图REST接口的layer3D资源和sptm接口请求切片但你自己要维护LOD误差判断、包围盒计算、三角网加载成本非常高。我的经验是二维底图手写三维切片用官方插件。两边接口是互不干扰的可以并存。同一台iServer服务既发二维地图又发三维场景Unity里同时挂官方插件和自写TileLoader效果很好。4.3 图例属性查询与交互高亮数据服务不仅能画图形还能做属性查询。Unity里点击地图上的某个地块弹窗显示地块编号、面积、权属人这是最常见的交互。实现思路是给地块Mesh添加Collider射线检测命中后用SMID字段去请求要素服务详情。void OnPointerClick(Vector3 screenPos) { Ray ray Camera.main.ScreenPointToRay(screenPos); if (Physics.Raycast(ray, out RaycastHit hit)) { var feature hit.collider.GetComponentFeatureTileItem(); StartCoroutine(QueryFeatureDetail(feature.SMID)); } } IEnumerator QueryFeatureDetail(int smid) { string url $http://your-server/iserver/services/data-plots/rest/data/plots/RegionShp/queryResult.json?queryParameter.attributeFilterSMID{smid}returnContenttrue; // 解析后显示 yield return null; }高亮点通常可以改变材质但要注意不要整个地块都高亮成一种纯色会遮挡底图。推荐用边缘发光的Shader只在地块轮廓上做半透明描边。5. 常见问题与排查实录5.1 请求跨域/被限制Unity打包后请求超图服务经常在控制台看到CORS错误。这和Unity本身无关是web服务器跨域限制。局域网iServer一般可以通过修改配置文件开放跨域或者用Unity编辑器里的WebPlayer设置。我的习惯是后端加一个轻量代理Unity只请求代理地址代理转发给iServer。这样既解决跨域也方便做token保护和数据过滤。5.2 瓦片灰屏/黑屏瓦片加载失败的表现基本是灰屏或黑屏。排查顺序是先用浏览器直接打开瓦片地址确认图片能不能正常返回再检查URL里的行列号和缩放级别是否有误最后看Unity端纹理格式是否支持。有一回服务端返回的是BMP格式的瓦片UnityWebRequestTexture默认不支持直接解码全部灰屏。解决办法是在URL后面加参数让服务器返回PNG或者在Unity里用ImageConversion.LoadImage处理字节流。5.3 坐标偏移严重前面提到的坐标系转换不准确是最常见原因。除此之外还要注意服务端用的是Web Mercator还是经纬度直投。有的超图服务返回的是EPSG:4326有的是EPSG:3857一旦用错整个底图会歪到天上去。项目里最好加一个“校准点”功能运行时输入两个已知坐标对应的Unity位置程序自动反推出投影参数和旋转角比手工算快得多。5.4 内存和掉帧问题瓦片加载器如果不限制缓存大小纹理内存会涨到让移动端直接闪退。另一个隐形问题是Texture2D默认带有mipChain会额外占33%内存在不需要mipmap时可以停掉。每张瓦片都生成一个独立的MaterialDraw Call会爆炸。建议合图或者用图集把多张瓦片合成一张再改变UV。5.5 常见问题速查表问题现象可能原因快速排查方法请求报404服务名或图层名拼写错误复制浏览器中能正常打开的URL片段对比返回403token过期或IP白名单限制在超图管理后台检查token状态瓦片错位投影方式混用检查服务返回的坐标参考代码地图加载极慢并发请求过多或服务端配置弱限制Unity端并发数开服务端缓存UI被地图覆盖渲染队列问题把地图Renderer的Order调到-100打包后连不上服务未开网络权限或ATS限制Android检查权限iOS配置ATS踩过几次坑之后我的体会是Unity接超图在线服务本质上是一个“坐标转换加资源调度”的问题。你得同时懂GIS数据规范和Unity渲染管线两类知识缺一个都会绕远路。先把坐标转换通透再谈加载和优化这是我认为最稳妥的接入顺序。后面项目如果用到真三维切片我再单独写一篇踩坑记录。