《WebGL編程指南》筆記(一)
前言
還是忍不住好奇心,先立flag再學習,這不是我的風格(本來是Q4的flag,想Q3偷跑的)。
主要是男神離職了,加速了這個進程;
不過說都說了,立又立了,反正也就一個早晚問題,就多多少少學點防身吧;
選定的書是《WebGL編程指南》,2014年的書,就是說下面的內容是2014年的時候就有的了;
現在也沒有第二本說webgl的書了,看來webgl貌似不行了?
隔壁《openGL編程指南》都第9版了~
anyway,開始吧~
PS:
1. 書本的代碼極少部分有bug;
2. 下面的代碼都是跑過沒問題的 ;
一. WebGL概述
1. WebGL的優勢
- 用文本編輯器即可開發(。。。這怎么聽起來像個缺點。。。IDE不香嗎?)
- 輕松發布三維圖形程序(應該說調試吧,畢竟不用編譯;發布的話感覺都差不多~)
- 充分利用瀏覽器功能(這個也是,有html基礎的話對比U3D,起碼少學2d的一部分內容了)
- 學習和使用WebGL很簡單(很簡單。。簡單。。。單。。。)
2. WebGL起源
三維渲染技術一般是微軟的Direct3D和OpenGL。Direct3D只能用于windows;而OpenGL是免費又跨平臺的。
現在(2022-7-7)已經有WebGL2.0了:https://blog.csdn.net/weixin_37683659/article/details/80160425
WebGL因為是渲染在Canvas上面的,所以做多線程會比較雞肋(Canvas依附主線程),但是 --
現在已經有webGPU和webWorker的配合使用了:https://zhuanlan.zhihu.com/p/457600943
未來可期~

- OpenGL ES:主要是嵌入式設備的,手機啥的;
- OpenGL:主要是PC主機上的;
- WebGL:基于OpenGL ES 2.0外圍包的一層供js調用的接口;
- GLSL:OpenGL著色器語言;
- GLSL ES:OpenGL ES著色器語言;
3. WebGL程序結構

