哈喽大家好啊,我是广州小井。上一节我们初步了解了相机和视图矩阵,并且通过 three.js
的 lookAt
方法实战感受了通过相机观测图形的效果,get 到了变换相机位置观测图形和变换图形自身是一个反方向但变换是等价的效果。那么这一节,我跟大家一起来推导一下视图矩阵,以加深对相机这个概念的理解!
因为要更好的学习 WebGL 就需要更多的上手敲代码,所以案例和演示代码就很显得重要了。为此,笔者打算将案例、源码都统一放到 github 仓库,并且搞了个在线文档供大家学习参考~大家可以给笔者点个 star!文章更新不迷路!
本文的同步地址:
理解视图矩阵
上一节在介绍相机的时候,我实战了一个示例程序,效果是当我们改变相机方向时,立方体是向相机的反方向旋转的。因此我们得出了移动相机观察立方体其实是移动立方体的等价逆变换。
相信大家一定知道,相机可以实现观察物体的关键便是视图矩阵 。但上一节内容中,关于这个矩阵是长什么样,它的意义是什么等疑问都没有深入展开,而是直接套用了 three.js
矩阵中的 lookAt
方法来实现了相机效果而已。
我们其实可以想象成在世界坐标系中,既有我们的场景,也有一个相机。而这个相机,他有坐标点、视线、和上方向,所以我们可以理解成相机存在于世界坐标系中,并且它有一个属于自己的坐标系(相机坐标系)。
如上图所示,相机(绿色坐标轴)位于世界坐标系(蓝色坐标轴)的一个位置观察立方体,并且它也有一个自身的坐标系。而相机的坐标系,完全可以通过我们上一节经常提的 视点、观察点、上方向 来求出。
或者我们可以这么理解,当我们没有设置相机时,相机就是默认在世界坐标系的原点、上方向为 Y
轴,视线沿着屏幕朝内(Z
的负半轴)。
既然相机有一个自身的坐标系,这也就意味着当我们把世界坐标系中的场景放到对应的相机坐标系 中,便是最终绘制在屏幕上图像的最终形态了。而这一变换坐标系的过程,便是依靠 顶点坐标 左乘 视图矩阵 的数学算式而实现的。这一点还是比较好理解的,毕竟这跟我们之前学习的平移矩阵、旋转矩阵 的图形变换,再到后来复合变换所用的模型矩阵是一个异曲同工的图形变换。因此,这也再一次说明了矩阵对于 WebGL 真的非常重要。
当然,相机跟场景还有一个特性那就是当相机、场景同时做等价变换时,相机中所看到的图像都是一样的。比如下图,我用相机水平方向拍摄一个圆柱和一个正方体:
现在我要相机跟场景中的物体 一起做绕 Z
轴做小幅度的逆时针旋转:
由这两图这可以看出,场景、相机同时进行了旋转变换,相机中拍摄出来的图像是不会变的。那这一点有什么用呢?这将会是下文推导视图矩阵的理论基础。因为相机在世界坐标系的某个地方观察到的场景以最终成像,将随着场景、相机做同时同等的视图变换而保持不变 。换句话说,我们使相机通过视图矩阵变换到世界坐标系的原点,同时让场景都左乘上这个视图矩阵,最终的成像依然是最初相机中的图像。
简单总结一下本小节:其实视图矩阵的作用就是把相机坐标系跟世界坐标系做一个转换,把世界坐标系中的场景"放到"相机里。 当然 WebGL 中的矩阵并不仅仅有上述的模型矩阵、视图矩阵,还有后续我们要学到的投影矩阵,他们统称为 MVP 矩阵,相信经过后续的学习,我们会逐步揭开图形成像的各种秘密。
推导视图矩阵
根据前文的讲述我们知道,视图矩阵会把原本世界中的场景放到相机里。因此,我们就要基于这一点,推导出基于世界坐标系的相机坐标系 。结合上一节学习的内容,我们可以知道相机会有 视点、观察目标点、上方向 三种数据信息,于是我们可以通过对这些相机的信息来推导出我们的视图矩阵。
视图矩阵推导的两个要点:
-
平移相机。使相机坐标系的原点跟世界坐标系的原点重合
-
旋转相机。在上述基础上,旋转相机坐标系,使其上方向跟世界坐标系的
Y
轴重合 ,并看向Z
轴的负方向
当经过上述的变换操作后,下图将会是相机最终的变换结果(相机往Z
轴负方向看去):
1. 平移相机
根据我们已学过的知识,上述几点变换的最终结果无非就是对相机进行了 平移矩阵 x 旋转矩阵 的复合变换。并且通过上图我们可以清晰地看出来,其实相机坐标系跟世界坐标系最大的区别就是Z
轴是朝屏幕内的。
首先我们从平移 入手。回顾之前我们推导出来的平移矩阵(矩阵最右边一列):
现在我们假设相机坐标 为 (cX, cY, cZ)
,为了使其跟世界坐标系原点 (0, 0, 0)
重合,那直接跟自身坐标相减即可(将相机平移到世界坐标原点),也就是 (-cX, -cY, -cZ)
。所以我们直接把后者代入平移矩阵即可得到如下矩阵:
将相机平移到原点后,紧接着我们要把视点到观察点的视线方向 变换为指向 Z
轴的负半轴 (0, 0, -1)
位置;并且还要把上方向转动到跟 Y
轴重合 ,也就是单位向量 (0, 1, 0)
的位置。既然要做旋转,那就不可以缺少旋转矩阵,正好之前学习图形变换的时候并没有对旋转矩阵进行推导,所以这里我们一起过一遍。
2. 推导旋转矩阵
虽然之前在讲矩阵实战的时候没有推导过旋转矩阵,但对于图形旋转我们还是实践过的,并且也用过 Matrix4
中封装好的旋转矩阵 api
。在此,我们首先回顾一下转轴公式:
如上图,这是绕 Z
轴旋转 的转轴公式。旋转矩阵也可以按照之前推导平移、缩放矩阵一样进行推导。现在我们就以绕 Z
轴旋转为例进行推导,还是之前的矩阵乘矢量的图如下:
回顾矩阵乘法可知:x' = ax + by + cz + dw
。再根据绕 Z
轴做旋转的转轴公式,我们可以推导出如下等式:
js
// 齐次坐标中 w 的值为 1
x' = ax + by + cz + dw = ax + by + cz + d = x·cosθ - y·sinθ
以此我们可以推算出系数a
为 cosθ
,系数b
为 -sinθ
,c
为0
。又因在齐次坐标中,w
的值为1
,所以可以推断出系数d
也是0
。以此类推,我们可以得到如下的旋转矩阵:
至于第三行,第四行为什么是(0,0,1,0)
和(0,0,0,1)
,我是这么理解的:任何矩阵乘单位矩阵 都为他本身,而此旋转矩阵是绕Z
轴旋转所求得的,所以不应该改变它原有的Z
和W
值。毕竟齐次坐标的W
的值一般固定为1
,而绕Z
轴旋转,Z
的值在旋转前后也是恒定不变的...
基于上述的绕Z
轴旋转矩阵,我就不一一推导其他的了,毕竟方式就这么个方式,大家也可以自己动手推导一下。所以这里我们直接看结果吧,看看基于X
轴、Y
轴旋转的旋转矩阵长什么样。首先是绕X
轴旋转的:
再看看绕Y
轴旋转的:
其实经过对这三个矩阵的观察,我们可以得出一个简单的结论:绕哪个轴转动,对应矩阵中的值都是一个单位向量
!比图下图:
于是,我们在推导视图矩阵前,重温(学习)了旋转矩阵的推导过程,分别看过绕三轴旋转的旋转矩阵各长什么样。那接下来,我们就要基于这三种旋转矩阵的基础,进入旋转相机的操作了!
3. 旋转矩阵的特性
上文大费周章地分别推导了绕三轴旋转的旋转矩阵,我们是否可以在上述的矩阵推导中发现一些规律呢?这将会在很大程度地帮助我们推导出视图矩阵。
我们还是回到上文提到的绕Z
轴旋转时产生的旋转矩阵和对应的转轴公式:
光看这两者你可能发现不了什么,所以我们还是要代数进去计算!试想如果整个世界坐标系绕Z
轴旋转θ
角,会发生什么事情?比如我们看下图,将平面坐标系绕Z
轴逆时针旋转90
度:
又上图我们可以直接看出单位矢量X
从(1,0,0)
变为(0,1,0)
;而单位矢量Y
从(0,1,0)
变为(-1,0,0)
。我们将变换前后的值和过程分别放到矩阵算数,中看看会发生什么奇妙的事情!
接着马上回顾三角函数的相关知识以后续代入算式(我早就忘记了):
如上是一张来自百度百科的特殊三角函数值的图,主要关注90°
那一列,并由此我们可以知道 cos90 = 0
和 sin90 = 1
。于是我把这一层关系代入到矩阵算式中:
咦?旋转矩阵中对应的列 居然和变换后的结果惊奇的一致?这是偶然还是规律?既然现在代入实数已经发现了一个特性,那接下来我们通过公式推算来验证它是一个规律而不是偶然,此后就可以放心使用这个特性了!
比如我把单位矢量X (1,0,0)
和Y (0,1,0)
绕Z轴旋转θ
度代入转轴公式可得:
如上图的推算过程,我们可以清晰地看到,平面坐标系绕Z
轴旋转后,单位矢量 X(1,0)
变为 X'(cosθ, sinθ)
,单位矢量 Y(0,1)
变为 Y'(-sinθ, cosθ)
。再回看一下我们的绕Z
轴旋转的旋转矩阵,是否就应证了这个特性呢?
4. 旋转相机
经过前面两轮数学知识的铺垫,我们可能已经忘记接下来要做什么了,这里重新回顾一下目标:旋转相机坐标系,使其上方向跟世界坐标系的 Y
轴重合 ,并看向 Z
轴的负方向。其实不用想得太复杂,归根到底,只要我们求出旋转矩阵,剩下的就是用世界坐标系的坐标轴左成旋转矩阵就ok了。
根据上一小节的推导,我们知道旋转矩阵的每一列就是对应的坐标轴旋转后的值。于是我们可以将旋转矩阵表示成以下:
如上图所示,X'
、Y'
、Z'
分别为相机的坐标轴,也就是将世界坐标轴经过一定旋转变换后得出来的坐标轴。我们可以简单地代入一下看看是不是这么回事,比如看 X轴(1,0,0,w)
左乘旋转矩阵后的变换结果:
验证完毕后,我们还需要思考如何把 X'
、Y'
、Z'
跟相机中的 视点、观察点、上方向
数据信息关联起来。其中最清晰的值就是 Y'
,毕竟就是我们给相机设置的上方向矢量!所以我们先搞定一个:
ini
// uX 为上方向坐标
Y' = up = (uX, uY, uZ)
紧接着是求 Z'
,现在我们还掌握着视点、观察点坐标的数据信息,所以我们可以接着推导:
上图中 视点C
和 观察点T
之间的"视线"即为我们所求的 Z'
,也就是 矢量CT
。已知两点坐标,想求出 矢量CT
只需要用: T点的坐标 - C点的坐标
即可。(相关的数学知识大家感兴趣自己查阅资料吧)所以我们再拿下 Z'
:
ini
// tX... 为目标点坐标,cX 为视点坐标
Z' = 矢量CT = (tX-cX, tY-cY, tZ-cZ)
注意 :因为我们要把相机的Z
轴看向世界坐标系Z
轴的负 方向,所以目标的矢量Z
值为(0,0,-1)
。因此,如果我们用矢量 (0,0,1)
去左乘旋转矩阵时,Z'
的值应该都要取负号,但如果我们用 (0,0,-1)
去左成的话就不用,大家注意一下即可。
剩下最后一个 X'
了,有看过我之前的文章的应该可以想到,X'
一定垂直 于 Y'
和Z'
所在的平面,所以我们可以根据矢量的叉积 求出 X'
。(忘了的可以回顾下:矢量乘法)于是我们可以直接得出关系式:
csharp
// 这里用 zX 代表 z轴的x值,同理用 yX 代表y轴的x值
X' = Z' x Y' = (zY·yZ-zZ·yY, zZ·yX-zX·yZ, zX·yY-zY·yX)
关于叉乘的计算就不再展开了(
跑远了),大家可以自行查阅。并且上述求X'
、Y'
、Z'
的推导中都没有做归一化的处理,想深入的同学自行了解就好,本文的重点还是在推导视图矩阵中。
经过不懈努力,我们已经推导出 X'
、Y'
、Z'
的矢量出来了,但回顾本小节的标题------旋转相机,我们目前的操作貌似是"走反了",因为上述的推算是基于 世界坐标系左乘旋转矩阵 求出相机坐标 X'
、Y'
、Z'
的。这时候聪明的朋友坐不住了,根据9年义务教育学到的数学知识,我给他们做个除法不就得了?
emmm!可惜啊,矩阵里面没有除法!但是不要慌,还有一个可以取代除法的操作,那就是逆矩阵 。简单来讲逆矩阵就好比是一个数的倒数 ,比如说 2
的倒数是 1/2
这样,所以我们坐成逆矩阵就相当于数学中的除法的作用了。因此,我们求出上述旋转矩阵的逆矩阵就行了。
当然,这一步也不会很复杂,因为有一个旋转矩阵有一个特性:旋转矩阵的逆 等于旋转矩阵的转置 (感兴趣的自行了解)。那转置在之前的数学知识中我们也已经学过了,就是将矩阵中的行列互换 而已。于是我们直接得出将相机坐标系变换到世界坐标系的旋转矩阵如下图。为了直观看出,这里依然用 X'
、Y'
等代表相机矢量,真正运算时再代入上述推理的 X'
、Y'
、Z'
值即可。
5. 求出视图矩阵
经过上述的努力,我们成功得出了平移矩阵、旋转矩阵。简单回顾一下我们推导视图矩阵的经过:
- 平移到世界坐标系原点
- 旋转相机坐标系跟直接坐标系的
Y
轴重合,指向Z
的负半轴
于是我们根据之前复合变换的求模型矩阵的知识,先平移后旋转,只需要将平移矩阵左乘旋转矩阵即可。so,很轻松地就可以推导出视图矩阵:
js
viewMatrix = translateMatrix * rotateMatrix
换成矩阵的算式即为:
上图就是最终的视图矩阵的结果了。(为了展示得更清晰,我把最右边那一列的结果写成了矢量叉乘的形式,小伙伴可以自行推导一下看是不是这样~)
总结
本文的最后,跟大家一起回顾本文的主要内容:
-
视图矩阵的意义就是将相机还原到世界坐标原点,上方向跟世界坐标系的
Y
轴重合,看向Z
轴负半轴 -
相机、场景同时经过视图矩阵做变换后,相机观察到的场景一致(成像一致)
-
推导出三个方向的旋转矩阵,并得出旋转矩阵的特性:每一列为变换后对应的矢量结果
-
通过视点、观察点、上方向即可推导出相机坐标系
-
视图矩阵其实就是平移矩阵左乘旋转矩阵得到的
本文内容相比之下还是有一定的理解成本的,如果一次两次没有看懂都是正常的。我会在文章的结尾贴出本人学习、理解视图矩阵中搜寻到的一些资料、文章供大家参考,希望可以在学习视图矩阵的路上对你有所帮助~
参考文献(排名不分先后):