原文地址:WebGL學習(1) - 三角形
還記得第一次看到canvas的粒子特效的時候,真的把我給驚艷到了,原來在瀏覽器也能做出這么棒的效果。結合《HTML5 Canvas核心技術》和網上的教程,經過半年斷斷續續的學習,對canvas的學習終於完結,對常用的canvas特效基本能做到信手拈來的。canvas特效請看:樣例列表
眾所周知,canvas是2D繪圖技術,雖然可以通過坐標變換,位置計算也能做到3D的效果。但3D場景數據量畢竟比2D要高一個數量級的,純粹用canvas的話,不管是性能和開發的復雜度會成為一個瓶頸。
這也是webGL出現的原因,解決web端3D渲染的場景。webGL會調用到GPU,處理大量重復的3D場景數據時,性能非常有優勢。同時webGL是基於openGL ES 2.0, 因此它處理3D場景是非常成熟的。但為什么不直接學習three.js呢?因為本人對圖形學感興趣,只是希望做一些自己喜歡的效果的同時深入了解計算機圖形學,沒指望通過它做商業項目。
為了讓學習更有動力和目的性,我們以實例為導向學習webGL,再從中展開到需要學習哪些知識點。這次我們來實現如下的動畫,該教程參考了《WebGL編程指南》
實際效果請看:旋轉的三角形

