引子
接著 WebGL 基礎概念,做一個繪製直線的簡單示例。
主要參考以下兩篇文章:
繪製一條線
下面不會對每個使用的函式進行詳細的解釋,個人比較喜歡先對整體邏輯有個感覺,實際使用時再按需去查資料。
建立 WebGL 上下文
在基礎概念中有提過是通過 Canvas 元素使用 WebGL :
<canvas id="demo" width="300" height="200"></canvas>
const canvasObj = document.querySelector("#demo");
const glContext = canvasObj.getContext("webgl");
if (!glContext) {
alert("瀏覽器不支援 WebGL");
return;
}
接著準備頂點資料。
準備頂點資料並緩衝
在 WebGL 中所有實物都是在 3D 空間中,繪製一條線需要兩個頂點,每個頂點都有一個 3D 座標:
let vertices = [
-0.5, -0.5, 0.0,
0.5, -0.5, 0.0
];
緩衝有多種型別,頂點緩衝物件的型別是 gl.ARRAY_BUFFER
。
/**
* 設定緩衝
* @param {*} gl WebGL 上下文
* @param {*} vertexData 頂點資料
*/
function setBuffers(gl, vertexData) {
// 建立空白的緩衝物件
const buffer = gl.createBuffer();
// 繫結目標
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
// WebGL 不支援直接使用 JavaScript 原始陣列型別,需要轉換
const dataFormat = new Float32Array(vertexData);
// 初始化資料儲存
gl.bufferData(gl.ARRAY_BUFFER, dataFormat, gl.STATIC_DRAW);
},
bufferData 方法會把資料複製到當前繫結緩衝物件,該方法提供了管理給定資料的引數:
- STATIC_DRAW : 緩衝區的內容可能經常使用,不會經常更改。
- DYNAMIC_DRAW : 緩衝區的內容可能經常被使用,並且經常更改。
- STREAM_DRAW : 緩衝區的內容可能不會經常使用。
直線的資料不會改變,每次渲染都保持不變,所以這裡使用的型別是 STATIC_DRAW
。現在已經把頂點資料儲存在顯示卡的記憶體中,接著開始準備頂點著色器。
頂點著色器
頂點著色器需要用 GLSL ES 語言編寫,在前端書寫形式有兩種:
- script 標籤包裹,使用時像獲取 DOM 物件一樣。
- 純字串。
<script id="shader" type="x-shader/x-vertex">
attribute vec3 vertexPos;
void main(void){
gl_Position = vec4(vertexPos, 1);
}
</script>
<script>
const shader = document.getElementById('shader').innerHTML,
</script>
每個頂點都有一個 3D 座標,建立了一個 vec3
型別輸入變數 vertexPos
,vec3
表示三元組浮點數向量。
main
是入口函式,gl_Position
是著色器內建的變數,GLSL 中一個變數最多 4 個分量,最後一個分量是用在透視除法上。gl_Position
設定的值會成為該頂點著色器的輸出。這裡請回想一下基礎概念中提到的狀態機。
下面是純字元形式:
/**
* 建立頂點著色器
* @param {*} gl WebGL 上下文
*/
function createVertexShader(gl) {
// 頂點著色器 glsl 程式碼
const source = `
attribute vec3 vertexPos;
void main(void){
gl_Position = vec4(vertexPos, 1);
}
`;
// 建立著色器
const shader = gl.createShader(gl.VERTEX_SHADER);
// 設定頂點著色器程式碼
gl.shaderSource(shader, source);
// 編譯
gl.compileShader(shader);
// 判斷是否編譯成功
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
alert("編譯著色器報錯: " + gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
為了讓 WebGL 使用該著色器,必須在執行時動態編譯它的原始碼。
- createShader 函式建立型別為
gl.VERTEX_SHADER
的著色器物件; - compileShader 函式進行編譯。
接著準備片段著色器。
片段著色器
片段著色器也是用 GLSL ES 語言編寫。片段著色器所做的是計算畫素最後的顏色輸出,這裡直接簡化指定輸出白色。gl_FragColor
是內建變數,表示顏色,4 個分量分別對應 R、G、B、A。
/**
* 建立片段著色器
* @param {*} gl WebGL 上下文
*/
function createFragmentShader(gl) {
// 片段著色器 glsl 程式碼
const source = `
void main(void){
gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
}
`;
// 建立著色器
const shader = gl.createShader(gl.FRAGMENT_SHADER);
// 設定片段著色器程式碼
gl.shaderSource(shader, source);
// 編譯
gl.compileShader(shader);
// 判斷是否編譯成功
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
alert("編譯著色器報錯: " + gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
},
兩個著色器都準備好後,需要進行連結合併才能使用。
著色器程式
著色器程式物件是多個著色器合併之後並最終連結完成的版本。當連結著色器至一個程式的時候,它會把每個著色器的輸出連結到下個著色器的輸入。當輸出和輸入不匹配的時候,會得到一個連線錯誤。
- createProgram 函式建立物件;
- attachShader 新增著色器;
- linkProgram 連結新增的著色器。
當需要啟用這個著色器的時候,把該物件作為引數呼叫 useProgram 函式。
/**
* 初始化著色器程式
* @param {*} gl WebGL 上下文
* @param {*} vertexShader 頂點著色器
* @param {*} fragmentShader 片段著色器
*/
function initShaderProgram(gl, vertexShader, fragmentShader) {
// 建立著色器物件
const shaderProgram = gl.createProgram();
// 新增著色器
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
// 多個著色器合併連結
gl.linkProgram(shaderProgram);
// 建立是否成功檢查
if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
alert("無法初始化著色器程式: " + gl.getProgramInfoLog(shaderProgram));
return null;
}
return shaderProgram;
}
目前為止,已經把輸入頂點資料傳送給了 GPU ,並指示了 GPU 如何在頂點和片段著色器中處理它。最後就剩下繪製了。
繪製
- vertexAttribPointer 函式告訴 WebGL 如何解釋頂點資料;
- enableVertexAttribArray 函式啟用頂點屬性,頂點屬性預設是禁用的;
useProgram
函式啟用著色器;drawArrays 函式進行繪製,第一個引數是繪製的圖元的型別,繪製的是直線,所以是
gl.LINE_STRIP
。/** * 初始化著色器程式 * @param {*} gl WebGL 上下文 * @param {*} shaderProgram 著色器程式物件 */ function draw(gl, shaderProgram) { // 獲取對應資料索引 const vertexPos = gl.getAttribLocation(shaderProgram, "vertexPos"); // 解析頂點資料 gl.vertexAttribPointer(vertexPos, 3, gl.FLOAT, false, 0, 0); // 啟用頂點屬性,頂點屬性預設是禁用的。 gl.enableVertexAttribArray(vertexPos); // 啟用著色器 gl.useProgram(shaderProgram); // 繪製 gl.drawArrays(gl.LINE_STRIP, 0, 2); }
例
這是示例,整體的邏輯大概是這樣的:
const canvasObj = document.querySelector("#demo");
const glContext = canvasObj.getContext("webgl");
let vertices = [-0.5, -0.5, 0.0, 0.5, -0.5, 0.0]; // 頂點資料
setBuffers(glContext, vertices); // 緩衝資料
const vertexShader = createVertexShader(glContext); // 頂點著色器
const fragmentShader = createFragmentShader(glContext); // 片段著色器
const shaderProgram = initShaderProgram(
glContext,
vertexShader,
fragmentShader
); // 著色器程式物件
draw(glContext, shaderProgram); // 繪製
這裡面涉及很多的方法和變數,一開始的時候真的懵,多看幾次親自敲下程式碼後會慢慢習慣。
接下來會對期間產生的一些疑問進行總結,見 JavaScript WebGL 基礎疑惑點。