
做三维场景项目时我踩过一个特别懵的坑二维地图里天地图加载得好好的换成 SceneView 之后要么黑屏要么地名标签悬在半空不对位要么干脆只看到一个深蓝色地球孤零零转。后来排查到深夜才发现问题根本不在天地图本身而是我没有搞清 ArcGIS JS 4.18 API 在三维视图里对图层类型、坐标系和底图组织方式的限制。这篇文章就专门聊一件事在 SceneView 下正确加载天地图服务把影像底图、注记层和三维场景对齐并且把我实测中反复踩到的坑和完整的排查链路都写出来。文章基于 ArcGIS API for JavaScript 4.18 版本适合正在用 4.x 做三维 Web GIS 项目、尤其需要在三维场景里接入国内在线地图服务的开发者参考。1. 为什么在 SceneView 里加载天地图比二维地图更“挑剔”1.1 天地图的服务类型里藏着两套坐标系的坑天地图官方提供的在线服务按内容分可以分成几大类影像底图img、影像中文注记cia、矢量底图vec、矢量中文注记cva、地形晕渲ter、地形注记cta。每一类又分两套投影版本后缀_w表示 Web墨卡托投影EPSG:3857/WGS84 Web Mercator后缀_c表示经纬度直投CGCS2000 或 WGS84 地理坐标系通常对应 EPSG:4490/4326。比如影像底图就有img_w和img_c两种访问路径矢量注记也有cva_w和cva_c两种。这个_w和_c的差异在二维 MapView 里可能不明显因为二维地图同样支持不同坐标系的叠加和动态投影很多坐标转换都被引擎内部悄悄处理掉了。但到了 SceneView 里这个问题会被无限放大。SceneView 的全球场景viewingMode: global本质上是把平面世界投影到一个三维球体表面底层的绘制坐标是基于地心坐标系ECEF的而所有输入到场景中的 2D 切片图层最终都会被引擎按照 Web 墨卡托网格去采样和贴图。天地图的_c系列虽然坐标更精确但它的瓦片切分规则是经纬度网格和 ArcGIS JS 默认的切片方案不匹配加载进三维场景后会出现错位、拉伸、纹理模糊甚至直接无法显示的情况。所以我在项目里的铁律就是凡是接到 ArcGIS JS 4.x 里做底图一律用_w后缀服务。不是因为_c不好而是没必要在引擎底层坐标系都对不齐的时候给自己增加无谓的排查成本。ArcGIS JS 4.18 官方也是按 Web Mercator 作为全球场景的默认底图坐标系设计的顺着它的规则走能省掉大量定位问题。1.2 SceneView 和 MapView 对图层类型的“偏见”不一样同样的图层在 MapView 里规规矩矩到了 SceneView 里可能就放飞自我。原因很简单三维场景里多了一个“高程”和“遮挡”的维度图层不再是一个简单的平面叠加层而是要被投影到地形表面、建筑表面或者作为地球纹理处理的。对于 4.18 版本我常用的图层在三维场景下的表现是这样的GraphicsLayer在 SceneView 中默认以广告牌或者贴地符号渲染不再像二维那样自由平铺适合 POI 点、面标注但不适合做影像底图。TileLayer可以加载 ArcGIS Server 发布的缓存切片也可以加载在线切片服务只要瓦片方案匹配可以正常显示在三维地表。WebTileLayer天生就是为了 XYZ 瓦片模板设计的天地图的 DataServer 接口刚好就是这种 XYZ 方式是接天地图最直接的入口。WMTSLayer能对接标准 WMTS 服务但天地图的 WMTS 服务在 4.18 里配置起来非常折腾TileMatrixSet 的定义、坐标系的声明稍微对不上就可能白屏。很多人在三维场景里接天地图失败就是一开始选了 WMTSLayer然后被那一堆 TileMatrix、TileMatrixSet 参数折磨。实际上对于天地图这种国内服务直接在WebTileLayer里写好 URL 模板反而比接入标准化 WMTS 更简单、更可控。2. 动手前必须处理的三件事密钥、域名白名单和 URL 模板2.1 天地图 key 申请里最容易忽略的域名白名单天地图从 2019 年左右开始全面要求开发者申请开发者密钥tk/key没有 key 的请求会在返回内容里夹杂错误信息或者直接不返回瓦片。申请流程不复杂在天地图官网注册账号进入控制台按提示创建应用系统会分配一个 key。但这里有一个非常容易踩的坑——创建应用时填写的 IP 白名单/域名白名单决定了这个 key 在哪个域名下才能生效。我遇到过最典型的场景本地开发时一切正常部署到测试服务器后瓦片全部变成灰块或空白控制台里报Failed to load resource: the server responded with a status of 404。当时第一反应是自己代码写错了排查了半天才发现是测试环境域名没有加入天地图 key 的白名单。天地图后台对白名单的校验比较严格如果在本地用localhost调的接口部署后必须把服务器的正式域名或者 IP 也加进去否则请求虽然发出去了但返回的可能是错误 JSON 或者 404不会正常给你图片瓦片。另外如果用的是武汉、四川等地区节点或者某些专网环境还需要确认网络出口能否正常访问t0.tianditu.gov.cn的 443 或 80 端口。很多企业内网会拦截外部地图服务请求这个问题和代码无关却最容易被忽略。2.2 坐标系选错三维里会有什么典型表现_w系列服务对应的瓦片网格系统和 ArcGIS JS 4.18 的默认场景保持一致_c系列则很容易导致下面几类问题三维场景中底图整体向某个方向偏移比如影像块与地名注记错位。瓦片加载出来但明显变形比如边界拉伸成锯齿状这是因为切片网格与场景投影不匹配。某些缩放级别下底图突然花掉或者黑掉尤其在高纬度地区经纬度直投影的变性问题在三维球面上会被放大。如果只是因为项目数据特殊必须使用_c系列也不是完全没有办法。你可以尝试在WebTileLayer上手动指定spatialReference或者把地图切换到局部模式viewingMode: local再用对应的投影坐标系去承载数据层。但总体来说这种方式在 4.18 里并不成熟与其把时间花在和引擎底层投影较劲上不如直接用_w。2.3 URL 模板我推荐 DataServer 接口的两套写法天地图目前对外提供两种取瓦片的方式一种是标准WMTS接口一种是DataServer的简化 XYZ 接口。以影像底图img_w为例WMTS 完整写法https://t0.tianditu.gov.cn/img_w/wmts?SERVICEWMTSREQUESTGetTileVERSION1.0.0LAYERimgSTYLEdefaultTILEMATRIXSETwFORMATtilesTILEMATRIX{level}TILEROW{row}TILECOL{col}tk你的密钥DataServer 简化写法https://t0.tianditu.gov.cn/DataServer?Timg_wx{col}y{row}l{level}tk你的密钥在WebTileLayer里我通常用 DataServer 这种。原因很简单参数少、容易排错、响应稳定。我不推荐把项目核心底图绑定在 WMTS 这种大而全的接口上因为一旦某个TILEMATRIXSET参数和天地图那边顺手调整不匹配排查起来会很头大。DataServer 的x/y/l参数和WebTileLayer的{col}/{row}/{level}正好一一对应几乎不用动脑子。这里的{col}、{row}、{level}是 ArcGIS JSWebTileLayer的占位符它会在运行时自动替换成当前视野所需瓦片的列号、行号和缩放级别。要注意不能把天地图的x参数直接等同于WebTileLayer的{x}占位符——如果你在模板里写了{x}ArcGIS JS 也能识别并替换成列号但更规范、更不容易混淆的写法是用{col}和{row}。3. 用 WebTileLayer 挂载天地图到 SceneView 的完整实现3.1 最小可运行代码Map SceneView WebTileLayer先看一段能直接跑起来的最小示例。这里的思路是把天地图影像底图包装成WebTileLayer再由Basemap加载到 Map 中最后交给SceneView渲染。我特意没有在 Map 里再设置其他底图否则默认底图会和天地图叠在一起看起来非常混乱。require([ esri/Map, esri/views/SceneView, esri/layers/WebTileLayer, esri/Basemap ], function(Map, SceneView, WebTileLayer, Basemap) { // 天地图影像底图Web墨卡托投影 const tiandituImg new WebTileLayer({ urlTemplate: https://t0.tianditu.gov.cn/DataServer?Timg_wx{col}y{row}l{level}tk你的密钥, title: 天地图影像, copyright: 天地图 }); // 用 Basemap 统一管理底图图层 const tiandituBasemap new Basemap({ baseLayers: [tiandituImg] }); const map new Map({ basemap: tiandituBasemap }); const view new SceneView({ container: viewDiv, map: map, center: [116.391, 39.904], // 北京 zoom: 12, viewingMode: global }); });有几个细节值得说明。第一我把urlTemplate写成以https开头因为现在很多项目都强制 HTTPS如果页面是 HTTPS 但瓦片请求是 HTTP浏览器会直接拦截混合内容出现“图片加载失败”的错觉。天地图对 HTTPS 支持是正常的所以直接写https最省心。第二Basemap的作用是把一个或多个图层打包成“底图”语义这个语义在 SceneView 里天然对应地球表面纹理。如果不用Basemap而直接把WebTileLayer扔进Map.layers在三维场景里它其实也能显示但作为叠加层在处理光照和环境光时效果可能不如“底图”那么稳定。第三viewingMode: global就是最常见的全球三维球模式对应你要加载全球底图的场景如果项目只是某个园区、某个城市的局部三维将来可能需要切换到local模式但加载天地图这种全球在线服务时global是首选。3.2 再叠加一个注记层把中文地名带回来影像底图本身没有地名、道路名、行政边界只有卫星照片。真实项目里通常要把注记层叠上去。注记层同样是WebTileLayer只是T参数换成了cia_w影像中文注记。把注记层也加进Basemap.baseLayers就能得到一套“卫星影像 中文标注”的标准底图组合。const tiandituLabel new WebTileLayer({ urlTemplate: https://t0.tianditu.gov.cn/DataServer?Tcia_wx{col}y{row}l{level}tk你的密钥, title: 天地图影像注记, copyright: 天地图 }); const tiandituBasemap new Basemap({ baseLayers: [tiandituImg, tiandituLabel] });这里有个图层顺序问题baseLayers数组里靠前的图层会被放在底层靠后的图层放在上面。所以正确的顺序一定是影像底图在前注记层在后否则注记会被影像盖住什么都看不清。这是新手最容易犯的错看到底图黑乎乎一片以为是 key 的问题其实是两个图层先后顺序反了。基于同样的思路矢量底图加矢量注记的组合也很容易做把img_w换成vec_wcia_w换成cva_w就行。如果项目需要地形晕渲效果就用ter_w加cta_w。3.3 用 LayerList 做显隐控制顺便加上常用三维控件项目做到后面用户肯定需要自己开关影像和注记至少需要在影像底图和矢量底图之间切换。用LayerList控件可以快速实现图层列表用户点一下就能控制显隐。还可以加上Home回到初始视角和Compass指北针两个三维场景常用控件。require([ esri/widgets/LayerList, esri/widgets/Home, esri/widgets/Compass ], function(LayerList, Home, Compass) { const layerList new LayerList({ view: view, container: layerListDiv }); const homeWidget new Home({ view: view }); view.ui.add(homeWidget, top-left); const compassWidget new Compass({ view: view }); view.ui.add(compassWidget, top-left); });这只是最基础的控件接入。实际项目里我经常通过监听LayerList的触发事件在用户切换底图时同步调整相机角度、光照等参数让底图切换过度的体验更加平滑。不过这些属于锦上添花先把底图正确加载出来才是真正要紧的事。4. 实测中遇到的三类问题以及完整的排查链路4.1 黑屏、白屏、灰块先看 Network再看 key最后找模板在三维场景里加载天地图失败90% 的表现都是“地球倒是转得很欢但表面一片黑/灰/白”。很多人第一反应是改代码其实正确的排查链路应该是从网络请求入手。第一步打开浏览器开发者工具切到 Network 面板过滤tianditu.gov.cn看有没有瓦片请求发出。如果压根没请求说明WebTileLayer没有被视图加载问题出在图层是否加进 Map、还是urlTemplate模板语法错误导致图层在初始化时直接报废。第二步如果请求发出了但状态码是 404 或异常 JSON就在浏览器新标签页里直接访问那个请求 URL把{col}、{row}、{level}手动替换成固定数字比如x100y50l10。如果直接打开 URL 能返回图片说明网络和 key 都没问题反而要回到模板占位符是否正确。如果直接打开 URL 也返回错误 JSON那就重点看 key 是否过期、是否被白名单拦截、请求参数是否写错。第三步检查模板里到底用的是{col}/{row}/{level}还是{x}/{y}/{z}。这两套占位符在WebTileLayer里都能用但容易写混。我的建议是固定使用{col}/{row}/{level}理由如前所述天地图 DataServer 接口的参数名正好是x/y/l一一对齐后视觉上大脑不容易短路。还有一种情况是本地离线部署了 ArcGIS JS API但 dojo 的 baseUrl 并没有指向本地路径导致页面在混用 CDN 和本地代码时出现加载异常。这种问题通常会在控制台报出明显的模块加载错误比如define is not defined或者dojo has not been loaded。排查时先看 Network 里有没有请求js.arcgis.com/4.18/如果自己的域名下有大量外部请求说明离线部署没生效。4.2 坐标偏移从“表面没问题”到“三维里错位”的典型复盘坐标偏移问题很隐蔽因为二维下有时看着还行一旦切到三维球面问题就暴露无遗。我复盘过一次完整过程当时在某项目里图省事直接用了img_c服务二维底图上只是觉得稍微有点不精准没仔细查。等用户把视角拉到整座城市上空影像上的重要建筑物边界和矢量数据里的地块边界明显朝东南方向偏移了几十米尤其在数据范围大的城市西北区域偏移更突出。后来我在排除 key、网络、模板都正常之后开始怀疑坐标系。我把同一位置的天地图img_c和img_w瓦片分别加载到两个测试页面手动对比同一缩放级别下的瓦片网格发现_c的瓦片网格在三维球面上和 ArcGIS JS 引擎预期的 Web Mercator 网格并不重合。由于场景引擎按照 Web Mercator 网格去采样_c瓦片被强制投影后自然会出现整体位移。最终我把所有_c替换成_w偏移问题一次解决。这给我的经验是如果出现整体性偏移不要急着在代码里加geometryService做坐标纠偏先检查你加载的服务坐标系是否与场景引擎相同。坐标系方向错了后面做多少都是白费。4.3 三维场景下卡顿、图层闪烁以及“查询报错”的关联排查三维场景比二维复杂性能问题也更明显。最常见的是拖动视角时瓦片加载不及时出现一块块灰底还有缩放过程中影像底图和注记层加载速度不一致导致注记先到、影像后到看起来像在闪。处理这类问题我一般分三步第一步精简图层数量。影像底图和注记层两个WebTileLayer就足够了不要为了“好看”再叠加多余的在线图层每个图层都是一份请求开销。第二步限制视图的最大缩放级别和最大屏幕误差避免用户无限放大导致浏览器发出大量超出注意范围的瓦片请求。比如用view.constraints.maxScale或对WebTileLayer的maxScale做控制。第三步关闭或降低三维场景中不必要的视觉效果比如view.environment.lighting.directShadows false三维光照的实时阴影对影像底图这种纹理材质很不友好看着像蒙了一层灰实际上就是光照影响。顺带提一个和 ArcGIS Server 相关的报错unable to complete operation. unable to perform query operation.这个错误经常出现在三维场景里叠加了某个 ArcGIS Server 图层之后。很多人会怀疑是不是天地图加载影响了服务查询但其实这个报错的根源通常在 ArcGIS Server 服务端而不是天地图。排查时先打开该服务的 REST 端点在浏览器里直接执行一次 Query 请求看看服务本身能否正常返回结果。如果服务直接查询就报错那是数据源或者服务配置问题只有服务本身正常才需要考虑是不是三维场景下视图范围、空间参考和图层定义不一致导致查询参数异常。不要一看到错误就把锅甩给天地图。5. 我的选型结论与一些项目落地建议5.1 为什么 WebTileLayer 是 4.18 版本最务实的选择对比下来4.18 版本里接天地图WebTileLayer是目前最省心的方案我几乎没有犹豫。理由很直接天地图 DataServer 接口本来就是一个标准的 XYZ 瓦片服务WebTileLayer就是 ArcGIS JS 为这类服务量身定做的图层类型。相比之下WMTSLayer适合的是正规 WMTS 服务需要你把天地图的坐标系、TileMatrixSet 定义、层名、样式名全部对清楚任何一项不一致都会导致白屏或错位。TileLayer主要面向 ArcGIS Server 发布的缓存服务如果你本地没有把天地图转成 ArcGIS Server 缓存用它反而更麻烦。还要注意4.18 是 2020 年甚至更早的版本它没有后续版本里那些开箱即用的在线底图优化很多底图加载逻辑需要开发者自己处理。用WebTileLayer的好处是它把瓦片请求的逻辑封装得足够简单你只需要关心 URL 模板和坐标系列剩下的交给引擎。5.2 如果本地有 ArcGIS Server 发布的缓存切片接进三维怎么操作有些项目因为内网隔离无法直接访问公网天地图或者需要更高性能、更可控的数据源会选择把天地图缓存到本地再通过 ArcGIS Server 发布成切片服务。这种情况下在 SceneView 里加载的方式就从WebTileLayer变成了TileLayer。用法上更简单const localTdtLayer new TileLayer({ url: https://你的内网服务器/arcgis/rest/services/tdtImg/MapServer, title: 本地天地图影像 }); const map new Map({ basemap: new Basemap({ baseLayers: [localTdtLayer] }) });用TileLayer时坐标系、切片方案都以 ArcGIS Server 服务自带的信息为准只要服务本身发布正确三维场景里显示就不会出大问题。但这里要重点检查服务是否支持外网/内网正确访问以及 ArcGIS Server 的跨域配置。很多项目把 API 离线部署到本地却忘了给 ArcGIS Server 配置跨域头导致页面在三维场景里加载本地切片时报Unable to complete operation按下 F12 一看请求直接被浏览器 CORS 策略拦了。另外如果本地服务的坐标系不是 Web Mercator而是一个地方坐标系那么你需要把SceneView的viewingMode设为local并让 Map 使用对应的投影坐标系对象。这种三维模式适合城市级或园区级的精细场景配合本地切片数据才是高效组合而不是硬套全球模式去加载地方数据。5.3 关于 4.18 版本本身我还想多啰嗦两句如果你是刚开始用 4.18建议直接采用官方推荐的 CDN 引入方式同时保留本地离线包作为备份。因为开发环境网络波动会直接影响瓦片调试你分不清页面加载失败到底是因为 API 没加载还是天地图请求有问题。离线部署时记得把three.js相关的依赖库也一起部署SceneView 的很多三维渲染能力依赖 Three.js缺少依赖时控制台会报一些很奇怪的Cannot read property xxx of undefined。这属于环境问题不是代码逻辑问题但排查起来却要花很长时间。版本升级这块如果项目允许我当然建议用更新的 4.x 版本因为后续版本在WebTileLayer、Basemap、三维渲染性能上都有不少改进。但如果项目因为历史原因锁定了 4.18本文这套方案完全够用不需要为了一个底图功能冒升级 API 版本的风险。5.4 个人经验补充几个值得长期坚持的小习惯天地图这种在线服务天然受网络环境和对方服务稳定性影响所以我在项目里养成了几个习惯在这里一并分享。首先是把 tk 密钥做成可配置项不要硬编码在业务代码里否则排查问题时要改代码、重新打包成本很高。其次是默认给每个WebTileLayer设置合理的copyright和title这不仅是规范问题在 LayerList 和打印导出时也能直接使用。第三是对瓦片请求做一次本地的超时/失败监控如果天地图某个节点不稳定前端至少能在界面上给出提示而不是让用户看到一片空白却不知道发生了什么。我还在实际项目里碰过一种情况某个环境里天地图的公网域名被防火墙拉黑但项目方没有告知结果影像底图全部加载不出来。后来我在代码里加了一个简单的网络探活机制页面初始化时先请求一次天地图的 0 级瓦片如果失败就弹出明确提示同时切换到本地缓存底图作为兜底。这个机制挽救了整整一天的上线时间。说实话接天地图这个功能本身不难难的是底图之外的那一圈工程化问题密钥管理、域名白名单、坐标系判断、性能优化、故障提示。把这些问题想清楚你在 SceneView 里加载天地图就不再是“碰运气”而是真正可控、可维护的工程能力。