入门AR,VR,OpenGL必备知识--3D图形学基础理论

96
ZhengYaWei
0.8 2017.10.02 00:46* 字数 5187

引言

请不要质疑你的眼睛,文章的题目就是“3D图形学基础理论”。可能有人要疑惑了,作为一个 iOS 开发者为什么要来学习什么 3D 图像学相关的东西,莫非这是要转行的节奏。开玩笑,怎么可能呢? 作为一个对技术有追求的人,怎么可能转行。接下来让我告诉你,为什么要学习3D图形学的一些基础理论。

  • 1、远的先不说,就拿 iOS来说。iOS 中在做一些基本动画开发的过程中,相信绝大多数开发者都是用过 Core Graphics 框架中的 CGAffineTransform 这个类,虽然只是普通的平面动画,但其实现原理和 3D 图形学中的矩阵很类似。有多少开发者想过其中的各种动画效果诸如缩放、旋转、平移等是如何做到的?
  • 2、当然看到CGAffineTransform这个类,自然很容易想到CATransform3D这个类,这里既然是3D动画,肯定和3D图形学有关了。
  • 3、再说一个很火的名词 AR,Apple 已经让一部分开发者早先接触了 ARKit 。ARKit 在接下来的一段时间中必然会很火。其实简单用 ARKit 实现一个 AR 的 Demo 很简单,因为 苹果封装的 ARKit 十分简单易用。学习ARKit重点在于理解 ARKit 中各个类的关系、运行原理。其中运行原理就涉及到 3D 图像学相关的知识。
  • 4、说到AR,自然会想到VR,VR全景视频、全景图片自然也会涉及到3D场景。
  • 5、除此之外,还有 OpenGL ,当然这个属于相对比较片底层的技术了。iOS 中的各种控件的底层再底层都是基于其实现的。AR、VR技术都和其脱不了干系。笔者目前所在的公司,就有一套公司自己的OpenGL绘图引擎,基于高德地图在地图上绘制各种各样的炫酷模型。
  • 6、还有苹果的3D游戏开发框架ScenceKit,毫无疑问会涉及。
    除了上述所讲到的这些,3D 图像学在实际开发中涉及了还有很多。所以还是有必要稍稍了解下。学一些大学的高数知识。

一、3D成像原理

三维(3D)这个术语表示显示的物体具有三个维度:宽度、高度和深度。如放在书桌上的纸张是一个二维物体,因为它没有让人感觉到所谓的深度。而一罐可口可乐放在桌上却是有一定的深度。

几个世纪以来,艺术家已经知道如何让一幅画看上去有深度。本质上来说,画其实是一个二维物体,只是显示在计算机屏幕上的二维图像或画板上的水墨,但是却可以提供深度的错觉。

有这样一个公式:2D + 透视 = 3D 通过下面这幅图,就可以看懂这个公式。我们可以看到 12 条线段组成了一个三维立方体的图像。

实际上为了真正的看到 3D 图像需要用两个眼睛观察一个物体或者为每只眼睛分别提供某个特定物体的一副独立而又唯一角度的图像。实际上人的每一只眼睛看到的是一副独立的二维图像,非常类似于每个视网膜(位于眼睛的后半部分)上显示了一副临时照片。随后大脑对这两幅图像进行图像组合,在脑海中形成一副合成的 3D 图片。中所周知现在的iPhone基本都是双摄像头,苹果之所以早在 iPhone7 Plus 就增加了双摄,其实那时已经是在为AR埋下伏笔了。因为同人眼一样,为了识别 3D 场景,AR 需要双摄像头的硬件设备。那些不在双摄像头的硬件上运行AR场景的在笔者认为是伪 AR ,因为笔者认为AR的核心是在于场景的识别和分析,而这一点就要依赖于双摄;再说一个典型的例子,VR 应用的头盔,一般都会分为左右屏,左右屏的实时图片或视频场景是有一定视角差的,正式基于此,佩戴头盔的玩家才能体验到 3D 场景。

单凭透视本身就足以创建三维的外观,但是如果长时间盯着上面的立方体看的话,会产生线条错乱的幻觉。我们如果用手遮住一只眼睛去看现实世界,仍然是三维的,也不会产生错乱的幻觉,这是因为现实世界中有关。如果上面的立方体通过纹理贴图、光照效果处理等就不会再产生错乱的感觉。

二、3D编程基本原则

2.1 坐标系统

