WebGL ThreeJS学习总结四

© Young 2017-05-13 19:07
Welcome to My GitHub

概述

通过前段时间的学习,现在已经能使用ThreeJS框架制作一些简单3D效果,对原生WebGL也有了简单的了解;在学习过程中察觉到自己在数学方面欠缺的太多,所以我决定先缓一缓技术方面的学习,补一补数学基础。

另外需要的注意的是以下内容大部分来自于《3D数学基础:图形与游戏开发》。

概念

一、数

自然数、整数、有理数、实数、复数等。

1、复数

复数为实数的延伸,它使任一多项方程式都有根。

任意复数都可以表达为x+y*i,i是复数当中的虚数单位,它是-1的平方根,后边的四元数会涉及到复数。

二、坐标

物体坐标系(模型坐标系)、世界坐标系、摄像机坐标系(视图坐标系)、投影坐标系、笛卡尔坐标系、多坐标系、惯性坐标系、嵌套式坐标系、描述坐标系、坐标系转换、齐次坐标等。

1、惯性坐标系

惯性坐标系的原点和物体坐标系原点重合,但惯性坐标系的轴平行于世界坐标系的轴;引入惯性坐标系的好处在于从物体坐标系转换到惯性坐标系只需要旋转,从惯性坐标系转换到世界坐标系只需要平移。

上边列了这么多坐标系的名称或者概念,很容易让人感到疑惑,为什么需要这么多坐标系?所有问题都放到世界坐标系中处理不就好了吗?

我的理解是有很多问题放到世界坐标系中处理反而会更复杂,比如我要计算我和吴小胖(一只猫)的距离,根据我的纬度和经度以及吴小胖的纬度和经度算,反而没有以我为原点拿标尺去量更简单。

三、向量

零向量、负向量、向量的大小、标量与向量相乘、标准化向量、向量的加法与减法、向量点乘、向量投影、向量叉乘等。

1、向量的模

向量有大小和方向,但是向量的大小也被称作为向量的模,并没有明确表示出来,需要进行计算,计算公式如下:

    a = [x1,y1];
    |a| = Math.sqrt(x1*x1+y1*y1);

2、单位向量

模为1的向量即为该方向的单位向量,计算公式如下:

    a = [x1,y1];
    na = a/|a| = [x1/Math.sqrt(x1*x1+y1*y1),y1/Math.sqrt(x1*x1+y1*y1)];

3、向量点乘

两个向量点乘结果越大,那么两个向量的夹角越小,两向量越靠近。

    a = [x1,y1];
    b = [x2,y2];
    //a、b向量夹角为c
    a*b = [x1,y1]*[x2,y2] = x1*x2+y1*y2 = |a|*|b|*cos(c);
    //因此可得两个向量夹角的公式
    c = arccos(a*b/(|a|*|b|));

4、向量叉乘

叉乘仅适用于3D向量,两个向量叉乘得到的向量垂直于原来的两个向量,另外叉乘的优先级比点乘高。

    a = [x1,y1,z1];
    b = [x2,y2,z2];
    //a、b向量夹角为c
    a*b = [x1,y1,z1]*[x2,y2,z2] = [y1*z2-z1*y2,z1*x2-x1*z2,x1*y2-y1*x2];
    |a*b| = |a|*|b|*sin(c);
    //因此可得两个向量夹角的公式
    c = arcsin(|a*b|/(|a|*|b|));

到这基本上可以去稍微过一眼ThreeJS框架中Vect2、Vect3、Vect4等向量类的源代码了,另外如果不习惯英文版,也可以去看国内大神翻译的中文版源代码
另外ThreeJS在设计时赋予Vect3、Vect2、Vect4等向量类多种意义,比如Vect3即可以表示一个3D坐标系中的点,也可以表示一个3D坐标系中向量等;这是一种有争议的设计方法,好处在于可以简化某些功能函数的设计,比如旋转函数,点可以旋转,向量也可以旋转,如果点和向量分开,那么旋转函数必须得定义接收多种参数的版本;反之则只需要定义接收一种参数的版本。坏处在于在逻辑上会带来混淆的情况。

四、矩阵

方阵、单位矩阵、余子式、代数余子式、行列式、转置矩阵、标准伴随矩阵、逆矩阵、正交矩阵、齐次矩阵、逆转置矩阵、零矩阵等。

1、方阵

方阵能描述任意线性变换,简单来说线性变换会保留直线,也就是线性变换的图形不可能发生弯曲和卷折。

2、矩阵的几何意义