- GLSL ES也是寫在js文件里面,所以感覺上是沒啥區別的。
二. WebGL入門
1. Canvas是什么?
- 步驟:1. 獲取canvas元素;2. 使用該元素獲取上下文(context);3. 在context上進行2d或者3d操作;
- canvas可以同時支持2d圖形或3d圖形;
- canvas的原點在左上角,向左是正X軸,向下是正Y軸;
- ctx.fillRect(矩形左上頂點的X軸坐標, 矩形左上頂點的Y軸坐標, 寬, 高)
- 下面是以繪制一個藍色矩形為例:
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>畫一個藍色矩形(canvas版)</title> </head> <body onload="main()"> <canvas id="example" width="400" height="400"> 請使用支持canvas的瀏覽器 </canvas> </body> <script src="DrawRectangle.js"></script> </html>
DrawRectangle.js
function main(){ const canvas = document.getElementById("example"); if(!canvas){ console.error("獲取不到canvas"); return; } // 獲取二維圖形的繪圖上下文 const ctx = canvas.getContext("2d"); // 繪制藍色矩形 ctx.fillStyle = 'blue'; // 設置填充顏色 ctx.fillRect(120, 10, 150, 150); // 使用填充顏色填充矩形 }
2. 最短的WebGL程序:清空繪圖區
- WebGL的context跟canvas的context是不一樣的;
- 引入的東西在:http://rodger.global-linguist.com/webgl/examples.zip 這里可以下載;
- gl.clearColor(red, green, blue, alpha),指定了清空畫布所需要的顏色;
- gl.clear(buffer),把指定的緩沖區設置為預設值;
- gl.clear(buffer),buffer可以用位操作符"|"間開指定多個值;
- gl.clear(gl.COLOR_BUFFER_BIT),使用 gl.clearColor(red, green, blue, alpha) 指定的顏色填充,默認值:(0.0, 0.0, 0.0, 0.0);
- gl.clear(gl.DEPTH_BUFFER_BIT),使用 gl.clearDepth(depth) 指定的深度緩沖區,默認值:(1.0);
- gl.clear(gl.STENCIL_BUFFER_BIT),使用 gl.clearStencil(s) 指定的模板緩沖區,默認值:(0);
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>使用指定顏色清空畫布</title> </head> <body onload="main()"> <canvas id="example" width="400" height="400"> 請使用支持canvas的瀏覽器 </canvas> </body> <script src="../lib/webgl-utils.js"></script> <script src="../lib/webgl-debug.js"></script> <script src="../lib/cuon-utils.js"></script> <script src="clearCanvas.js"></script> </html>
clearCanvas.js
function main(){ const canvas = document.getElementById("example"); // 獲取二維圖形的繪圖上下文 const gl = getWebGLContext(canvas); if(!gl){ console.error("webgl渲染失敗"); return; } // 指定清空畫布所用的顏色 gl.clearColor(0, 0, 1, 1); // 使用上面指定的顏色清空畫布 gl.clear(gl.COLOR_BUFFER_BIT); }
3. 繪制一個點(版本1)
- webGL的繪制依賴著色器(shader)的繪圖機制;
- 著色器(shader)提供了二維和三維的繪圖方法;
- 著色器程序是以字符串的形式嵌入到js文件中;
- 頂點著色器(Vertex shader):用來描述頂點特性(如位置、顏色等);
- 頂點(Vertex):指二維或三維空間中的一個點;
- 片元著色器(Fragment shader):進行逐片元處理過程,比如光照;
- 片元(Fragment):一個webGL術語,可以理解為像素;
- 瀏覽器渲染過程如下圖:

- 這些以字符串形式出現的就是我們的“OpenGL ES著色器語言(GLSL ES)”;
- vec4():表示由4個浮點數組成的矢量(又叫向量);
- gl_Position:中vec4分別表示(x, y, z, w),其中w表示齊次坐標,取值范圍在(0, 1];
- gl_Position:齊次坐標(x, y, z, w)等價于三維坐標(x/w, y/w, z/w);
- gl_Position:如果w趨近于0,表示的點將趨近無窮遠;
- gl_PointSize:表示點尺寸(像素),默認為1;
- gl_FragColor:指定片元顏色,vec4分別表示(r, g, b, a)值;
- gl.drawArrays(mode, first, count):執行頂點著色器,按照mode指定的方式繪制;
- mode - gl.POINTS,gl.LINES,gl.LINE_STRIP,gl.LINE_LOOP,gl.TRIANGLES,gl.TRIANGLE_STRIP,gl.TRIANGLE_FAN;
- first - 從那個頂點開始繪制;
- count - 需要繪制到第幾個頂點;
- webGL坐標系統 - 采用笛卡爾坐標系;
- webGL坐標系統 - 面向屏幕:X軸正向-水平向右;Y軸正向-垂直向上;Z軸正向-垂直屏幕向外;
- webGL坐標系統 - 原點在畫布中心;
cuon-utils.js中關于創建program對象的部分
// 初始化著色器 function initShaders(gl, vshader, fshader) { var program = createProgram(gl, vshader, fshader); if (!program) { console.log('Failed to create program'); return false; } // 使用這個program對象 gl.useProgram(program); // initShaders的時候順手把program注入到gl里面 gl.program = program; return true; } // 新建并返回一個program對象 function createProgram(gl, vshader, fshader) { // 新建著色器對象 var vertexShader = loadShader(gl, gl.VERTEX_SHADER, vshader); var fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fshader); if (!vertexShader || !fragmentShader) { return null; } // 新建program對象 var program = gl.createProgram(); if (!program) { return null; } // 綁定(Attach)著色器對象到program gl.attachShader(program, vertexShader); gl.attachShader(program, fragmentShader); // 鏈接program對象 gl.linkProgram(program); // 檢查鏈接狀態 var linked = gl.getProgramParameter(program, gl.LINK_STATUS); if (!linked) { var error = gl.getProgramInfoLog(program); console.log('Failed to link program: ' + error); gl.deleteProgram(program); gl.deleteShader(fragmentShader); gl.deleteShader(vertexShader); return null; } return program; }
draw_point_1.js
function main(){ const canvas = document.getElementById("example"); // 獲取二維圖形的繪圖上下文 const gl = getWebGLContext(canvas); if(!gl){ console.error("webgl渲染失敗"); return; } // 頂點著色程序 const VSHADER_SOURCE = "void main(){" + " gl_Position = vec4(0.0, 0.0, 0.0, 1.0);" + // 設置坐標,必須有值 " gl_PointSize = 10.0;" + // 設置尺寸,默認為1 "}"; // 片源著色器程序 const FSHADER_SOURCE = "void main(){" + " gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);" + // 設置顏色 "}"; // 初始化著色器 if(!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)){ console.log('初始化著色器失敗'); return; } // 清空canvas gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); // 繪制一個點 gl.drawArrays(gl.POINTS, 0, 1); }
3. 繪制一個點(版本2)
- JavaScript程序可以通過attribute變量和uniform變量傳值給頂點著色器;
- 傳值流程圖如下:

- uniform變量:用于傳輸那些對所有頂點都相同或與頂點無關的數據;
- attribute變量:用于從外部向頂點著色器內傳數據,只有頂點著色器可以使用它;
- attribute變量:聲明 - attribute [類型] [變量名];
- attribute變量:每一個變量都會有一個存儲地址,需要通過getAttribLocation()來向webGL獲取該地址;
- attribute變量:外部js通過 - gl.getAttribLocation([webGL著色器程序], [變量名]) 來獲取變量地址;
- attribute變量:獲取變量地址后通過 - gl.vertexAttrib3f([變量名], [浮點數1], [浮點數2], [浮點數3]) 來修改;
- attribute變量:gl.vertexAttrib3f的同族函數:vertexAttrib[n]f[v]() - n為1~4數字,表示多少個浮點數,v為這個方法的向量(vector)版,用法如下:
var position = new Float32Array([1.0, 2.0, 3.0, 1.0]); gl.vertexAttrib4fv(a_Position, positon);
- attribute變量:同族里面還有一個int型的 - vertexAttribI4[u]i[v](),但是在macbook上firefox和chrome都用不了;
draw_point_2.js
function main(){ const canvas = document.getElementById("example"); // 獲取二維圖形的繪圖上下文 const gl = getWebGLContext(canvas); if(!gl){ console.error("webgl渲染失敗"); return; } // 頂點著色程序 const VSHADER_SOURCE = "attribute vec4 a_Position;" + "attribute float a_PointSize;" + "void main(){" + " gl_Position = a_Position;" + // 設置坐標 " gl_PointSize = a_PointSize;" + // 設置尺寸 "}"; // 片源著色器程序 const FSHADER_SOURCE = "void main(){" + " gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);" + // 設置顏色 "}"; // 初始化著色器 if(!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)){ console.log('初始化著色器失敗'); return; } // 在著色器程序(program)中獲取指定名稱的頂點著色器變量地址,估計是數組地址 let a_Position = gl.getAttribLocation(gl.program, 'a_Position'); // a_Position的值為0 let a_PointSize = gl.getAttribLocation(gl.program, 'a_PointSize'); // a_Position的值為1 if(a_Position < 0 || a_PointSize < 0){ console.log("獲取失敗"); return; } // 把頂點位置傳給attribute變量。1f表示1個浮點數,如此類推 gl.vertexAttrib3f(a_Position, 0.0, 0.0, 0.0); gl.vertexAttrib1f(a_PointSize, 20.0); // 清空canvas gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); // 繪制一個點 gl.drawArrays(gl.POINTS, 0, 1); }
4. 通過鼠標點擊繪點
- canvas坐標系和webGL坐標系如下圖:

- webGL的坐標是分量值,可以理解為按照百分比來算的;
- 換個說法:上圖canvas如果是長方形,那么webGL坐標x或y軸方向取值范圍也會是-1 ~ 1;
- webgl_x = (canvas_x - width / 2) / (width / 2);
- webgl_y = -(canvas_y - height / 2) / (height / 2);
- 另外下面demo,當點擊渲染正方形后,會發現鼠標在正方形的中心點;
draw_point_mouse.js
function main(){ const canvas = document.getElementById("example"); // 獲取二維圖形的繪圖上下文 const gl = getWebGLContext(canvas); if(!gl){ console.error("webgl渲染失敗"); return; } // 頂點著色程序 const VSHADER_SOURCE = "attribute vec4 a_Position;" + "attribute float a_PointSize;" + "void main(){" + " gl_Position = a_Position;" + // 設置坐標 " gl_PointSize = a_PointSize;" + // 設置尺寸 "}"; // 片源著色器程序 const FSHADER_SOURCE = "void main(){" + " gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);" + // 設置顏色 "}"; // 初始化著色器 if(!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)){ console.log('初始化著色器失敗'); return; } // 在著色器程序(program)中獲取指定名稱的頂點著色器變量地址,估計是數組地址 let a_Position = gl.getAttribLocation(gl.program, 'a_Position'); // a_Position的值為0 let a_PointSize = gl.getAttribLocation(gl.program, 'a_PointSize'); // a_Position的值為1 if(a_Position < 0 || a_PointSize < 0){ console.log("獲取失敗"); return; } // 指定半徑 gl.vertexAttrib1f(a_PointSize, 30); let g_points = []; // 存儲點擊的點坐標 // 注冊鼠標響應事件 canvas.onmousedown = function(e){ let x = e.clientX; // 鼠標相對于屏幕的水平坐標 let y = e.clientY; // 鼠標相對于屏幕的垂直坐標 let rect = e.target.getBoundingClientRect(); // canvas對象的位置信息 // (x - rect.left):鼠標在x軸方向上的偏移,相當于canvas的x軸坐標 x = ((x - rect.left) - canvas.width / 2) / (canvas.width / 2); // (y - rect.top):鼠標在y軸方向上的偏移,相當于canvas的y軸坐標 y = -((y - rect.top) - canvas.height / 2) / (canvas.height / 2); g_points.push({x, y}); // 清空canvas gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); // 繪制點 for(let i = 0; i < g_points.length; i++){ gl.vertexAttrib3f(a_Position, g_points[i].x, g_points[i].y, 0.0); gl.drawArrays(gl.POINTS, 0, 1); } }; }
5. 改變點的顏色
- 使用uniform變量給片元著色器傳值,從而讓每個點擊的點上色;
- webgl的顏色是分量值,與rgb的轉換關系:webgl_color = rgb_color / 255;
- precision為精度限定詞,總的來說精度越高,執行效率越低;
- uniform變量:通過 gl.getUniformLocation([webGL著色器程序], [變量名]) 來獲取變量地址;
- uniform變量:變量不存在返回null;
- uniform變量:通過 gl.uniform4f([變量名], [值1], [值2], [值3], [值4]) 設置值;
- uniform變量:同族函數 - uniform[1234][fi][v]() 其中:f - 浮點;i - 整型;v:向量
draw_point_mouse_color.js
function main(){ const canvas = document.getElementById("example"); // 獲取二維圖形的繪圖上下文 const gl = getWebGLContext(canvas); if(!gl){ console.error("webgl渲染失敗"); return; } // 頂點著色程序 const VSHADER_SOURCE = "attribute vec4 a_Position;" + "attribute float a_PointSize;" + "void main(){" + " gl_Position = a_Position;" + // 設置坐標 " gl_PointSize = a_PointSize;" + // 設置尺寸 "}"; // 片源著色器程序 const FSHADER_SOURCE = "precision mediump float;" + // 定義使用中等精度的浮點數 "uniform vec4 u_FragColor;" + "void main(){" + " gl_FragColor = u_FragColor;" + // 設置顏色 "}"; // 初始化著色器 if(!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)){ console.log('初始化著色器失敗'); return; } // 在著色器程序(program)中獲取指定名稱的頂點著色器變量地址,估計是數組地址 let a_Position = gl.getAttribLocation(gl.program, 'a_Position'); // a_Position的值為0 let a_PointSize = gl.getAttribLocation(gl.program, 'a_PointSize'); // a_Position的值為1 let u_FragColor = gl.getUniformLocation(gl.program, 'u_FragColor'); if(a_Position < 0 || a_PointSize < 0 || !u_FragColor){ console.log("獲取失敗"); return; } // 指定半徑 gl.vertexAttrib1f(a_PointSize, 5); // 清空canvas gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); let g_points = []; // 存儲點擊的點坐標 let COLORS_CARD = [ [243, 155, 58], [249, 206, 82], [101, 148, 68], [132, 185, 182], [113, 100, 144] ]; // 色卡 // 注冊鼠標響應事件 canvas.onmousedown = function(e){ let x = e.clientX; // 鼠標相對于屏幕的水平坐標 let y = e.clientY; // 鼠標相對于屏幕的垂直坐標 let rect = e.target.getBoundingClientRect(); // canvas對象的位置信息 // (x - rect.left):鼠標在x軸方向上的偏移,相當于canvas的x軸坐標 x = ((x - rect.left) - canvas.width / 2) / (canvas.width / 2); // (y - rect.top):鼠標在y軸方向上的偏移,相當于canvas的y軸坐標 y = -((y - rect.top) - canvas.height / 2) / (canvas.height / 2); g_points.push({ x, y, // 輪取色卡值,因為webgl的顏色是分量值,webgl_color = rgb_color / 255 color: COLORS_CARD[g_points.length % COLORS_CARD.length].map(item => item / 255) }); // 清空canvas gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); // 繪制點 for(let i = 0; i < g_points.length; i++){ gl.vertexAttrib3f(a_Position, g_points[i].x, g_points[i].y, 0.0); gl.uniform4f(u_FragColor, ...g_points[i].color, 1); gl.drawArrays(gl.POINTS, 0, 1); } }; }
三. 繪制和變換三角形
1. 繪制多個點
- 構成三維模型的基本單位是三角形;
- 緩沖區對象:可以一次性地向著色器傳入多個頂點數據;
- 寫入緩沖區對象的五個步驟:
- 創建緩沖區對象(gl.createBuffer())
- 綁定緩沖區對象(gl.bindBuffer())
- 把數據寫入緩沖區對象(gl.bufferData())
- 把緩沖區對象分配給一個attribute變量(gl.vertexAttribPointer())
- 開啟attribute變量(gl.enableVertexAttribArray())

- createBuffer:返回一個WebGLBuffer的實例;
- createBuffer:可以通過 gl.deleteBuffer(buffer) 刪除指定的緩沖區對象;
- bindBuffer:允許使用buffer的緩沖區并綁到target指定的目標上;
- bindBuffer:gl.bindBuffer([target], [buffer]);
- bindBuffer:target -
- gl.ARRAY_BUFFER:表示緩沖區對象中包含了頂點的數據;
- gl.ELEMENT_ARRAY_BUFFER:表示緩沖區中包含了頂點的索引值;
- bindBuffer:buffer - 通過createBuffer返回的緩沖區對象;
- bufferData:向綁定再target上的緩沖區對象中寫入數據data
- bufferData:gl.bufferData([target], [data], [usage]);
- bufferData:target同上為 - gl.ARRAY_BUFFER / gl.ELEMENT_ARRAY_BUFFER;
- bufferData:data - 寫入的數據,類型化數組(關于類型化數組的描述可以參考:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Typed_arrays);
- bufferData:usage - 表示程序將如何使用緩沖區對象中的數據,可以幫助webGL優化操作、增加效率,取值范圍如下 :
- gl.STATIC_DRAW:只向緩沖區對象寫入一次數據,但要繪制很多次;
- gl.STREAM_DRAW:只向緩沖區對象寫入一次數據,然后繪制若干次;
- gl.DYNAMIC_DRAW:會向緩沖區對象寫入多次數據,并且繪制很多次;
- 類型化數組的常用操作如下圖,感覺就跟node的buffer操作差不多:

- vertexAttribPointer:將綁定到gl.ARRAY_BUFFER的緩沖區對象分配給由location指定的attribute變量;
- vertexAttribPointer:gl.ertexAttribPointer(location, size, type, normalized, stride, offset);
- vertexAttribPointer:location - getAttribLocation的返回值,綁定了attribute變量;
- vertexAttribPointer:size - 指定緩沖區中每個頂點的分量個數,取值[1, 4],依次表示 x、y、z、w,不足按xyz默認為0,w默認為1補齊;
- vertexAttribPointer:type - 指定數據格式,包括:
- gl.UNSIGNED_BYTE:相當于類型化數組的 - Uint8Array;
- gl.SHORT:相當于類型化數組的 - Int16Array;
- gl.UNSIGNED_SHORT:UInt16Array;
- gl.INT:Int32Array;
- gl.UNSIGNED_INT:Uint32Array;
- gl.FLOAT:Float32Array;
- vertexAttribPointer:normalized - 是否將非浮點類型的數字歸一化到 [0, 1] 或 [-1, 1] 區間;
- vertexAttribPointer:stride - 指定相鄰兩個頂點的字節數,默認為0(0表示按照type的位數直接平分offset之后剩下的buffer);
- vertexAttribPointer:offset - 以字節為單位,指定緩沖區對象中的偏移量(即attribute變量從緩沖區中的何處開始存儲);

- vertexAttribPointer:在第二部分第五章有補充用法的demo;
- enableVertexAttribArray:開啟location指定的attribute變量;
- enableVertexAttribArray:enableVertexAttribArray(location);
- enableVertexAttribArray:可以使用disableVertexAttribArray(location)來關閉;
- drawArrays不能超過緩沖區的點數量;
draw_multi_points.js
function main(){ const canvas = document.getElementById("example"); // 獲取二維圖形的繪圖上下文 const gl = getWebGLContext(canvas); if(!gl){ console.error("webgl渲染失敗"); return; } // 頂點著色程序 const VSHADER_SOURCE = "attribute vec4 a_Position;" + "void main(){" + " gl_Position = a_Position;" + // 設置坐標 " gl_PointSize = 10.0;" + // 設置尺寸 "}"; // 片源著色器程序 const FSHADER_SOURCE = "void main(){" + " gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);" + // 設置顏色 "}"; // 初始化著色器 if(!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)){ console.log('初始化著色器失敗'); return; } // 兩個一組表示三角形三個頂點的x,y坐標 const vertices = new Float32Array([ 0, 0.5, -0.5, -0.5, 0.5, -0.5 ]); const n = 3; // 頂點個數 // (1)創建緩沖區對象 const vertexBuffer = gl.createBuffer(); if(!vertexBuffer){ console.error("創建緩沖區對象失敗"); return; } // (2)把緩沖區對象綁定到目標 gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); // (3)向緩沖區對象寫入數據 gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); const a_Position = gl.getAttribLocation(gl.program, "a_Position"); // (4)把緩沖區對象分配給a_Position變量 gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0); // (5)鏈接a_Position變量與分配給他的緩沖區對象 gl.enableVertexAttribArray(a_Position); // 清空canvas gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); // 繪制三個點 gl.drawArrays(gl.POINTS, 0, 3); }
2. 繪制三角形、正方形
- gl.drawArrays:有7種不同的繪制模式:
- gl.POINTS:一系列點,繪制在 v0, v1, v2... ;
- gl.LINES:一系列線段,繪制在 (v0, v1), (v2, v3), (v4, v5)... ;
- gl.LINE_STRIP:一系列連接的線段,繪制在 (v0, v1), (v1, v2), (v2, v3)... ;
- gl.LINE_LOOP:一系列連接且閉合的線段,繪制在 (v0, v1), (v1, v2), (v2, v3), ..., (vn, v0);
- gl.TRIANGLES:一系列單獨的三角形,繪制在 (v0, v1, v2), (v3, v4, v5)... ;
- gl.TRIANGLE_STRIP:一系列條帶狀的三角形,繪制在 (v0, v1, v2), (v2, v1, v3), (v2, v3, v4)... ;
- gl.TRIANGLE_STRIP:當前點為奇數點,點序:(n, n + 1, n + 2);當前點為偶數,點序:(n + 1, n, n + 2);
- gl.TRIANGLE_STRIP - 確保每個三角形都是逆時針方向連接 => 統一法向量方向 => 光反射方向;
- gl.TRIANGLE_FAN:一系列三角形組成類似扇形的圖形,繪制在 (v0, v1, v2), (v0, v2, v3), (v0, v3, v4)... ;
draw_function_demo.js
function main(){ const canvas = document.getElementById("example"); // 獲取二維圖形的繪圖上下文 const gl = getWebGLContext(canvas); if(!gl){ console.error("webgl渲染失敗"); return; } // 頂點著色程序 const VSHADER_SOURCE = "attribute vec4 a_Position;" + "void main(){" + " gl_Position = a_Position;" + // 設置坐標 " gl_PointSize = 10.0;" + // 設置尺寸 "}"; // 片源著色器程序 const FSHADER_SOURCE = "void main(){" + " gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);" + // 設置顏色 "}"; // 初始化著色器 if(!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)){ console.log('初始化著色器失敗'); return; } // 兩個一組表示三角形三個頂點的x,y坐標 const vertices = new Float32Array([ 0, 0.5, -0.5, -0.5, 0.5, -0.5, -0.5, 0.5, 0.5, 0.5 ]); const n = 3; // 頂點個數 // (1)創建緩沖區對象 const vertexBuffer = gl.createBuffer(); if(!vertexBuffer){ console.error("創建緩沖區對象失敗"); return; } // (2)把緩沖區對象綁定到目標 gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); // (3)向緩沖區對象寫入數據 gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); const a_Position = gl.getAttribLocation(gl.program, "a_Position"); // (4)把緩沖區對象分配給a_Position變量 gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0); // (5)鏈接a_Position變量與分配給他的緩沖區對象 gl.enableVertexAttribArray(a_Position); // 清空canvas gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); // 繪制三個點,不能超過緩沖區的點數 gl.drawArrays(gl.POINTS, 1, 4); // 繪制三角形 gl.drawArrays(gl.TRIANGLES, 0, 3); // 繪制線段 gl.drawArrays(gl.LINES, 0, 3); // 繪制線段 gl.drawArrays(gl.LINE_STRIP, 0, 3); // 繪制線段 gl.drawArrays(gl.LINE_LOOP, 0, 3); // 繪制四方形,相當于兩個直角三角形相連 gl.drawArrays(gl.TRIANGLE_STRIP, 1, 4); // 繪制四方形,4分3個正方形 gl.drawArrays(gl.TRIANGLE_FAN, 1, 4); }
3. 移動
- 移動前坐標:(x, y, z, w),移動后坐標:(x_after, y_after, z_after, w_after),位移向量:(x_delta, y_delta, z_delta, w_delta),關系如下:
- x + x_delta = x_after;y + y_delta = y_after;z + z_delta = z_after;w + w_delta = 1;
- 其中變化后的必為1;
- GLSL中兩個vec4變量相加表示為:兩個變量各自對應的位置相加,如下圖:
move.js
function main(){ const canvas = document.getElementById("example"); // 獲取二維圖形的繪圖上下文 const gl = getWebGLContext(canvas); if(!gl){ console.error("webgl渲染失敗"); return; } // 頂點著色程序 const VSHADER_SOURCE = "attribute vec4 a_Position;" + "uniform vec4 u_Translation;" + "void main(){" + " gl_Position = a_Position + u_Translation;" + // 設置坐標 " gl_PointSize = 10.0;" + // 設置尺寸 "}"; // 片源著色器程序 const FSHADER_SOURCE = "void main(){" + " gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);" + // 設置顏色 "}"; // 初始化著色器 if(!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)){ console.log('初始化著色器失敗'); return; } // 兩個一組表示三角形三個頂點的x,y坐標 const vertices = new Float32Array([ -0.5, -0.5, 0.5, -0.5, -0.5, 0.5, 0.5, 0.5 ]); // (1)創建緩沖區對象 const vertexBuffer = gl.createBuffer(); if(!vertexBuffer){ console.error("創建緩沖區對象失敗"); return; } // (2)把緩沖區對象綁定到目標 gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); // (3)向緩沖區對象寫入數據 gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); const a_Position = gl.getAttribLocation(gl.program, "a_Position"); const u_Translation = gl.getUniformLocation(gl.program, "u_Translation"); // (4)把緩沖區對象分配給a_Position變量 gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0); gl.uniform4f(u_Translation, 0.25, 0.25, 0.0, 0.0); // 按照向量 (0.25, 0.25) 移動 // (5)鏈接a_Position變量與分配給他的緩沖區對象 gl.enableVertexAttribArray(a_Position); // 清空canvas gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); // 繪制三個點,不能超過緩沖區的點數 gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); }
4. 旋轉
- 通過 旋轉軸(向量值)、旋轉方向(順逆時針)、旋轉角度來描述一個旋轉;
- 正旋轉 -> 角度為正值 -> 沿旋轉軸方向的逆時針旋轉;
- 設旋轉前向量為(x, y),旋轉后向量為(x_after, y_after),旋轉角度為p,2D旋轉公式為:
- x_after = x * cos(p) - y * sin(p);
- y_after = x * sin(p) + y * cos(p);
- z_after = z;
- 推理用到的公式:sin(A +/- B) = sinA * cosB +/- cosA * sinB;cos(A +/- B) = cosA * cosB -/+ sinA * sinB;
- 弧度(rad):弧長等于半徑的弧,其所對的圓心角為1弧度。
- 角度弧度轉換:180o = π * rad;
rotate.js
function main(){ const canvas = document.getElementById("example"); // 獲取二維圖形的繪圖上下文 const gl = getWebGLContext(canvas); if(!gl){ console.error("webgl渲染失敗"); return; } // 頂點著色程序 const VSHADER_SOURCE = "attribute vec4 a_Position;" + "uniform float u_CosA, u_SinA;" + "void main(){" + // 分別設置旋轉后坐標,x、y、z、w一個不能少 " gl_Position.x = a_Position.x * u_CosA - a_Position.y * u_SinA;" + " gl_Position.y = a_Position.x * u_SinA + a_Position.y * u_CosA;" + " gl_Position.z = a_Position.z;" + " gl_Position.w = 1.0;" + " gl_PointSize = 10.0;" + // 設置尺寸 "}"; // 片源著色器程序 const FSHADER_SOURCE = "void main(){" + " gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);" + // 設置顏色 "}"; // 初始化著色器 if(!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)){ console.log('初始化著色器失敗'); return; } // 兩個一組表示三角形三個頂點的x,y坐標 const vertices = new Float32Array([ -0.5, -0.5, 0.5, -0.5, -0.5, 0.5, 0.5, 0.5 ]); const ANGLE = 90; // 旋轉90度 const radian = Math.PI * ANGLE / 180.0; // 轉為弧度 const cosA = Math.cos(radian); const sinA = Math.sin(radian); // (1)創建緩沖區對象 const vertexBuffer = gl.createBuffer(); if(!vertexBuffer){ console.error("創建緩沖區對象失敗"); return; } // (2)把緩沖區對象綁定到目標 gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); // (3)向緩沖區對象寫入數據 gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); const a_Position = gl.getAttribLocation(gl.program, "a_Position"); const u_SinA = gl.getUniformLocation(gl.program, "u_SinA"); const u_CosA = gl.getUniformLocation(gl.program, "u_CosA"); // (4)把緩沖區對象分配給a_Position變量 gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0); gl.uniform1f(u_SinA, sinA); gl.uniform1f(u_CosA, cosA); // (5)鏈接a_Position變量與分配給他的緩沖區對象 gl.enableVertexAttribArray(a_Position); // 清空canvas gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); // 繪制三個點,不能超過緩沖區的點數 gl.drawArrays(gl.TRIANGLES, 0, 3); }
5. 矩陣轉換
- 對于平移、縮放、旋轉這些點位置的操作可以通過矩陣來進行統一的描述;
- 設原來的點p(x, y, z, 1), 操作后的點p1(x1, y1, z1, 1),則有如下矩陣:

- 平移矩陣,Tx、Ty、Tz分別為在x、y、z方向上的偏移量:

- 2D旋轉矩陣,β為轉角:

- 縮放矩陣,Sx、Sy、Sz分別為x、y、z方向上的縮放比:

6. 使用矩陣轉換
- webGL中的矩陣是按列主序的,就是 [a, e, i, m, b, f, j, n, c, g, k, o, d, h, l, p];
- gl.uniformMatrix4fv(location, transpose, array)
- location:uniform變量位置
- transpose:是否置換矩陣,webgl不支持所以必為true;
- array:4x4矩陣數據;
- 置換矩陣:置換操作將交換矩陣的行和列;
matrix.js
function main(){ const canvas = document.getElementById("example"); // 獲取二維圖形的繪圖上下文 const gl = getWebGLContext(canvas); if(!gl){ console.error("webgl渲染失敗"); return; } // 頂點著色程序 const VSHADER_SOURCE = "attribute vec4 a_Position;" + "uniform mat4 u_xformMatrix;" + "void main(){" + " gl_Position = u_xformMatrix * a_Position;" + " gl_PointSize = 10.0;" + // 設置尺寸 "}"; // 片源著色器程序 const FSHADER_SOURCE = "void main(){" + " gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);" + // 設置顏色 "}"; // 初始化著色器 if(!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)){ console.log('初始化著色器失敗'); return; } // 兩個一組表示三角形三個頂點的x,y坐標 const vertices = new Float32Array([ -0.5, -0.5, 0.5, -0.5, -0.5, 0.5 ]); // 旋轉 const ANGLE = 45; // 旋轉90度 const radian = Math.PI * ANGLE / 180.0; // 轉為弧度 const cosA = Math.cos(radian); const sinA = Math.sin(radian); // 角度矩陣 const xformMatrix_rotate = new Float32Array([ cosA, sinA, 0.0, 0.0, -sinA, cosA, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ]); // 平移 const Tx = 0.1, Ty = 0.2, Tz = 0.0; const xformMatrix_move = new Float32Array([ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, Tx, Ty, Tz, 1.0 ]); // 縮放 const Sx = 1, Sy = 2, Sz = 1; const xformMatrix = new Float32Array([ Sx, 0.0, 0.0, 0.0, 0.0, Sy, 0.0, 0.0, 0.0, 0.0, Sz, 0.0, 0.0, 0.0, 0.0, 1.0 ]); // (1)創建緩沖區對象 const vertexBuffer = gl.createBuffer(); if(!vertexBuffer){ console.error("創建緩沖區對象失敗"); return; } // (2)把緩沖區對象綁定到目標 gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); // (3)向緩沖區對象寫入數據 gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); const a_Position = gl.getAttribLocation(gl.program, "a_Position"); const u_xformMatrix = gl.getUniformLocation(gl.program, "u_xformMatrix"); // (4)把緩沖區對象分配給a_Position變量 gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0); gl.uniformMatrix4fv(u_xformMatrix, false, xformMatrix); // (5)鏈接a_Position變量與分配給他的緩沖區對象 gl.enableVertexAttribArray(a_Position); // 清空canvas gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); // 繪制三個點,不能超過緩沖區的點數 gl.drawArrays(gl.TRIANGLES, 0, 3); }
四. 高級變換與動畫基礎
1. 基于cuon-matrix.js進行矩陣轉換
- 這里介紹了這本書專用的一個庫“cuon-matrix.js”,下面的代碼都是基于這個庫的了;
- 這個庫主要用來做矩陣的轉換,其他框架其實也有自己的矩陣轉換庫;
- 使用這些矩陣轉換庫時一定要搞清楚:這些庫輸出的矩陣是按行主序還是按列主序的;
- cuon-matrix.js會把一系列轉換后的結果輸出到elements里面;
matrix_base_cuon.js
function main(){ const canvas = document.getElementById("example"); // 獲取二維圖形的繪圖上下文 const gl = getWebGLContext(canvas); if(!gl){ console.error("webgl渲染失敗"); return; } // 頂點著色程序 const VSHADER_SOURCE = "attribute vec4 a_Position;" + "uniform mat4 u_xformMatrix;" + "void main(){" + " gl_Position = u_xformMatrix * a_Position;" + " gl_PointSize = 10.0;" + // 設置尺寸 "}"; // 片源著色器程序 const FSHADER_SOURCE = "void main(){" + " gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);" + // 設置顏色 "}"; // 初始化著色器 if(!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)){ console.log('初始化著色器失敗'); return; } // 兩個一組表示三角形三個頂點的x,y坐標 const vertices = new Float32Array([ -0.5, -0.5, 0.5, -0.5, -0.5, 0.5 ]); // 生成一個矩陣 const xformMatrix = new Matrix4(); // 旋轉 const ANGLE = 180; // 旋轉90度 xformMatrix.setRotate(ANGLE, 0, 0, 1); // 平移 const Tx = 0.1, Ty = 0.2, Tz = 0.0; xformMatrix.setTranslate(Tx, Ty, Tz); // 縮放 const Sx = 1, Sy = 2, Sz = 1; xformMatrix.setScale(Sx, Sy, Sz); // (1)創建緩沖區對象 const vertexBuffer = gl.createBuffer(); if(!vertexBuffer){ console.error("創建緩沖區對象失敗"); return; } // (2)把緩沖區對象綁定到目標 gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); // (3)向緩沖區對象寫入數據 gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); const a_Position = gl.getAttribLocation(gl.program, "a_Position"); const u_xformMatrix = gl.getUniformLocation(gl.program, "u_xformMatrix"); // (4)把緩沖區對象分配給a_Position變量 gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0); gl.uniformMatrix4fv(u_xformMatrix, false, xformMatrix.elements); // (5)鏈接a_Position變量與分配給他的緩沖區對象 gl.enableVertexAttribArray(a_Position); // 清空canvas gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); // 繪制三個點,不能超過緩沖區的點數 gl.drawArrays(gl.TRIANGLES, 0, 3); }
2. 復合變換
- 3x3矩陣乘法法則:

- 矩陣乘法滿足結合律,即:矩陣 A、B、C,( A * B ) * C = A * ( B * C );
- 但是矩陣乘法不滿足交換律,即:矩陣 A、B、C,A * B * C ≠ B * A * C;
- 因為矩陣相乘滿足結合律,所以矩陣的復合變換可以先把多次轉換的矩陣相乘之后再乘以原矩陣;
- 又因為矩陣相乘不滿足交換律,所以矩陣的復合變換次序不一樣時,輸出不一樣的模型矩陣;
- 上述多次轉換的過程叫:模型變換(model transformation)或稱建模變換(modeling transformation);
- 上述多次轉換后得到的矩陣叫:模型矩陣(model matrix);
function main(){ const canvas = document.getElementById("example"); // 獲取二維圖形的繪圖上下文 const gl = getWebGLContext(canvas); if(!gl){ console.error("webgl渲染失敗"); return; } // 頂點著色程序 const VSHADER_SOURCE = "attribute vec4 a_Position;" + "uniform mat4 u_xformMatrix;" + "void main(){" + " gl_Position = u_xformMatrix * a_Position;" + " gl_PointSize = 10.0;" + // 設置尺寸 "}"; // 片源著色器程序 const FSHADER_SOURCE = "void main(){" + " gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);" + // 設置顏色 "}"; // 初始化著色器 if(!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)){ console.log('初始化著色器失敗'); return; } // 兩個一組表示三角形三個頂點的x,y坐標 const vertices = new Float32Array([ -0.5, -0.5, 0.5, -0.5, -0.5, 0.5 ]); // 生成一個矩陣 const xformMatrix = new Matrix4(); // 旋轉 const ANGLE = 180; // 旋轉90度 xformMatrix.rotate(ANGLE, 0, 0, 1); // 平移 const Tx = 0.1, Ty = 0.2, Tz = 0.0; xformMatrix.translate(Tx, Ty, Tz); // 縮放 const Sx = 1, Sy = 2, Sz = 1; xformMatrix.scale(Sx, Sy, Sz); // (1)創建緩沖區對象 const vertexBuffer = gl.createBuffer(); if(!vertexBuffer){ console.error("創建緩沖區對象失敗"); return; } // (2)把緩沖區對象綁定到目標 gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); // (3)向緩沖區對象寫入數據 gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); const a_Position = gl.getAttribLocation(gl.program, "a_Position"); const u_xformMatrix = gl.getUniformLocation(gl.program, "u_xformMatrix"); // (4)把緩沖區對象分配給a_Position變量 gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0); gl.uniformMatrix4fv(u_xformMatrix, false, xformMatrix.elements); // (5)鏈接a_Position變量與分配給他的緩沖區對象 gl.enableVertexAttribArray(a_Position); // 清空canvas gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); // 繪制三個點,不能超過緩沖區的點數 gl.drawArrays(gl.TRIANGLES, 0, 3); }
3. 動畫
- requestAnimationFrame:目的是為了讓各種網頁動畫效果(DOM動畫、Canvas動畫、SVG動畫、WebGL動畫)能夠有一個統一的刷新機制,從而節省系統資源,提高系統性能,改善視覺效果。
- requestAnimationFrame:類似于settimeout,不過跟的是屏幕刷新率,吃的主線程資源。
- requestAnimationFrame:詳情參考:https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestAnimationFrame
- requestAnimationFrame:在不同刷新率的屏幕動畫播放的速度會不一樣,所以要加入時間戳來保證速率。
- requestAnimationFrame:說是切tab的時候會停止執行。但我試過chrome上切tab,setInterval也會停下來。
- requestAnimationFrame:嘗試掛兩個回調進去也是莫得問題,應該是跑同一個時鐘周期,這樣就很漂亮了。
- requestAnimationFrame:返回一個requestId,可以通過cancelAnimationFrame(requestId)中止動畫。
animation.js
function main(){ const canvas = document.getElementById("example"); // 獲取二維圖形的繪圖上下文 const gl = getWebGLContext(canvas); if(!gl){ console.error("webgl渲染失敗"); return; } // 頂點著色程序 const VSHADER_SOURCE = "attribute vec4 a_Position;" + "uniform mat4 u_xformMatrix;" + "void main(){" + " gl_Position = u_xformMatrix * a_Position;" + " gl_PointSize = 10.0;" + // 設置尺寸 "}"; // 片源著色器程序 const FSHADER_SOURCE = "void main(){" + " gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);" + // 設置顏色 "}"; // 初始化著色器 if(!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)){ console.log('初始化著色器失敗'); return; } function draw(animationOps){ // 兩個一組表示三角形三個頂點的x,y坐標 const vertices = new Float32Array([ -0.5, -0.5, 0.5, -0.5, -0.5, 0.5 ]); // 生成一個矩陣 const xformMatrix = new Matrix4(); // 旋轉 const ANGLE = animationOps.rotate || 0; // 旋轉90度 xformMatrix.rotate(ANGLE, 0, 0, 1); // 平移 const Tx = animationOps.translate.x || 0, Ty = animationOps.translate.y || 0, Tz = animationOps.translate.z || 0; xformMatrix.translate(Tx, Ty, Tz); // 縮放 const Sx = animationOps.scale.sx || 1, Sy = animationOps.scale.sy || 1, Sz = animationOps.scale.sz || 1; xformMatrix.scale(Sx, Sy, Sz); // (1)創建緩沖區對象 const vertexBuffer = gl.createBuffer(); if(!vertexBuffer){ console.error("創建緩沖區對象失敗"); return; } // (2)把緩沖區對象綁定到目標 gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); // (3)向緩沖區對象寫入數據 gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); const a_Position = gl.getAttribLocation(gl.program, "a_Position"); const u_xformMatrix = gl.getUniformLocation(gl.program, "u_xformMatrix"); // (4)把緩沖區對象分配給a_Position變量 gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0); gl.uniformMatrix4fv(u_xformMatrix, false, xformMatrix.elements); // (5)鏈接a_Position變量與分配給他的緩沖區對象 gl.enableVertexAttribArray(a_Position); // 清空canvas gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); // 繪制三個點,不能超過緩沖區的點數 gl.drawArrays(gl.TRIANGLES, 0, 3); } let last = Date.now(); let current_ang = 0; function tick() { const ANGLE_STEP = 45; // 角速度,一秒45度 const now = Date.now(); const elapsed = now - last; last = now; const new_ang = current_ang + (ANGLE_STEP * elapsed) / 1000.0; current_ang = new_ang; draw({ rotate: new_ang, translate: { x: 0, y: 0, z: 0 }, scale: { x: 1, y: 1, z: 1 } }); requestAnimationFrame(tick); } tick(); }

浙公網安備 33010602011771號