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, 0)
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
,也許以後會寫寫,不過本篇點到爲止~