矩阵并不神秘,它只是用一种紧凑的方式来表达坐标转换所需的数学运算。

    //3D坐标系中的基向量乘以任意矩阵M的情况
    M = [m11,m12,m13,
        m21,m22,m23,
        m31,m32,m33];


    x = [1,0,0];
    y = [0,1,0];
    z = [0,0,1];


    x*M = [m11,m12,m13];


    y*M = [m21,m22,m23];


    z*M = [m31,m32,m33];
    //由此可见矩阵的每一行都可以解释为转换后的基向量

给出一个期望的变换,我们要做的一切就是计算基向量的变换,然后将变换后的基向量填入矩阵,这样就得到此次变换的矩阵形式。

3、单位矩阵

对角线上的元素都是1,其余元素全是0的n阶矩阵称为n阶单位矩阵,记为I(n)或者E(n)。

4、余子式与代数余子式

假设矩阵M有r行、c列,从中移除第i行和第j列后剩下的矩阵称为M的余子式;而在余子式前面添加符号Math.pow(-1,(i+j))后得到区分正负的余子式称为M的代数余子式;

需要注意的是可以移除多行,比如移除选定的k行(i1、i2……ik)和g列(j1、j2……jg)后,代数余子式前边的符号为Math.pow(-1,(i1+i2+.....+ik+j1+j2+......+jg))

5、行列式

任意方阵都存在一个标量,称为该标量的行列式记为det(M)或者|M|,行列式有很多有趣的几何解释,在2D中矩阵的行列式等于以基向量为边的平行四边形的有符号的面积;在3D中矩阵的行列式等于以基向量为边的平行六面体的有符号体积;

    /**
     * 从矩阵中任意选择一行或者一列,对该行或者该列中的每个元素都乘以各自的代数余子式,
     * 这些乘积的和就是该矩阵的行列式。
     */


    //2*2矩阵行列式
    [m11,m12,
     m21,m22] = m11*m22-m12*m21;


    //3*3矩阵行列式
    [m11,m12,m13,
     m21,m22,m23,
     m31,m32,m33] = m11*|[m22,m23,m32,m33]|-m12*|[m21,m23,m31,m33]|+m13|[m21,m22,m31,m32]|


    //以此类推

6、转置矩阵

在线性代数中,矩阵A的转置是另一个矩阵T,由下列等价动作建立:

把A的横行写为T的纵行;
把A的纵行写为T的横行;

形式上来说,m行n列的矩阵的转置是 n行m列的矩阵。

7、标准伴随矩阵

某个矩阵的代数余子式矩阵的转置矩阵被称为该矩阵的标准伴随矩阵。

8、逆矩阵

在线性代数中,给定一个n阶方阵A,若存在一个n阶方阵B,使得A*B = I,其中I为n阶单位矩阵,则称A是可逆的且B是A的逆矩阵。

7、可逆矩阵和非可逆矩阵

并非所有矩阵都是有逆的,如果一个矩阵有逆矩阵,那么称它为可逆矩阵或者非奇异矩阵;如果一个矩阵没有逆矩阵,那么称它为非可逆矩阵或者奇异矩阵;奇异矩阵的行列式为0,非奇异矩阵的行列式不为0。

可逆矩阵的逆矩阵为其标准伴随矩阵除以其行列式,这种方式适合低阶可逆矩阵求逆矩阵;其实还存在其他方法比如高斯消元法,适用于高阶可逆矩阵求逆矩阵;
矩阵转置的逆等于矩阵逆的转置;
矩阵乘积的逆等于矩阵逆的相反顺序的乘积,扩展到多个矩阵也适用;
矩阵的逆在几何上非常有用,因为它可以使得我们可以计算变换的反向或者相反变换,从而撤销原变换。

8、正交矩阵

若方阵M是正交,当且仅当方阵和其转置矩阵的乘积等于单位矩阵。

这是一条非常有用的性质,因为在实际应用中经常需要计算矩阵的逆,而3D图形计算中正交矩阵出现得又是如此频繁,例如旋转和镜像矩阵,如果知道某个矩阵是正交的,就可以避免计算逆矩阵了,大大减少计算量。
计算逆矩阵时,仅在预先知道矩阵是正交的情况下才能利用正交矩阵的优点,如果预先不知道,检查正交性通常是浪费时间。
有时候可能得到略微得到违反正交性的矩阵,例如外部坏数据或者浮点数运算等,这种情况下需要做矩阵正交化,得到一个正交矩阵,这个矩阵要尽可能的和原矩阵相同(至少希望是这样);
构造一组正交基向量的标准算法是施密特正交化,它的基本思想是,对每一行,从中减去它平行于已处理过行的部分,最后得到垂直向量。

