首页 > 代码库 > WebGL学习系列-基础矩阵变换

WebGL学习系列-基础矩阵变换

前言

在图形学中,特别是涉及到3D的时候,矩阵变换起着非常重要的作用。在实际使用的过程当中,通常每一帧画面可能都会涉及到成千上万个顶点的坐标变换,如果没有矩阵变换计算,一个是计算复杂,一个是难以达到我们想要的计算效率。本小节将介绍通过矩阵计算来实现基本的图形变换。

矩阵

矩阵是一种多个数据集合的表示方式,定义为:由 m × n 个数aij<script type="math/tex" id="MathJax-Element-7">a_{ij}</script>排成的m行n列的数表称为m行n列的矩阵,简称m × n矩阵。记作:
技术分享

矩阵的存在主要是由于它的运算,下面来简单看一下:

加法

[2637]+[1324]=[39511]<script type="math/tex" id="MathJax-Element-8"> \begin{bmatrix} 2& 3 & \\ 6& 7& \end{bmatrix} + \begin{bmatrix} 1& 2 & \\ 3& 4& \end{bmatrix} =\begin{bmatrix} 3& 5 & \\ 9& 11& \end{bmatrix} </script>
可以看到,矩阵加法就是对应的位置的数值相加。

减法

[2637]?[1324]=[1313]<script type="math/tex" id="MathJax-Element-9"> \begin{bmatrix} 2& 3 & \\ 6& 7& \end{bmatrix} -\begin{bmatrix} 1& 2 & \\ 3& 4& \end{bmatrix} =\begin{bmatrix} 1& 1 & \\ 3& 3& \end{bmatrix} </script>
可以看到,矩阵减法就是对应的位置的数值相减。

乘法

[1?10321]????321110???=[(1?3+0?2+2?1)(?1?3+3?2+1?1)(1?1+0?1+2?0)(?1?1+3?1+1?0)]=[5412]<script type="math/tex" id="MathJax-Element-10"> \begin{bmatrix} 1 & 0 &2 \\ -1 & 3 &1 \end{bmatrix} *\begin{bmatrix} 3 &1 \\ 2 &1 \\ 1 &0 \end{bmatrix} =\begin{bmatrix} (1*3+0*2+2*1) &(1*1+0*1+2*0) \\ (-1*3+3*2+1*1) &(-1*1+3*1+1*0) \end{bmatrix} =\begin{bmatrix} 5 & 1\\ 4 & 2 \end{bmatrix} </script>

乘法规则示意图如下:

技术分享

在矩阵乘法中,注意,左边矩阵的列数必须等于右边矩阵的行数。

转置

把矩阵的行和列位置进行互换的矩阵,称为矩阵的转置。
[142536]???123456???<script type="math/tex" id="MathJax-Element-11"> 例如: \begin{bmatrix} 1 & 2 &3 \\ 4 & 5 &6 \end{bmatrix}的转置矩阵为 \begin{bmatrix} 1 & 4\\ 2 & 5\\ 3 & 6 \end{bmatrix} </script>
[142536]T=???123456???<script type="math/tex" id="MathJax-Element-12"> 通常写成: \begin{bmatrix} 1 & 2 &3 \\ 4 & 5 &6 \end{bmatrix}^T =\begin{bmatrix} 1 & 4\\ 2 & 5\\ 3 & 6 \end{bmatrix} </script>

矩阵有很多的特性以及计算定律,这里只是介绍一些基本概念,以后需要用到再继续填充本小节的内容。

矩阵跟图形变换的关系

之前我们学习过了基本的图形变换,知道比如要平移一个三角形,实际上只要对三角形三个顶点都进行平移,重新绘制出来的三角形就是平移后的三角形。在3D的世界里,我们会通过对顶点的各种运算,然后重绘,来得到各种图形的基本变换。基本变换包括平移、旋转和缩放,然而从之前的学习中可以发现,每一种基本的变换都使用了不同的公式,形式不一,这达不到一种很好的效果,特别对硬件而言,要针对多种变换去优化代价是比较大的。矩阵的出现恰好解决了这个难题,通过矩阵运算,使各种基本变换(包括以后学习的各类高级变换)达成了一致的计算模型,使得硬件可以针对矩阵运算模型进行优化。

