跳到主要內容
黯羽輕揚每天積累一點點

基本變換和動畫_WebGL 筆記 10

免費2016-01-06#JS#WebGL#webgl transform#webgl translate#webgl rotate#webgl scale#webgl animation#webgl animate#webgl平移旋转缩放#webgl动画

不像 canvas 繪圖有方便的變換 API,WebGL 中需要自行實現

寫在前面

[上一篇筆記](/articles/glsl-es(opengl-es 著色器語言)-webgl 筆記 9/) 中,我們全面了解了 GLSL ES 的語法,必須掌握的 WebGL 基礎知識已經基本都有了(也就是說 API 沒了..),接下來的內容基本都是數學,而變換只是其中最淺顯的一部分

一.變換的數學原理

一句兩句說不清楚,例如 translate(平移)就是給圖像每一個點的坐標的各個分量添上一個 delta,點 (0, 3, 2) 沿 y 軸正方向移動 2 個單位長度,結果是 (0, 5, 2)。原理就是這麼簡單,說不清楚是因為牽扯到公式推導和矩陣表示,如下:

x' = x + deltaX;      how? why?      |1 0 0 tx|
y' = y + deltaY;    ------------>    |0 1 0 ty|
z' = z + deltaZ;                     |0 0 1 tz|
                                     |0 0 0 1 |

三維坐標為什麼需要用 4x4 矩陣表示 translate 變換?這樣做有什麼好處?那 rotate 和 scale 呢?

感興趣可以自行摸索(搜索),完整過程是公式推導->齊次坐標->矩陣乘法->變換矩陣->CTM,理解了 CTM 後,就完全理解變換的數學原理了

P.S.太數學的東西暫時不想寫,但早晚會寫。所以這裡不展開變換矩陣,以後再說

二.WebGL 變換矩陣

變換矩陣不都長一個樣嗎?不不不,上一篇筆記中說了,GLSL ES 中的矩陣是列主序的,比如 translate 矩陣就得是這個樣子:

Matrix4.prototype.setTranslate = function(x, y, z) {
    var e = this.elements;
    e[0] = 1;  e[4] = 0;  e[8]  = 0;  e[12] = x;
    e[1] = 0;  e[5] = 1;  e[9]  = 0;  e[13] = y;
    e[2] = 0;  e[6] = 0;  e[10] = 1;  e[14] = z;
    e[3] = 0;  e[7] = 0;  e[11] = 0;  e[15] = 1;
    return this;
};

P.S.以上代碼摘自《WebGL 編程指南》源碼 cuon-matrix.js,API 風格用著很順手,所以..以後就直接用了

注意:WebGl 和 OpenGL 一樣,矩陣元素是按列主序存儲的,其實 CSS 中 transform: matrix(a,b,c,d,e,f) 也是按列主序的,但線性代數裡面一般習慣按行主序

三.矩陣庫

一個簡單的矩陣庫是必須的,至少要支持 3 種基本變換和矩陣乘法,當然最好有一套友好的(用著舒服的)API

《WebGL 編程指南》提供的矩陣庫 cuon-matrix.js 就不錯,小巧只提供最基本的功能,如果覺得不好用可以去找找其它矩陣庫,但不建議使用 Three.js,因為功能多而全,不利於我們深入了解 WebGL

以後的例子將使用 cuon-matrix.js,具體用法如下:

// 創建 4x4 矩陣,默認填充成單位陣
var m4 = new Matrix4();
// m4.elements === [
// 1, 0, 0, 0,
// 0, 1, 0, 0,
// 0, 0, 1, 0,
// 0, 0, 0, 1]

// 填充為平移矩陣,參數為 deltaX, deltaY, deltaZ
m4.setTranslate(0.25, 0.25, 0.0);
// m4.elements === [
// 1, 0, 0, 0,
// 0, 1, 0, 0,
// 0, 0, 1, 0,
// 0.25, 0.25, 0, 1]

// 在平移的基礎上旋轉
m4.rotate(30, 0, 0, 1);    // 逆時針 30 度,(0, 0, 1) 是旋轉軸 z 軸
// m4.elements === [
// 0.8660253882408142, 0.5, 0, 0,
// -0.5, 0.8660253882408142, 0, 0,
// 0, 0, 1, 0,
// 0.25, 0.25, 0, 1]

// ...
// setXXX 就是填充
// xxx 就是求 CTM,即 m4 = m4 x new Matrix4().setXXX

也有很方便很實用的功能,比如:

// 複製
var newM4 = new Matrix4(oldM4);
// 矩陣乘法,乘積放在 m4_1 中
m4_1.multiply(m4_2);
// 矩陣轉置
m4.transpose();

四.基本變換

變換是對圖像上的每一個點的坐標做同樣的運算,因為片元是根據頂點生成的,所以我們只需要對頂點做運算,也就是說只需要修改頂點著色器,如下:

// 頂點著色器源程序
var vsSrc = 'attribute vec4 a_Position;' +
    'uniform mat4 u_transformMatrix;' +
    'void main() {' +
    'gl_Position = u_transformMatrix * a_Position;' +   // 設置坐標
    // 一點小把戲,為了把變換前的三角形顯示出來
    'if (u_transformMatrix == mat4(0.0)) {gl_Position = a_Position;}' +