9、齐次矩阵

4*4齐次矩阵只是3D运算的一种方便记法而已,并不代表四维空间。

10、逆转置矩阵

在图形学中,同样的一个模型视图可以用来变换点、线、多边形以及其它几何体,也可以用来变换多边形表面的切向量,但是却不能在任何情况用来变换法线(部分情况下可以),比如下图:

上图是针对一个多边形以及一条边上的法线进行缩放变换;X轴上缩放为原来的一半,左边是变换前的状态,中间是将同样的模型变换应用到法线上的结果,显然是错的,因为法线并不垂直于切线,最右边的图是正确的结果。

结论:用法向量乘以模型矩阵的逆转置矩阵,可以求得变换后的法向量。

11、零矩阵

零矩阵即所有元素皆为0的矩阵。

五、变换

旋转、缩放、正交投影、镜像、切变、变换组合、变换分类等。

在讨论变换之前有一点很重要,我们必须清楚变换的主体是啥。也许你会说这还用说吗?当然是变换物体咯,其实不然,在某些情况下变换坐标系反而更方便。

1、3D空间绕任意过原点轴旋转矩阵推导

    /**
     * 看了半天才看懂,摘录下来加深印象;
     * 首先假设该任意轴过原点,用向量n表示(向量n为单位向量),然后某个向量v绕n旋转得到新向量m;
     * 此次旋转的矩阵表示为R(n,c),也就是绕n旋转c角度;
     * 向量乘法有点乘和叉乘的区别,所以下边表达式中.表示点乘,*表示叉乘,普通乘法省略操作符。
     */
    m = v.R(n,c);


    /**
     * 将向量分解为垂直于n向量的v2和平行为n向量的v1;
     * 这么做的好处在于将复杂的3D问题转换为简单的2D问题;
     * 那么绕n旋转v就可以分为绕n旋转v1和绕n旋转v2;
     * v1平行于n,旋转之后依然为v1;
     * v2垂直于n,旋转之后为m2。
     */
    v = v1+v2;
    m = v1+m2;


    //v1是v在n上的投影
    v1 = |v1|n;
       = |v|cos(c1)n;
       = |v||n|cos(c1)/|n|n;
       = v.n/|n|n;//n为单位向量长度为1
       = (v.n)n;


    //v1+v2 = v
    v2 = v - v1;
       = v - (v.n)n;


    //向量w垂直于向量n和向量v2
    w = n*v2;//叉乘
      = n*(v-v1);
      = n*v - n*v1;//两方向相同的向量叉乘结果为向量0
      = n*v - 0;
      = n*v;


    m2 = w1+v3;


    w1 = |cos(c-90)|w;
       = |cos(c)cos(90)+sin(c)sin(90)|w;
       = sin(c)w;//180>c>90


    v3 = -|sin(c-90)|v2;
       = -|sin(c)cos(90)-cos(c)sin(90)|v2;
       = -|-cos(c)|v2;//180>c>90
       = cos(c)v2;


    m2 = sin(c)w+cos(c)v2;
       = sin(c)(n*v)+cos(c)(v - (v.n)n);


    m = v1+m2;
      = (v.n)n+sin(c)(n*v)+cos(c)(v - (v.n)n);


    /**
     * 现在已经得到了原向量v、旋转角度c、过原点旋转轴向量n以及旋转后向量m的表达式;
     * 那么如果已经旋转角度、过原点旋转轴、原向量v就可以得到旋转后向量m了;
     * 那么要得到期望变换的矩阵,只需要计算基向量的变换,然后把变换后的基向量整合起来就可以了。
     */


    //在3D坐标中,基向量就是x、y、z轴的正方向向量
    x = [1,0,0];
    n = [nx,ny,nz];
    x1 = (x.n)n+sin(c)(n*x)+cos(c)(x - (x.n)n);
    //...
    x1 = [(nx)(nx)(1-cos(c))+cos(c),(nx)(ny)(1-cos(c))+(nz)sin(c),(nx)(nz)(1-cos(c))-(ny)sin(c)];
    //后续依次代入y轴和z轴即可

