Cesium海量树木渲染优化:3D Tiles实例化解决万棵树卡顿

发布时间:2026/9/1 5:11:12
Cesium海量树木渲染优化:3D Tiles实例化解决万棵树卡顿 简介面向Cesium三维地图开发者与WebGIS学习者这份“Cesium中添加大量树-其它树”资源包重点解决在Cesium中快速布置大规模树木模型的问题。资源以3D Tiles/glTF思路组织多级LOD树模型从模型制作、纹理贴图到瓦片化加载均有对应文件覆盖2、4、8、16、32直至1024等不同细节层级便于理解细节层次切换与优化渲染性能。压缩包共53个文件包含10个obj模型、10个mtl材质、10个gltf、10个bin二进制、10个png纹理、2个jpg贴图及1个json元数据整体仅1.33MB轻量且结构清晰。已有140人学习下载适合需要借助现成模型快速实验Cesium大场景树木渲染、学习LOD分层加载与模型轻量化处理的开发者。 今年接了个智慧林业的案子要把规划出来的两万棵“其它树”放进 Cesium 三维场景里和地形、河网、道路一起交给业主审查。最开始我觉得这不是什么大事一棵一棵加 Entity 呗结果树到五百棵就开始卡两千棵直接变成 PPT看着业主的离线地形一块一块加载我心里是绝望的。折腾了大半个月最后用 3D Tiles 实例化方案把五千棵试点树扛了下来普通笔记本稳定在 50 帧左右。这篇就把我在 Cesium 里添加大量树的完整思路写出来尤其是这些不负责当主角、却量大管饱的“其它树”到底该怎么处理。1. 一棵树是实体一万棵树是性能事故1.1 Entity 种树的显性效果和隐性成本很多新手会先试这段代码const treeEntity viewer.entities.add({ position: Cesium.Cartesian3.fromDegrees(116.39, 39.9, 10), model: { uri: /models/tree.glb } });单看一棵树没问题模型加载、姿态调整、点击拾取都很方便。但 Entity 的背后是独立模型、独立渲染状态、独立变换矩阵。Cesium 要到绘制阶段才会把这些 Entity 归并可一旦树的数量上千每个 glm 模型又由几十个 mesh 组成场景图里的节点数量就会爆炸CPU 每个渲染帧要做海量状态切换和矩阵上传。这就好比一个人带十个小孩过马路交警还能协调可一万个人排方阵过马路交警手里的哨子吹冒烟也忙不过来。GPU 不是吃不下那一万棵树而是 CPU 根本没能力把每一棵树的“入场券”高效地递到 GPU 手里。1.2 从 60FPS 到 8FPS我的实测路程我在同一台项目电脑上做过一组粗测树模型都是同一棵低模桦树glb 大约 1.2MB顶点数不高。结果如下实现方式树数量帧率现象Entity glb10058能流畅漫游Entity glb80023拖动视角有粘滞感Entity glb30008基本不可用BillboardCollection1000050远景效果可接受3D Tiles 实例化1000055近景模型也能稳住这个结果说明问题不在显卡顶点吞吐而在 draw call。Entity 每棵树是一次甚至几十次 draw call3000 棵树的 CPU 开销已经让主线程喘不过气。BillboardCollection 和 3D Tiles 实例化都能把大量重复实例合并成少量 draw call所以数量级能直接拉上去。1.3 瓶颈不是显卡是 Draw Call 和场景图我见过不少同学第一反应是换显卡、加显存但针对大量重复树真正卡的地方有两个Draw Call 过多WebGL 是状态机每次切换材质、纹理、模型都要重新绑定CPU 需要把所有状态提交给 GPU。几千棵树就是几万个 draw call帧率自然崩。场景图节点过多Cesium 的 Entity 会维护大量场景对象拾取、更新、遮挡剔除都要遍历这些对象数量上万后主线程时间被吃掉大半。搞清楚这一点后续选型就很清晰要么把重复几何体合成一个 draw call比如实例化要么干脆把树降级成广告牌只保留一个四边形面片。上万棵树想要流畅必须走这两条路。2. 技术路线怎么选3D Tiles、实例化 glTF、还是公告板2.1 公告板做远景 LOD性价比最高如果你要种的树在几百米外看用户根本分不清树冠是模型还是贴图那 BillboardCollection 是首选。它本身是 Primitive底层对大量公告板做了批量合并我用一万棵树的点位测过帧率依然很稳。示例很单纯const billboards viewer.scene.primitives.add(new Cesium.BillboardCollection()); treePoints.forEach((p) { billboards.add({ position: Cesium.Cartesian3.fromDegrees(p.lng, p.lat, p.height), image: /textures/tree_canopy.png, scale: p.scale, rotation: p.rotation }); });但公告板有个致命弱点视角一旦压低树就像纸片一样贴在地上。所以我的建议是只把公告板用在中远景 LOD等到视角靠近了再切成真正的 3D 模型层。2.2 实例化 glTF 的复制逻辑实例化不是把模型复制成一万个文件而是只保留一份网格数据每棵树只存一份“变换矩阵”位置、绕 Z 轴旋转、缩放。渲染时 GPU 拿着同一份网格套上不同矩阵画一万次。扩展名通常叫EXT_mesh_gpu_instancing业内也叫 GPU Instancing。这类文件加载到 Cesium 后每棵树仍然能被当成独立的 feature 拾取因为实例 ID 会保留在属性里。对“其它树”这种重复度极高的对象实例化是所有方案里几何利用率最高的。要注意的是不是所有浏览器和显卡都支持该扩展项目里最好做一下能力检测。2.3 3D Tiles 是量大管饱的正路如果树的规模超过几千棵而且还要和地形一起考虑 LOD、遮挡剔除我建议直接把它做成 3D Tiles。Cesium 对 3D Tiles 的优化是最深层的视锥剔除、屏幕空间误差、模型 LOD、样式控制、拾取全都原生支持。从数据准备角度看你只需要准备两样东西点位数据包含每棵树的经纬度、地表高度、树种类型、缩放、旋转角树模型文件最好是 glb/glTF单位统一成米Z 轴朝上。然后用程序把这些点位和模型烘焙成带实例化信息的 tile最终输出一个 tileset.json 和瓦片文件放到本地静态目录或服务器里。Cesium 加载时只需要一行代码const tileset new Cesium.Cesium3DTileset({ url: /data/othertrees/tileset.json }); viewer.scene.primitives.add(tileset);2.4 “其它树”多树种的组合处理标题里特别写了“其它树”我理解成项目资源里那批不强调单棵个性、但种类繁多的普通生态树槐树、银杏、樱花、松树可能混在一起。这种场景千万别把所有树种硬塞进同一个模型网格。正确做法是按 treeType 字段分组同一种树共用一个模板网格不同树种的 glb 模型可以出现在同一个 3D Tiles 里用属性区分同一树种可以通过随机旋转、缩放、换颜色制造差异感。例如点位表里写treeType0代表银杏treeType1代表樱花。在生成瓦片时把对应模型实例化到对应点位上Cesium 端就能通过属性识别并控制样式后面要做季节变色、点击统计都很方便。3. 要把 5000 棵树铺到山上我推荐这么做3.1 数据源头点位表里有哪些字段先说数据这是整个链路里最不能省的一步。我拿到手的数据经常是 CAD 或 GIS 导出的点层字段五花八门。为了后续能直接转 3D Tiles建议你提前清洗成一张标准表x经度或投影坐标 Xy纬度或投影坐标 Yz地表高度可以为空后续采样补treeType树种编号scale缩放系数比如 0.8 到 1.5rotation绕 Z 轴旋转角度单位弧度或度customId唯一 ID方便拾取关联数据库如果点位是经纬度一定要确认坐标系是 WGS84如果是投影坐标转程前先统一转回 WGS84不然后面树的位置会偏出几公里。3.2 用离线工具把点云变成实例化瓦片这里我不建议在浏览器里实时生成最好在 Node 脚本或桌面工具里提前烘焙。基本流程是选定一个基准原点通常取数据中心的某个点把每棵树的 WGS84 经纬度转成相对原点的局部坐标读取树模型的 glb为每个点写入一个实例矩阵给每个实例写入 treeType、scale、rotation 等属性输出成 i3dm 或 glTF 实例化瓦片并生成 tileset.json。工具层面如果你已经有 Cesium ion可以直接把点云和模型传上去生成如果不方便传公有云本地可以用 gltf-transform 这类工具组装实例化 glTF再转 3D Tiles。我在项目里是写了一个 Python 脚本解析 CSV再用模型库生成瓦片全程离线运行这样换地形、换范围都很方便。3.3 在 Cesium 里加载并调整姿态瓦片准备好之后加载代码很简单但要记得设置基准坐标系const tileset new Cesium.Cesium3DTileset({ url: /data/othertrees/tileset.json, maximumScreenSpaceError: 8 }); viewer.scene.primitives.add(tileset); const center Cesium.Cartesian3.fromDegrees(116.39, 39.9, 0); tileset.modelMatrix Cesium.Transforms.eastNorthUpToFixedFrame(center); viewer.zoomTo(tileset);这里有一个关键点如果你把树的位置信息都烘焙进瓦片Cesium 端就不要再用矩阵去“二次挪动”每一棵树。遇到需要整体调整朝向的场景比如树要随着风向统一倾斜可以整体旋转tileset.modelMatrix但每棵树的姿态最好在数据生成阶段就定死浏览器里逐棵改矩阵的性能损耗非常大。3.4 点击拾取和树类型联动烘焙完实例化属性之后拾取仍然能用。Cesium 3D Tiles 的原生拾取会把命中的 tile feature 返回给你const handler new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas); handler.setInputAction((click) { const picked viewer.scene.pick(click.position); if (Cesium.defined(picked) picked instanceof Cesium.Cesium3DTileFeature) { const treeType picked.getProperty(treeType); const customId picked.getProperty(customId); console.log(选中树类型:, treeType, ID:, customId); } }, Cesium.ScreenSpaceEventType.LEFT_CLICK);有了这个能力你就能做很实用的交互点击一棵树弹出树种信息、统计各种树的数量、做图例联动高亮。这些功能在业务汇报时特别加分而且处理大量树时也不会有明显性能下降。4. 加完树之后这批坑我正在踩4.1 树怎么牢牢站在地形上树悬空是大量种植时最影响观感的问题。常规做法是先用Cesium.sampleTerrainMostDetailed把点位高度采样出来再进入瓦片生成流程。如果你用的是 1.terrain 这类离线地形数据同样支持采样前提是地形 provider 已经能正常返回高度。const positions treePoints.map((p) Cesium.Cartesian3.fromDegrees(p.lng, p.lat) ); const updatedPositions await Cesium.sampleTerrainMostDetailed( viewer.terrainProvider, positions );采样后拿到每个点的高度生成树底座时我会留一点“埋地余量”比如把树的 y 轴往下沉 0.2 米否则树根会轻微悬空旋转视角后特别明显。如果点特别密还可以给树底座加一个简单的圆片地台视觉上更自然。4.2 动态光照和阴影的取舍Cesium 场景开了动态光照后树的受光面会随太阳方向变化效果非常真实。但如果你顺手把阴影也全开了上万棵树会让帧率瞬间崩到个位数。我在项目里的折中方案是近景 500 米内保留真实阴影远景树用 3D Tiles 的样式把颜色稍微压暗模拟处在阴影里涉及航拍漫游时直接关掉树的阴影只留环境遮挡。如果你确实需要整片树林都有影子可以单独做一个低面数树冠阴影层用半透明黑色公告板模拟性能开销低得多。4.3 可视域分析时的树遮挡很多项目会同时做可视域分析比如从瞭望塔能看到哪些区域。树木模型如果参与射线检测分析结果会变成“大量树挡住视线”这往往不是业务想要的。更合理的做法是把树木图层单独放进一个PrimitiveCollection做可视域分析时临时隐藏它分析完再显示。const treeCollection new Cesium.PrimitiveCollection(); viewer.scene.primitives.add(treeCollection); treeCollection.add(tileset); // 分析时 treeCollection.show false;这样避免了树对通视分析的干扰也保留了树木展示的完整性。类似思路也可以用在天际线分析、视域裁剪上。4.4 显存和请求量的平衡一万棵树如果每棵树都单独用一张贴图显存分分钟爆掉。我的经验是同一种树族尽量共用一张 1024 或 512 的树冠贴图模型分近、中、远三档每档的面数和贴图分辨率都不一样。3D Tiles 本身支持按屏幕空间误差切 LOD你可以在瓦片金字塔里放不同精度的树模型。同时把maximumScreenSpaceError从默认值往上调比如 8 到 16离得远的树就不会疯狂加载高模请求量能下降一个量级。前提是你接受远处的树稍显模糊但这在种树场景里几乎看不出来。5. 最后分享几个让树木更真实的细节5.1 树不要一个朝向很多批量种植报表里会给同一朝向导致整片树林像阅兵方阵。我在生成瓦片时会引入一个伪随机旋转角同一种树之间至少旋转 30 到 90 度这样树冠形状交错林缘线自然很多。注意随机种子要固定否则每次刷新树木位置全变了业务方会以为数据错乱。5.2 分近中远三档模型如果你只准备一个高模种到近处很精致但远看时瓦片加载压力会非常大。我更建议准备三档近景5000 面以内带完整枝干和树冠片中景1000 面简化枝干保留树冠轮廓远景100 面或纯公告板。在 3D Tiles 里把这三种模型分别放到不同层级的 tile 中Cesium 会根据相机距离自动切换。实测五万棵树的场景只要 LOD 层级合理帧率也能保持住。5.3 用 style 做季节/类型变色烘焙属性之后不用改数据就能让树在 Cesium 端变色。例如银杏树种类型的树在秋季变黄樱花树变粉tileset.style new Cesium.Cesium3DTileStyle({ color: { conditions: [ [${treeType} 0, color(#d9a441)], [${treeType} 1, color(#f4a3b8)], [true, color(#7cba5d)] ] } });这个功能非常适合演示汇报不用重新生成瓦片只改样式就能展示不同季节、不同树种的分布。我最开始只顾着把树铺上去没想到这个细节后来被业主问“能不能让银杏变黄”现学现卖加上去效果出奇的好。如果你第一次做“在 Cesium 里添加大量树”我建议别一上来就挑战三万棵。先拿两千棵跑通点位表到 3D Tiles 再到 Cesium 拾取的完整链路把模型、贴图、高度采样工具链都固定下来再往上涨数量。数据链路通了剩下的无非是量变。本文还有配套的精品资源点击获取