0. 前言
Primitive API
是公开的 API 的最底层了,它面向的场景是高性能、可自定义材质着色器(Appearance API + FabricMaterial Specification
)、动态三维物体。
尽管如此,Primitive API
依然封装了大量几何体类、材质类、WebWorker,而且目前凋谢自定义着色器 API 的只有三维模型类的新架构,还没下放到 Primitive API
。
如果 API 包袱不想那么重,又心愿能够应用本人的模型格局(必须是三角面),那么公有的 DrawCommand + VertexArray
接口就十分适合了,它的格调曾经是最靠近 CesiumJS WebGL 底层的一类 API 了。
DrawCommand
,是 Cesium 封装 WebGL 的一个优良设计,它把绘图数据(VertexArray
)和绘图行为(ShaderProgram
)作为一个对象,待机会适合,也就是 Scene
执行 executeCommand
函数时,帧状态对象上所有的指令对象就会应用 WebGL 函数执行,要什么就 bind 什么,做到了在绘图时的用法统一,下层利用接口只需生成指令对象。
0.1. 源码中的 DrawCommand
譬如在 Primitive.js
模块中的 createCommands
函数,它就是负责把 Primitive
对象的参数化数据或 WebWorker 计算来的数据合并生成 DrawCommand
的中央:
function createCommands(/* 参数省略 */) {
// ...
const length = colorCommands.length;
let vaIndex = 0;
for (let i = 0; i < length; ++i) {
let colorCommand;
// ...
colorCommand = colorCommands[i];
if (!defined(colorCommand)) {colorCommand = colorCommands[i] = new DrawCommand({
owner: primitive, // 入参,即 Primitive 对象
primitiveType: primitive._primitiveType,
});
}
colorCommand.vertexArray = primitive._va[vaIndex]; // VertexArray
colorCommand.renderState = primitive._frontFaceRS; // 渲染状态
colorCommand.shaderProgram = primitive._sp; // ShaderProgram
colorCommand.uniformMap = uniforms; // 对立值
colorCommand.pass = pass; // 该指令的通道程序
}
// ...
}
1. 创立
1.1. 形成因素 – VertexArray
Cesium 把 WebGL 的顶点缓冲和索引缓冲包装成了 Buffer
,而后为了不便,将这些顶点相干的缓冲绑定在了一个对象里,叫做 VertexArray
,外部会启用 WebGL 的 VAO
性能。
最疾速创立 VertexArray
的方法,就是调用其静态方法 VertexArray.fromGeometry()
,然而这须要 Geometry API
来帮忙。
这里想间接应用 Buffer
来阐明,那么就得先创立 Buffer
:
const positionBuffer = Buffer.createVertexBuffer({
context: context,
sizeInBytes: 12,
usage: BufferUsage.STATIC_DRAW,
typedArray: new Float32Array([/* ... */])
})
const attributes = [
{
index: 0,
enabled: true,
vertexBuffer: positionBuffer,
componentsPerAttribute: 3,
componentDatatype: ComponentDatatype.FLOAT,
normalize: false,
offsetInBytes: 0,
strideInBytes: 0, // 严密组合在一起,没有 byteStride
instanceDivisor: 0 // 不实例化绘制
}
]
调用 Buffer
公有类的静态方法 createVertexBuffer()
,即可创立内置了 WebGLBuffer
的顶点缓冲对象 positionBuffer
,而后应用一般的对象数组创立出 顶点属性 attributes
,每个对象就形容了一个顶点属性。接下来就能够拿这些简略的资料创立 VertexArray
了:
const va = new VertexArray({
context: context,
attributes: attributes
})
Context
封装了 WebGL 的各种函数调用,你能够从 Scene
中或间接从 FrameState
上获取到。
这一步创立的
Buffer
,顶点坐标是直角坐标系下的,是最原始的坐标值,除非在着色器里做矩阵变换,或者这些直角坐标就在世界坐标系的地表左近。它是一堆没有具体语义的、纯正数学几何的坐标,与渲染管线无关。所以,对于地表某处的坐标点,通常要配合 ENU 转换矩阵 + 内置的 MVP 转换矩阵来应用,见 1.6 的例子。
这里还有一个例子,应用了两个顶点属性(VertexAttribute):
const positionBuffer = Buffer.createVertexBuffer({
context: context,
sizeInBytes: 12,
usage: BufferUsage.STATIC_DRAW
})
const normalBuffer = Buffer.createVertexBuffer({
context: context,
sizeInBytes: 12,
usage: BufferUsage.STATIC_DRAW
})
const attributes = [
{
index: 0,
vertexBuffer: positionBuffer,
componentsPerAttribute: 3,
componentDatatype: ComponentDatatype.FLOAT
},
{
index: 1,
vertexBuffer: normalBuffer,
componentsPerAttribute: 3,
componentDatatype: ComponentDatatype.FLOAT
}
]
const va = new VertexArray({
context: context,
attributes: attributes
})
这里把坐标缓冲和法线缓冲离开存到两个对象里了,其实 WebGL 能够用字节交织的格局,把全副顶点属性的缓冲都合并成一个的形式的,就不具体讲了,读者能够自行查阅 WebGL 中 WebGLBuffer 的用法。
1.2. 形成因素 – ShaderProgram
WebGL 的着色器也被 CesiumJS 封装了,自带缓存机制,并应用大量正则等伎俩做了着色器源码匹配、解析、治理。
着色器代码由 ShaderSource
治理,ShaderProgram
则治理起多个着色器源码,也就是着色器自身。应用 ShaderCache
作为着色器程序的缓存容器。它们的层级关系如下:
Context
┖ ShaderCache
┖ ShaderProgram
┖ ShaderSource
你能够本人创立 ShaderSource
、ShaderProgram
,并通过 Context
增加到 ShaderCache
中。
举例:
new ShaderSource({sources : [GlobeFS]
})
new ShaderProgram({
gl: context._gl,
logShaderCompilation: context.logShaderCompilation,
debugShaders: context.debugShaders,
vertexShaderSource: vertexShaderSource,
vertexShaderText: vertexShaderText,
fragmentShaderSource: fragmentShaderSource,
fragmentShaderText: fragmentShaderText,
attributeLocations: attributeLocations,
})
然而通常会抉择更间接的形式:
const vertexShaderText = `attribute vec3 position;
void main() {gl_Position = czm_projection * czm_modelView * vec4(position, 1.0);
}`
const fragmentShaderText = `uniform vec3 color;
void main() {gl_FragColor=vec4( color , 1.);
}`
const program = ShaderProgram.fromCache({
context: context,
vertexShaderSource: vertexShaderText,
fragmentShaderSource: fragmentShaderText,
attributeLocations: attributeLocations
})
应用 ShaderProgram.fromCache
静态方法会主动帮你把着色器缓存到 ShaderCache
容器中。
着色器代码能够间接应用内置的常量和主动对立值,这是默认会加上去的。
attributeLocation
是什么?它是一个很一般的 JavaScript 对象:
{
"position": 0,
"normal": 1,
"st": 2,
"bitangent": 3,
"tangent": 4,
"color": 5
}
它批示顶点属性在着色器中的地位。
1.3. 形成因素 – WebGL 的对立值
这个比较简单:
const uniforms = {color() {return Cesium.Color.HONEYDEW}
}
应用一个 JavaScript 对象即可,每个成员必须得是 办法 ,返回的值合乎 Uniform 的要求即可:
Cesium.Matrix2/3/4
→mat2/3/4
Cesium.Cartesian2/3/4
→vec2/3/4
Cesium.Number
→float
Cesium.Color
→vec4
Cesium.Texture
→sampler2D
- …
请查阅 Renderer/createUniform.js
中的代码,例如 UniformFloatVec3
就能够对应 Color
和 Cartesian4
等等。
这个 uniforms
对象最终会在 Context
执行绘制时,与零碎的主动对立值(AutomaticUniforms
)合并。
Context.prototype.draw = function (/*...*/) {
// ...
continueDraw(this, drawCommand, shaderProgram, uniformMap);
// ...
}
1.4. 渲染状态对象 – RenderState
渲染状态对象是必须传递给 DrawCommand
的。渲染状态对象类型是 RenderState
,它与 ShaderProgram
相似,都提供了静态方法来“缓存式”创立:
const renderState = RenderState.fromCache({
depthTest: {enabled: true}
})
哪怕什么都不传递:RenderState.fromCache()
,外部也会返回一个渲染状态。
它传递渲染数据之外所有参加 WebGL 渲染的状态值,在 RenderState
中有具体的默认列表参考,上述代码显式指定要进行深度测试。
1.5. 其它形成因子
创立绘图指令除了 1.1 ~ 1.3 成分之外,还有其它可选项。
① 绘制的通道类型 – Pass
CesiumJS 不是粗犷地把帧状态对象上的 Command 遍历一遍就绘制了的,在 Scene 的渲染过程中,除了生成三大 Command,还有一步要对 Command 进行通道排序。
通道,是一个枚举类型,保留在 Pass.js
模块中。不同通道有不同的优先级,譬如在 1.6 中指定的通道是 Cesium.Pass.OPAQUE
,即不通明通道。在 1.93 版本,通道的程序为枚举值:
const Pass = {
ENVIRONMENT: 0,
COMPUTE: 1,
GLOBE: 2,
TERRAIN_CLASSIFICATION: 3,
CESIUM_3D_TILE: 4,
CESIUM_3D_TILE_CLASSIFICATION: 5,
CESIUM_3D_TILE_CLASSIFICATION_IGNORE_SHOW: 6,
OPAQUE: 7,
TRANSLUCENT: 8,
OVERLAY: 9,
NUMBER_OF_PASSES: 10,
}
可见,OPAQUE
(不通明通道)的优先级比 TRANSLUCENT
(通明通道)高。
这个通道与其它图形 API 的通道可能略不一样,因为你只能应用这个值去指定程序,而不是本人写一个通道来合成渲染(例如 ThreeJS 或 WebGPU)。
② 绘制的图元类型 – WebGL 绘制常数
即指定 VertexArray
中顶点的拓扑格局,在 WebGL 中是通过 drawArrays
指定的:
gl.drawArrays(gl.TRIANGLES, 0, 3)
这个 gl.TRIANGLES
就是图元类型,是一个常数。Cesium 全副封装在 PrimitiveType.js
模块导出的枚举中了:
console.log(PrimitiveType.TRIANGLES) // 4
默认就是 PrimitiveType.TRIANGLES
,所以在 1.6 代码中咱们并不需要传递。
③ 离屏绘制容器 – Framebuffer
CesiumJS 反对把后果画到 Renderbuffer
,也就是 RTR(Render to RenderBuffer)
离屏绘制。绘制到渲染缓冲,是须要帧缓冲容器的,CesiumJS 把 WebGL 1/2 中帧缓冲相干的 API 都封装好了(严格来说,把 WebGL 中的 API 根本都封装了一遍)。
本文只简略提一提,对于帧缓冲离屏绘制,当前有机会再介绍,法克鸡丝的博客有比拟零碎的介绍(尽管比拟旧,不过思路还是在的)。
④ 模型坐标变换矩阵 – Matrix4
将 Matrix4
类型的变量在创立 DrawCommand
时传递进去,它最终会传递到 CesiumJS 的外部对立值:czm_model
(模型矩阵)上,而无需你在 uniform
中指定,你能够在顶点着色器中应用它来对 VertexArray
中的顶点进行模型矩阵变换。见 1.6 中的顶点着色器经典的 MVP 相乘。
⑤ 其它
- cull/occlude: 视锥剔除 + 地平线剔除组合技,Boolean
- orientedBoundingBox/boundingVolume: 范畴框
- count: number,WebGL 绘制时要画多少个点
- offset: number,WebGL 绘制时从多少偏移量开始用顶点数据
- instanceCount: number,实例绘制无关
- castShadows/receiveShadows: Boolean,暗影相干
- pickId: string,若没定义,在 Pick 通道的绘制中将应用深度数据;若定义了将在 GLSL 中转化为 pick id
- …
这些都能够在 DrawCommand
中找到对应的字段,按需设置即可。
1.6. 咱们来实际一发纯色三角形
万事俱备,间接硬搓一个能产生三角形绘制指令的 StaticTrianglePrimitive
,为了便于在官网沙盒中应用,我给官网 API 加上了命名空间:
const modelCenter = Cesium.Cartesian3.fromDegrees(112, 23, 250000 / 2)
const modelMatrix = Cesium.Transforms.eastNorthUpToFixedFrame(modelCenter)
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;
void main(){gl_FragColor = vec4(u_color, 1.0);
}`
const createCommand = (frameState, matrix) => {
const attributeLocations = {"position": 0,}
const uniformMap = {u_color() {return Cesium.Color.HONEYDEW},
}
const positionBuffer = Cesium.Buffer.createVertexBuffer({
usage: Cesium.BufferUsage.STATIC_DRAW,
typedArray: new Float32Array([
10000, 50000, 5000,
-20000, -10000, 5000,
50000, -30000, 5000,
]),
context: frameState.context,
})
const vertexArray = new Cesium.VertexArray({
context: frameState.context,
attributes: [{index: 0, // 等于 attributeLocations['position']
vertexBuffer: positionBuffer,
componentsPerAttribute: 3,
componentDatatype: Cesium.ComponentDatatype.FLOAT
}]
})
const program = Cesium.ShaderProgram.fromCache({
context: frameState.context,
vertexShaderSource: vertexShaderText,
fragmentShaderSource: fragmentShaderText,
attributeLocations: attributeLocations,
})
const renderState = Cesium.RenderState.fromCache({
depthTest: {enabled: true}
})
return new Cesium.DrawCommand({
modelMatrix: matrix,
vertexArray: vertexArray,
shaderProgram: program,
uniformMap: uniformMap,
renderState: renderState,
pass: Cesium.Pass.OPAQUE,
})
}
/* ----- See Here ↓ ------ */
class StaticTrianglePrimitive {
/**
* @param {Matrix4} modelMatrix matrix to WorldCoordinateSystem
*/
constructor(modelMatrix) {this._modelMatrix = modelMatrix}
/**
* @param {FrameState} frameState
*/
update(frameState) {const command = createCommand(frameState, this._modelMatrix)
frameState.commandList.push(command)
}
}
// try!
const viewer = new Cesium.Viewer('cesiumContainer', {
contextOptions: {requestWebgl2: true}
})
viewer.scene.globe.depthTestAgainstTerrain = true
viewer.scene.primitives.add(new StaticTrianglePrimitive(modelMatrix))
显示进去的成果就是一个白绿色的三角形:
图中为大湾区,因为我设的 ENU 坐标核心就是大湾区左近。三角形的高度被我设为了 5000 米。
2. 意义 – 自定义 Primitive(PrimitiveLike)
如果有一个对象或者一个函数,返回的是可绘制的 DrawCommand
,那么只需把返回的指令对象传递给 FrameState
就能够在这一帧把下面的数据和绘图逻辑展现进去。
认真想想,具备创立 DrawCommand
的对象其实不少。有 Primitive
、BillboardCollection
、SkyAtmosphere
、SkyBox
、Sun
、Model
等(3DTiles 瓦片上的模型是通过 Model
绘制的)。
我这里就间接给论断了:
- 具备创立
DrawCommand
性能的,无论是函数,还是对象,都能够直接参与 Cesium 最底层的绘图; - 原型链上具备
update
办法的类,且update
办法承受一个FrameState
对象,并在执行过程中向这个帧状态对象增加DrawCommand
的,就能增加至scene.primitives
这个PrimitiveCollection
中。
前一种有具体的 API,即 Globe
下的 GlobeSurfaceTileProvider
(由 QuadtreePrimitive
应用)创立 DrawCommand
;前面的就多了。
能准确管制 DrawCommand
,就能够在 Cesium 场景中做你想做的绘图。
点到为止
DrawCommand
是 CesiumJS 渲染器之前的最初一道数据封装,前面就是对这些指令对象上的资源进行散发、绑定、执行。读者有趣味的话,还能够自行钻研 ClearCommand
和 ComputeCommand
,兴许当前会写写,不过本篇点到为止~
3. 参考资料
- 博客园 – 法克鸡丝 – Cesium 原理篇:6 Render 模块 (5: VAO&RenderState&Command)
- 知乎 – 三维网格 – Cesium 高性能扩大之 DrawCommand(一):入门