Bootstrap

Cesium 高性能扩展 DrawCommand 解析

前言

我们在 Cesium 自定义几何体一般有两种方式,一种是通过直接使用 Primitive 来实现,另外一种就是 DrawCommand 来扩展;下面代码是通过 Primitive 来实现一个三角形:

const viewer = new Cesium.Viewer('cesiumContainer');
viewer.scene.globe.depthTestAgainstTerrain = true


// original sample begins here
const mypositions = Cesium.Cartesian3.fromDegreesArrayHeights([
  114.4864783553646, 30.604215790554694, 30,
  114.48382898892015, 30.60431560806962, 30,
  114.48353342631442, 30.601700422371984, 30
]);

// unroll 'mypositions' into a flat array here
const numPositions = mypositions.length;
const pos = new Float64Array(numPositions * 3);
for (let i = 0; i < numPositions; ++i) {
  pos[i * 3] = mypositions[i].x;
  pos[i * 3 + 1] = mypositions[i].y;
  pos[i * 3 + 2] = mypositions[i].z;
}

const colors = [
  0.4, 0.8, 1.0, 1.0,
  0.5, 0.5, 0.5, 1.0,
  0.5, 0.5, 0.5, 1.0
]

const normal = [0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0]
const geometry = new Cesium.Geometry({
  attributes: {
    position: new Cesium.GeometryAttribute({
      componentDatatype: Cesium.ComponentDatatype.DOUBLE, // not FLOAT
      componentsPerAttribute: 3,
      values: pos
    }),
    color: new Cesium.GeometryAttribute({
      componentDatatype: Cesium.ComponentDatatype.FLOAT,
      componentsPerAttribute: 4,
      values: new Float64Array(colors)
    }),
    normal: new Cesium.GeometryAttribute({
      componentDatatype: Cesium.ComponentDatatype.FLOAT,
      componentsPerAttribute: 3,
      values: new Float32Array(normal)
    })
  },
  indices: new Uint32Array([0, 1, 2]),
  primitiveType: Cesium.PrimitiveType.TRIANGLES,
  boundingSphere: Cesium.BoundingSphere.fromVertices(pos)
});

const trangleInstance = new Cesium.GeometryInstance({
  geometry,
  show: new Cesium.ShowGeometryInstanceAttribute(true)
});

const fragmentShaderSource = `
    varying vec3 v_positionEC;
    varying vec3 v_normalEC;
    varying vec4 v_color;

    void main()
    {
        vec3 positionToEyeEC = -v_positionEC;

        vec3 normalEC = normalize(v_normalEC);
    #ifdef FACE_FORWARD
        normalEC = faceforward(normalEC, vec3(0.0, 0.0, 1.0), -normalEC);
    #endif

        vec4 color = czm_gammaCorrect(v_color);

        czm_materialInput materialInput;
        materialInput.normalEC = normalEC;
        materialInput.positionToEyeEC = positionToEyeEC;
        czm_material material = czm_getDefaultMaterial(materialInput);
        material.diffuse = color.rgb;
        material.alpha = color.a;

        gl_FragColor = czm_phong(normalize(positionToEyeEC), material, czm_lightDirectionEC);
    }
`
const vertexShaderSource = `
    attribute vec3 position3DHigh;
    attribute vec3 position3DLow;
    attribute vec3 normal;
    attribute vec4 color;
    attribute float batchId;

    varying vec3 v_positionEC;
    varying vec3 v_normalEC;
    varying vec4 v_color;

    void main()
    {
        vec4 p = czm_computePosition();

        v_positionEC = (czm_modelViewRelativeToEye * p).xyz;      // position in eye coordinates
        v_normalEC = czm_normal * normal;                         // normal in eye coordinates
        v_color = color;

        gl_Position = czm_modelViewProjectionRelativeToEye * p;
    }
`

const primitive = new Cesium.Primitive({
  geometryInstances: trangleInstance,
  asynchronous: false,
  appearance: new Cesium.PerInstanceColorAppearance({
    closed: true,
    translucent: false,
    vertexShaderSource,
    fragmentShaderSource,
  })
});
viewer.scene.primitives.add(primitive);

viewer.camera.flyTo({
  destination: Cesium.Cartesian3.fromDegrees(114.4864783553646, 30.604215790554694, 500),
  duration: 2
}) 

image.png
一个简单的需求,如果我们想通过 uniform 传入 time 来不断的改变每个顶点的颜色;虽然说 Primitive 定义集合体的方式可以通过自定义 Appearance 来修改着色器,但是 API 并不支持自定义传入 uniform 值等;而通过 DrawCommand 进行扩展就可以通过 uniformMap 来传递参数实现 JavaScript -> WebGl 的交互;
image.png

DrawCommand 是什么

Primitive API 是公开的 API 的最底层了,它面向的场景是高性能、可自定义材质着色器(Appearance API + FabricMaterial Specification)、静态三维物体。
尽管如此,Primitive API 仍然封装了大量几何体类、材质类、WebWorker,而且目前开放自定义着色器 API 的只有三维模型类的新架构,还没下放到 Primitive API。
如果 API 包袱不想那么重,又希望可以使用自己的模型格式(必须是三角面),那么私有的 DrawCommand + VertexArray 接口就非常合适了,它的风格已经是最接近 CesiumJS WebGL 底层的一类 API 了。
DrawCommand,是 Cesium 封装 WebGL 的一个优秀设计,是 Cesium 渲染器的核心类,一个 cesium 用来绘制渲染的底层的命令;常用的接口 Entity、Primitive、Cesium3DTileSet,以及地形和影像的渲染等等,底层都是一个个 DrawCommand 完成的。它把绘图数据(VertexArray)和绘图行为(ShaderProgram)作为一个对象,待时机合适,也就是 Scene 执行 executeCommand 函数时,帧状态对象上所有的指令对象就会使用 WebGL 函数执行,要什么就 bind 什么,做到了在绘图时的用法一致,上层应用接口只需生成指令对象。