webGL渲染流程
webGL的渲染流程如下,其中第2,3,4步是重點,里面細節比較多。接着我們就按這個流程一步一步解決問題
- 獲取webGL繪圖上下文
- 初始化着色器
- 創建、綁定緩沖區對象
- 向頂點着色器和片元着色器寫入數據
- 設置canvas背景色,清空canvas
- 繪制
webGL繪圖上下文
webGL是canvas基礎之上的3D繪圖技術,只是上下文不同,get3DContext函數作用就是依次降級獲取上下文。
var canvas = document.getElementById("canvas"),
gl = get3DContext(canvas, true);
function get3DContext(canvas, opt) {
var names = ["webgl", "experimental-webgl", "webkit-3d", "moz-webgl"];
var context = null;
for (var i = 0, len = names.length; i < len; i++) {
try {
context = canvas.getContext(names[i], opt);
} catch (e) {}
if (context) {
break;
}
}
return context;
}
着色器
着色器就是嵌入到js中的webGL代碼,是由GLSL語言編寫的,可以把着色器看成是js代碼連接webGL的中間件。頂點着色器和片元着色器分別用於操作頂點和顏色光照,《WebGL編程指南》中是把着色器寫成字符串,但從可維護性考慮,還是寫在script標簽中比較好。GLSL語言與C語言非常像,只要熟悉了GLSL特有的部分,其實還是比較簡單的。
限定符
限定符只能用於全局變量,有3種類型:
- attribute用於表示頂點信息
- uniform用於表示除頂點外的其他信息,可以是除結構體和數組之外的任意類型
- varying用於頂點着色器向片元着色器傳輸數據
GLSL特有的數據類型
-
向量:
vec2, vec3, vec4 : 表示有2,3,4個浮點數的向量
ivec2, ivec3, ivec4 : 表示有2,3,4個整形的向量
bvec2, bvec3, bvec4 : 表示有2,3,4個布爾值的向量 -
矩陣:
mat2, mat3, mat4 : 表示有2x2,3x3,4x4的浮點數的矩陣
頂點着色器
<script type="x-shader/x-vertex" id="vs">
attribute vec4 a_Position; //頂點,4個浮點的矢量,attribute變量傳輸與頂點有關的數據,表示逐頂點的信息
uniform mat4 u_xformMatrix; //變換矩陣,4*4浮點矩陣, uniform變量傳輸的是所有頂點都相同的數據
void main() {
gl_Position=u_xformMatrix*a_Position;
}
</script>
片元着色器
<script type="x-shader/x-fragment" id="fs">
precision mediump float; // 精度限定
uniform vec4 u_FragColor; // 顏色
void main() {
gl_FragColor = u_FragColor;
}
</script>
接着就是創建着色器了,首先從頁面script標簽取出着色器代碼,初始化着色器;接着創建程序對象,最后連接程序對象。中間的步驟其實非常的啰嗦,已經把這幾個步驟封裝,我們只需要調用createShaders就可以了。
/**
* 根據script id創建着色器
* @param {Object} gl context
* @param {String} vid script id
* @param {String} fid script id
* @return {Boolen}
*/
function createShaders(gl, vid, fid) {
var vshader, fshader, element, program;
[vid, fid].forEach(function(id) {
element = document.getElementById(id);
if (element) {
switch (element.type) {
// 頂點着色器的時候
case "x-shader/x-vertex":
vshader = element.text;
break;
// 片段着色器的時候
case "x-shader/x-fragment":
fshader = element.text;
break;
default:
break;
}
}
});
if (!vshader) {
console.log("VERTEX_SHADER String not exist");
return false;
}
if (!fshader) {
console.log("FRAGMENT_SHADER String not exist");
return false;
}
program = createProgram(gl, vshader, fshader);
if (!program) {
console.log("Failed to create program");
return false;
}
gl.useProgram(program);
gl.program = program;
return true;
}
/**
* 創建連接程序對象
* @param {Object} gl 上下文
* @param {String} vshader 頂點着色器代碼
* @param {String} fshader 片元着色器代碼
* @return {Object}
*/
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;
}
// 創建程序對象
var program = gl.createProgram();
if (!program) {
return null;
}
// 連接着色器對象
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
// 連接程序對象
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;
}
/**
* 加載着色器
* @param {Object} gl 上下文
* @param {Object} type 類型
* @param {String} source 代碼字符串
* @return {Object}
*/
function loadShader(gl, type, source) {
// 創建着色器對象
var shader = gl.createShader(type);
if (shader == null) {
console.log("unable to create shader");
return null;
}
// 設置着色器程序
gl.shaderSource(shader, source);
// 編譯着色器
gl.compileShader(shader);
// 檢查編譯結果
var compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (!compiled) {
var error = gl.getShaderInfoLog(shader);
console.log("Failed to compile shader: " + error);
gl.deleteShader(shader);
return null;
}
return shader;
}
緩沖區
創建好緩沖區對象后,需要把它分配給變量,然后使它生效。注意頂點數組使用的是類型化數組Float32Array,這樣更加高效。vertexAttribPointer方法這里指定了每個頂點分量的個數為2,因為我們目前只定義x,y坐標,z坐標使用系統默認。
/**
* 創建緩沖區
* @param {Array} data
* @param {Object} bufferType
* @return {Object}
*/
function createBuffer(data, bufferType) {
// 生成緩存對象
var buffer = gl.createBuffer();
if (!buffer) {
console.log("Failed to create the buffer object");
return null;
}
// 綁定緩存(gl.ARRAY_BUFFER<頂點>||gl.ELEMENT_ARRAY_BUFFER<頂點索引>)
gl.bindBuffer(bufferType || gl.ARRAY_BUFFER, buffer);
// 向緩存中寫入數據
gl.bufferData(bufferType || gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
// 將綁定的緩存設為無效
// gl.bindBuffer(gl.ARRAY_BUFFER, null);
// 返回生成的buffer
return buffer;
}
// 創建緩沖區並傳人頂點
var vertices = new Float32Array([-0.5, 0.5, -0.5, -0.5, 0.5, 0.5, 0.5, -0.5]);
if (!createBuffer(vertices)) return;
// 分配緩沖區對象給a_Position變量
// (地址,每個頂點分量的個數<1-4>,數據類型<整形,符點等>,是否歸一化,指定相鄰兩個頂點間字節數<默認0>,指定緩沖區對象偏移量<默認0>)
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
// 啟動
gl.enableVertexAttribArray(a_Position);
寫入數據
首先要獲取變量的地址,然后再給變量賦值,感覺挺麻煩的。attribute標記的變量使用getAttribLocation獲取,同理uniform標記的變量使用getUniformLocation獲取。
我們的動畫要使圖形繞坐標原點旋轉,那么這就需要用到矩陣的變換,矩陣相關的知識就不詳細說明了。要注意webGL使用的是列主序的矩陣,計算好變換矩陣后,把值賦予變量就ok。
// 獲取 u_FragColor變量的存儲地址並賦值
var u_FragColor = gl.getUniformLocation(gl.program, 'u_FragColor');
if (!u_FragColor) return;
//顏色模式為rgba,值范圍0~1
gl.uniform4f(u_FragColor, 1.0, 0.0, 0.0, 1.0);
// 繞z軸旋轉
var deg=Math.PI/180*(angle++),
cos=Math.cos(deg),
sin=Math.sin(deg);
// webgl中是按列主序 旋轉加位移
var xformMatrix=new Float32Array([
cos,sin,0.0,0.0,
-sin,cos,0.0,0.0,
0.0,0.0,1.0,0.0,
0.3,0.0,0.0,1.0
]);
// v表示可以向着色器傳輸多個數值(地址變量,webgl中必須false,矩陣)
gl.uniformMatrix4fv(u_xformMatrix,false,xformMatrix);
背景操作
每次執行動畫前進行清屏,和canvas中的設置fillStyle,執行clearRect,效果一樣。
// 設置清屏顏色
gl.clearColor(0.0, 0.0, 0.0, 1.0);
// 清屏
gl.clear(gl.COLOR_BUFFER_BIT);
繪制
最后渲染圖形,注意第一個參數,指定不同的值,它就渲染為不同的圖形,大家可以用不同的值試試效果。
- POINTS 點
- LINES 線段
- LINE_STRIP 線條
- LINE_LOOP 回路
- TRIANGLES 三角形
- TRIANGLE_STRIP 三角帶
- TRIANGLE_FAN 三角扇
// (基本圖形,第幾個頂點,執行幾次),修改基本圖形項可以生成點,線,三角形,矩形,扇形等
gl.drawArrays(gl.TRIANGLES, 0, 3);
最后主體代碼如下:
var canvas = document.getElementById("canvas"),
gl = get3DContext(canvas, true);
function main() {
if (!gl) {
console.log("Failed to get the rendering context for WebGL");
return;
}
if (!createShaders(gl, "fs", "vs")) {
console.log("Failed to intialize shaders.");
return;
}
// 創建緩沖區並傳人頂點
var vertices = new Float32Array([ -0.5, 0.5, -0.5, -0.5, 0.5, 0.5, 0.5, -0.5 ]);
if (!createBuffer(vertices)) {
console.log("Failed to create the buffer object");
return;
}
// 獲取頂點位置
var a_Position = gl.getAttribLocation(gl.program, "a_Position");
if (a_Position < 0) {
console.log("Failed to get the storage location of a_Position");
return;
}
// 分配緩沖區對象給a_Position變量
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(a_Position);
// 獲取 u_FragColor變量的存儲地址並賦值
var u_FragColor = gl.getUniformLocation(gl.program, "u_FragColor");
if (!u_FragColor) {
console.log("Failed to get the storage location of u_FragColor");
return;
}
gl.uniform4f(u_FragColor, 1.0, 0.0, 0.0, 1.0);
// 獲取矩陣變量
var u_xformMatrix = gl.getUniformLocation(gl.program, "u_xformMatrix");
if (!u_xformMatrix) {
console.log("Failed to get the storage location of u_xformMatrix");
return;
}
var xformMatrix,
angle = 0;
// 設置清屏顏色
gl.clearColor(0.0, 0.0, 0.0, 1.0);
// 執行動畫
(function animate() {
var deg = (Math.PI / 180) * angle++,
cos = Math.cos(deg),
sin = Math.sin(deg);
// 旋轉加位移
xformMatrix = new Float32Array([
cos, sin, 0.0, 0.0,
-sin, cos, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.3, 0.0, 0.0, 1.0
]);
// v表示可以向着色器傳輸多個數值(地址變量,webgl中必須false,矩陣)
gl.uniformMatrix4fv(u_xformMatrix, false, xformMatrix);
gl.clear(gl.COLOR_BUFFER_BIT);
// (基本圖形,第幾個頂點,執行幾次),修改基本圖形項可以生成點,線,三角形,矩形,扇形等
gl.drawArrays(gl.TRIANGLES, 0, 3);
requestAnimationFrame(animate);
})();
}
main();
總結
相比canvas,webGL的api要原始得多,涉及到很多底層的openGL細節,但經過封裝后,我們可以把那部分細節看成一個黑箱。大部分的操作都是基於矩陣變換,盡管有很多方便的第三方矩陣庫,但有牢固的線性代數基礎還是大有裨益的,GLSL編程語言也是一樣需要熟練掌握。
