文章目录
如果给一万架无人机分别创建 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。