
做倾斜摄影项目的人越来越多但很多团队一上来就撞上同一个问题手里只有一份OSGB或者3DTiles倾斜摄影数据领导说要在Web端展示还要叠加业务功能。打开Cesium觉得太重纯用Three.js又发现它压根不认3dtiles这种格式卡在第一步。这篇文章就是用我自己踩过的坑给你趟一条路讲清楚在Three.js里加载3dtiles倾斜摄影数据的完整方案。内容覆盖数据怎么准备、工具怎么选、坐标系怎么解、代码怎么写、性能怎么调最后附上常见问题排查手册。不论你是刚接触倾斜摄影的WebGIS新人还是已经在用Cesium想换轻量化方案的老手都可以照着落地。1. 为什么要在Three.js里加载3DTiles先搞清楚需求再选方案1.1 倾斜摄影数据为什么一定要用3DTiles倾斜摄影产出的原始数据以OSGB格式为主一个中等规模的场景动辄几十GB包含了几十万个独立的瓦片文件。这种数据直接丢给Web端是不现实的浏览器加载不了这么多碎文件GPU也扛不住一帧渲染几十万模型。3DTiles就是专门解决这个问题的。它是OGC在2019年发布的标准格式设计目标是把海量三维地理数据倾斜摄影模型、点云、矢量、BIM切成带空间索引的瓦片树客户端按需加载、按层级调度。它的核心思想跟地图的瓦片金字塔本质一样离得近加载高精度瓦片离得远加载低精度瓦片当前视野外的瓦片直接不加载。所以正常情况下拿到倾斜摄影原始成果后的第一件事就是转成3DTiles。转出来的产物长这样一个tileset.json入口文件描述整棵瓦片树的结构和坐标变换关系大量.b3dm瓦片文件里面装着带贴图的三角网格模型和属性信息可能还有.json格式的瓦片索引文件和tileset各级节点的包围盒信息。有了这套东西Web端不管用Cesium还是Three.js都能做到只加载眼前需要的数据而不是一把梭把几十GB塞进来。1.2 Three.js与Cesium两条技术路线的真实对比聊到在Web端加载3dtiles很多人第一反应是Cesium。Cesium确实对3dtiles支持最好毕竟标准就是它家推的加载倾斜摄影开箱即用还自带地球、坐标系、相机控制。那为什么还有一堆人要在Three.js里折腾这件事我接触过的项目通常是这样业务方要在三维场景里做数字孪生要把设备模型、管线、动效、粒子、图表都叠到倾斜摄影模型之上。Cesium的优势在GIS但它的渲染能力、材质系统、动画生态和Three.js比还是有差距。你想在Cesium里做一个高质量的水面反射、做一个复杂的粒子爆炸特效、或者接一套成熟的三维引擎后期处理管线开发成本明显更高。Three.js这边则是反过来渲染生态极其丰富什么后期、物理引擎、动画系统都有成熟方案但加载3dtiles需要自己接渲染内核。说白了偏GIS分析、卫星影像、地形、多源数据叠加选Cesium省心偏可视化表现、业务交互、复杂特效选Three.js 3dtiles插件灵活两边都要也可以做Cesium和Three.js的深度融合但那是另一个复杂度量级不建议新手碰。我这篇文章讲的是纯Three.js方案不依赖Cesium。你要做的是在Three.js里加载倾斜摄影底图然后在它上面自由发挥业务功能。1.3 适用场景判断先确认你适不适合走这条路先说结论如果项目核心需求是看地形、量距离、切剖面或者需要同时叠加大量WMS、WMTS影像服务建议直接用Cesium。如果项目核心需求是在倾斜摄影模型周边做精细交互、做自定义业务动画、做高品质渲染效果那Three.js值得投入。另外还有一类典型场景是内部分离数据预处理用Cesium或者其他GIS工具完成Web端展示纯用Three.js。这种方案的好处是前端团队可以完全掌控渲染管线遇到性能问题能自己定位到渲染层面去优化不会被GIS框架的黑盒机制卡住。我见过不少团队在两个方案之间反复横跳浪费了大量时间。建议把需求列成清单逐条打勾之后再做决定。别看着Cesium支持好就选Cesium也别看着Three.js渲染强就一股脑入先判断清楚自己到底要解决什么问题。2. 数据准备把倾斜摄影成果转成Three.js能吃下的3DTiles2.1 倾斜摄影原始数据形态与特点做航测的人对ContextCapture原Smart3D肯定不陌生国内还有大疆智图、重建大师等软件都能产出倾斜摄影模型。这些软件导出的原始成果一般是OSGB目录结构一个工程文件夹里包含Data目录里面是Tile_00_00这种命名的瓦片目录层级目录里放.osgb文件metadata.xml记录坐标系、原点、瓦片范围等信息部分软件还会产出S3C索引文件Cesium读取这种格式可以快速预览。OSGB的本质是二进制格式的模型瓦片里面是带LOD的树状结构每个瓦片包含多个层级的模型用以支持远近切换。几十GB的数据通常就是几万个这样的瓦片文件堆出来的。这种原始成果有几个特点直接决定了后续处理方式瓦片之间有重叠和接边关系不能简单地把单个osgb文件当独立模型用坐标通常是项目本地坐标系或者CGCS2000等地理坐标数值范围很大纹理贴图打包在二进制里不便于直接编辑。所以直接把OSGB丢给Three.js是不行的。Three.js的GLTF/OBJ加载器只认单个模型文件处理不了这种分块、分层、带空间索引的数据组织。这也是为什么我们一定要先把原始成果转换成3DTiles。2.2 转换工具链选型CesiumLab、osgb23dtiles与命令行方案3DTiles转换工具有不少我用过的几款简单说下各自的适用情况。CesiumLab是目前国内用得最多的转换工具操作界面友好支持OSGB转3DTiles、SHP转3DTiles、FBX转3DTiles、点云转3DTiles等一整套流程。对非开发人员非常友好基本就是拖进去、选参数、点开始。我早期做项目验证时经常用它优点是快缺点是版本更新时性能有波动某些场景转换出来的数据量偏大。osgb23dtiles是一个开源命令行工具GitHub上能找到优点是免费、跨平台、支持放大倍率等参数调节。转换速度快适合批量处理。但命令行工具有个通病参数需要自己摸索文档不够细新手容易踩坑。超图iServer和GeoServer等GIS平台的转换组件则适合已有GIS平台在跑的团队可以走服务端流程化发布适合生产环境。我个人建议的评估标准就三条转换后数据体积膨胀率是否可接受是否支持你的输入坐标系和投影参数转换速度和批处理能力是否满足项目周期。比如你用CesiumLab把某个区域的OSGB转成3DTiles打开tileset.json看瓦片数量和层级深度再对比原始数据量基本就能判断出转换质量。如果瓦片数爆炸或者LOD层级过深后续加载性能一定会出问题需要调转换参数。2.3 延伸需求shp转3dtiles、fbx转3dtiles分别解决什么问题实际项目里除了倾斜摄影模型本身还经常遇到两类转换需求。shp转3dtiles核心场景是城市级建筑白模。很多智慧城市项目没有精细的BIM模型只有测绘部门的矢量轮廓数据也就是shp面图层每栋楼的底部轮廓带楼层数属性。这类数据转换成3dtiles后可以看到一排排按轮廓拉伸出来的建筑体块叠加在倾斜摄影场景里用来表达规划方案或者缺少倾斜模型的区域。工具上CesiumLab就有SHP转3dtiles功能可以直接设置拉伸高度字段和底部高程。要注意的是拉伸出来的模型只有型体没有贴图视觉效果比较素需要配合材质方案使用。fbx转3dtiles解决的是设备级模型在三维场景中的批量复用问题。数字孪生项目里经常要把风机、阀门、管道、车辆这类精细模型摆到倾斜摄影场景的正确位置。FBX是建模软件通用的交换格式转成3dtiles后可以参与瓦片调度适合几千个设备模型逐个摆放大批量加载的场景。单个设备模型如果不多其实直接在Three.js里加载GLB效率更高没必要转3DTiles这个要区分清楚。3. 核心实现Three.js加载3DTiles的完整落地步骤3.1 渲染内核选型3d-tiles-renderer还是loaders.gl在Three.js里加载3dtiles目前最靠谱的路子是用社区渲染库而不是自己从零解析格式。主流的两个选择是3d-tiles-renderer和loaders.gl。3d-tiles-renderer是目前Three.js社区里最成熟的3dtiles加载库由NASA-AMMOS团队维护内部实现了一套完整的瓦片调度和LOD管理逻辑。它把瓦片渲染封装成一个group对象挂到Three.js场景里开发体验很接近原生Three.js。代码简单、上手快、和Three.js版本同步良好这是我推荐给大多数项目的原因。loaders.gl是Visgl生态的一部分提供更细粒度的3dtiles解析能力灵活度高但你需要自己处理调度、LOD、瓦片生命周期这些事代码量会大很多。适合有特殊需求、愿意自己掌控调度逻辑的团队。还有一条路是用math.gl/geospatial这类库辅助解析但对大多数场景来说直接选3d-tiles-renderer就够了。这篇文章的全部代码都基于它来实现。3.2 从零搭一个最小可运行示例先装依赖假设项目已经初始化为一个Vite环境npm install three 3d-tiles-renderer然后写一个最简单的入口import * as THREE from three; import { TilesRenderer } from 3d-tiles-renderer; const scene new THREE.Scene(); scene.background new THREE.Color(0x111122); const camera new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 0.1, 100000 ); camera.position.set(0, 2000, 2000); camera.lookAt(0, 0, 0); const renderer new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); document.body.appendChild(renderer.domElement); // 创建TilesRenderer并挂载到场景 const tilesRenderer new TilesRenderer(./data/tileset.json); tilesRenderer.setCamera(camera); tilesRenderer.setResolutionFromRenderer(camera, renderer); scene.add(tilesRenderer.group); // 加个轨道控制器方便观察 import { OrbitControls } from three/examples/jsm/controls/OrbitControls.js; const controls new OrbitControls(camera, renderer.domElement); controls.target.set(0, 0, 0); // 动画循环 function animate() { requestAnimationFrame(animate); tilesRenderer.update(); renderer.render(scene, camera); } animate(); // 窗口变化处理 window.addEventListener(resize, () { camera.aspect window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); });这段代码核心只有三行new TilesRenderer生成瓦片渲染器setCamera把相机关联进去update()在每帧驱动瓦片调度。如果数据是标准的经纬度或者地心坐标系瓦片而且场景里没有叠加其他地理参照数据跑起来基本就能看到模型了。但你大概率不会这么顺利因为坐标系的问题马上就会找上来。3.3 坐标系处理整个流程里最大的翻车点倾斜摄影3dtiles数据里tileset.json有一个transform字段它是一个4x4的矩阵记录了瓦片从局部坐标系到大地坐标系的变换关系。如果转换工具输出的是地心坐标系那tileset.json里的矩阵会把瓦片定位到地球曲面上的正确位置。问题在于Three.js的场景坐标系和地心坐标系差距巨大。地心坐标的原点在地球中心坐标值动辄几百万米。把这些数值直接丢给Three.js的相机和矩阵运算浮点数精度会严重丢失结果就是模型抖动、穿模、甚至完全显示不出来。所以Three.js里加载3dtiles第一步总是统一坐标系。通常做法是把瓦片的坐标原点偏移到场景原点附近。具体实现上我惯用的方式是读取tileset.json的transform矩阵提取出平移分量用它构造一个偏移矩阵然后在加载后把偏移量补偿回去const tilesRenderer new TilesRenderer(./data/tileset.json); tilesRenderer.addEventListener(load-tileset, (e) { const tileset e.tileset; const transform tileset.root.transform; // 提取平移分量 const translation new THREE.Vector3( transform[12], transform[13], transform[14] ); // 把瓦片组整体平移到原点附近 tilesRenderer.group.position.copy(translation).multiplyScalar(-1); });这段逻辑相当于把整个倾斜摄影模型从原来的大坐标位置挪到场景原点附近避免浮点精度问题。如果你的瓦片本身是局部坐标系生成平移值很小那也可以不做这个操作。另一个常见做法在数据生产端解决转换3dtiles时直接设置可加载到本地原点或者压缩坐标参数有些工具支持把坐标平移到局部原点再输出。这样前端就不需要再做一次偏移。横跨坐标系的瓦片如果和地形高程叠加还会遇到高程基准不一致的问题。比如影像底图用的是EGM96大地水准面倾斜摄影用的可能是项目自定义的椭球面两者之间会有几米到几十米的高程差。这种问题靠前端很难完全消除最好在数据生产阶段就对高程基准做统一。3.4 相机、控制器与瓦片调度的联动3d-tiles-renderer的瓦片调度依赖当前相机的位置和视锥体。它内部会计算哪块瓦片可见、哪个层级应该被加载所以相机参数设置很重要。首先是PerspectiveCamera的near/far这两个值决定了深度裁剪范围。倾斜摄影模型动辄几公里范围如果far设小了远处的瓦片会被裁剪掉如果near设大了近处会穿模。我的起步建议值near 0.1到1far 相机到模型最远点的距离乘以3。其次OrbitControls的缩放距离限制也要和瓦片层级匹配。你把相机拉得太近需要加载最高精度的瓦片如果瓦片树最深层级不够精细画面就会糊拉得太远低层级瓦片如果被裁剪掉又会被背景色填满。比较隐蔽的一个坑是相机到了模型内部。倾斜摄影模型是实体的墙体和地面会遮挡视线相机钻进去会出现全是黑色的情况。OrbitControls默认不处理碰撞需要自己限制相机的俯仰角、距离范围或者做简单的包围盒碰撞检测。最后一个要注意的是setResolutionFromRenderer。这个方法的作用是告诉瓦片调度器当前渲染精度用于判断哪个层级瓦片足够清晰。如果忘了调用或者调用时机不对瓦片可能一直加载不到合适精度。4. 性能调优、单体化与踩坑实录4.1 LOD调度参数调整让加载更平滑3dtiles能跑起来只是第一步真正让人头疼的是加载过程。倾斜摄影数据几十GB瓦片成千上万调度策略一旦不合适表现就是两种极端要么loading半天不出东西要么瓦片疯狂闪跳切换层级时模型忽隐忽现。3d-tiles-renderer本身有一些可调属性我实际调过之后最有效的几个errorTarget控制瓦片误差阈值值越小越倾向加载高精度瓦片画面更清晰但调度压力更大maxDepth限制瓦片树最大加载深度适合数据层级过多、越深层越卡的情况preferFrontToBack控制瓦片加载顺序设置为true时优先加载视野前方的瓦片感知上会更快出现内容maxTilesSelected和maxTilesLoading限制同时选中和同时加载的瓦片数量防止单帧调度量爆炸。一个常用的安全配置组合是这样tilesRenderer.errorTarget 8; tilesRenderer.maxDepth 20; tilesRenderer.preferFrontToBack true; tilesRenderer.maxTilesLoading 64;这些参数没有一劳永逸的标准答案跟数据规模、瓦片大小、服务器带宽、目标帧率都有关系。我一般做法是先默认跑一遍用性能面板看瓦片加载数和帧率曲线再针对性地调。4.2 大场景内存、显存与加载策略优化倾斜摄影模型最怕的就是全量加载。一个2GB的数据如果所有瓦片都被加载进显存普通显卡直接爆掉。所以优化的核心思路是按需加载 及时释放。3d-tiles-renderer内部有瓦片缓存管理机制超出缓存限制的瓦片会被自动卸载但默认的缓存策略不一定是为你这个场景定制的。我测试下来有两个方向调低LRU缓存上限减少显存占用代价是反复旋转视角时会重新加载瓦片出现白模等待调高缓存上限减少重复加载但显存占用会明显上升。这里没有一个完美答案取决于你的部署环境。如果是用户浏览器访问要保守一点如果是公司内网工作站显存够大可以放宽。另一个容易被忽略的点是纹理压缩。倾斜摄影的纹理很多是JPG挤在二进制里尺寸和格式参差不齐。加载时可以用renderer.capabilities判断环境支持情况考虑在服务端对纹理做格式转换比如压缩为KTX2或者使用纹理图集能显著降低显存带宽压力。如果页面里除了倾斜摄影还有大量业务模型、标注标签、特效粒子记得要给倾斜摄影单独分层管理比如把倾斜摄影瓦片放在一个单独的Group里业务动态内容放在另一个Group这样切换业务功能时可以单独控制显隐不用全场景重绘。4.3 常见问题速查表从黑屏到闪跳的解决实录下面这几类问题是群里和评论区被问得最多的我把排查路径整理成了一张表方便直接对照。现象常见原因排查与解决瓦片加载了但什么都不显示坐标偏移过大模型在视野外检查tileset.json的transform做坐标原点偏移模型闪烁、抖动相机near/far设置不当浮点精度丢失调整相机裁剪面确保场景坐标在原点附近加载到一半白模LOD高层级瓦片尚未加载完或加载失败检查瓦片文件是否齐全、请求是否404调高errorTarget旋转视角时疯狂加载刷新LRU缓存过小或preferFrontToBack未开启增大缓存、开启前置优先加载画面很暗或者贴图发黑纹理格式不支持或光照设置错误检查renderer.outputEncoding设置确认材质是否受光照影响锯齿严重抗锯齿级别不够或像素比过低设置renderer.setPixelRatio(Math.min(devicePixelRatio, 2))有一个很容易忽略的问题部分转换工具生成的3dtiles文件名包含中文字符或者特殊字符部署到Nginx后静态资源请求会404导致瓦片加载不出来浏览器控制台一片红。这个排查起来很伤神我的建议是转换后统一校验文件命名全部改成英文字符。还有个小细节tileset.json内部会有相对路径引用瓦片文件如果你部署时改了目录层级路径就失效了。所以前端项目里最好把3dtiles数据放在单独静态目录不要和代码混合打包。4.4 单体化交互热词背后的真实需求关于搜索引擎里经常出现的cesium 3dtiles 单体化相关需求很多人其实是想在Three.js里实现类似效果鼠标点选一栋楼整栋楼高亮并弹出该楼的属性信息。3dtiles单体化的本质是让瓦片里的每个模型对象能被单独选中。实现思路两种第一种是数据端单体化。在转换前就把每栋楼的模型拆分成独立的瓦片节点或节点组每个节点携带属性信息。前端在点击时通过射线检测命中的节点读取节点属性并高亮处理。这种方式交互最好但对数据生产要求高。第二种是前端属性叠加。倾斜摄影瓦片本身不带建筑边界但你可以用shp转出的建筑轮廓数据在Three.js里生成对应的边界多边形挂在场景里做隐形碰撞体。射线检测命中的是这些多边形命中的多边形对应一栋楼再通过轮廓数据拿到建筑属性最后通过控制特定瓦片的材质来高亮。这个方案适合已有shp轮廓数据、不想重转倾斜摄影的项目。我在项目中实测下来第二种方案实现成本低对现有数据流程改动小。核心逻辑是把建筑轮廓shp转成GeoJSON再解析成Three.js的多边形Mesh设置visible false作为不可见的拾取层射线检测时优先检测这层不可见Mesh命中后读取userData里的属性信息同时搜索附近瓦片节点做高亮。这样能在纯Three.js环境里做到类似Cesium的点击拾取效果。具体高亮可以用OutlinePass或者颜色替换看你的项目风格。最后说几句实在话这套方案我已经先后在智慧园区、风电场数字化、城市更新等好几个项目里落地过一些体会挺深刻。开始用Three.js加载3dtiles时总会遇到为什么Cesium里好好的到Three.js里就出问题的困惑。后来想明白了一个道理Cesium把很多步骤封装好了坐标、相机、地形、影像服务全都替你做了Three.js则需要你把每一步都自己串起来。这不是Three.js的缺陷而是灵活性的代价。如果你手头刚拿到倾斜摄影数据建议别一上来就写代码。先用CesiumLab把数据转好用Cesium先预览确认数据没问题再切到Three.js环境去接。这样能把数据问题和代码问题隔离开排查起来省很多时间。实际操作中我的经验是先保证最小路径跑通再做性能优化最后再上业务功能。一上来就想要单体化、粒子特效、光照后期都没有意义因为倾斜摄影数据的加载本身就是一个需要慢慢调的过程。等瓦片调度稳定了帧率稳住了再往上叠东西你的踩坑成本会低很多。后续如果你还想扩展可以往这几个方向深入场景中同时叠加BIM模型和倾斜摄影的配准方案、用Viewer插件实现倾斜摄影模型的分屏对比、以及把加载过程封装成React/Vue组件供团队复用。这些都是这套基础方案之上很自然的延伸。