下面是 Primitive.js 模块中的 createCommands 函数,它就是负责把 Primitive 对象的参数化数据或 WebWorker 计算来的数据合并生成 DrawCommand 的地方:

// Primitives 绘制
commandFunc(
	this,
  appearance,
  material,
  translucent,
  twoPasses,
  this._colorCommands,
  this._pickCommands,
  frameState
 );

function createCommands(
  primitive,
  appearance,
  material,
  translucent,
  twoPasses,
  colorCommands,
  pickCommands,
  frameState
) {
  var uniforms = getUniforms(primitive, appearance, material, frameState);

  var depthFailUniforms;
  if (defined(primitive._depthFailAppearance)) {
    depthFailUniforms = getUniforms(
      primitive,
      primitive._depthFailAppearance,
      primitive._depthFailAppearance.material,
      frameState
    );
  }

  var pass = translucent ? Pass.TRANSLUCENT : Pass.OPAQUE;

  var multiplier = twoPasses ? 2 : 1;
  multiplier *= defined(primitive._depthFailAppearance) ? 2 : 1;
  colorCommands.length = primitive._va.length * multiplier;

  var length = colorCommands.length;
  var vaIndex = 0;
  for (var i = 0; i < length; ++i) {
    var colorCommand;

    if (twoPasses) {
      colorCommand = colorCommands[i];
      if (!defined(colorCommand)) {
        colorCommand = colorCommands[i] = new DrawCommand({
          owner: primitive,
          primitiveType: primitive._primitiveType,
        });
      }
      colorCommand.vertexArray = primitive._va[vaIndex];
      colorCommand.renderState = primitive._backFaceRS;
      colorCommand.shaderProgram = primitive._sp;
      colorCommand.uniformMap = uniforms;
      colorCommand.pass = pass;

      ++i;
    }

    colorCommand = colorCommands[i];
    if (!defined(colorCommand)) {
      colorCommand = colorCommands[i] = new DrawCommand({
        owner: primitive,
        primitiveType: primitive._primitiveType,
      });
    }
    colorCommand.vertexArray = primitive._va[vaIndex];
    colorCommand.renderState = primitive._frontFaceRS;
    colorCommand.shaderProgram = primitive._sp;
    colorCommand.uniformMap = uniforms;
    colorCommand.pass = pass;

    if (defined(primitive._depthFailAppearance)) {
      if (twoPasses) {
        ++i;

        colorCommand = colorCommands[i];
        if (!defined(colorCommand)) {
          colorCommand = colorCommands[i] = new DrawCommand({
            owner: primitive,
            primitiveType: primitive._primitiveType,
          });
        }
        colorCommand.vertexArray = primitive._va[vaIndex];
        colorCommand.renderState = primitive._backFaceDepthFailRS;
        colorCommand.shaderProgram = primitive._spDepthFail;
        colorCommand.uniformMap = depthFailUniforms;
        colorCommand.pass = pass;
      }

      ++i;

      colorCommand = colorCommands[i];
      if (!defined(colorCommand)) {
        colorCommand = colorCommands[i] = new DrawCommand({
          owner: primitive,
          primitiveType: primitive._primitiveType,
        });
      }
      colorCommand.vertexArray = primitive._va[vaIndex];
      colorCommand.renderState = primitive._frontFaceDepthFailRS;
      colorCommand.shaderProgram = primitive._spDepthFail;
      colorCommand.uniformMap = depthFailUniforms;
      colorCommand.pass = pass;
    }

    ++vaIndex;
  }
}

在进行扩展开发、视觉特效提升、性能优化、渲染到纹理(RTT),甚至基于 Cesium 封装自己的开发框架,定义独家数据格式等等,都需要开发人员对 DrawCommand 熟练掌握。而这部分接口,Cesium 官方文档没有公开,网上的相关资料也比较少,学习起来比较困难;

创建 DrawCommand

首先创建一个 DrawCommand 对象:

var drawCommand = new Cesium.DrawCommand({
    modelMatrix: modelMatrix,
    vertexArray: va,
    shaderProgram: shaderProgram,
    uniformMap: uniformMap,
    renderState: renderState,
    pass: Cesium.Pass.OPAQUE
})

创建 DrawCommand 对象必须包含以下属性:

  1. vertexArray:顶点数组,向 GPU 传递构造物体的顶点属性、索引(可选的)数组等几何信息;
  2. shaderProgram:着色器程序对象,负责编译、连接顶点着色器(vertexShader)、片元着色器(fragmentShader);
  3. pass:渲染通道,在着色器中可以通过 czm_pass 开头判断其渲染通道,在 WebGL 中先渲染不透明物体,然后半透明物体;Cesium 提供的常用渲染通道有:
    • ENVIRONMENT:环境,如天空盒(星空背景)
    • COMPUTE :用于并行加速计算
    • GLOBE :地形瓦片等
    • TERRAIN_CLASSIFICATION :地形分类
    • CESIUM_3D_TILE :3D Tiles 瓦片
    • CESIUM_3D_TILE_CLASSIFICATION :3D Tiles 分类(单体化)
    • OPAQUE :不透明物体
    • TRANSLUCENT :半透明物体

