站内搜索

输入关键词开始搜索

Cesium 无人机模型实例化:一份 GLB 渲染万级编队

文章目录

如果给一万架无人机分别创建 Entity 和 Model,CPU 调度、对象数量和绘制提交都会成为瓶颈。这个案例只加载一份 CesiumDrone.glb,用实例变换批量绘制机群。

海湾上空的无人机实例编队与数量、剔除和文字编队控制面板

实现思路导图

flowchart TD
  ROOT[无人机模型实例化]
  ROOT --> SOURCE[单份 GLB 资源]
  ROOT --> FORMATION[编队数据]
  FORMATION --> ENU[ENU 局部位置]
  FORMATION --> TEXT[文字目标点]
  ROOT --> GPU[GPU 实例绘制]
  GPU --> MATRIX[矩阵图集]
  GPU --> PICK[拾取颜色图集]
  ROOT --> OPT[运行优化]
  OPT --> CULL[CPU 视锥剔除]
  OPT --> DIFF[差量矩阵更新]

截图不是静态模型预览:场景中已经生成 200 架无人机,面板同时给出总数、当前可见数和剔除比例。改变数量、移动相机或生成文字编队后,这三项统计会跟着实例集合变化。

实例共享模型资源

每个实例只需要一个模型矩阵、颜色或拾取 id。网格、材质和纹理由所有实例共享。页面提供 50 到 10000 的数量档位,切换时重建实例集合:

const COUNT_PRESETS = [50, 200, 500, 1000, 2000, 5000, 10000];

const nextCollection = new ModelInstanceCollectionV2({
  url: MODEL_URL,
  instances,
  enableCpuCulling: controls.cpuCulling,
  enableDiffUpdate: true,
  allowPicking: true,
});
collection.value = viewer.value.scene.primitives.add(nextCollection);

实例矩阵进入纹理或批量缓冲区,Shader 按实例索引读取自己的变换。

这里的核心不是把同一个 Model 对象复制一万份,而是让 Cesium 只维护一份源模型。实现会把每个实例的 4×4 矩阵按列写进一张高度为 4 的浮点纹理:一列对应一个实例,四个 texel 对应矩阵四行。顶点 Shader 用 gl_InstanceID 算出纹理横坐标,读回矩阵后再乘源模型矩阵。

float u = (float(gl_InstanceID) + 0.5) / float(textureSize(u_v2MatrixAtlas, 0).x);
mat4 instanceMatrix = mat4(
  texture(u_v2MatrixAtlas, vec2(u, 0.125)),
  texture(u_v2MatrixAtlas, vec2(u, 0.375)),
  texture(u_v2MatrixAtlas, vec2(u, 0.625)),
  texture(u_v2MatrixAtlas, vec2(u, 0.875))
);

这样做以后,实例数量主要增加矩阵纹理和顶点处理量,不再线性增加 GLB 解析、材质对象和 DrawCommand 数量。

位置在局部坐标中生成

机群先在局部 ENU 平面中排列,再乘局部到世界的变换。这样可以用米为单位生成规则编队,不必直接在经纬度上做偏移。

const enu = Cesium.Transforms.eastNorthUpToFixedFrame(center);
const worldMatrix = Cesium.Matrix4.multiply(
  enu,
  localMatrix,
  new Cesium.Matrix4(),
);

巡航动画更新的是批量矩阵数据,不创建新的 Model。

完整变换顺序是:经纬度中心建立 ENU 参考系 → 世界坐标转到参考系局部坐标 → 叠加航向与缩放 → 写入矩阵图集 → Shader 还原到 Cesium 世界坐标。参考系靠近机群中心还能减小大地坐标直接进入单精度 Shader 时的抖动。

CPU 视锥剔除

开启剔除后,程序使用相机视锥和实例包围体判断可见性,只把视野内实例提交到可见列表。面板显示总实例、可见实例和可见比例。

CPU 剔除不是总能更快。实例少或大部分都在视野内时,判断成本可能高于节省的提交成本。案例把它做成开关,便于对同一视角对比。

实现中的剔除粒度是“单架无人机”。每帧先用相机视锥检测实例包围球,再为可见记录生成紧凑矩阵图集。只有可见集合或矩阵版本变化时才重建图集;相机不动且编队不变时可以复用上一帧结果。

这也解释了面板中“可见数”的意义:它不是被遮挡查询后的像素可见数,只代表通过视锥测试的实例数。建筑遮挡、地球背面和模型内部遮挡不由这一步解决。

差量更新为什么有效

巡航时真正改变的是 position、angle 或 scale。updateMany 逐条比较新旧记录,只给矩阵、拾取色或包围体对应的 dirty 标记加版本。渲染阶段根据版本决定重建哪张图集,避免每帧重新生成 id、拾取资源和整套模型。

数量变化、源 GLB 变化和参考系变化属于结构变化,需要重建集合;单架移动、航向调整和高亮属于数据变化,可以差量提交。把两类操作分开,才不会让“实例化”省下的绘制开销又被 JavaScript 对象重建吃掉。

文字编队

textFormation 先把文字绘制到离屏 Canvas,再读取非透明像素并采样成目标点。实例按索引映射到这些目标点,通过插值从巡航位置过渡到文字位置。

平面文字适合俯视,竖直文字适合正面观察。目标点少于实例数时,多余实例需要隐藏、排到辅助阵列或维持巡航,不能全部堆在同一个位置。

