基本变换和动画_WebGL笔记10

写在前面

上一篇笔记中,我们全面了解了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 |

三维坐标为什么需要用4×4矩阵表示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

包含上述代码的完整的例子,请查看:

七.总结

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

至此,WebGL 2D就结束了,仔细想想好像也没学到什么,大部分API都在3D那边吗?不是,大部分常用API我们都用过了,剩下的除了一点点有用的就只有一些很高级的功能(比如动态纹理)了,3D场景几乎全都是用矩阵“搓”出来的,没什么可用API了,没有类似于Camera.setPosition(0, 0, 3)这样的神奇方法

视角是什么,是矩阵,光照是什么,矩阵,那阴影呢,矩阵,雾化呢,说了都是矩阵。。。

参考资料

  • WebGL编程指南》

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

code