不是必须但是很常用的属性:

  1. modelMatrix:模型变换矩阵,用于指定所绘制物体的参考系,包括平移、旋转、缩放三方面参数。如果不设置,则参考系为世界坐标系,原点在地球球心;
  2. uniformMap:用于传递 uniform 具体的值,是一个回调函数字典对象,key 是 uniform 变量名,value 是回调函数,回调函数的返回值可以是:
    • number :数字类型,或者数字数组;
    • boolean :布尔类型,true 或者 false,或者数组;
    • Cartesian2 :二维向量;
    • Cartesian3 :三维向量;
    • Cartesian4 :四维向量;
    • Color :颜色,本身也是四维向量;
    • Matrix2 :2x2 矩阵;
    • Matrix3 :3x3 矩阵,一般可以传法线矩阵(normalMatrix);
    • Matrix4 :4x4 矩阵,如 modelMatrix、viewMatrix、projectionMatrix 等等都是这个类型;
    • Texture :二维贴图;
    • CubeMap :立方体贴图。
  3. renderState:渲染状态对象,封装如深度测试(depthTest)、剔除(cull)、混合(blending)、frontFace 、polygonOffset、lineWidth 等绘制状态类型的参数设置;
  4. cull/occlude: 视锥剔除 + 地平线剔除组合技,Boolean;
  5. orientedBoundingBox/boundingVolume: 范围框;
  6. count: number,WebGL 绘制时要画多少个点;
  7. offset: number,WebGL 绘制时从多少偏移量开始用顶点数据;
  8. instanceCount: number,实例绘制有关;
  9. castShadows/receiveShadows: Boolean,阴影相关;
  10. pickId: string,若没定义,在 Pick 通道的绘制中将使用深度数据;若定义了将在 GLSL 中转化为 pick id;

下面介绍一下上面的这些属性如何创建;

相关 API 介绍
modelMatrix:模型变换矩阵

4*4 的模型变换矩阵,主要用来将模型坐标系转换到世界坐标系。
将 Matrix4 类型的变量在创建 DrawCommand 时传递进去,它最终会传递到 CesiumJS 的内部统一值:czm_model(模型矩阵)上,而无需你在 uniform 中指定,你可以在顶点着色器中使用它来对 VertexArray 中的顶点进行模型矩阵变换。
如下示例代码:

var origin = Cesium.Cartesian3.fromDegrees(-95.0, 40.0, 200000.0);
p.modelMatrix = Cesium.Transforms.eastNorthUpToFixedFrame(origin);
vertexArray:VAO

它主要构造模型的顶点数据,主要用来绘制图形,Cesium 把 WebGL 的顶点缓冲和索引缓冲包装成了 Buffer,然后为了方便,将这些顶点相关的缓冲绑定在了一个对象里,叫做 VertexArray,内部会启用 WebGL 的 VAO 功能。
image.png
顶点数组的创建有多种方法,通常可以将几何数据用 Cesium.Geometry 来表达,然后用Cesium.VertexArray.fromGeometry 可以用更少代码量完成创建。关键参数:

  • attributeLocations :顶点属性索引,key 为属性名称,value 为顶点属性缓冲区在同一个着色器程序中的索引,相当于将 js 中的顶点数组,传递到 shader 中的 attribute 变量。在后面创建shaderProgram时还需要用到;
  • context:从Primitive.update方法的frameState参数中获取;
  • geometry:Cesium.Geometry,Cesium 自带的几何类型都提供一个静态方法 createGeometry 来生成这个类型的几何对象。
// 创建/组织几何数据
var box = new Cesium.BoxGeometry({
    vertexFormat: Cesium.VertexFormat.POSITION_ONLY,
    maximum: new Cesium.Cartesian3(250000.0, 250000.0, 250000.0),
    minimum: new Cesium.Cartesian3(-250000.0, -250000.0, -250000.0)
});
var geometry = Cesium.BoxGeometry.createGeometry(box);

// 创建顶点属性索引,key 为属性名称,value 为顶点属性缓冲区在同一个着色器程序中的索引。
// 相当于将 js 中的顶点数组,传递到 shader 中的 attribute 变量
var attributeLocations = Cesium.GeometryPipeline.createAttributeLocations(geometry)

// 创建顶点数组对象
var va = Cesium.VertexArray.fromGeometry({
    context: frameState.context,
    geometry: geometry,
    attributeLocations: attributeLocations
});
function createVertexArray(primitive, frameState) {
  var attributeLocations = primitive._attributeLocations;
  var geometries = primitive._geometries;
  var scene3DOnly = frameState.scene3DOnly;
  var context = frameState.context;

  var va = [];
  var length = geometries.length;
  for (var i = 0; i < length; ++i) {
    var geometry = geometries[i];

    va.push(
      VertexArray.fromGeometry({
        context: context,
        geometry: geometry,
        attributeLocations: attributeLocations,
        bufferUsage: BufferUsage.STATIC_DRAW,
        interleave: primitive._interleave,
      })
    );

    if (defined(primitive._createBoundingVolumeFunction)) {
      primitive._createBoundingVolumeFunction(frameState, geometry);
    } else {
      primitive._boundingSpheres.push(
        BoundingSphere.clone(geometry.boundingSphere)
      );
      primitive._boundingSphereWC.push(new BoundingSphere());

      if (!scene3DOnly) {
        var center = geometry.boundingSphereCV.center;
        var x = center.x;
        var y = center.y;
        var z = center.z;
        center.x = z;
        center.y = x;
        center.z = y;

        primitive._boundingSphereCV.push(
          BoundingSphere.clone(geometry.boundingSphereCV)
        );
        primitive._boundingSphere2D.push(new BoundingSphere());
        primitive._boundingSphereMorph.push(new BoundingSphere());
      }
    }
  }

  primitive._va = va;
  primitive._primitiveType = geometries[0].primitiveType;

  if (primitive.releaseGeometryInstances) {
    primitive.geometryInstances = undefined;
  }

  primitive._geometries = undefined;
  setReady(primitive, frameState, PrimitiveState.COMPLETE, undefined);
}

其他在 Cesium 源码中也给出了示例,比较接近原生的 webgl 的写法了,index 为顶点属性的存放索引。如下:


// Example 1. Create a vertex array with vertices made up of three floating point
// values, e.g., a position, from a single vertex buffer.  No index buffer is used.
// 单个顶点的数据,不使用索引缓冲区 - 先创建 Buffer
// 创建内置了 WebGLBuffer 的顶点缓冲对象 positionBuffer
var positionBuffer = Buffer.createVertexBuffer({
  context: context,
  typedArray : new Float32Array([0, 0, 0]),
  usage: BufferUsage.STATIC_DRAW
});