笛卡尔坐标系统相信很多人都了解,被熟悉的主要有 2D 和 3D 笛卡尔坐标系。关于这两个坐标系统不用做过多解释,很容易理解。这里我主要想说两个概念坐标裁剪和视口。

  • 坐标裁剪
    窗口(电脑屏幕)是以像素为单位度量的。开始在窗口绘制点、线或形状之前,必须告诉编程框架如何把制定的坐标翻译为屏幕坐标。我们可以通过指定占据窗口的笛卡尔控件区域完成这个任务,这个区域就称为裁剪区域。如下图显示了两种常见的裁剪区域。
  • 视口: 把绘图左边映射到窗口坐标
    裁剪区域的宽度和高度很少和窗口的宽度或高度相匹配(以像素为单位)。因此,坐标系统必须从笛卡尔坐标系映射到物理屏幕像素坐标。这个映射就是通过视口的设置来指定的。视口就是窗口内部用于绘制裁剪区域的客户端区域。视口简单的把裁剪区域映射到窗口的一个区域。通常视口被定义为整个窗口,但这并非是严格的。如下面两幅图所示,视口分别被定义为裁剪区域的两倍、与裁剪区域相同的大小。

2.2 从3D到2D(投影)

不管我们觉得自己的眼睛看到的三维图像有多么真实,但是屏幕上的像素实际上是二维的。所以我们要理解的第一个概念是投影。通过指定投影,可以指定在窗口的视景体,并制定如何对它进行变换。投影主要分为两种:正投影和透视投影。

如下图所示,两种投影方式,一种是平行投影也叫作正投影,正投影的特点是所有的投影线都是平行的,另外一种则是透视投影,透视投影的特点是投影线是相交于一点的,相交于这个点叫做投影中心。这一点在现实生活中可以得到很好的验证,想想此刻你顺着平行的火车轨道望去,会发现随着距离的增加,火车的轨道原来越靠近。在后面我会专门抽出一个模块来说一下透视投影的几何原理,这里暂时知道即可。

正投影和透视投影

三、向量

3.1 向量基本介绍

说道这里就涉及到一些数学知识了,不要害怕,我会结合实际的空间来说,高等数学中关于向量和矩阵的知识并不是那么的难。实际上作为开发者的我们也只要掌握一些基础的便足够。

3D 笛卡尔坐标系中的一个点,是通过三个值如(x,y,z)来表示的。这三个值组合起来实际上表示两个重要的值:方向数量。如下图所示的(X,Y,Z)实际上表示了一个方向,方向是朝箭头方向。一个向量的数量就是这个向量的长度。对于 x 轴向量(1,0,0)来说,向量长度是 1。我们把长度为 1 的向量称为单位向量。

OpenGL 中的 math3d 库有这样的两个数据类型M3DVector3f 和 M3DVector4f 。前者是一个三维向量(x,y,z);后者是一个四维向量(x,y,z,w),典型情况下 w 值为 1.0 ,x、y、z 值通过除以 w 进行缩放。明明就是 3D 图形学,又不是 4D 图像学,为何要使用 4 个分量来表示一个向量 ?之所以这样做是因为 3D 顶点变化是要乘以一个 4 * 4 的变换矩阵。有个规则是必须用一个四分量向量乘以一个 4 * 4 的矩阵,后面讲到矩阵时会详细说明。

3.2 向量的运算

向量可以进行加法、减法运算,也可以简单的通过减法、减法进行缩放。然而这里主要说关于向量有趣的两个运算: 点乘和叉乘。

  • 点乘
    两个三分量之间的点乘运算将得到一个标量(只有一个值),它表示两个向量之间夹角对应的三角函数 Cos 值。
  • 叉乘
    两个向量之间叉乘所得的结果是另外一个向量,这个新向量与原来两个向量定义的平面垂直。和点乘不同,在进行叉乘运算的时候向量的顺序非常重要,不同的叉乘顺序会得到两个方向相反的向量。

四、矩阵及其空间变化

4.1 理解场景变化

4.1.1 视觉坐标

视觉坐标是相对于观察则而言的,无论进行何种变换,都可以将它视为绝对的屏幕坐标。这样,视觉坐标就表示一个虚拟固定的坐标系,通常作为参考坐标系使用。

4.1.2 两种变换

在说矩阵之前,先来说一下3D场景中的一些变化。关于变化,主要有两种变换视图变换和模型变换。

  • 视图变换
    所谓的视图就是观察者或相机的位置。视图变换允许我们把观察点放置在任何希望的位置,允许在任何方向上观察。确定视图变换就像在场景中放置照相机并让它指定某个方向。
    总的来说在任何其他模型变换之前,必须先进行视图变换。这是因为对视觉坐标系而言,视图变化就移动了当前的工作坐标系,后续的一切变换随后都是基于新调整的坐标系进行的。
  • 模型变换
    模型变换用于操作模型和其中的特定的对象。模型变换通常是现将模型对象移动到需要的位置上,然后再对他们进行旋转和移动。就拿先旋转后平移和先平移后旋转来说得到的是两种不同情况。如下图所示,无论是先执行旋转还是平移,后一个操作(旋转或平移)都是基于第一个操作结果产生的坐标系进行下一步操作。

