1. 引言
Cesium是一款三維地球和地圖視覺化開源JavaScript庫,使用WebGL來進行硬體加速圖形,使用時不需要任何外掛支援,基於Apache2.0許可的開源程式,可以免費用於商業和非商業用途
Cesium官網:Cesium: The Platform for 3D Geospatial
Cesium GitHub站點:CesiumGS/cesium: An open-source JavaScript library for world-class 3D globes and maps (github.com)
API文件:Index - Cesium Documentation
透過閱讀原始碼,理清程式碼邏輯,有助於擴充套件與開發,筆者主要參考了以下兩個系列的文章
渲染是前端視覺化的核心,本文描述Cesium渲染模組的Shader
2. WebGL中的Shader
以下大致是一個最簡的WebGL繪製程式碼:
<canvas id="canvas"></canvas>
<script>
const vertexSource = `
attribute vec3 aPos;
void main()
{
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}
`
const fragmentSource = `
void main()
{
gl_FragColor = vec4(1.0, 0.5, 0.2, 1.0);
}
`
const canvas = document.getElementById('canvas');
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
const gl = canvas.getContext('webgl2');
if (!gl) {
alert('WebGL not supported');
}
const vertices = new Float32Array([
-0.5, -0.5, 0.0,
0.5, -0.5, 0.0,
0.0, 0.5, 0.0,
]);
const vbo = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
const vao = gl.createVertexArray();
gl.bindVertexArray(vao);
gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(0)
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertexSource);
gl.compileShader(vertexShader);
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragmentSource);
gl.compileShader(fragmentShader);
const shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);
gl.clearColor(0.2, 0.3, 0.3, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(shaderProgram);
gl.drawArrays(gl.TRIANGLES, 0, 3);
</script>
其中,著色器(Shader)是執行在GPU上的小程式,這些小程式為圖形渲染管線的某個特定部分而執行,從基本意義上來說,著色器只是一種把輸入轉化為輸出的程式。著色器可以是一個頂點著色器(vertex shader)或片元著色器(fragment shader),每個ShaderProgram都需要這兩種型別的著色器。上述程式碼中建立著色器和著色器程式的程式碼:
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertexSource);
gl.compileShader(vertexShader);
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragmentSource);
gl.compileShader(fragmentShader);
const shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);
建立著色器的步驟大致為:
- 使用
WebGLRenderingContext.createShader()
初始化著色器 - 透過
WebGLRenderingContext.shaderSource()
掛接 GLSL 原始碼 - 最後呼叫
WebGLRenderingContext.compileShader()
完成著色器(shader)的編譯
此時 WebGLShader 仍不是可用的形式,它需要被新增到一個 WebGLProgram
裡
建立著色器程式的步驟大致為:
- 使用
WebGLRenderingContext.createProgram()
初始化著色器程式 - 透過
WebGLRenderingContext.attachShader()
然後附著頂點著色器和片段著色器 - 最後呼叫
WebGLRenderingContext.linkProgram()
完成著色器(shader)的連線
使用著色器程式(上述程式碼中):
// Use the program
gl.useProgram(shaderProgram);
// Draw a single triangle
gl.drawArrays(gl.TRIANGLES, 0, 3);
3. Cesium中的Shader
Cesium渲染模組中的Shader物件包含從建立GLSL到建立Shader Program整個流程
流程大致為:
- Cesium中支援分段編寫GLSL程式碼,包括ShaderStruct、ShaderFunction、ShaderDestination
- 將分段的程式碼組合成GLSL,即ShaderSource
- ShaderBuilder使用ShaderSource建立的ShaderProgram會快取起來,即ShaderCache
- 需要新的ShaderProgram時,先查詢快取中是否有,有就複用,無則建立
在Cesium原始碼中建立ShaderProgram大多也是這個流程,例如PolylineCollection.js
:
const fs = new ShaderSource({
defines: defines,
sources: ["in vec4 v_pickColor;\n", this.material.shaderSource, PolylineFS],
});
const vsSource = batchTable.getVertexShaderCallback()(PolylineVS);
const vs = new ShaderSource({
defines: defines,
sources: [PolylineCommon, vsSource],
});
this.shaderProgram = ShaderProgram.fromCache({
context: context,
vertexShaderSource: vs,
fragmentShaderSource: fs,
attributeLocations: attributeLocations,
});
ShaderProgram.fromCache
只是簡單的指向ShaderCache.getShaderProgram
ShaderProgram.fromCache = function (options) {
// ...
return options.context.shaderCache.getShaderProgram(options);
};
ShaderCache.getShaderProgram
邏輯就是先查詢快取中是否有Shader,有就複用,無則建立:
ShaderCache.prototype.getShaderProgram = function (options) {
// ...
let cachedShader;
if (defined(this._shaders[keyword])) {
cachedShader = this._shaders[keyword];
} else {
const shaderProgram = new ShaderProgram();
cachedShader = {
cache: this,
shaderProgram: shaderProgram,
keyword: keyword,
derivedKeywords: [],
count: 0,
};
}
return cachedShader.shaderProgram;
};
注意,此時的ShaderProgram只是個空殼,它並沒有真正的建立WebGLProgram物件,但是它具備了建立WebGLProgram物件所需要的條件
可以參考原始碼中PointCloud.js
:
function createShaders(){
// ...
drawCommand.shaderProgram = ShaderProgram.fromCache({
context: context,
vertexShaderSource: vs,
fragmentShaderSource: fs,
attributeLocations: attributeLocations,
});
drawCommand.shaderProgram._bind();
}
所以shaderProgram._bind()
才建立了WebGLProgram物件
ShaderProgram.prototype._bind = function () {
initialize(this);
this._gl.useProgram(this._program);
};
function initialize(shader) {
// ...
reinitialize(shader);
}
function reinitialize(shader) {
// ...
const program = createAndLinkProgram(gl, shader, shader._debugShaders);
}
透過多處呼叫,最後createAndLinkProgram(gl, shader)
實現了建立:
function createAndLinkProgram(gl, shader) {
const vsSource = shader._vertexShaderText;
const fsSource = shader._fragmentShaderText;
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vsSource);
gl.compileShader(vertexShader);
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fsSource);
gl.compileShader(fragmentShader);
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
const attributeLocations = shader._attributeLocations;
if (defined(attributeLocations)) {
for (const attribute in attributeLocations) {
if (attributeLocations.hasOwnProperty(attribute)) {
gl.bindAttribLocation(
program,
attributeLocations[attribute],
attribute
);
}
}
}
gl.linkProgram(program);
gl.deleteShader(vertexShader);
gl.deleteShader(fragmentShader);
// ...
return program;
}
值得一說的是,真正的WebGLProgram物件是直到需要繪製時才建立,不需要繪製的就不會建立,這樣有效節省了資源:
function beginDraw(context, framebuffer, passState, shaderProgram, renderState) {
// ...
bindFramebuffer(context, framebuffer);
applyRenderState(context, renderState, passState, false);
shaderProgram._bind();
}
4. 參考資料
[1]WebGLProgram - Web API 介面參考 | MDN (mozilla.org)
[2]WebGLShader - Web API 介面參考 | MDN (mozilla.org)
[3]Cesium原理篇:6 Render模組(3: Shader) - fu*k - 部落格園 (cnblogs.com)
[4]Cesium渲染模組之概述 - 當時明月在曾照彩雲歸 - 部落格園 (cnblogs.com)
[5]Cesium DrawCommand 1 不談地球 畫個三角形 - 嶺南燈火 - 部落格園 (cnblogs.com)