// 顶点属性 attributes
var attributes = [
  {
    index: 0,
    enabled: true,
    // 缓冲区中写入数据
    vertexBuffer: positionBuffer,
    componentsPerAttribute: 3,
    componentDatatype: ComponentDatatype.FLOAT,
    normalize: false,
    offsetInBytes: 0,
    strideInBytes: 0, // 紧密组合在一起,没有 byteStride
    instanceDivisor: 0 // 不实例化绘制
  }
];

var va = new VertexArray({
  context: context,
  attributes: attributes
});

// Example 2. Create a vertex array with vertices from two different vertex buffers.
// Each vertex has a three-component position and three-component normal.
// 每个顶点都含有三个分量的一个位置属性以及法向量;
// 坐标缓冲和法线缓冲分开存到两个对象里
var positionBuffer = Buffer.createVertexBuffer({
  context: context,
  sizeInBytes: 12,
  usage: BufferUsage.STATIC_DRAW
});
var normalBuffer = Buffer.createVertexBuffer({
  context: context,
  sizeInBytes: 12,
  usage: BufferUsage.STATIC_DRAW
});
var attributes = [
  {
    index: 0,
    vertexBuffer: positionBuffer,
    componentsPerAttribute: 3,
    componentDatatype: ComponentDatatype.FLOAT
  },
  {
    index: 1,
    vertexBuffer: normalBuffer,
    componentsPerAttribute: 3,
    componentDatatype: ComponentDatatype.FLOAT
  }
];
var va = new VertexArray({
  context: context,
  attributes: attributes
});

// Example 3. Creates the same vertex layout as Example 2 using a single
// vertex buffer, instead of two.
// 两个顶点缓冲区数据
var buffer = Buffer.createVertexBuffer({
  context: context,
  sizeInBytes: 24,
  usage: BufferUsage.STATIC_DRAW
});
var attributes = [
  {
    vertexBuffer: buffer,
    componentsPerAttribute: 3,
    componentDatatype: ComponentDatatype.FLOAT,
    offsetInBytes: 0,
    strideInBytes: 24
  },
  {
    vertexBuffer: buffer,
    componentsPerAttribute: 3,
    componentDatatype: ComponentDatatype.FLOAT,
    normalize: true,
    offsetInBytes: 12,
    strideInBytes: 24
  }
];
var va = new VertexArray({
  context: context,
  attributes: attributes
});

上面方式创建的 Buffer,顶点坐标是直角坐标系下的,是最原始的坐标值,除非在着色器里做矩阵变换,或者这些直角坐标就在世界坐标系的地表附近。它是一堆没有具体语义的、纯粹数学几何的坐标,与渲染管线无关。所以,对于地表某处的坐标点,通常要配合 ENU 转换矩阵 + 内置的 MVP 转换矩阵来使用。

顶点数组对象(Vertex Array Object,统称 VAO)是 WebGL(1.0)的一个扩展,通过它可以简化缓冲区的绑定过程,即可以减少代码的调用次数,也提升了 WebGL 状态切换的效率。VAO 的作用:负责记录 bindBuffer 和 vertexAttribPointer 的调用状态。

Pass:绘制的通道类型

CesiumJS 不是粗暴地把帧状态对象上的 Command 遍历一遍就绘制了的,在 Scene 的渲染过程中,除了生成三大 Command(computeList、overlayList、commandList),还有一步要对 Command 进行通道排序。
通道,是一个枚举类型,保存在 Pass.js 模块中。不同通道有不同的优先级,在着色器中可以通过 czm_pass 开头判断其渲染通道。
比如下面代码,判断当前物体渲染模式是否为透明,剔除不透明几何体的绘制;

   if ((czm_pass == czm_passTranslucent) && isOpaque())
   {
     gl_Position *= 0.0; // Cull opaque geometry in the translucent pass
   }
primitiveType:绘制的图元类型

即指定 VertexArray 中顶点的拓扑格式,在 WebGL 中是通过 drawArrays 指定的:

gl.drawArrays(gl.TRIANGLES, 0, 3)

这个 gl.TRIANGLES 就是图元类型,是一个常数。Cesium 全部封装在 PrimitiveType.js 模块导出的枚举中了:

console.log(PrimitiveType.TRIANGLES) // 4

DrawCommand 中 primitiveType 默认就是 PrimitiveType.TRIANGLES;

Framebuffer:离屏绘制容器

CesiumJS 支持把结果画到 Renderbuffer,也就是 RTR(Render to RenderBuffer) 离屏绘制。绘制到渲染缓冲,是需要帧缓冲容器的,CesiumJS 把 WebGL 1/2 中帧缓冲相关的 API 都封装好了(严格来说,把 WebGL 中的 API 基本都封装了一遍)。

// 创建带有颜色和深度纹理的帧缓冲区
var width = context.canvas.clientWidth;
var height = context.canvas.clientHeight;
var framebuffer = new Framebuffer({
  context : context,
  colorTextures : [new Texture({
    context : context,
    width : width,
    height : height,
    pixelFormat : PixelFormat.RGBA
  })],
  depthTexture : new Texture({
    context : context,
    width : width,
    height : height,
    pixelFormat : PixelFormat.DEPTH_COMPONENT,
    pixelDatatype : PixelDatatype.UNSIGNED_SHORT
  })
});
shaderProgram:着色器程序

这个类是 cesium 封装的一个 program 类,核心功能其实就是简化 shader 与 program 的绑定、shader 的编译,并使用大量正则等手段做了着色器源码匹配、解析、管理,同时做了一些缓存处理
着色器代码由 ShaderSource 管理,ShaderProgram 则管理起多个着色器源码,也就是着色器本身。使用 ShaderCache 作为着色器程序的缓存容器。它们的层级关系如下:

