原文地址: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,目前只用到前兩種
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程式語言也是一樣需要熟練掌握。