目标点的生成过程分四步:Canvas 绘制文字 → 读取 alpha 通道 → 按步长稀疏采样有效像素 → 把像素坐标映射为米制局部坐标。随后为每架无人机保存起点与目标点,在动画时间 t 内做缓动插值。文字模式改变的是实例矩阵目标,不会生成一套新的模型资源。

拾取和模型动画

实例 id 用于场景拾取和高亮。GLB 自身动画是共享模型动画,机群巡航则是实例矩阵动画,两者是两条独立时间线。

拾取图集给每个实例分配唯一颜色。Cesium 进入 pick pass 时输出该颜色,点击后再反查实例记录,因此拿到的是某一架无人机,而不是整批 GLB。高亮只改对应记录,下一次矩阵或拾取图集同步后生效。

性能边界

实例化适合“网格和材质相同、变换不同”的对象。每架无人机都换模型或使用完全不同材质时,共享收益会下降。数量继续上升后,瓶颈会从 DrawCall 转到顶点吞吐、矩阵纹理带宽和 CPU 单体剔除;这时应降低模型面数、分区剔除,或让 GPU 完成更粗粒度的可见性判断。

这个案例的主线是:共享 GLB 资源,把差异收敛到实例矩阵,再用批量更新、可选剔除和目标点重排实现大规模机群。

从零复现:无人机实例化教程

案例源码位于 src/demos/cesiumdemos/model-instancing/,核心实现是 lib/ModelInstanceCollectionV2.js。先用 50 个实例验证矩阵,再逐级增加到 10000。

第 1 步:加载一份源 GLB

使用 Cesium Model 加载 assets/CesiumDrone.glb,等待模型 ready 后取得其原始模型矩阵、包围球和渲染命令。实例集合只保留这一份网格、材质和纹理资源。

完成标志:不启用实例化时,单架无人机的尺寸、朝向和动画正常。源模型轴向错误应在统一的 sourceModelMatrix 中修正。

第 2 步:生成编队位置

以编队中心建立 ENU 参考系,在局部坐标中生成方阵、圆阵或随机阵列。把局部点乘参考矩阵得到世界位置,并为每架无人机保存稳定 id。

instances.push({
  id: index,
  position: worldPosition,
  angle: heading,
  scale: droneScale,
});

稳定 id 用于差量更新和拾取,不能在剔除后用“可见数组索引”代替。

第 3 步:构建实例矩阵图集

把世界位置转换到集合参考系局部坐标,组合平移、航向和缩放矩阵。每个实例的 16 个 float 按四行写入高度为 4 的浮点纹理。

图集宽度等于实例数。超出设备最大纹理宽度时,应改成二维分页图集或拆分实例集合,而不是继续申请失败纹理。

第 4 步:在 CustomShader 中读取矩阵

顶点阶段用 gl_InstanceID 计算图集 U 坐标,读取四个 texel 还原 mat4,再乘源模型矩阵。渲染命令的 instanceCount 设置为当前提交数量。

先让所有矩阵只有不同平移,确认数量与位置正确;再加入旋转和缩放。这样能区分矩阵行列顺序错误与编队生成错误。

第 5 步:实现差量更新

updateMany 比较位置、角度、缩放、id 和批次字段,只标记受影响资源。矩阵变化增加 matrixVersion,拾取字段变化增加 pick 版本,位置变化还要更新包围体。

巡航每帧只重算发生变化的记录和矩阵图集,不重新解析 GLB,也不创建新的 Primitive。

第 6 步:加入 CPU 视锥剔除

用相机的 CullingVolume 检测每架无人机包围球。将可见记录压紧成新的矩阵图集,并缓存“可见 id 集合 + 矩阵版本”。两者都没变时复用图集。

面板中的可见数是视锥可见,不是遮挡可见。先在 500 个实例下开关对比 CPU 时间,再决定是否默认启用。

第 7 步:实现文字编队

离屏 Canvas 绘制文字,读取 alpha 非零像素并按步长采样目标点。将像素坐标映射到 ENU 米制平面,为实例保存巡航起点和文字终点。

动画用缓动后的 t 插值矩阵。目标点少于实例数时,把多余实例排到背景阵列;目标点多于实例数时按均匀间隔抽样,保持字形轮廓。

第 8 步:实现单实例拾取

为每个稳定 id 分配唯一 pick color,写入拾取图集。pick pass 输出实例颜色,点击后反查原始记录并更新高亮状态。

验收与排错

  • 所有无人机叠在原点:检查矩阵图集纹理坐标和 instanceCount。
  • 旋转后模型缩放异常:检查矩阵乘法顺序与行列布局。
  • 开启剔除后点击错对象:可见索引必须映射回稳定 sourceIndex。
  • 一万实例仍有大量 DrawCall:确认底层命令真正设置了实例数量,而不是循环提交 Model。

相关文章

Cesium 加载 GLB 模型:Entity 与 Primitive 怎么选

对比 Entity ModelGraphics 和 Model.fromGltfAsync 两条加载路径,整理姿态、动画、批量加载和清理方式

Cesium 三维风场:用 WebGL 2 GPU 粒子描绘全球气流

拆解案例中的风场数据、GPU 粒子更新、轨迹渲染、颜色映射和 Vue 参数控制