4.2模型视图矩阵

与其说矩阵的几何意义这么生涩难懂,不如说的是矩阵在几何中到底是有什么作用呢?一般来说,方阵可以描述任意的线性变换。也就说,在几何当中,我们用矩阵表示几何体的空间变换。比如我们在程序中常用的平移、旋转、缩放等等

模型视图矩阵是一个 4 * 4 矩阵,它表示一个变化后的坐标系,可以用来放置对象和确定对象的位置。顶点作为一个单列矩阵(也就是一个响亮)的形式来表示,并乘以一个模型视图矩阵来获得一个相对视觉坐标系的经过变换的新坐标。

如下代码所形成的矩阵计算所示,一个包含单个顶点数据的矩阵乘以模型视图矩阵后得到新的视觉坐标。顶点数据实际是4个数据,其中包含一个附加值 w ,它表示一个缩放因子,默认情况下是 1.0 ,一般情况下很少去改动。

[x]        [a  b  c  d]          [x0]
[y]        [a  b  c  d]          [y0]
[z]  *    [a  b  c  d]   =     [z0]
[w]       [a  b  c  d]          [w0]

接下来我们就来详细说明,上面是矩阵计算是怎么做到:将一个顶点乘以一个矩阵来对它进行变换。

4.2.1 矩阵构造

矩阵的前三列的前三个元素只是方向向量,表示空间上 x y z 轴上的方向(在这里用向量来表示一个方向)。一般情况下这三个向量之前总是成 90 度夹角,并且通常为单位长度。数学书中中称之为标准正交(向量为单位长度)和正交(向量不是单位长度)。如下图对矩阵进行了标注。另外要注意下图的矩阵最后一行都为 0 ,只有一个元素为 1。



如果有一个包含不同坐标系的位置和方向的 4 * 4 矩阵,然后用一个表示原来坐标系的向量(表示为一个矩阵或向量)乘以这个矩阵,得到的结果是一个转换到新坐标系下的新向量。这就意味着,空间中任何位置和方向都可以由一个 4 * 4矩阵唯一确定。如果一个对象的所有向量乘以这个矩阵,那么我们将得到整个对象变换到的空间中的位置和方向。

4.2.2 单位矩阵

在讲矩阵的变化之前,先简单说下单位矩阵的概念。将一个向量乘以一个单位矩阵,就相当于用这个向量乘以 1 ,不会发生任何变化。单位矩阵中除了一条对角线上的元素为 1,其他元素全为 0 。

4.3 矩阵变化操作

矩阵的变化操作主要分为缩放、平移、旋转。为了更好的理解矩阵的变化操作,以及矩阵是怎么形成的,笔者就直接从缩放讲起。把缩放讲清除,之后的平移以及旋转都是按照同样的套路进行推导。

4.3.1 缩放

为了更好的理解缩放,我先从 2D 环境说起,之后在延伸到3D环境。



如果沿着坐标轴进行缩放,那么每一个坐标轴都有缩放因子,所以2D环境下有两个缩放因子Kx和Ky,那么基向量p和q根据缩放因子的影响,我们可以得到下面公式。


根据变化,可以得到在2D环境下的缩放矩阵。如下所示:


同理,通过2D环境下的缩放矩阵,我们可以得到3D环境下的缩放矩阵。

4.3.2 平移

一个平移矩阵仅仅是将顶点沿着 3 个左边轴中的一个或者多个进行移动。这个应该是很好理解的,这里就不做过多解释。列如在 OpenGL的 math3d 库中,我们就可以通过m3dTranslationMatrix44 函数使用矩阵直接做平移操作。

4.3.3 旋转

关于旋转操作的矩阵实际上推导起来稍稍有些麻烦,需要通过大量的图解和注释来说明。笔者感觉没必要进行详细的推导,毕竟我们不是专业研究数学的,只要知旋转矩阵形成过程和缩放以及平移的原理是一样,只是推到起来会更麻烦些。这里我就简单的列出一些推到结果。当然,有兴趣的同学可以自行研究下。

五、关于CGAffineTransform的实现

在Core Graphics框架图形绘制的时候,经常会有对图形进行平移、缩放、旋转这样的要求。那么我们该如何实现呢?这就需要Core Graphics框架中的CGAffineTransform(矩阵)这个结构体来进行实现了。下面我们就对CGAffineTransform这个矩阵结构体,进行简单的说明。

// CGAffineTransform结构体样式
struct CGAffineTransform {
  CGFloat a, b, c, d;
  CGFloat tx, ty;
};

齐次坐标概念