2、3D空间或者2D平面沿任意方向缩放

    /**
     * 设n为平行于缩放方向的单位向量,k为缩放因子,向量v为原始向量,m为缩放后的向量;
     * 我们需要推导出一个表达式,可以通过n、v、k来计算m;
     * 向量乘法有点乘和叉乘的区别,所以下边表达式中.表示点乘,*表示叉乘,普通乘法省略操作符。
     */


     v = v1+v2;
     v1 = (v.n)n;


     m2 = v2;//向量v在n方向上缩放k变成向量m,因为v2、m2均垂直于n,因此该方面上的分向量不变,所以v2=m2
        = v-v1;
        = v-(v.n)n;
     m1 = kv1;
        = k(v.n)n;


     m = m1+m2;
       = v-(v.n)n+k(v.n)n;
       = v+(v.n)n(k-1);


    //2D平面
    x = [1,0];
    n = [nx,ny];


    //3D空间
    x = [1,0,0];
    n = [nx,ny,nz];


    //具体求解矩阵略...

3、正交投影

正交投影的原理就是让某个方向的缩放因子为0

    /**
     * S表示缩放矩阵,S(n,k)表示在向量n方向上缩放,缩放因子为k。
     */


    //2D平面,向x轴投影,也就是y轴方向上缩放,缩放因子为0
    Px = S([0,1],0) = [1,0,
                       0,0];


    //3D空间,向xy平面投影,也就是z轴方向上缩放,缩放因子为0
    Pxy = S([0,0,1],0) = [1,0,0,
                          0,1,0,
                          0,0,0];


    //任意方向的投影矩阵略,可以根据沿任意方向的缩放矩阵推导。

4、镜像

镜像也叫反射,其作用是将物体沿直线(2D)或者平面(3D)翻折,根据缩放公式使缩放因子为-1,可以得到镜像矩阵。

5、切变

切变是一种坐标系扭曲的变换,切变的时候角度会发生变化,但是面积或者体积却保持不变,基本思想是将某一坐标的乘积加到另一个上。

    //2D平面中,将y乘以某个系数s,然后加到x上 x1 = sy+x
    Hx(s) = [1,0,
             s,1];


    //3D空间中,将z乘以不同的系数s、t,然后分别加到x、y上 x1 = sz+x y1 = tz+y
    Hxy(s,t) = [1,0,0,
                0,1,0,
                s,t,1];

6、变换组合

    /**
     * 视图矩阵
     * 在世界坐标系中默认相机上方向是(0,1,0)、相机轴方向是(0,0,1)、右方向是(1,0,0);
     * 摄像机坐标系中相机轴方向是视线方向的反方向,假设视线方向为(fx,fy,fz),那么相机轴方向为(-fx,-fy,-fz);
     * 我们要把这三个方向变换到上方向(ux,uy,uz)、相机轴方向是(-fx,-fy,-fz)、右方向是(sx,sy,sz);
     * 所需要的矩阵为M。
     */


     M = [m0,m3,m6,
          m1,m4,m7,
          m2,m5,m8];


     O = [1,0,0,
          0,1,0,
          0,0,1];


     V = [sx,sy,sz,
          ux,uy,uz,
          -fx,-fy,-fz];


     M*O = V;


     M = [sx,sy,sz,
          ux,uy,uz,
          -fx,-fy,-fz];


    /**
     * 如果摄像机世界坐标为(eyeX, eyeY, eyeZ);
     * 视线焦点视界坐标为(centerX, centerY, centerZ);
     * 上方向向量为(upX, upY, upZ)。
     */
    fx = centerX - eyeX;
    fy = centerY - eyeY;
    fz = centerZ - eyeZ;


    //视线方向向量单位化
    rlf = 1 / Math.sqrt(fx*fx + fy*fy + fz*fz);
    fx *= rlf;
    fy *= rlf;
    fz *= rlf;


    //右方向向量等于视线方向向量和上方向向量叉乘
    sx = fy * upZ - fz * upY;
    sy = fz * upX - fx * upZ;
    sz = fx * upY - fy * upX;


    //右方向向量单位化
    rls = 1 / Math.sqrt(sx*sx + sy*sy + sz*sz);
    sx *= rls;
    sy *= rls;
    sz *= rls;


    //上方向向量单位化等于视线方向向量单位化和右方向向量单位化叉乘
    ux = sy * fz - sz * fy;
    uy = sz * fx - sx * fz;
    uz = sx * fy - sy * fx;


    //WebGL矩阵列主序
    M[0] = sx;
    M[1] = ux;
    M[2] = -fx;
    M[3] = 0;


    M[4] = sy;
    M[5] = uy;
    M[6] = -fy;
    M[7] = 0;


    M[8] = sz;
    M[9] = uz;
    M[10] = -fz;
    M[11] = 0;


    M[12] = 0;
    M[13] = 0;
    M[14] = 0;
    M[15] = 1;


    //因为上述假设是建立在摄像机坐标系和世界坐标系在同一原点的情况下的,因此摄像机坐标系需要进行平移变换


    M = M.translate(-eyeX, -eyeY, -eyeZ);