3阶旋转矩阵

之前通过推导,得到了点的旋转公式如下:

x=xcosβ?ysinβy=xsinβ+ycosβz=z(1)
<script type="math/tex; mode=display" id="MathJax-Element-77">x‘=x\cos \beta -y\sin \beta \y‘=x\sin \beta + y\cos \beta \z‘=z\(公式1) </script>
为了进行计算的矩阵化,我们需要用到矩阵乘法,如下:
???xyz???=???adgbehcfi???????xyz???
<script type="math/tex; mode=display" id="MathJax-Element-78"> \begin{bmatrix} x‘\\ y‘\\ z‘ \end{bmatrix} =\begin{bmatrix} a & b &c \\ d & e &f \\ g & h &i \end{bmatrix} *\begin{bmatrix} x\\ y\\ z \end{bmatrix} </script>
我们的目标是找到一个矩阵,通过矩阵计算,能够把一个点给转换成目标点,如果能够找到这样的矩阵,那么只需要提供旋转矩阵,便可以得到旋转后的点坐标。根据矩阵的运算,有:
x=ax+by+czy=dx+ey+fzz=gx+hy+iz(2)
<script type="math/tex; mode=display" id="MathJax-Element-79"> x‘ = ax + by + cz\y‘ = dx + ey + fz\z‘ = gx + hy + iz\(公式2) </script>
对比公式1和公式2,先看x<script type="math/tex" id="MathJax-Element-80">x‘</script>,如下:
x=xcosβ?ysinβx=ax+by+cz
<script type="math/tex; mode=display" id="MathJax-Element-81"> x‘=x\cos \beta -y\sin \beta \x‘ = ax + by + cz </script>
显然可得出:
a=cosβb=?sinβc=0
<script type="math/tex; mode=display" id="MathJax-Element-82"> a=\cos\beta\\ b=-\sin\beta\c=0 </script>
再来看y<script type="math/tex" id="MathJax-Element-83">y‘</script>,如下:
y=xsinβ+ycosβy=dx+ey+fz
<script type="math/tex; mode=display" id="MathJax-Element-84"> y‘=x\sin \beta + y\cos \beta \y‘ = dx + ey + fz </script>
显然可得出:
d=sinβe=cosβf=0
<script type="math/tex; mode=display" id="MathJax-Element-85"> d=\sin\beta\\ e=\cos\beta\f=0 </script>
最后来看下z<script type="math/tex" id="MathJax-Element-86">z‘</script>,如下:
z=zz=gx+hy+iz
<script type="math/tex; mode=display" id="MathJax-Element-87"> z‘=z\z‘ = gx + hy + iz </script>
显然可得出:
g=0h=0i=1
<script type="math/tex; mode=display" id="MathJax-Element-88"> g=0\\ h=0\i=1 </script>
于是,我们找到了旋转矩阵,满足点的旋转计算:
???xyz???=???cosβsinβ0?sinβcosβ0001???????xyz???
<script type="math/tex; mode=display" id="MathJax-Element-89"> \begin{bmatrix} x‘\\ y‘\\ z‘ \end{bmatrix} =\begin{bmatrix} \cos\beta & -\sin\beta &0 \\ \sin\beta & \cos\beta &0 \\ 0 & 0 &1 \end{bmatrix} *\begin{bmatrix} x\\ y\\ z \end{bmatrix} </script>

平移

通过旋转矩阵的推导,对于平移操作,如果我们也想找到相应的3*3矩阵来进行平移计算,先来看看x<script type="math/tex" id="MathJax-Element-26">x‘</script>的情况:

x=x+Txx=ax+by+cz
<script type="math/tex; mode=display" id="MathJax-Element-27"> x‘=x+T_{x}\x‘ = ax + by + cz </script>
可以发现,第一个等式有个常量Tx<script type="math/tex" id="MathJax-Element-28">T_{x}</script>,而第二个等式都是变量,这显然无法推导出a,b,c的值。既然3*3矩阵无法满足我们的需求,我们来试试4*4矩阵,也就是我们要找到这样的矩阵,满足如下计算:
?????xyz1?????=?????aeimbfjncgkodhlp???????????xyz1?????
<script type="math/tex; mode=display" id="MathJax-Element-29"> \begin{bmatrix} x‘\\ y‘\\ z‘\1 \end{bmatrix} =\begin{bmatrix} a & b &c & d\\ e & f &g & h\\ i & j &k & l \\ m & n &o & p \\ \end{bmatrix} *\begin{bmatrix} x\\ y\\ z\1 \end{bmatrix} </script>
之前我们曾提到过,用4个值来表示一个三维坐标称为齐次坐标表示,比如,三维空间中,坐标(x,y,z)可以用齐次坐标(x,y,z,w)<script type="math/tex" id="MathJax-Element-30">(x‘,y‘,z‘,w)</script>表示 ,归一化为(x/w,y/w,z/w,1)<script type="math/tex" id="MathJax-Element-31">(x‘/w,y‘/w,z‘/w,1)</script>,而我们上面的假设显然是使用了齐次坐标的归一化表示,不影响点的坐标。

该4*4矩阵的乘法表示如下:

x=ax+by+cz+dy=ex+fy+gz+hz=ix+jy+kz+l1=mx+ny+oz+p
<script type="math/tex; mode=display" id="MathJax-Element-32"> x‘ = ax + by + cz+d\y‘ = ex + fy + gz+h\z‘ = ix + jy + kz+l\1 = mx + ny+oz + p </script>
根据最后一个等式可以推出:
m=n=o=0,p=1
<script type="math/tex; mode=display" id="MathJax-Element-33">m = n = o = 0 , p=1</script>
此外,我们将x,y,z<script type="math/tex" id="MathJax-Element-34">x‘,y‘,z‘</script>的等式跟平移等式进行对比,平移等式为:
x=x+Txy=y+Tyz=z+Tz
<script type="math/tex; mode=display" id="MathJax-Element-35"> x‘=x+T_{x}\y‘=y+T_{y}\z‘=z+T_{z}\</script>
对比x<script type="math/tex" id="MathJax-Element-36">x‘</script>等式,很容易得出:
a=1,b=0,c=0,d=Tx
<script type="math/tex; mode=display" id="MathJax-Element-37"> a=1,b=0,c=0,d=T_{x} </script>
对比y<script type="math/tex" id="MathJax-Element-38">y‘</script>等式,很容易得出:
e=0,f=1,g=0,h=Ty
<script type="math/tex; mode=display" id="MathJax-Element-39"> e=0,f=1,g=0,h=T_{y} </script>
对比z<script type="math/tex" id="MathJax-Element-40">z‘</script>等式,很容易得出:
i=0,j=0,k=1,l=Tz
<script type="math/tex; mode=display" id="MathJax-Element-41"> i=0,j=0,k=1,l=T_{z} </script>
于是,我们找到了平移矩阵,满足点的平移计算:
?????xyz1?????=??????100001000010TxTyTz1????????????xyz1?????
<script type="math/tex; mode=display" id="MathJax-Element-42"> \begin{bmatrix} x‘\\ y‘\\ z‘\1 \end{bmatrix} =\begin{bmatrix} 1 & 0 &0 & T_{x}\\ 0 & 1 &0 & T_{y}\\ 0 & 0 &1 & T_{z}\0 & 0 &0 & 1 \end{bmatrix} *\begin{bmatrix} x\\ y\\ z\1 \end{bmatrix} </script>

4阶旋转矩阵

我们发现平移需要用到4*4,而之前旋转矩阵用到的是3*3矩阵,这显然还不太统一,于是,我们想办法把3*3的旋转矩阵转为4*4的旋转矩阵。经过前面的学习,我们很容易得出如下方程组:

x=xcosβ?ysinβy=xsinβ+ycosβz=zx=ax+by+cz+dy=ex+fy+gz+hz=ix+jy+kz+l1=mx+ny+oz+p
<script type="math/tex; mode=display" id="MathJax-Element-94">x‘=x\cos \beta -y\sin \beta \y‘=x\sin \beta + y\cos \beta \z‘=z\x‘ = ax + by + cz+d\y‘ = ex + fy + gz+h\z‘ = ix + jy + kz+l\1 = mx + ny+oz + p </script>
现在,我们基本可以一眼就看出来每个变量的值是多少了,这里不再详细说明,直接给出最后的旋转矩阵:
?????xyz1?????=?????cosβsinβ00?sinβcosβ0000100001???????????xyz1?????
<script type="math/tex; mode=display" id="MathJax-Element-95"> \begin{bmatrix} x‘\\ y‘\\ z‘\1 \end{bmatrix} =\begin{bmatrix} \cos \beta & -\sin \beta &0 & 0\\ \sin \beta & \cos \beta &0 & 0\\ 0 & 0 &1 & 0\0 & 0 &0 & 1 \end{bmatrix} *\begin{bmatrix} x\\ y\\ z\1 \end{bmatrix} </script>

缩放矩阵

经过前面的学习,我们很容易得到求缩放矩阵相关方程组:

x=x?Sxy=y?Syz=z?Szx=ax+by+cz+dy=ex+fy+gz+hz=ix+jy+kz+l1=mx+ny+oz+p
<script type="math/tex; mode=display" id="MathJax-Element-100">x‘=x * S_{x} \y‘=y * S_{y} \z‘=z * S_{z}\x‘ = ax + by + cz+d\y‘ = ex + fy + gz+h\z‘ = ix + jy + kz+l\1 = mx + ny+oz + p </script>
一样非常简单可以求得缩放矩阵,满足点的缩放操作:
?????xyz1?????=??????Sx0000Sy0000Sz00001????????????xyz1?????
<script type="math/tex; mode=display" id="MathJax-Element-101"> \begin{bmatrix} x‘\\ y‘\\ z‘\1 \end{bmatrix} =\begin{bmatrix} S_{x} & 0 &0 & 0\\ 0 & S_{y} &0 & 0\\ 0 & 0 &S_{z} & 0\\ 0 & 0 &0 & 1 \end{bmatrix} *\begin{bmatrix} x\\ y\\ z\1 \end{bmatrix} </script>

矩阵示例

前面讲了那么多的矩阵变换,我们尝试使用矩阵来实现平移效果。首先来看一下顶点着色器代码:

// 顶点着色器代码(决定顶点的位置、大小、颜色)
var VSHADER_SOURCE = 
  ‘attribute vec4 a_Position;\n‘ +
  ‘uniform mat4 u_xformMatrix;\n‘ +
  ‘void main() {\n‘ +
  ‘  gl_Position = u_xformMatrix * a_Position;\n‘ + //webgl内置支持矩阵乘法
  ‘  gl_PointSize = 10.0;\n‘ +      // 设置顶点的大小
  ‘}\n‘;

以上定义了一个叫u_xformMatrix的4*4矩阵变量,然后与顶点进行了相乘,得到最后的顶点坐标。
接着来看一下矩阵的定义:

// webgl按列的模式来读,所以定义矩阵时要使用转置矩阵
var xformMatrix = new Float32Array([
    1.0 , 0.0 , 0.0 , 0.0,
    0.0 , 1.0 , 0.0 , 0.0,
    0.0 , 0.0 , 1.0 , 0.0,
    Tx , Ty , Tz , 1.0
]);
var u_xformMatrix = context.getUniformLocation(context.program, ‘u_xformMatrix‘);
context.uniformMatrix4fv(u_xformMatrix , false , xformMatrix);

最终得到的效果图如下:
技术分享

矩阵变换实现三角形的旋转和缩放的原理是一样的,只需要修改矩阵的定义即可,大家可以尝试一下。

模型矩阵

前面讲到的矩阵变换都是针对单个变换的,如果想要对一个三角形同时进行平移和缩放,该如何处理呢?

如果仔细想想,不难得到如下等式:

=?(?)
<script type="math/tex; mode=display" id="MathJax-Element-122">变换后点坐标 = 缩放矩阵 * (平移矩阵 * 点坐标)</script>

