学习WebGL之变换矩阵

本系列所有文章目录

本文将介绍3D渲染中的一个重要概念变换矩阵,下面是例子的运行截图,可以前往我的博客查看代码演示。

在介绍本文的代码之前,先要了解一个概念:矩阵。学过线性代数的朋友应该都知道矩阵相当于是一个二维数组,有自己的运算规则。下面就通过几个例子简单了解一下矩阵的特性。

3X3矩阵的加法



从图中可以看出3X3矩阵就像是一个3X3的表格,每个单元格中填写一个数。它的加法就是把两个矩阵对应位置的元素加起来放在结果矩阵对应的位置上。那么如果相加的两个矩阵尺寸不一样怎么办?答案是无法运算。矩阵的加减要求两边的矩阵必须尺寸相等。

下面是减法的运算,和加法相似,很好理解。


接下来是乘法。

乘法稍微有些复杂,所以我用符号来代替数值,方便观察规律。我们看结果的第一行第一列aj + bm + cp,它就是左边的矩阵第一行和右边的矩阵第一列逐个相乘再相加的结果。


再看结果的第二行第一列aj + bm + cp,它就是左边的矩阵第二行和右边的矩阵第一列逐个相乘再相加的结果。


差不多已经可以总结出规律了,结果矩阵的第n行第m列的结果就是左边的矩阵第n行和右边的矩阵第m列逐个相乘再相加的结果。读者可以看看其他的值是不是这样计算出来的。

最后说一下除法,矩阵的除法有些特殊,比如说B/A,可以换算成B*inv(A)。inv(A)是A的逆矩阵,因为不是所有矩阵都有逆矩阵,所以除法在矩阵计算中并不总是可用。逆矩阵的求解比较复杂,这里暂时就不解释了。本文目前也没有用到求逆矩阵的地方。如果读者感兴趣的话,可以自行百度或者翻一翻以前的线性代数课本。

变换矩阵

说完了矩阵,那么什么是变换矩阵呢?在图形绘制过程中,有三种变换,分别是平移,缩放,旋转。如果我们想要用代码表示一个3D环境中的变换需要几个变量呢,首先要有平移tx, ty, tz,然后是缩放sx, sy, sz,最后是旋转rx, ry, rz。在渲染的时候把这些变量附加到原始的位置数据上就可以实现变换了。这种方式虽然可行但不够好,尤其是在GPU上这种方式产生的运算负担远大于使用矩阵。

attribute vec4 position;
varying vec4 fragColor;
uniform float elapsedTime;
uniform mat4 transform;
void main() {
    fragColor = position * 0.5 + 0.5;
    gl_Position = transform * position;
    gl_PointSize = 20.0;
}

这是本文代码例子中的Vertex Shader,新增了一个uniform uniform mat4 transform;mat4这个类型前文有提到过,是4X4的矩阵。它是Shader内置的类型,支持直接加减乘等操作。使用矩阵会产生更少的运算指令,GPU可以更好的优化运算过程。那么应该怎么使用呢?接下来我就一一介绍每一种变换矩阵。

矩阵处理库

本文的例子使用的处理矩阵的库是glmatrix,我们可以使用它快速的构造各种我们需要的矩阵。

平移矩阵

假设有一个点(1, 2, 3),经过大小为(1, 2, 3)的平移,最终必定会平移到(1+1, 2+2, 3+3)的位置。使用矩阵计算如下。

这里补充一点,如果左边的矩阵的列数等于右边的矩阵的行数,它们就可以相乘,结果矩阵的行数等于左边矩阵的行数,列数等于右边矩阵的列数。

平移矩阵就是一个4X4的单位矩阵的第4行的前三个元素用tx,ty,tz填充之后的矩阵。下面就是一个单位矩阵。


下面是glmatrix中使用平移矩阵的用法。

translateMatrix = mat4.create();//创建单位矩阵
mat4.translate(translateMatrix, translateMatrix, vec3.fromValues(tx,ty,tz));//平移之后在赋值给translateMatrix

translate的第一个参数是输出矩阵,第二个要要处理的矩阵,create出来的是标准变换矩阵,经过translate之后,就是平移矩阵了。

缩放矩阵


缩放矩阵的三个缩放元素sx,sy,sz,分布在从左到右的对角线上,矩阵相乘后位置的x,y,z分别乘以了sx,sy,sz,从而实现了缩放。


代码实现如下。

scaleMatrix = mat4.create();//创建单位矩阵
mat4.scale(scaleMatrix, scaleMatrix, vec3.fromValues(sx,sy,sz));//缩放之后在赋值给scaleMatrix

旋转矩阵

旋转矩阵相比于上面两个略微有些复杂,旋转包含两个重要元素,旋转的角度,绕什么轴旋转。具体原理可以参考三维空间中的旋转:旋转矩阵、欧拉角。代码实现如下。

rotateMatrix = mat4.create();//创建单位矩阵
mat4.rotate(rotateMatrix, rotateMatrix, Math.PI/2, vec3.fromValues(0,0,1.0));//缩放之后在赋值给rotateMatrix

Math.PI/2是弧度,0.0,0.0,1.0是旋转轴的向量。

综合三个矩阵

现在我们得到了三个矩阵,接下来就是把它们相乘,相乘之后的结果将同时具有这三种变换的能力。

var transform = mat4.create();
mat4.multiply(transform, rotateMatrix, scaleMatrix);
mat4.multiply(transform, transform, translateMatrix);