关于摄像机轴方向为视线方向的反方向可以看下图示例;

透视投影矩阵比想象中的复杂,就不再复制粘贴了,《深入探索透视投影变换》这篇文章写得很详细;另外上文作者的其它文章也挺不错,导致正交投影矩阵也不用复制粘贴了,链接如下:《推导正交投影变换》

7、变换分类

线性变换、仿射变换、可逆变换、等角变换、正交变换、刚体变换等。

六、3D中的方位与角位移

1、欧拉角

简单来说欧拉角的基本思想就是任何角位移都可以分解为绕三个互相垂直的轴的三个旋转组成,任意三个轴和任意顺序都可以,但最有意义的是使用笛卡尔坐标系(旋转物体自身的坐标系,而不是世界坐标系)并按一定顺序所组成的旋转序列;

作为前端工程师我们得知道手机里边陀螺仪设备所返回的数据,就是一个欧拉角位移数据;

alpha表示设备绕Z轴旋转的角度,范围为0~360;
beta表示设备绕X轴旋转的角度,范围为-180~180;
gamma表示设备绕Y轴旋转的角度,范围为-90~90;

这里存在几个问题:

其一:为什么每个角度的范围不一致?

首先得明确一点为什么要有范围,主要是因为如果没有范围,比如alpha为10和370其实是等价的,一个角位移存在多个描述会导致一些麻烦(可以思考下会有什么麻烦?);再来就是为什么不同方向的角位移限制不一致,其实原因还是基于上述理由,如果都限制为0~360,还是有些角位移有多种表示,导致连一些基本的问题,比如两组欧拉角代表的角位移是否相同都很难回答。
至于为什么范围分别是0~360、-180~180、-90~90,这里就不过多展开了,同时也压根不准备证明欧拉角定理(欧拉定理是以瑞士数学家莱昂哈德·欧拉命名,于1775年,欧拉使用简单的几何论述证明了这定理)。

其二:怎么判断手机陀螺仪的顺序?

很简单,范围越大表示次序越靠前,因为最外层的运动范围更大一些,也就是说手机陀螺仪欧拉角的顺序是ZXY

说到欧拉角还有一个经典的问题得说下:

万向节死锁

简单来说就是在旋转过程中有两个轴重合导致,失去一个维度,导致此后的角位移没办法用欧拉角表示,也就是说欧拉角失效了,在欧拉角进行插值操作时大多数情况下会导致抖动、路径错误,物体会突然飘起来。
优酷里边有个视频教程讲的挺清楚的欧拉旋转
为什么用三个数来表达3D方位一定会导致如万向锁这样的问题?这是有数学原因的,它涉及到一些非常高级的数学概念,如“簇”;下边的四元数通过使用四个数来表达方位,从而避免了这些问题。

尽管矩阵有一些缺点,比如占用了更多的内存,不直观,可能会出现病态矩阵的情况,但是当和图形API交流时,最终必须用矩阵来描述所需的转换,因此我们得了解如何将欧拉角转换为矩阵。

另外需要注意欧拉角可以转换为矩阵形式,旋转矩阵也可以转换为欧拉角的形式,这里不准备过多阐述。

2、四元数

老实说这是一个比较复杂的概念(至少当我写这篇博客的时候感觉如此),一个四元数包含一个标量分量和一个3D向量分量,经常记标量分量为w,记向量分量为单一的v或者分开的x,y,z。

    [w,v]
    [w,(x,y,z)]

数学家们慢慢接受复数之后,发现复数集存在于一个2D平面上,该平面有两个轴,实轴和虚轴;这样就能将复数(x+y*i)解释为2D向量,也能用来旋转2D中的向量;然后很自然的数学家们就想找到一种方法将复数从2D扩展到3D,刚开始他们认为这种新的复数应该有一个实部和两个虚部,然而这种方案一直没有进展,直到后来意识到应该有三个虚部和一个实部,就这样四元数诞生了。

四元数能被解释为角位移的绕某个轴n旋转a角度,公式如下:

    q = [cos(a/2),sin(a/2)*n]

另外四元数有slerpsquad插值方式,可以有效避免欧拉角插值过程中的问题。

发表评论

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