拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Cesium与Three.js融合实战:BIM模型与GIS场景无缝集成

1. 为什么要把 Cesium 和 Three.js 放在同一个场景里1.1 两个引擎各自的强项与短板做过三维 GIS 项目的人多半有过这种纠结项目里既要展示大范围地形、影像、倾斜摄影、3D Tiles 这类地理数据又要在场景里塞进精细的 BIM 模型、工业设备、动画特效、自定义材质。单用 Cesium地理坐标系、LOD 调度、全球地形这些它天生就强但一旦涉及高度定制的渲染效果、复杂的自定义着色器、非地理坐标的局部精细场景写起来就束手束脚。单用 Three.js渲染自由度高、生态丰富、社区示例多可它没有地理坐标概念没有瓦片调度加载个几平方公里的倾斜摄影就能把内存吃满。所以融合的核心动机很直接让 Cesium 管地球和地理数据调度让 Three.js 管局部精细渲染和特效。这不是为了炫技而是被项目需求逼出来的。我接过一个园区数字孪生的活场景里既有几十平方公里的地形和影像底图又有一栋 BIM 小别墅需要做到构件级展示还要在楼顶叠加动态光照和流动线效果。纯 Cesium 做 BIM 构件级交互很别扭纯 Three.js 又扛不住大范围地理数据最后只能走融合路线。1.2 融合的三种主流思路对比在动手之前得先想清楚用哪种融合方式。市面上常见的做法有三种各有取舍。融合方式实现原理优点缺点适用场景双 Canvas 叠加Cesium 一个 canvasThree.js 一个 canvasCSS 叠在一起实现简单互不干扰两个相机要手动同步深度关系难处理性能开销大快速验证、特效层与底图分离Cesium 作为 Three.js 的贴图Three.js 渲染地球Cesium 离屏渲染当纹理渲染统一地理调度能力被削弱等于放弃 Cesium 优势不推荐Three.js 挂到 Cesium 场景把 Three.js 的相机和 Cesium 相机同步Three.js 的物体通过自定义 Primitive 注入 Cesium 渲染循环共享深度、共享相机、性能好需要理解 Cesium 的 Primitive 机制生产环境首选我实测下来第三种是最稳的。它的关键点在于Cesium 的Scene允许你插入自定义的Primitive而 Three.js 的WebGLRenderer可以复用 Cesium 已经创建好的WebGLContext。这样两个引擎共用一套 WebGL 上下文深度缓冲也是共享的BIM 模型和地形之间就不会出现谁盖住谁的穿帮问题。1.3 坐标系对齐是融合的第一道坎融合最容易翻车的地方不是渲染而是坐标。Cesium 用的是 WGS84 地理坐标系加 ECEF 地心直角坐标系Three.js 用的是普通的右手笛卡尔坐标系。你要把一栋 BIM 小别墅放到园区某个经纬度上就必须做坐标转换。我的做法是以 BIM 模型所在位置的一个基准点比如别墅的西南角作为局部坐标系原点用 Cesium 的Transforms.eastNorthUpToFixedFrame求出该点的东-北-天ENU局部坐标系到 ECEF 的转换矩阵。Three.js 场景里所有物体的坐标都基于这个局部原点最后统一乘上这个矩阵就落到地球正确位置了。// 基准点别墅西南角的经纬度和高程 const origin Cesium.Cartesian3.fromDegrees(116.397, 39.908, 50); // 求出 ENU 到 ECEF 的转换矩阵 const enuToEcef Cesium.Transforms.eastNorthUpToFixedFrame(origin); // Three.js 里的局部坐标 (x, y, z) 转成 Cesium 世界坐标 function localToWorld(x, y, z) { const local new Cesium.Cartesian3(x, y, z); return Cesium.Matrix4.multiplyByPoint(enuToEcef, local, new Cesium.Cartesian3()); }注意ENU 坐标系里X 轴指向东Y 轴指向北Z 轴指向天。而 Three.js 默认 Y 轴朝上。所以从建模软件导出的模型往往需要先绕 X 轴旋转 -90 度把 Z-up 转成 Y-up否则模型会躺在地上。2. 环境搭建与核心依赖选型2.1 Cesium 的引入方式与版本选择Cesium 的引入有几种方式直接 script 标签引 CDN、npm 安装、或者用官方推荐的 Vite 插件。做融合项目我强烈建议用 npm 加构建工具因为你要改 Cesium 的源码行为、要注入自定义 PrimitiveCDN 那种黑盒方式根本没法调试。npm install cesium three版本上Cesium 建议用 1.1xx 之后的版本这些版本对CustomPrimitive和Scene的扩展支持比较完善。Three.js 用 r150 以上注意 r155 之后默认颜色管理变了如果 BIM 模型颜色发灰多半是色彩空间没配对。import * as Cesium from cesium; import * as THREE from three;有个坑要提前说Cesium 默认会去访问它的在线资源服务加载默认影像和地形。如果项目在内网或者网络受限环境一定要提前把Ion.defaultAccessToken处理掉并且显式指定离线影像和地形否则场景会一直转圈或者报资源访问失败。2.2 复用 WebGL 上下文的关键代码融合的核心就一段代码让 Three.js 用 Cesium 的 canvas 和 context。const viewer new Cesium.Viewer(cesiumContainer, { // 关掉不需要的控件减少干扰 animation: false, timeline: false, baseLayerPicker: false, geocoder: false, homeButton: false, sceneModePicker: false, navigationHelpButton: false, }); // 拿到 Cesium 的 canvas 和 WebGL 上下文 const canvas viewer.canvas; const gl canvas.getContext(webgl2) || canvas.getContext(webgl); // Three.js 复用这个上下文 const renderer new THREE.WebGLRenderer({ canvas: canvas, context: gl, }); renderer.autoClear false; // 关键不要让 Three.js 清屏否则 Cesium 的画面会被擦掉autoClear false这行是重中之重。Cesium 每帧先渲染地球和地理数据然后轮到 Three.js 渲染 BIM 和特效。如果 Three.js 清屏Cesium 渲染的东西就全没了。我第一次做的时候忘了这行画面一片黑排查了半天。2.3 相机同步的实现细节两个引擎要看到同一个世界相机必须同步。Cesium 的相机是Camera对象Three.js 是PerspectiveCamera。同步的核心是把 Cesium 相机的视图矩阵和投影矩阵搬到 Three.js。function syncCamera() { // Cesium 相机的视图矩阵世界坐标到相机坐标 const viewMatrix viewer.camera.viewMatrix; // Cesium 相机的投影矩阵 const projectionMatrix viewer.camera.frustum.projectionMatrix; // 转成 Three.js 的矩阵格式 const threeView new THREE.Matrix4().fromArray( Cesium.Matrix4.toArray(viewMatrix) ); const threeProjection new THREE.Matrix4().fromArray( Cesium.Matrix4.toArray(projectionMatrix) ); // 应用到 Three.js 相机 threeCamera.matrixWorldInverse.copy(threeView); threeCamera.projectionMatrix.copy(threeProjection); threeCamera.matrixWorld.copy(threeView).invert(); }这里有个细节Cesium 的矩阵是列主序Three.js 的fromArray默认也是列主序所以能直接对应。但如果你发现 BIM 模型位置偏了或者朝向不对八成是矩阵转置的问题可以试着加.transpose()验证。3. BIM 模型从建模到进场景的完整链路3.1 BIM 模型格式选择与导出BIM 模型常见的格式有 IFC、RVT、SKP 等。Cesium 和 Three.js 都不能直接吃这些格式必须转换。我的经验是BIM 模型先转成 glTF/GLB再进场景。glTF 是 WebGL 生态的通用格式Three.js 原生支持Cesium 也能通过 3D Tiles 或者直接加载。转换链路一般是Revit 导出 IFC用 IfcOpenShell 或者 Blender 的 BIM 插件处理再导出 GLB。如果是 SketchUp 的模型比如那个 BIM 小别墅文件可以直接在 SketchUp 里导出 GLB但要注意单位——SketchUp 默认是英寸或者毫米导出时一定要统一成米否则模型进场景会大得离谱或者小得看不见。实操心得导出 GLB 时勾选应用修改器和导出材质但不要勾选压缩压缩后的模型在 Three.js 里加载容易丢材质。模型面数控制在 50 万三角面以内超过这个量级即使有 LOD浏览器也会卡。3.2 模型轻量化与 LOD 处理BIM 模型最大的问题是面数爆炸。一栋小别墅如果每个螺栓、每根钢筋都建出来轻松上百万面。进场景之前必须做轻量化。轻量化有三个层次一是删掉看不见的内部构件二是对重复构件比如窗户、栏杆做实例化三是生成多级 LOD。Three.js 里可以用THREE.LOD对象管理多级模型根据相机距离切换。const lod new THREE.LOD(); lod.addLevel(highDetailModel, 0); // 0-50米用高模 lod.addLevel(midDetailModel, 50); // 50-200米用中模 lod.addLevel(lowDetailModel, 200); // 200米外用低模 scene.add(lod);Cesium 那边如果模型要参与地理调度建议转成 3D Tiles。3D Tiles 自带 LOD 和空间索引加载大场景时性能优势明显。转换工具可以用 Cesium 官方的 3D Tiles 工具链把 GLB 切成瓦片。3.3 模型节点与构件级交互BIM 的价值在于构件级信息。用户点一扇门要能弹出这扇门的型号、材质、供应商。这就要求模型在导出时保留节点结构进场景后能通过射线拾取定位到具体构件。Three.js 的Raycaster可以做拾取但前提是模型的每个构件是独立的 Mesh而不是合并成一个大 Mesh。导出 GLB 时如果勾选了合并网格构件信息就丢了。所以导出时不要合并网格让每个构件保持独立。const raycaster new THREE.Raycaster(); const mouse new THREE.Vector2(); canvas.addEventListener(click, (event) { const rect canvas.getBoundingClientRect(); mouse.x ((event.clientX - rect.left) / rect.width) * 2 - 1; mouse.y -((event.clientY - rect.top) / rect.height) * 2 1; raycaster.setFromCamera(mouse, threeCamera); const intersects raycaster.intersectObjects(bimModel.children, true); if (intersects.length 0) { const hit intersects[0].object; // hit.name 就是构件名可以关联 BIM 属性数据 showComponentInfo(hit.name); } });这里有个坑Cesium 的 canvas 上还叠着 Cesium 自己的拾取逻辑。如果点击事件同时被 Cesium 和 Three.js 处理可能会冲突。我的做法是给 Three.js 的拾取加一个判断——先判断点击位置有没有 Three.js 物体有就拦截事件没有就交给 Cesium。4. GIS 数据加载与场景调度实战4.1 地形与影像底图的加载GIS 场景的地基是地形和影像。Cesium 加载地形用CesiumTerrainProvider影像用ImageryLayer。如果项目在内网地形和影像都要离线部署。// 离线地形 const terrain await Cesium.CesiumTerrainProvider.fromUrl(/terrain/tileset.json); viewer.terrainProvider terrain; // 离线影像 const imagery new Cesium.UrlTemplateImageryProvider({ url: /imagery/{z}/{x}/{y}.png, }); viewer.imageryLayers.addImageryProvider(imagery);地形数据常见的是 DEMCesium 需要的是 quantized-mesh 格式。如果你手头是 GeoTIFF 的 DEM得先用工具切成瓦片。影像数据如果是 MVT 格式矢量瓦片Cesium 也能加载但需要额外的 MVT 解析支持。注意DEM 分割和投影转换是 GIS 数据处理的常见需求。DEM 按经纬度分块时注意边界要重叠一个像素否则拼接处会有裂缝。投影转换时CAD 的 6 位坐标比如 500000, 4000000通常是高斯投影坐标转经纬度要用对应的带号和中央经线转错了位置会偏几百米。4.2 3D Tiles 与倾斜摄影的加载倾斜摄影是三维 GIS 的重头戏。Cesium 加载倾斜摄影用 3D Tiles核心是Cesium3DTileset。const tileset await Cesium.Cesium3DTileset.fromUrl(/photogrammetry/tileset.json, { maximumScreenSpaceError: 16, // 数值越小越清晰但性能开销越大 maximumNumberOfLoadedTiles: 1000, }); viewer.scene.primitives.add(tileset);maximumScreenSpaceError这个参数很关键。默认是 16调小到 8 画面更精细但加载的瓦片数量翻倍调大到 32 性能好但远处会糊。我一般根据项目硬件配置在 8 到 24 之间调。倾斜摄影的单体化是个老大难问题。所谓单体化就是让倾斜摄影里的每栋楼能单独选中、单独上色。Cesium 支持 3D Tiles 单体化但需要数据生产阶段就做好分层分户或者用裁剪面加动态贴图模拟。如果数据没做单体化后期硬做效果很差。4.3 动态光照与仿真效果Cesium 自带动态光照通过viewer.scene.globe.enableLighting true开启太阳位置会根据时间变化。但要做更精细的仿真比如模拟一天中建筑阴影的变化就需要结合 Three.js 的光照系统。我的做法是Cesium 管全局光照太阳、天空Three.js 管局部光照建筑内部的灯光、设备发光。两套光照系统通过共享相机和深度缓冲协调。Three.js 这边用DirectionalLight模拟太阳方向从 Cesium 的太阳位置算出来。// 从 Cesium 获取太阳方向 const sunPosition Cesium.Simon1994PlanetaryPositions.computeSunPositionInEarthInertialFrame( viewer.clock.currentTime ); // 转成 Three.js 的平行光方向 const sunDir new THREE.Vector3(sunPosition.x, sunPosition.y, sunPosition.z).normalize(); directionalLight.position.copy(sunDir.multiplyScalar(1000));流动线、雷达扫描、悬浮岛这些特效用 Three.js 的着色器做最灵活。比如箭头流动线本质是一条带纹理的线纹理 UV 随时间偏移就产生了流动感。5. 性能优化与常见问题排查5.1 帧率上不去的排查思路融合场景性能问题八成出在三个地方绘制调用太多、瓦片加载太频繁、内存泄漏。先看绘制调用。打开浏览器的性能面板看每帧的 draw call 数量。超过 1000 就要警惕了。BIM 模型如果每个构件一个 draw call一栋楼就能上千。解决办法是合批——把相同材质的构件合并成一个 Mesh。Three.js 的BufferGeometryUtils.mergeGeometries可以做这件事但合并后构件级拾取就没了需要额外维护一个映射表。再看瓦片加载。Cesium 的maximumScreenSpaceError调太小或者相机移动太快会导致瓦片疯狂加载。可以限制maximumNumberOfLoadedTiles并开启preloadWhenHidden控制预加载。内存泄漏常见于反复创建销毁对象。Three.js 的几何体、材质、纹理用完要手动dispose()Cesium 的 Primitive 移除后也要destroy()。我见过一个项目跑两小时就崩最后查出来是每次点击都 new 一个材质从不释放。5.2 常见问题速查表问题现象可能原因排查方向解决方案画面全黑Three.js 清了屏检查 autoClear设 renderer.autoClear falseBIM 模型位置偏移坐标系没对齐检查 ENU 矩阵用 eastNorthUpToFixedFrame 重新计算模型躺在地上Z-up 没转 Y-up检查模型旋转绕 X 轴旋转 -90 度模型颜色发灰色彩空间不匹配检查 outputColorSpace设 THREE.SRGBColorSpace拾取不到构件网格被合并检查导出设置导出时不合并网格帧率低于 30draw call 过多看性能面板合批或做 LOD瓦片加载卡顿SSE 太小检查 maximumScreenSpaceError调到 16-24内存持续增长资源没释放看内存曲线手动 dispose 和 destroy地形有裂缝DEM 边界没重叠检查瓦片切分边界重叠一个像素坐标偏几百米投影带号错误检查中央经线用正确的带号转换5.3 几个我踩过的坑第一个坑是 Cesium 的默认旋转地球效果。项目启动时地球会转一下如果此时 Three.js 的物体已经加进去了会跟着一起转位置全乱。解决办法是在viewer.scene.preRender里做相机同步确保第一帧就对齐。第二个坑是 Cesium 模型节点命名。从 Revit 导出的构件名可能带特殊字符或者中文Three.js 加载后object.name可能被转义。做构件映射时最好用 ID 而不是名字。第三个坑是动态光照的性能。开启enableLighting后地形阴影计算很吃性能。如果项目对帧率敏感可以只在需要的时候开或者用烘焙好的阴影贴图代替实时计算。第四个坑是移动监控摄像头带 GIS 信息这种需求。摄像头位置是经纬度视野是个锥体。在 Cesium 里画锥体用Cesium.ConeGraphics但要让它朝向正确得算方向向量。我一开始直接用 heading/pitch/roll结果摄像头朝向总是差一点后来改成用两个点摄像头位置和目标点算方向才准。6. 从 Demo 到生产工程化的一些建议6.1 模块划分与代码组织融合项目代码容易乱因为两套引擎的 API 混在一起。我的组织方式是按职责分模块cesium/目录放 Cesium 相关地形、影像、瓦片three/目录放 Three.js 相关BIM、特效、光照bridge/目录放融合逻辑相机同步、坐标转换、拾取协调。这样改哪块找哪块不会牵一发动全身。坐标转换的工具函数单独抽出来因为到处都要用。我封装了一个CoordBridge类提供localToWorld、worldToLocal、lngLatToLocal等方法所有模块统一调用。6.2 资源加载与进度管理大场景资源多加载要有进度反馈。Cesium 的瓦片加载有tileLoadProgress事件Three.js 的 GLTF 加载有onProgress回调。把两者的进度加权合并给用户一个总进度条。资源加载顺序也有讲究。先加载地形和影像地基再加载 3D Tiles中景最后加载 BIM 和特效近景。这样用户先看到大场景轮廓再逐步看到细节体验比一次性全加载好。6.3 后续可扩展的方向这套融合框架搭好后能扩展的方向很多。比如接入实时数据做动态模拟——传感器数据驱动 BIM 构件变色或者车辆 GPS 数据驱动场景里的车模型移动。再比如做剖面分析用 Cesium 的裁剪面切地形用 Three.js 的裁剪面切 BIM两者同步就能看到地下管线和地质的对应关系。还有个方向是 WebGPU。Cesium 和 Three.js 都在往 WebGPU 迁移等两边都稳定支持后融合场景的性能还能再上一个台阶。不过现阶段 WebGPU 的兼容性还不够生产项目还是老老实实用 WebGL。我在实际项目里最大的体会是融合不是目的解决问题才是。如果项目只需要展示地理数据别硬塞 Three.js如果只需要展示 BIM也别硬上 Cesium。只有当项目同时有大范围地理场景和局部精细渲染需求时融合才是划算的。而且融合的复杂度不低团队里最好有人懂 GIS有人懂图形渲染否则遇到坐标问题或者性能问题会卡很久。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门