Context
  ┖ ShaderCache
    ┖ ShaderProgram
      ┖ ShaderSource

使用 ShaderProgram.fromCache 静态方法会自动帮你把着色器缓存到 ShaderCache 容器中。

const vs = `
  attribute vec3 position;
  void main(){
      gl_Position = czm_projection  * czm_modelView * vec4( position , 1. );
  }
`;
const fs = `
  uniform vec3 color;
  void main(){
      gl_FragColor=vec4( color , 1. );
  }
`;
const shaderProgram = Cesium.ShaderProgram.fromCache({
  context: context,
  vertexShaderSource: vs,
  fragmentShaderSource: fs,
  // attributeLocations 就是写入缓冲区数据的位置;也就是当前创建的物体的缓冲区;
  attributeLocations: attributeLocations
})

这里需要注意的是:

  1. attributeLocations 属性是构造 geometry 的点的属性和定义图元的可选索引数据,包括 position、color、normal、st 等等;也就是 Cesium.GeometryAttributes 对应的相关属性,就是一个普通的对象;
{
  "position": 0,
  "normal": 1,
  "st": 2,
  "bitangent": 3,
  "tangent": 4,
  "color": 5
}
  1. 在写着色器代码时候,Cesium 内部封装了很多内置的 uniform变量,都是以 czm_ 开头,比如常见的 czm_projection、czm_modelView(投影矩阵、模型矩阵)等,内置 uniform 变量无需在我们的 shader 代码中声明,直接使用即可。如果我们需要自己声明变量,可以通过构造 DrawCommand 的 uniformMap 传入;

cesium 着色器内置变量:https://github.com/CesiumGS/cesium/blob/main/Source/Renderer/AutomaticUniforms.js

uniformMap:uniform 变量

用于传递自定义 uniform 变量的值,是一个回调函数,key 是 uniform 变量名,value 是回调函数,回调函数的返回值可以是:

  • number :数字类型,在 shader 中类型为 float;
  • boolean :布尔类型,true 或者 false,在 shader 中类型为 bool;
  • Cesium.Cartesian2 :二维向量,在 shader 中类型为 vec2;
  • Cesium.Cartesian3 :三维向量,在 shader 中类型为 vec3;
  • Cesium.Cartesian4 :四维向量,在 shader 中类型为 vec4;
  • Cesium.Color :颜色,本身也是四维向量,在 shader 中类型为 vec4;
  • []:元素为上述类型的数组
  • Cesium.Matrix2 :2x2 矩阵,在 shader 中类型为 mat2;
  • Cesium.Matrix3 :3x3 矩阵,一般可以传法线矩阵(normalMatrix),在 shader 中类型为 mat3;
  • Cesium.Matrix4 :4x4 矩阵,如 modelMatrix、viewMatrix、projectionMatrix 等等都是这个类型,在 shader 中类型为 mat4;
  • Cesium.Texture :二维贴图,在 shader 中类型为 sampler2D;
  • Cesium.CubeMap :立方体贴图,在 shader 中类型为 samplerCube;
  • {}:结构体。
// 传入 color 变量
const uniformMap = {
  color() {
    return Cesium.Color.GRAY
  }
}

// 在片段着色器中使用
const fs = `
	uniform vec3 color;
	void main(){
		gl_FragColor=vec4(color , 1.);
	}
`;

这个 uniforms 对象最终会在 Context 执行绘制时,与系统的自动统一值(AutomaticUniforms)合并。

Context.prototype.draw = function (/*...*/) {
  // ...
  continueDraw(this, drawCommand, shaderProgram, uniformMap);
  // ...
}
renderState:渲染状态对象

渲染状态对象,它传递渲染数据之外一切参与 WebGL 渲染的状态值,封装如深度测试(depthTest)、剔除(cull)、混合(blending)、frontFace 、polygonOffset、lineWidth 等绘制状态类型的参数设置。下面代码是 Cesium 源码中给出的示例:

var defaults = {
  frontFace: Cesium.WindingOrder.COUNTER_CLOCKWISE,
  cull: {
    enabled: false,
    face: Cesium.CullFace.BACK
  },
  lineWidth: 1,
  polygonOffset: {
    enabled: false,
    factor: 0,
    units: 0
  },
  scissorTest: {
    enabled: false,
    rectangle: {
      x: 0,
      y: 0,
      width: 0,
      height: 0
    }
  },
  depthRange: {
    near: 0,
    far: 1
  },
  depthTest: {
    enabled: false,
    func: Cesium.DepthFunction.LESS
  },
  colorMask: {
    red: true,
    green: true,
    blue: true,
    alpha: true
  },
  depthMask: true,
  stencilMask: ~0,
  blending: {
    enabled: false,
    color: {
      red: 0.0,
      green: 0.0,
      blue: 0.0,
      alpha: 0.0
    },
    equationRgb: Cesium.BlendEquation.ADD,
    equationAlpha: Cesium.BlendEquation.ADD,
    functionSourceRgb: Cesium.BlendFunction.ONE,
    functionSourceAlpha: Cesium.BlendFunction.ONE,
    functionDestinationRgb: Cesium.BlendFunction.ZERO,
    functionDestinationAlpha: Cesium.BlendFunction.ZERO
  },
  stencilTest: {
    enabled: false,
    frontFunction: Cesium.StencilFunction.ALWAYS,
    backFunction: Cesium.StencilFunction.ALWAYS,
    reference: 0,
    mask: ~0,
    frontOperation: {
      fail: Cesium.StencilOperation.KEEP,
      zFail: Cesium.StencilOperation.KEEP,
      zPass: Cesium.StencilOperation.KEEP
    },
    backOperation: {
      fail: Cesium.StencilOperation.KEEP,
      zFail: Cesium.StencilOperation.KEEP,
      zPass: Cesium.StencilOperation.KEEP
    }
  },
  sampleCoverage: {
    enabled: false,
    value: 1.0,
    invert: false
  }
};

