写作背景

Cesium在 1.97 版本之前是有一个 ModelInstanceCollection 类用于完成模型的实例化绘制的,但是从 1.97 版本(2022 年 9 月 1 日发布)开始这个类被移除了。官方的说法是这个类和新的 Model (负责模型渲染的类) 架构不适配了,由于 ModelInstanceCollection 这个类一直是私有接口,并没有通过文档暴露给用户,所以 Cesium 团队就给直接移除了,说是后续再考虑加回来,但是没说啥时候。

这几天我正好也遇到了这个需求,要在 Cesium 上做模型的实例化绘制。既然 Cesium 原来就实现了一版实例化绘制,那我就站在巨人的肩膀上,把它那些代码找回来作为蓝本,参考价值是非常大的。集成到目前较高版本的 Cesium 上会遇到一系列问题,一个一个改掉就是了,后续有新的需求再陆续往上加,这样比从0开始写要来的快速和稳妥。完成上述工作后通过一些测试也印证了实例化绘制的确是性能优化的好方式。


实例化绘制大量模型


原理分析

拿来主义确实可以帮我们快速解决问题,但是理解消化吸收也是非常有必要的,尤其是对于实例化绘制这种典型的性能优化方式。因此,我也借这个机会把 Cesium 实现实例化的思路梳理一遍,方便以后查阅,同时分享出来希望得到大家的指正。

首先,创建多个顶点缓冲区,用来存放顶点属性。最重要的是顶点位置相关的属性,Cesium 将每个实例相对于所有实例包围球中心点的模型矩阵的前三行按顺序存到从顶点缓冲区中,然后按步进和偏移量取数据组织成 czm_modelMatrixRow0、czm_modelMatrixRow1、czm_modelMatrixRow2 三个实例化属性,代码结构如表1所示。

    const instancedAttributes = {
      czm_modelMatrixRow0: {
        index: 0, // updated in Model
        vertexBuffer: collection._vertexBuffer,
        componentsPerAttribute: 4,
        componentDatatype: ComponentDatatype.FLOAT,
        normalize: false,
        offsetInBytes: 0,
        strideInBytes: componentSizeInBytes * vertexSizeInFloats,
        instanceDivisor: 1,
      },
      czm_modelMatrixRow1: {
        index: 0, // updated in Model
        vertexBuffer: collection._vertexBuffer,
        componentsPerAttribute: 4,
        componentDatatype: ComponentDatatype.FLOAT,
        normalize: false,
        offsetInBytes: componentSizeInBytes * 4,
        strideInBytes: componentSizeInBytes * vertexSizeInFloats,
        instanceDivisor: 1,
      },
      czm_modelMatrixRow2: {
        index: 0, // updated in Model
        vertexBuffer: collection._vertexBuffer,
        componentsPerAttribute: 4,
        componentDatatype: ComponentDatatype.FLOAT,
        normalize: false,
        offsetInBytes: componentSizeInBytes * 8,
        strideInBytes: componentSizeInBytes * vertexSizeInFloats,
        instanceDivisor: 1,
      },
    };

 表1 将实例相对于中心点的模型矩阵存放在顶点缓冲区中

Cesium 让每个实例相对于所有实例包围球的中心点渲染,一般情况下是完全可行的。但是如果实例相隔很远,比如一个模型在中国,另一个模型在美国,相对中心点的坐标会非常大,那么由于精度的丢失,在近距离下拖动模型会出现抖动问题;我们可以修改源码,改成相对于相机渲染以解决这个问题。

接下来,改造模型( Model )的着色器,主要工作量在顶点着色器中,加入接收实例化属性的 attribute 变量,将表1代码中拆成 vec4 传给GPU的 czm_modelMatrixRow0、czm_modelMatrixRow1、czm_modelMatrixRow2 重新组装成矩阵,代码结构如表2所示。然后经过一些常规变换,可以得到各个实例化模型每个顶点的裁剪坐标,模型的结构和位置最终就确定下来了。

mat4 czm_instanced_model = mat4(czm_modelMatrixRow0.x, czm_modelMatrixRow1.x, czm_modelMatrixRow2.x, 0.0, czm_modelMatrixRow0.y, czm_modelMatrixRow1.y, czm_modelMatrixRow2.y, 0.0, czm_modelMatrixRow0.z, czm_modelMatrixRow1.z, czm_modelMatrixRow2.z, 0.0, czm_modelMatrixRow0.w, czm_modelMatrixRow1.w, czm_modelMatrixRow2.w, 1.0);

表2 着色器中组装相对于中心点的矩阵 

如果想让实例化模型运动起来,可以从实例化集合(ModelInstanceCollection)中取出对应的实例对象,重新设置它的模型矩阵(modelMatrix)即可,示意代码如表3所示。这会触发 ModelInstanceCollection 更新顶点缓冲区中相对于中心点的模型矩阵,这样实例就动起来了。

collection._instances[i].modelMatrix = modelMatrix;

 表3 更新实例化对象的模型矩阵以改变其位置

最后,在执行绘制命令( draw call )的时候,Cesium 通过 WebGL 的接口指定相应属性为实例化属性( WebGL 1.0 下通过 ANGLE_instanced_arrays 扩展的 vertexAttribDivisorANGLE 方法;2.0 调用原生的 gl.vertexAttribDivisor 方法)并进行绘制( WebGL 1.0 下用扩展的 drawElementsInstancedANGLE / drawArraysInstancedANGLE 方法,2.0调用原生的 gl.drawElementsInstanced / gl.drawArraysInstanced 方法)。这样 Cesium 实例化绘制的逻辑就基本清晰了。

Logo

GitCode 天启AI是一款由 GitCode 团队打造的智能助手,基于先进的LLM(大语言模型)与多智能体 Agent 技术构建,致力于为用户提供高效、智能、多模态的创作与开发支持。它不仅支持自然语言对话,还具备处理文件、生成 PPT、撰写分析报告、开发 Web 应用等多项能力,真正做到“一句话,让 Al帮你完成复杂任务”。

更多推荐