所谓的其次坐标就是把一个图形用一个三维矩阵表示,其中第三列总是(0,0,1),用来作为坐标系的标准。也就是z轴,是不发生改变的。

|a  b  0|
|c  d  0|
|tx ty 1|

接下来就来看看二维空间中的点坐标是如和借助齐次坐标进行仿射变化。具体运算原理如下:

               |a  b  0|

  [X,Y,  1]    |c  d  0|   =  [aX + cY + tx   bX + dY + ty  1] ;

                |tx ty 1|

运算原理就是如此简单。CGAffineTransform的平移、旋转、缩放操作,都是借助这个公式进行计算的,只不过平移、旋转、缩放只不过是其中的一些特殊情况而已。

CGAffineTransform的平移、旋转、缩放变换。

  • 平移变换
    条件: a = d = 1 ,b = c = 0
              |1  0  0|

  [X,Y,  1]    |0  1  0|   =  [X + tx   ,  Y + ty  , 1] ;

                |tx ty 1|

坐标由 [X,Y, 1] 变成了 [X + tx , Y + ty , 1],与原坐标相比,z轴没发生任何的改变,x 轴方向平移了 tx 个单位, y 轴方向平移了 ty 个单位。平移对应Core Graphics框架中的CGAffineTransformMakeTranslation(CGFloat tx,CGFloat ty)API。

  • 缩放变换
    条件: b = c = 0 ,tx = ty = 0。
               |a  0  0|

  [X,Y,  1]    |0  d  0|   =  [aX  ,  dY  ,1] ;

                |0 0 1|

坐标由[X,Y, 1]变为 [aX , dY ,1] ,X轴扩大了 a 倍,Y 轴扩大了 d 倍。缩放对应Core Graphics框架中的 GAffineTransformMakeScale(CGFloat sx, CGFloat sy) API。

  • 旋转变换
    条件 : tx=ty=0,a=cosɵ,b=sinɵ,c=-sinɵ,d=cosɵ
           |cosɵ   sinɵ  0|

  [X,Y,  1]    |-sinɵ  cosɵ  0|   = [Xcosɵ - Ysinɵ ,   Xsinɵ + Ycosɵ , 1] ;

                |tx     ty    1|

其中 ɵ 是旋转的角度,逆时针为正,顺时针为负。旋转对应Core Graphics框架中的 CGAffineTransformMakeRotation(CGFloat angle) API。

六、关于CATransform3D实现

回想一下前面所说的矩阵的相关知识,如下图是 3D 仿射变化矩阵,和我们前面在矩阵那一块讲的结果一致。


3D仿射变化矩阵

先来看一下这个结构体长什么样。

//CATransform3D基本结构体
struct CATransform3D{
   CGFloat m11, m12, m13, m14;
   CGFloat m21, m22, m23, m24;
   CGFloat m31, m32, m33, m34;
   CGFloat m41, m42, m43, m44;
};

下面的代码是 CATransform3D 实现 3D 效果的简单使用。

-(CATransform3D)getTransForm3DWithAngle:(CGFloat)angle{
    CATransform3D transform =CATransform3DIdentity;//获取一个标准默认的CATransform3D仿射变换矩阵
    transform.m34=1.0/-2000;//透视效果
    transform=CATransform3DRotate(transform,angle,0,1,0);//获取旋转angle角度后的rotation矩阵。
    return transform;
}

上面最重要的是m34这个属性,CATransform3DRotate获取的旋转如果之前联合的transform不支持透视,那在x、y轴上做旋转是只有frame放大缩小的变化,我们需要的是在旋转的时候要使得离视角近的地方放大,离视角远的地方缩小,就是所谓的视差来形成3D的效果。


根据前面的讲解,通过上图不难看到 m34 实际上影响了 z 轴方向的translation(移动) ,所以实际开发中为了实现 3D 效果,我们只需要简单控制 m34 这个参数即可。除了 m34 这个参数之外,另外几个参数各有其用,有兴趣的可以看下这篇文章。另外, 要知道,m34= -1/D, 默认值是0,也就是说D无穷大,这意味layer in projection plane(投射面)和 layer in world coordinate 重合了。D越小透视效果越明显。所谓的D,是eye(观察者)到投射面的距离。

结语

本文显示介绍了 3D 成像原理、3D编程基本原则,之后介绍了向量和矩阵,矩阵这一块对平移、旋转、缩放进行了简单的推到。最后基于前面所讲的这些基础知识和 iOS 中的CGAffineTransform、CATransform3D做了简单的关联和解释。
该篇文章仅仅只能作为 3D 图像学的简单入门,对于向接触 AR、VR等相关 3D 方向的开发者来说,这些都是工作必备的知识。欢迎指正!!!!!

计算机基础