根据矩阵乘法结合律,又可得:

=(?)?
<script type="math/tex; mode=display" id="MathJax-Element-123">变换后点坐标 = (缩放矩阵 * 平移矩阵) * 点坐标</script>

我们来看个多重变换的例子,把一个三角形平移后再进行缩放,先来看下顶点着色器代码:

// 顶点着色器代码(决定顶点的位置、大小、颜色)
var VSHADER_SOURCE = 
  ‘attribute vec4 a_Position;\n‘ +
  ‘uniform mat4 u_translateMatrix;\n‘ +
  ‘uniform mat4 u_scaleMatrix;\n‘ +
  ‘void main() {\n‘ +
  ‘  gl_Position = u_scaleMatrix * u_translateMatrix * a_Position;\n‘ +
  ‘  gl_PointSize = 10.0;\n‘ +      // 设置顶点的大小
  ‘}\n‘;

可以看到,顶点着色器先计算两个变换矩阵相乘,最后再乘以顶点坐标。
变换矩阵的赋值如下所示:

var Tx = 0.5 , Ty = 0.5 , Tz = 0.0;
// 按列方向读取,定义时使用矩阵转置
var translateMatrix = new Float32Array([
    1.0 , 0.0 , 0.0 , 0.0,
    0.0 , 1.0 , 0.0 , 0.0,
    0.0 , 0.0 , 1.0 , 0.0,
    Tx , Ty , Tz , 1.0
]);
var u_translateMatrix = context.getUniformLocation(context.program, ‘u_translateMatrix‘);
context.uniformMatrix4fv(u_translateMatrix , false , translateMatrix);

// 缩小一半
var Sx = Sy = Sz = 0.5;
// 按列方向读取,定义时使用矩阵转置
var scaleMatrix = new Float32Array([
    Sx , 0.0 , 0.0 , 0.0,
    0.0 , Sy , 0.0 , 0.0,
    0.0 , 0.0 , Sz , 0.0,
    0.0 , 0.0 , 0.0 , 1.0
]);
var u_scaleMatrix = context.getUniformLocation(context.program, ‘u_scaleMatrix‘);
context.uniformMatrix4fv(u_scaleMatrix , false , scaleMatrix);

最终效果图如下:
技术分享

我们在顶点着色器中计算了两个矩阵相乘,实际上,三角形三个顶点我们重复计算了三遍,如果有一万个顶点,这样子处理是非常浪费时间的,更高效的做法是先计算好两个矩阵相乘,然后顶点着色器只定义一个最终的矩阵即可,我们再来看看之前定义的矩阵:

=(?)?
<script type="math/tex; mode=display" id="MathJax-Element-132">变换后点坐标 = (缩放矩阵*平移矩阵) * 点坐标</script>
实际上,我们把点坐标左边的矩阵计算统称为模型矩阵,所以上面的式子又可以写成:
=?
<script type="math/tex; mode=display" id="MathJax-Element-133">变换后点坐标 = 模型矩阵 * 点坐标</script>
这样定义的好处是顶点着色器确实只需要定义最终的模型矩阵变量,而模型矩阵变量可以使用js进行计算,这样是非常高效的,这也是我们推荐的方式。

小结

本篇探讨了一下基本的矩阵变换,这对于认识到矩阵的巨大用处是非常重要的。在3D的世界里,其实就是各种矩阵的计算,然后得到变换后的顶点的坐标,最后再绘制出来。知道了矩阵的使用原理,为以后更加复杂的投影变换打下基础。

源码

点击下载(基础矩阵变换)

参考

<<WebGL编程指南>>
百度百科-向量
百度百科-矩阵

<script type="text/javascript"> $(function () { $(‘pre.prettyprint code‘).each(function () { var lines = $(this).text().split(‘\n‘).length; var $numbering = $(‘
    ‘).addClass(‘pre-numbering‘).hide(); $(this).addClass(‘has-numbering‘).parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($(‘
  • ‘).text(i)); }; $numbering.fadeIn(1700); }); }); </script>

    WebGL学习系列-基础矩阵变换