var renderState = Cesium.RenderState.fromCache(defaults)

DrawCommand 使用以及渲染流程

使用 DrawCommand

DrawCommand 的使用需要通过实现 Primitive 接口来完成, 如下代码,实现一个简单三角形绘制,点击例子可查看结果;

class StaticTrianglePrimitive {
  /**
   * @param {Matrix4} modelMatrix matrix to WorldCoordinateSystem
   */
  constructor(modelMatrix) {
    this.modelMatrix = modelMatrix || Cesium.Matrix4.IDENTITY.clone()
    this.drawCommand = null;
    this.num = 0;
  }

  /**
   * 创建 DrawCommand
   * @param {Cesium.Context} context
   */
  createCommand(context) {
    const attributeLocations = {
      "position": 0,
    }
    const uniformMap = {
      u_color() {
        return Cesium.Color.HONEYDEW
      },
      u_time() {
        return new Date().getTime();
      }
    }

    const positionBuffer = Cesium.Buffer.createVertexBuffer({
      usage: Cesium.BufferUsage.STATIC_DRAW,
      typedArray: new Float32Array([
        10000, 50000, 5000,
        -20000, -10000, 5000,
        50000, -30000, 5000,
      ]),
      context: context,
    })

    const vertexArray = new Cesium.VertexArray({
      context: context,
      attributes: [{
        index: 0, // 等于 attributeLocations['position']
        vertexBuffer: positionBuffer,
        componentsPerAttribute: 3,
        componentDatatype: Cesium.ComponentDatatype.FLOAT
      }]
    })

    const vertexShaderText = `
    attribute vec3 position;
    void main() {
      gl_Position = czm_projection * czm_view * czm_model * vec4(position, 1.0);
    }`
    const fragmentShaderText = `
    uniform vec3 u_color;
    uniform float u_time;
    void main(){
      // float time = sin(czm_frameNumber / 60.0);
      float time = sin(u_time / 60.0);
      float x_color = fract(u_color.x * time);
      gl_FragColor = vec4(x_color, u_color.yz, 1.0);
    }`

    const program = Cesium.ShaderProgram.fromCache({
      context: context,
      vertexShaderSource: vertexShaderText,
      fragmentShaderSource: fragmentShaderText,
      attributeLocations: attributeLocations,
    })

    const renderState = Cesium.RenderState.fromCache({
      depthTest: {
        enabled: true
      }
    })

    this.drawCommand = new Cesium.DrawCommand({
      modelMatrix: this.modelMatrix,
      vertexArray: vertexArray,
      shaderProgram: program,
      uniformMap: uniformMap,
      renderState: renderState,
      pass: Cesium.Pass.OPAQUE,
    })
  }
  
  /**
   * 实现 Primitive 接口,供 Cesium 内部在每一帧中调用
   * @param {Cesium.FrameState} frameState
   */
   update(frameState) {
    this.num++
    // console.log(this.drawCommand, 'this.drawCommand');
    if (!this.drawCommand) {
      this.createCommand(frameState.context)
    }
    this.drawCommand.uniformMap.u_time = () => this.num
    frameState.commandList.push(this.drawCommand)
  }
}

const viewer = new Cesium.Viewer('cesiumContainer', {
  contextOptions: {
    requestWebgl2: true
  }
})

const modelCenter = Cesium.Cartesian3.fromDegrees(112, 23, 0)
const modelMatrix = Cesium.Transforms.eastNorthUpToFixedFrame(modelCenter)

viewer.scene.globe.depthTestAgainstTerrain = true
viewer.scene.primitives.add(new StaticTrianglePrimitive(modelMatrix))
DrawCommand 渲染流程

创建完的 DrawCommand 会通过 update 函数,加载到 frameState 的 commandlist 队列中,比如 Primitive 中 update 加载 drawcommand 的伪代码:

Primitive.prototype.update = function(frameState) {
    var commandList = frameState.commandList;
    var passes = frameState.passes;
    if (passes.render) {
    
        var colorCommand = colorCommands[j];
        commandList.push(colorCommand);
    }

    if (passes.pick) {
        var pickLength = pickCommands.length;
        var pickCommand = pickCommands[k];
        commandList.push(pickCommand);
    }
}

进入队列后就开始听从安排,随时准备上前线(渲染)。Scene 会先对所有的 commandlist 会排序,Pass 值越小优先渲染,通过 Pass 的枚举可以看到最后渲染的是透明的和 overlay:

function createPotentiallyVisibleSet(scene) {
    for (var i = 0; i < length; ++i) {
        var command = commandList[i];
        var pass = command.pass;

        // 优先 computecommand,通过 GPU 计算
        if (pass === Pass.COMPUTE) {
            computeList.push(command);
        } 
        // overlay 最后渲染
        else if (pass === Pass.OVERLAY) {
            overlayList.push(command);
        } 
        // 其他 command
        else {
            var frustumCommandsList = scene._frustumCommandsList;
            var length = frustumCommandsList.length;

            for (var i = 0; i < length; ++i) {
                var frustumCommands = frustumCommandsList[i];
                frustumCommands.commands[pass][index] = command; 
            }
        }
    }
}

Pass.OVERLAY 包括 billboard、shadowmap 的等等。Pass.COMPUTE 主要是 ImageryLayer 中对墨卡托影像切片动态投影以及 sun 等绘制通道(动态计算);frustumCommandList 就包含受离相机的近距离和远距离的约束定义命令列表。
根据渲染优先级排序后,会先渲染环境相关的 command,比如 skybox,大气层等,接着,开始渲染其他command:

