1. 先搞清楚:ArcGIS JS里的3D Tiles到底是怎么回事
很多做GIS开发的朋友都有过这样的经历:手头拿到一批倾斜摄影模型或者人工精修的三维模型,想直接在Web端展示,结果一搜资料,满屏都是Cesium的教程,放到ArcGIS JS里却发现加载不出来,或者加载出来是个空场景。
其实问题很简单——ArcGIS JS从4.x版本开始,对3D Tiles的支持走的是自己的体系。它底层托管的是I3S标准,和Cesium那边主推的3D Tiles(b3dm、pnts这些格式)虽然概念上都是瓦片化三维数据,但存储结构和读取方式并不完全互通。如果你手上的数据是官方文档里说的那种标准3D Tiles,直接扔给ArcGIS JS往往是行不通的。
我为什么强调这一点?因为我在实际项目里遇到过太多次“数据好好的,就是加载不出来”的情况。排查到最后,十有八九都出在数据格式和ArcGIS平台解析能力不匹配上。这篇教程不会只贴官方API,而是把我从数据准备、本地调试、加载配置到性能优化的完整实战链路拆开讲,尤其是那些文档里不会写、但真实项目里必然会踩的坑。
先说结论:ArcGIS JS里可以加载3D Tiles,但路径不是“拿个url直接怼进去”那么简单。你需要先搞清楚数据是什么格式、来源是什么、是否需要转换,然后用正确的图层类型去承载它。整个过程涉及数据转换、服务部署、图层配置、样式交互、性能调优五个环节,这篇文章会逐个展开。
如果你正在做数字孪生、城市规划、园区可视化这类项目,并且技术栈锁定在ArcGIS生态内,那这篇文章的内容可以帮你少走至少两周弯路。
2. 数据准备是最大的坑:先把格式理清楚再谈加载
2.1 3D Tiles和I3S到底有什么区别
先做一个基础扫盲。3D Tiles是Cesium提出的一种三维瓦片格式标准,后来也成了OGC的社区标准。它的核心思路是把海量三维数据切成金字塔结构的瓦片,按需加载,从而支撑大场景的流畅渲染。瓦片类型包括b3dm(批量三维模型)、pnts(点云)、i3dm(实例化三维模型)等。
I3S是Esri推出的同类标准,全称Indexed 3D Scene Layer,被OGC采纳为官方标准之一。ArcGIS Pro、ArcGIS Online、ArcGIS JS全都原生支持I3S。它同样采用瓦片金字塔和按需加载机制,但在文件组织方式、索引结构、属性编码上和3D Tiles有差异。
所以当你在ArcGIS JS里看到SceneLayer这个图层类型时,它默认加载的是I3S服务。而对于外部3D Tiles数据,ArcGIS JS从4.7版本开始有了一定程度的有限支持——注意“有限”这个词,后面细说。
这里有一个很容易混淆的点:有些教程说“ArcGIS JS加载3D Tiles”,实际上加载的是经过ArcGIS平台转换或兼容处理后的数据,并非原始Cesium格式。
2.2 常见的3D Tiles数据来源和转换路径
把原始数据转成ArcGIS能用的格式,通常有三条路:
路径一:原始数据是.ply、.obj、.fbx等常规三维模型,目标是变成I3S服务。这类数据可以用ArcGIS Pro的“创建3D对象场景图层”工具,或者用其域创新这类三维数据处理工具先导出.ply,再走转换管线。具体操作是把模型导入ArcGIS Pro,在场景中设置为场景图层,然后用“共享为Web图层”或本地的场景图层包(.slpk)输出。
路径二:原始数据已经是Cesium风格的3D Tiles(b3dm/pnts),目标是让ArcGIS JS能加载。这种情况最麻烦。早期版本只能通过ArcGIS Enterprise的Data Interoperability扩展或第三方转换工具,先转成.slpk或I3S格式。如果数据量不大,也可以用FME这类ETL工具做格式转换。
路径三:数据量小、只做验证演示,直接使用ArcGIS官方示例数据或在线服务。这是最省事的,建议新手先用官方示例跑通整个流程,再处理自己的数据。
2.3 一个真实踩坑案例:.ply转3D Tiles再到ArcGIS JS
我之前接到一个项目,客户给了一批无人机扫描的实景模型,格式是.ply。团队里有同事说“其域创新能导出.ply,直接转3d tiles”,于是我们用工具转出了一套b3dm格式的3D Tiles。
然后把服务地址填到ArcGIS JS里,结果控制台直接报错Failed to load layer。一开始以为是地址写错了,反复检查没问题。后来一步步排查,确认是ArcGIS JS对这个b3dm的数据结构解析不了——它内部对Tile的TileSet、Tile、Content节点的处理逻辑,和Cesium的标准存在兼容差异。
最终解决方案是:用ArcGIS Pro打开原始.ply(需要先装好适合的格式支持),通过“3D对象场景图层”转成.slpk,再在ArcGIS Enterprise或ArcGIS Online上发布为场景服务。前端加载问题直接消失。
所以我的建议是:如果你自己就是数据生产方,从源头就按I3S流程走,不要先把数据整成Cesium 3D Tiles再想办法转回来,这是典型的绕远路。
2.4 数据转换时的重要参数和注意事项
用ArcGIS Pro做模型转换时,有几个选项直接影响前端效果:
- 纹理压缩:选DXT格式,Web端渲染更快,文件体积更小。
- LOD层级数量:默认生成多层,关注最小和最大级别的间距,过密会增加生成的瓦片数量,影响前端加载。
- 坐标系:确保输入模型有正确的地理参考。如果模型自身没带坐标系,需要在转换前设置好,否则发布到Web上会悬浮在地球错误的角落,甚至场景都定位不到。
| 数据转换项 | 建议选择 | 原因 |
| 纹理压缩格式 | DXT | Web端GPU直接支持,加载性能好 |
| LOD策略 | 按默认或按模型复杂度微调 | 层级太密瓦片数量爆炸 |
| 坐标系 | 和场景底图一致 | 避免投影漂移 |
| 输出格式 | .slpk或I3S | ArcGIS JS原生支持 |
3. 实战操作:在ArcGIS JS里加载一个3D Tiles图层
3.1 创建一个3D Scene
先建一个带3D场景的HTML页面。需要加载ArcGIS JS 4.x版本的CSS和JS文件。注意版本号:4.7以前压根没有SceneLayer对3D Tiles的支持,4.20以后兼容性明显提升,我自己用的是4.25,稳定性和性能都满意。
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>ArcGIS JS 3D Tiles示例</title> <link rel="stylesheet" href="https://js.arcgis.com/4.25/esri/themes/dark/main.css" /> <script src="https://js.arcgis.com/4.25/"></script> <style> html, body, #viewDiv { height: 100%; margin: 0; padding: 0; } </style> </head> <body> <div id="viewDiv"></div> </body> </html>3.2 添加SceneLayer的两种方式
方式一:直接通过url加载I3S服务
const layer = new SceneLayer({ url: "https://your-server/rest/services/YourSceneService/SceneServer", title: "我的三维模型" });这个url要指向ArcGIS场景服务的REST端点。如果用ArcGIS Enterprise发布,url格式通常是https://server/arcgis/rest/services/xxx/SceneServer。
方式二:通过portalItem加载ArcGIS Online上的场景图层
const layer = new SceneLayer({ portalItem: { id: "xxxxxxxxxxxxxxxx" // ArcGIS Online上图层的itemId } });第一种方式最常用,适合自己部署服务的项目。第二种适合直接用ArcGIS Online公共数据或者组织内部共享数据的场景。
3.3 本地文件调试时最大的坑:跨域
很多新手会在本地把HTML文件往浏览器一拖,然后加载一个C:/models/SceneServer之类的路径。结果当然是错。
这里的原因有两个:
SceneLayer的url必须是标准的HTTP(S)协议地址,不能是文件系统的绝对路径。- 如果你想把本地的.slpk文件跑起来,绝大多数情况下需要自己在本地起一个服务。ArcGIS JS在前端通过fetch请求场景服务接口,浏览器对跨域请求有严格限制,没有正确的CORS头,请求直接失败。
我习惯用的本地调试方案很简单:在项目根目录跑一个简单的静态服务器,然后把.slpk放到可以访问的位置。如果你用Python,直接:
python -m http.server 8080然后在代码里写http://localhost:8080/xxx.slpk,这样做的本质就是给浏览器一个合法的HTTP上下文,让它能正常发起数据请求。所以,遇到加载不出来的时候,先想想自己有没有把项目跑在HTTP服务上,十次里有七次是这个问题。
真正生产环境就更简单了:把.slpk通过ArcGIS Pro发布成托管场景服务,Web服务器和CORS全由ArcGIS平台解决。
3.4 Camera视角定位
加载图层后,默认视角可能不在数据所在位置。你需要用camera把视图定位到模型的经纬度坐标。
view.goTo({ position: { longitude: 116.397, latitude: 39.908, height: 1000 }, heading: 0, tilt: 60 });这个参数的含义很简单:朝向正北,视角倾斜60度,距离地面1公里,正好适合观察中等大小的建筑模型。
3.5 一个完整的加载示例
require([ "esri/views/SceneView", "esri/layers/SceneLayer", "esri/Map" ], function(SceneView, SceneLayer, Map) { const sceneLayer = new SceneLayer({ url: "https://your-server/rest/services/YourScene/SceneServer", title: "测试模型" }); const map = new Map({ basemap: "gray-vector", ground: "world-elevation", layers: [sceneLayer] }); const view = new SceneView({ container: "viewDiv", map: map, camera: { position: { longitude: 116.397, latitude: 39.908, height: 1000 }, heading: 0, tilt: 60 } }); });这段代码跑通后,你应该能在场景里看到模型。如果白屏,按我第6章的排查思路去看。
4. 样式与交互:让3D图层有“生命力”而不只是一堆模型
3D Tiles图层加载出来只是第一步。我见过很多项目,模型出来了,但用户不会关注到重点,因为所有建筑都是一个颜色、一个样式,不会变亮,也没有信息弹窗。以下三种能力几乎是每个项目必备。
4.1 分类渲染:不同属性不同颜色
数字孪生项目里,最常见的需求是按建筑类型或者当前状态上色。比如工业区是红色,商业区是蓝色,住宅区是黄色。
const renderer = { type: "unique-value", field: "BldType", uniqueValueInfos: [ { value: "工业", symbol: { type: "polygon-3d", symbolLayers: [{ type: "extrude", size: 10, material: { color: "#cd4242" } }] } }, { value: "商业", symbol: { type: "polygon-3d", symbolLayers: [{ type: "extrude", size: 10, material: { color: "#4286cd" } }] } }, { value: "住宅", symbol: { type: "polygon-3d", symbolLayers: [{ type: "extrude", size: 10, material: { color: "#e8d44d" } }] } } ] }; layer.renderer = renderer;这里有个基础知识要补充:SceneLayer既可以是实景三维的网格模型,也可以是白模(建筑体块)。如果是白模,用extrude符号做拉伸显示效果很自然。如果是实景模型,通常不需要重新定义符号材质,直接用默认照片纹理就好。
4.2 点击高亮+属性弹窗
高亮设置在一个叫highlightOptions的属性里。这个在WebGI里其实就是改变选中对象的描边颜色和透明度。
layer.highlightOptions = { color: "#00ffff", haloOpacity: 0.9, fillOpacity: 0.2 }; view.on("click", function(event) { view.hitTest(event).then(function(response) { if (response.results.length > 0) { const graphic = response.results[0].graphic; if (graphic && graphic.attributes) { const content = Object.keys(graphic.attributes).map(key => { return key + ": " + graphic.attributes[key]; }).join("<br/>"); const popup = { title: "模型属性", content: content }; view.popup.open({ location: event.mapPoint, features: [graphic], title: popup.title, content: popup.content }); } } }); });这段代码做了什么:点击场景任意位置,通过hitTest检测是否命中了图层里的模型,然后弹出一个显示属性信息的弹窗。真实项目中,比如园区招商系统,点击一栋楼显示楼栋编号、面积、楼层数、入驻企业,就是这么实现的。
4.3 图层管理的几个实用小技巧
- 隐藏/显示图层:
layer.visible = false/true,不用重新加载,适合做图层开关。 - 透明度控制:
layer.opacity = 0.5,做对比分析时非常好用。 - 图例同步:如果你的renderer是连续色带(color ramp),用
layer.renderer.addBreak之类的方法更新图例。
5. 性能优化:3D加载卡顿的根源和处理思路
5.1 先调这三个关键参数
maximumScreenSpaceError:控制瓦片细分程度的阈值。数值越大,加载的瓦片越粗糙,渲染性能越好;数值越小,画面越精细,性能越差。默认值通常比较平衡,但如果卡顿明显,把它从默认值往上调,比如从16调成32,能明显减少瓦片请求数量。
tileCacheSize:瓦片缓存大小。这个值越大,GPU内存占用越多。如果你的浏览器端内存比较大,可以适当加大缓存,减少重复加载。
const layer = new SceneLayer({ url: "https://your-server/rest/services/YourScene/SceneServer", maximumScreenSpaceError: 32, tileCacheSize: 200 });- view.goTo动画时长:默认动画约1秒,如果设备性能差,可以把动画关闭。
view.goTo({ target: { longitude: 116.397, latitude: 39.908, height: 1000 } }, { animate: false });5.2 数据层面优化才是关键
前端参数调来调去,天花板很低。真正决定加载速度的是数据本体。
- 控制三角面数量:原始模型动不动几百万面,发布前用ArcGIS Pro的Decimation工具减面,可以降到一两百万面,视觉效果几乎无损。
- 纹理图集优化:把一堆零散贴图合并成一张图集,减少GPU纹理切换次数。
- 按图层拆数据:如果场景包含建筑、道路、植被,拆成多个SceneLayer,这样用户只需要看到哪个就请求哪个,避免把所有数据一次性加载。
5.3 用性能面板看问题在哪
打开浏览器的DevTools网络面板,可以直观看到哪些瓦片在持续下载、哪些瓦片尺寸巨大。如果某个瓦片动辄几十MB,大概率是数据分割时空间粒度太大。
我在ArcGIS JS的性能分析里还注意到一个现象:场景加载时,如果模型带有很多互不共享纹理的要素,GPU绘制调用会急剧增加。解决办法是尽量让模型共享材质,少用独立材质,这是从建模阶段就要规划的。
6. 加载失败的排查链路:照着这个顺序走
6.1 最常见的三种错误
| 现象 | 原因 | 解决办法 |
| 控制台报跨域错误 | 请求的服务没有设置CORS头,或本地文件路径访问 | 起HTTP服务,或确认服务端CORS配置 |
| Layer failed to load | url不对、格式不兼容、服务不存在 | 检查REST端点、数据格式是否I3S |
| 图层加载了但空白 | 相机视角没定位到数据区域,坐标系偏移,或者LOD层级没有数据 | 用goTo定位正确坐标,检查服务坐标参考系 |
6.2 逐步排查的完整过程
如果你遇到“模型加载不出来”,建议按这个顺序排查,不要跳步:
- 打开控制台(F12),看Network面板,找到SceneLayer相关的请求。如果请求直接显示失败,优先看HTTP状态码,403或404通常是地址或权限问题。
- 在浏览器地址栏直接访问你的SceneLayer REST端点,比如
https://your-server/rest/services/xxx/SceneServer。如果正常,应该返回JSON描述信息,里面能看到layerType、spatialReference这些字段。如果连JSON都返回不了,说明地址本身就不通。 - 检查layerType字段,ArcGIS JS的SceneLayer要求layerType是
SceneLayer。如果显示的是SceneService或者3DObject,可能要调整url路径,选择具体的子图层地址。 - 验证坐标系:把JSON里的spatialReference和你的camera位置比对。如果数据是WGS84,camera坐标也用经纬度;如果数据是Web Mercator或某个地方坐标系,camera要匹配,否则视角飞到天上,看起来像没加载。
6.3 一个案例:坐标系不匹配导致的“模型消失”
有一次我把上海的模型数据发布到本地服务,camera设定的是上海市中心经纬度,结果场景一打开什么也没有。我检查图层属性,发现坐标系是WGS84,按理说没问题,但模型偏偏没出现。
后来我把camera的height从1000改成100000,才发现模型悬浮在地球大气层外,因为数据源的坐标系基准和场景底图的基准有细微偏差。这种问题很隐蔽,控制台不报错,只是画面不对——排查思路里加一条:先放大视野看模型是否在偏离的位置。
7. 我用三个月踩出来的经验总结
7.1 数据准备占七成工作量
很多人以为3D Tiles加载是个前端问题,其实前端代码半天就写完了,数据转换和发布才是最大的时间杀手。如果你手头数据格式不对,预留一周时间做转换和调优是比较合理的。
7.2 官方文档没有告诉你的事
ArcGIS JS的官方API文档里对SceneLayer的标准用法描述很清晰,但没有覆盖到所有的兼容异常。比如我从ArcGIS Online加载一个公开的I3S服务时,会遇到token过期问题。公共数据服务经常对匿名访问有限制,你需要切换到登录态或者申请一个API key。
这个问题常见场景是:你把某个在线服务的url直接写在代码里,本地面向公网部署时没报错,但用户访问量大到一定程度,服务端开始限流,表现为有时候能加载,有时候不能。这就是服务配额问题,不是你代码的问题。
7.3 对3D Tiles在ArcGIS JS里的最终判断
如果项目必须用ArcGIS生态,那花时间学习3D Tiles的加载没问题,但要明确一点:ArcGIS的强项是GIS数据管理、分析和完整的平台体系,3D可视化这块它对3D Tiles的“原生感”确实不如Cesium丰富。
如果你的核心诉求是纯粹三维大场景展示,且不受ArcGIS平台限制,直接选Cesium会更省事。但如果你的项目需要叠加ArcGIS的要素查询、空间分析、权限体系,那ArcGIS JS这条路线就是正确的,把数据转换管线跑顺,后续维护会非常省心。
最后给一个具体建议:在项目初期,用一个小模型样例跑通全流程,从原始数据到转换到发布再到前端加载,一天之内能验证可行性,再动工处理全量数据。不要一上来就把几百GB的倾斜摄影模型丢给转换工具,转一次等一天,最后失败重来,非常浪费时间。