'}';

我們聲明瞭矩陣類型 uniform 變量 u_transformMatrix,用來接受變換矩陣,接下來要把值傳進去

首先要有一個變換矩陣,現做一個:

// 變換
var transformMatrix = new Matrix4();
// 先平移
transformMatrix.setTranslate(0.25, 0.25, 0.0);  // 向右上平移
// 再旋轉
transformMatrix.rotate(30, 0, 0, 1);    // 逆時針 30 度,(0, 0, 1) 是旋轉軸 z 軸
// 再縮放
transformMatrix.scale(0.5, 0.5, 1);     // x, y 縮放一半,z 不變

這就折騰出了一個很複雜的變換矩陣,然後想辦法賦值給 uniform 變量,如下:

// 把變換矩陣傳遞給頂點著色器
var u_transformMatrix = gl.getUniformLocation(glUtil.program, 'u_transformMatrix');
gl.uniformMatrix4fv(u_transformMatrix, false, transformMatrix.elements);

給 mat 類型著色器變量賦值用到了 gl.uniformMatrix4fv,參數含義如下:

gl.uniformMatrix4fv(location, transpose, array)
---
transpose   在 WebGL 中只能為 false,表示矩陣是不是轉置矩陣,WebGL 沒有提供矩陣轉置的方法
array       類型化數組,4x4 矩陣按列主序存放

最後再 draw 出來,就完成了

五.動畫

動畫就是不斷擦除重繪產生的視覺效果,比如旋轉就是通過旋轉角度不斷遞增繪製出來的,要保證流暢就要保證角度均勻遞增。那麼實現動畫的關鍵變成了保證均勻變化,而單純的 setInterval 就不行,所以需要加入時間控制,具體如下:

// 變換
var angle = 0;
var ROTATE_SPEED = 60;  // 60 度/秒
var transformMatrix = new Matrix4();
var now;
var lastTime;
var delta = 0;
var u_transformMatrix = gl.getUniformLocation(glUtil.program, 'u_transformMatrix');
// draw
function draw() {
    now = Date.now();
    delta = (now - lastTime) * ROTATE_SPEED / 1000;
    // 角度遞增
    angle += delta;
    angle %= 360;
    // console.log(angle);
    // 旋轉
    transformMatrix.setRotate(angle, 0.0, 0.0, 1.0);
    // 把旋轉矩陣傳遞給頂點著色器
    gl.uniformMatrix4fv(u_transformMatrix, false, transformMatrix.elements);
    // clear
    gl.clear(gl.COLOR_BUFFER_BIT);
    // 繪製變換後的三角形
    gl.drawArrays(gl.TRIANGLES, 0, arrVtx.length / 2);

    // 記錄時間
    lastTime = now;

    // 延遲遞歸
    window.requestAnimationFrame(draw);
}
// 動畫
window.requestAnimationFrame(draw);
lastTime = Date.now();

旋轉速度是 度/秒,下一次繪製時轉過了多少度是根據時間算的,這樣就保證了旋轉角度均勻變化

注意requestAnimationFrame 類似於 setTimeout/setInterval,區別是 requestAnimationFrame只在標籤頁處於激活狀態時才會生效,與 setTimeout/setInterval 不同,性能也要更高一些

P.S.在本例中當然看不出標籤頁非激活狀態動畫停止,因為我們是根據時間計算的角度,本來中間停了一段,但恢復激活的時候旋轉了一個大角度,把中間停的部分都追上了,css3 動畫內部原理也是這樣的:

CSS3 動畫在 Tab 切換回來的時候,動畫表現並不暫停;通過 Chrome frames 工具測試發現,Tab 切換之後,計算渲染繪製都停止,Tab 切換回來時似乎通過內置 JS 計算了動畫位置實現重繪,造成動畫不暫停的感覺

P.S.摘自 CSS3 動畫那麼強,requestAnimationFrame 還有毛線用? « 張鑫旭 - 鑫空間 - 鑫生活,關於 requestAnimationFrame 的更多信息可以查看這篇文章,很有價值

六.DEMO

包含上述代碼的完整的例子,請查看:

在 console 可以看到每一步變換後矩陣的值

七.總結

變換矩陣也叫模型矩陣,3D 中必不可少的 mvp 就是(Model Matrix、View Matrix、Projection Matrix)

至此,WebGL 2D 就結束了,仔細想想好像也沒學到什麼,大部分 API 都在 3D 那邊嗎?不是,大部分常用 API 我們都用過了,剩下的除了一點點有用的就只有一些很高級的功能(比如動態紋理)了,3D 場景幾乎全都是用矩陣"搓"出來的,沒什麼可用 API 了,沒有類似於 Camera.setPosition(0, 0, 3) 這樣的神奇方法

視角是什麼,是矩陣,光照是什麼,矩陣,那陰影呢,矩陣,霧化呢,說了都是矩陣。。。

參考資料

  • WebGL 編程指南》

評論

暫無評論,快來發表你的看法吧

提交評論