注意相乘的顺序translateMatrix * rotateMatrix * scaleMatrix,这样可以保证先缩放再旋转,最后再平移。因为缩放和旋转的中心点是0,0,0点,如果先平移再缩放,每个顶点的位置已经改变,缩放和旋转出来的结果自然就不对了。

代码实现

最后回到本文的代码实现中来,我把之前的代码整理了一下,公用的东西移到了glBase.js文件里,在演示代码的资源Tab中可以找到它。glMatrix的js文件也可以在其中找到。

在glBase.js中,会调用window上的两个回调。一个是WebGL配置完成时的回调。

function setupGLEnv(canvasID) {
  canvas = document.getElementById(canvasID);
  gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
  if (!gl) {
    alert('天呐!您的浏览器竟然不支持WebGL,快去更新浏览器吧。');
  }
  program = makeProgram();
  if (window.onWebGLLoad) {
    window.onWebGLLoad();
  }
}

另一个是每一次渲染时的回调。

function renderLoop() {
  ...

  if (window.onWebGLRender) {
    window.onWebGLRender(deltaTime, elapsedTime);
  }

  collectedFrameDuration += deltaTime;
  collectedFrameCount++;
  ...
}

下面是本文的例子所有的代码。

var triangleBuffer = null;

function makeBuffer() {
  var triangle = [
    0.0, 0.5, 0.0, -0.5, -0.5, 0.0,
    0.5, -0.5, 0.0,
  ];
  buffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(triangle), gl.STATIC_DRAW);
  return buffer;
}

window.onWebGLLoad = function () {
  triangleBuffer = makeBuffer();
}

window.onWebGLRender = function render(deltaTime, elapesdTime) {
  gl.viewport(0, 0, canvas.width, canvas.height);
  gl.clearColor(1.0, 0.0, 0.0, 1.0);
  gl.clear(gl.COLOR_BUFFER_BIT);

  gl.useProgram(program);
  gl.bindBuffer(gl.ARRAY_BUFFER, triangleBuffer);
  positionLoc = gl.getAttribLocation(program, 'position');
  gl.enableVertexAttribArray(positionLoc);
  gl.vertexAttribPointer(positionLoc, 3, gl.FLOAT, false, 4 * 3, 0);

  elapsedTimeUniformLoc = gl.getUniformLocation(program, 'elapsedTime');
  gl.uniform1f(elapsedTimeUniformLoc, elapesdTime);

  var rotateMatrix = mat4.create();
  mat4.rotate(rotateMatrix, rotateMatrix, elapesdTime / 1000.0, vec3.fromValues(1, 1, 1));

  var scale = 0.1 * (Math.sin(elapesdTime / 1000.0) + 1.0) + 0.4;
  var scaleMatrix = mat4.create();
  mat4.scale(scaleMatrix, scaleMatrix, vec3.fromValues(scale, scale, scale));

  var xoffset = Math.sin(elapesdTime / 1000.0);
  var translateMatrix = mat4.create();
  mat4.translate(translateMatrix, translateMatrix, vec3.fromValues(xoffset, 0, 0));

  var transform = mat4.create();
  mat4.multiply(transform, rotateMatrix, scaleMatrix);
  mat4.multiply(transform, transform, translateMatrix);

  transformUniformLoc = gl.getUniformLocation(program, 'transform');
  gl.uniformMatrix4fv(transformUniformLoc, false, transform);

  gl.drawArrays(gl.TRIANGLES, 0, 3);
}

我使用了那两个回调配置我的相关代码。新增的代码逻辑仅有一处。

var rotateMatrix = mat4.create();
mat4.rotate(rotateMatrix, rotateMatrix, elapesdTime / 1000.0, vec3.fromValues(1, 1, 1));

var scale = 0.1 * (Math.sin(elapesdTime / 1000.0) + 1.0) + 0.4;
var scaleMatrix = mat4.create();
mat4.scale(scaleMatrix, scaleMatrix, vec3.fromValues(scale, scale, scale));

var xoffset = Math.sin(elapesdTime / 1000.0);
var translateMatrix = mat4.create();
mat4.translate(translateMatrix, translateMatrix, vec3.fromValues(xoffset, 0, 0));

var transform = mat4.create();
mat4.multiply(transform, rotateMatrix, scaleMatrix);
mat4.multiply(transform, transform, translateMatrix);

transformUniformLoc = gl.getUniformLocation(program, 'transform');
gl.uniformMatrix4fv(transformUniformLoc, false, transform);

根据当前的时间配置了三个矩阵,相乘后赋值给uniform transformtransformmat4类型的,所以我使用了uniformMatrix4fv来赋值,第一个参数是uniform的位置,第二个是指是否需要将矩阵进行转置操作,不需要就选择false,第三个就是矩阵本身。

Vertex Shader

attribute vec4 position;
varying vec4 fragColor;
uniform float elapsedTime;
uniform mat4 transform;
void main() {
    fragColor = position * 0.5 + 0.5;
    gl_Position = transform * position;
    gl_PointSize = 20.0;
}

Vertex Shader中增加了上面说的uniform transform,并且在赋值给gl_Position之前使用transformposition进行变换。

本篇主要介绍了什么是变换矩阵,如何使用变换矩阵以及怎样和Vertex Shader配合。下一篇就要开始介绍3D渲染最基础的概念之一,透视投影矩阵。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容