function executeCommands(scene, passState) {
    // 地球
    var commands = frustumCommands.commands[Pass.GLOBE];
    var length = frustumCommands.indices[Pass.GLOBE];
    for (var j = 0; j < length; ++j) {
        executeCommand(commands[j], scene, context, passState);
    }

    // 球面
    us.updatePass(Pass.GROUND);
    commands = frustumCommands.commands[Pass.GROUND];
    length = frustumCommands.indices[Pass.GROUND];
    for (j = 0; j < length; ++j) {
        executeCommand(commands[j], scene, context, passState);
    }
    
    // 其他非透明的
    var startPass = Pass.GROUND + 1;
    var endPass = Pass.TRANSLUCENT;
    for (var pass = startPass; pass < endPass; ++pass) {
        us.updatePass(pass);
        commands = frustumCommands.commands[pass];
        length = frustumCommands.indices[pass];
        for (j = 0; j < length; ++j) {
            executeCommand(commands[j], scene, context, passState);
        }
    }

    // 透明的
    us.updatePass(Pass.TRANSLUCENT);
    commands = frustumCommands.commands[Pass.TRANSLUCENT];
    commands.length = frustumCommands.indices[Pass.TRANSLUCENT];
    executeTranslucentCommands(scene, executeCommand, passState, commands);    
    
    // 后面在渲染Overlay

接着就是渲染的过程,也就是把之前VAO,Texture 等等渲染到 FBO 的过程:

DrawCommand.prototype.execute = function(context, passState) {
    // Contex开始渲染
    context.draw(this, passState);
};

Context.prototype.draw = function(drawCommand, passState) {
    passState = defaultValue(passState, this._defaultPassState);
    var framebuffer = defaultValue(drawCommand._framebuffer, passState.framebuffer);

    // 准备工作
    beginDraw(this, framebuffer, drawCommand, passState);
    // 开始渲染
    continueDraw(this, drawCommand);
};

function beginDraw(context, framebuffer, drawCommand, passState) {
    var rs = defaultValue(drawCommand._renderState, context._defaultRenderState);
    // 绑定FBO
    bindFramebuffer(context, framebuffer);
    // 设置渲染状态 
    applyRenderState(context, rs, passState, false);

    // 设置ShaderProgram
    var sp = drawCommand._shaderProgram;
    sp._bind();
}

function continueDraw(context, drawCommand) {
    // 渲染参数
    var primitiveType = drawCommand._primitiveType;
    var va = drawCommand._vertexArray;
    var offset = drawCommand._offset;
    var count = drawCommand._count;
    var instanceCount = drawCommand.instanceCount;

    // 设置Shader中的参数
    drawCommand._shaderProgram._setUniforms(drawCommand._uniformMap, context._us, context.validateShaderProgram);

    // 绑定VAO数据
    va._bind();
    var indexBuffer = va.indexBuffer;

    // 渲染
    if (defined(indexBuffer)) {
        offset = offset * indexBuffer.bytesPerIndex; // offset in vertices to offset in bytes
        count = defaultValue(count, indexBuffer.numberOfIndices);
        if (instanceCount === 0) {
            context._gl.drawElements(primitiveType, count, indexBuffer.indexDatatype, offset);
        } else {
            context.glDrawElementsInstanced(primitiveType, count, indexBuffer.indexDatatype, offset, instanceCount);
        }
    }

    va._unBind();
}
其他

其他有关 Command 的比如 ClearCommand、ComputeCommand;

  • ComputeCommand 需要配合 ComputeEngine 一起使用,可以认为是一个特殊的 DrawCommand,它不是为了渲染,而是通过渲染机制,实现 GPU 的计算,通过 Shader 计算结果保存到纹理传出的一个过程,实现在 Web 前端高效的处理大量的数值计算,下面,我们通过学习之前 ImageryLayer 中对墨卡托影像切片动态投影的过程来了解该过程。
  • ClearCommand 用于清空缓冲区的内容,包括颜色,深度和模板。用户在创建的时候,指定清空的颜色值等属性;

案例:实现四棱锥绘制

import * as Cesium from 'cesium';

class MyPrimitive {

  constructor(modelMatrix) {
    this.modelMatrix = modelMatrix || Cesium.Matrix4.IDENTITY.clone()
    this.drawCommand = null;
    this._texture = null;
  }

  createAnPyramidGeometry() {
    // 处理顶点数据
    const positions = []
    const st = [
      0.5, 0.5,
      0.0, 1.0,
      1.0, 1.0,
      0.0, 0.0,
      1.0, 0.0
    ]

    const _height = 300000
    const center = [116.138641, 23.814026]
    const point1 = vector2Add(center, [-2.0, 2.0])
    const point2 = vector2Add(center, [2.0, 2.0])
    const point3 = vector2Add(center, [-2.0, -2.0])
    const point4 = vector2Add(center, [2.0, -2.0])

    this.rotateLine = {
      v1: new Cesium.Cartesian3(...transformPos(center, 0)),
      v2: new Cesium.Cartesian3(...transformPos(center, _height)),
    }
    /* 
        1 ******************** 2 
          * *                *
          *    *          *  *
          *      *      *    *
          *         * 0      *
          *       *    *     *
          *    *          *  *
          * *                *
        3 ******************** 4 
    */
    positions.push(
      ...transformPos(center, 0),
      ...transformPos(point1, _height),
      ...transformPos(point2, _height),
      ...transformPos(point3, _height),
      ...transformPos(point4, _height)
    )

    const indices = [
      0, 2, 1,
      0, 1, 3,
      0, 3, 4,
      0, 4, 2,
      1, 3, 2,
      2, 3, 4
    ]

    const normal = [
      0, 0.5, 0,
      -0.5, 0, 0,
      0, -0.5, 0,
      0.5, 0, 0,
      0, 0, 1,
    ]

    function vector2Add(vec1, vec2) {
      return [vec1[0] + vec2[0], vec1[1] + vec2[1]]
    }

    function transformPos(lonlat, height) {
      let pos = Cesium.Cartesian3.fromDegrees(lonlat[0], lonlat[1], height)
      return [pos.x, pos.y, pos.z]
    }

    let geometry = new Cesium.Geometry({
      attributes: {
        position: new Cesium.GeometryAttribute({
          componentDatatype: Cesium.ComponentDatatype.FLOAT,
          componentsPerAttribute: 3,
          values: new Float32Array(positions)
        }),
        st: new Cesium.GeometryAttribute({
          componentDatatype: Cesium.ComponentDatatype.FLOAT,
          componentsPerAttribute: 2,
          values: new Float32Array(st)
        }),
        normal: new Cesium.GeometryAttribute({
          componentDatatype: Cesium.ComponentDatatype.FLOAT,
          componentsPerAttribute: 3,
          values: new Float32Array(normal)
        }),
      },
      indices: indices,
      boundingSphere: Cesium.BoundingSphere.fromVertices(positions)
    })

    return geometry
  }
  /**
   * 创建 DrawCommand
   * @param {Cesium.Context} context
   */
  createCommand(context) {
    let geometry = this.createAnPyramidGeometry()
    const attributeLocations = Cesium.GeometryPipeline.createAttributeLocations(geometry)

    let vertexArray = Cesium.VertexArray.fromGeometry({
      context: context,
      geometry: geometry,
      attributeLocations,
    });

    const vertexShaderText = `
    attribute vec3 position;
    attribute vec2 st;
    varying vec2 v_st;
    uniform vec3 v1;
    uniform vec3 v2;

    mat4 RodriguesRotation(vec3 v1, vec3 v2, float theta) {
      vec3 _p = v2 - v1;
      vec3 p = normalize(_p);

      float x = p.x;
      float y = p.y;
      float z = p.z;

      float xy = x * y;
      float xx = x * x;
      float xz = x * z;
      float yy = y * y;
      float yz = y * z;
      float zz = z * z;

      float sintheta = sin(radians(theta));
      float costhta = cos(radians(theta));
      float _m00 = costhta + xx * (1.0 - costhta);
      float _m01 = z * sintheta + xy * (1.0 - costhta);
      float _m02 = -y * sintheta + xz * (1.0 - costhta);
      float _m03 = 0.0;

      float _m10 = -z * sintheta + xy * (1.0 - costhta);
      float _m11 = costhta + yy * (1.0 - costhta);
      float _m12 = x * sintheta + yz * (1.0 - costhta);
      float _m13 = 0.0;

      float _m20 = y * sintheta + xz * (1.0 - costhta);
      float _m21 = -x * sintheta + yz * (1.0 - costhta);
      float _m22 = costhta + zz * (1.0 - costhta);
      float _m23 = 0.0;

      return mat4(_m00, _m01, _m02, _m03, _m10, _m11, _m12, _m13, _m20, _m21, _m22, _m23,0, 0, 0, 1.0);
    }
    void main() {
      float upLimit = 0.3;
      float ty = abs(cos(czm_frameNumber * 0.03)) * upLimit;
      mat4 translateY = mat4(1, 0, 0, 0, 0, 1, 0, ty, 0, 0, 1, 0, 0, 0, 0, 1);
      mat4 rotateAxis = RodriguesRotation(v1, v2, czm_frameNumber);
      vec4 rotateVec = rotateAxis * vec4(position, 1.0);

      v_st = st;
      gl_Position = czm_projection * czm_view * czm_model * rotateVec * translateY;
    }
    `
    const fragmentShaderText = `
    varying vec2 v_st; // 纹理坐标 一般是从顶点着色器中传递过来
    uniform sampler2D texture_map; // 声明 sampler2D 的纹理数据常量

    void main() {
      gl_FragColor = texture2D(texture_map, v_st);
    }`
    let shaderProgram = Cesium.ShaderProgram.fromCache({
      context: context,
      vertexShaderSource: vertexShaderText,
      fragmentShaderSource: fragmentShaderText,
      attributeLocations,
    })

    const uniformMap = {
      u_color() {
        return Cesium.Color.HONEYDEW
      },
      texture_map: () => {
        if(Cesium.defined(this._texture)){
          return this._texture;
        } else {
          return context.defaultTexture;
        }
      },
      v1: () => {
        return this.rotateLine.v1
      },
      v2: () => {
        return this.rotateLine.v2
      }
    }


    let renderState = Cesium.RenderState.fromCache({
      cull: {
        enabled: false,
        face : Cesium.CullFace.BACK
      },
      depthTest: {
        enabled: false
      }
    })

    this.drawCommand = new Cesium.DrawCommand({
      vertexArray: vertexArray,
      shaderProgram: shaderProgram,
      uniformMap: uniformMap,
      renderState: renderState,
      pass: Cesium.Pass.OPAQUE
    })

  }
  //创建纹理
  createTexture(context) {
    if (!this._image) {
      this._image = new Image()
      this._image.src = `http://localhost:5173/src/js/fY6uRrz.png`
      let that = this
      this._image.onload = () => {
        let vTexture = new Cesium.Texture({
          context: context,
          source: this._image,
          // width: 64,
          // height: 64,
          // sampler: new Cesium.Sampler({
          //   wrapS: Cesium.TextureWrap.MIRRORED_REPEAT
          // })
        });
        that._texture = vTexture
      }
    }
  }
  /**
   * 实现Primitive接口,供Cesium内部在每一帧中调用
   * @param {Cesium.FrameState} frameState
   */
  update(frameState) {
    if (!this.drawCommand) {
      this.createCommand(frameState.context)
    }

    if (this._texture === null) {
      this._texture = this.createTexture(frameState.context)
    }
    frameState.commandList.push(this.drawCommand)
  }
}

export default MyPrimitive;

image.png

总结

DrawCommand 就是 Cesium 一个更底层的渲染指令。就是原型链上具备 update 方法的类,且 update 方法接受一个 FrameState 对象,并在执行过程中向这个帧状态对象添加 DrawCommand 的,就能添加至 scene.primitives 这个 PrimitiveCollection 中;
优点就是能精确控制 DrawCommand,就可以在 Cesium 场景中做你想做的绘图。

参考

How to draw transparent objects_.png

;