一看就懂的OpenGL ES教程——走进3D的世界之坐标系统(上篇)

我正在参加「掘金·启航计划」

通过阅读本文,你将获得以下收获:
1.矩阵变换拓展到3D相关知识
2.OpenGL的坐标系统
3.OpenGL坐标系统之间的转换
4.了解投影变换

上篇回顾

上篇一看就懂的OpenGL ES教程——仿抖音滤镜的奇技淫巧之高斯模糊滤镜 基本已经将如何用OpenGL es实现滤镜效果讲得差不多了,基本原理懂了,要实现更复杂的滤镜效果都是针对具体算法的处理上了,所以滤镜打怪升级系列也算是圆满落幕。从今天开始,将进入到一个非常令人兴奋的领域,即3D领域。

(本来是想写一篇更炫酷效果的仿抖音滤镜的博文,无奈滤镜相关的博文耗时已经远超过我的计划,所以只能先安排非常重要有趣的3D渲染了,更炫酷效果的仿抖音滤镜的博文后面有时间也会补上。)

如果你已经能使用OpenGL实现一些视频滤镜效果了,并因此沾沾自喜,那我可能要泼冷水了,因为对于OpenGL,你可能还没真正入门。

814081a1f5a856d3bb2061c5bcb02242.jpeg

今天我们必须重新审视OpenGL了,OpenGL究竟是什么(发出带有哲学味道的思考)。

在该系列的第一篇博文一看就懂的OpenGL ES教程——图形渲染管线的那些事中曾提到:

OpenGL就是一个建立在图形硬件(一般就是gpu)之上的软件编程接口,这些接口有一套官方制定的规范,具体实现由制造商(一般是显卡制造商)去实现,而编程者通过这些软件编程接口,就可以在计算机中绘制出2D以及3D的图形。

在之前的章节中,为了方便初学者入门,其实一直都是以2D的维度去理解OpenGL的(有心的读者应该有注意到之前传入的顶点坐标的z分量都是0),但这样其实只是管中窥豹,这样子是无法真正理解OpenGL的渲染流程的,只有在3D的维度,才能真正理解OpenGL,也才能充分真正发挥OpenGL的作用。(如果只是用OpenGL渲染2D图形,那就有种杀鸡用牛刀的味道了)

所以今天的主题,就是研究OpenGL是如何将一个3D的物体渲染成为2D屏幕上的图像的

前置知识——矩阵变换拓展到3D

首先必备的前置理论知识依然是一看就懂的OpenGL ES教程——仿抖音滤镜的奇技淫巧之变换滤镜(理论基础篇) 所提到的变换矩阵,可以说这是贯穿全文的一大核心,之前博文已经比较详细推导了平移、缩放、旋转矩阵的推导,那么现在将变换拓展到3D,推导过程也是类似的,这里就不赘述,大家可以根据一看就懂的OpenGL ES教程——仿抖音滤镜的奇技淫巧之变换滤镜(理论基础篇) 中的推导自行推导。

之前推导出2D空间中的点平移变换矩阵(包含齐次坐标)为:

image.png

表示一个将2D空间中的某个点(x,y)在x轴y轴方向上分别平移xtytx_t,y_t的矩阵。那么扩展到3D,很容易得到以下平移变换矩阵:

[100Tx010Ty001Tz0001](xyz1)=(x+Txy+Tyz+Tz1)\begin{bmatrix} \color{red}1 & \color{red}0 & \color{red}0 & \color{red}{T_x} \\ \color{green}0 & \color{green}1 & \color{green}0 & \color{green}{T_y} \\ \color{blue}0 & \color{blue}0 & \color{blue}1 & \color{blue}{T_z} \\ \color{purple}0 & \color{purple}0 & \color{purple}0 & \color{purple}1 \end{bmatrix} \cdot \begin{pmatrix} x \\ y \\ z \\ 1 \end{pmatrix} = \begin{pmatrix} x + \color{red}{T_x} \\ y + \color{green}{T_y} \\ z + \color{blue}{T_z} \\ 1 \end{pmatrix}

表示一个将3D空间中的某个点(x,y,z)在x轴y轴z轴方向上分别平移$T_x,T_y,T_z$个单位的变换矩阵

对于缩放矩阵来说:

image.png

同样的,扩展到3D,很容易得到以下缩放变换矩阵:

[S10000S20000S300001](xyz1)=(S1xS2yS3z1)\begin{bmatrix} \color{red}{S_1} & \color{red}0 & \color{red}0 & \color{red}0 \\ \color{green}0 & \color{green}{S_2} & \color{green}0 & \color{green}0 \\ \color{blue}0 & \color{blue}0 & \color{blue}{S_3} & \color{blue}0 \\ \color{purple}0 & \color{purple}0 & \color{purple}0 & \color{purple}1 \end{bmatrix} \cdot \begin{pmatrix} x \\ y \\ z \\ 1 \end{pmatrix} = \begin{pmatrix} \color{red}{S_1} \cdot x \\ \color{green}{S_2} \cdot y \\ \color{blue}{S_3} \cdot z \\ 1 \end{pmatrix}

表示一个将3D空间中的某个点(x,y,z)在x轴、y轴、z轴方向上分别缩放S1S2S3S_1,S_2,S_3倍的变换矩阵。

旋转变换之前推出来的2D旋转变换矩阵是:

image.png

表示将2D空间中的某个点(x,y)在以原点为旋转轴逆时针旋转α\alpha的变换矩阵。

那么扩展到3D空间就比较复杂了,因为不同的旋转轴是有所不同的(上面说的2D旋转变换,其实就是以z轴为旋转轴的旋转)。扩展到3D空间,比如下图为以x轴为旋转轴的情景:

image.png

以下是分别以x轴、y轴、z轴为旋转轴得到的旋转变换矩阵:

沿x轴旋转:

[10000cosθsinθ00sinθcosθ00001](xyz1)=(xcosθysinθzsinθy+cosθz1)\begin{bmatrix} \color{red}1 & \color{red}0 & \color{red}0 & \color{red}0 \\ \color{green}0 & \color{green}{\cos \theta} & – \color{green}{\sin \theta} & \color{green}0 \\ \color{blue}0 & \color{blue}{\sin \theta} & \color{blue}{\cos \theta} & \color{blue}0 \\ \color{purple}0 & \color{purple}0 & \color{purple}0 & \color{purple}1 \end{bmatrix} \cdot \begin{pmatrix} x \\ y \\ z \\ 1 \end{pmatrix} = \begin{pmatrix} x \\ \color{green}{\cos \theta} \cdot y – \color{green}{\sin \theta} \cdot z \\ \color{blue}{\sin \theta} \cdot y + \color{blue}{\cos \theta} \cdot z \\ 1 \end{pmatrix}

沿y轴旋转:

[cosθ0sinθ00100sinθ0cosθ00001](xyz1)=(cosθx+sinθzysinθx+cosθz1)\begin{bmatrix} \color{red}{\cos \theta} & \color{red}0 & \color{red}{\sin \theta} & \color{red}0 \\ \color{green}0 & \color{green}1 & \color{green}0 & \color{green}0 \\ – \color{blue}{\sin \theta} & \color{blue}0 & \color{blue}{\cos \theta} & \color{blue}0 \\ \color{purple}0 & \color{purple}0 & \color{purple}0 & \color{purple}1 \end{bmatrix} \cdot \begin{pmatrix} x \\ y \\ z \\ 1 \end{pmatrix} = \begin{pmatrix} \color{red}{\cos \theta} \cdot x + \color{red}{\sin \theta} \cdot z \\ y \\ – \color{blue}{\sin \theta} \cdot x + \color{blue}{\cos \theta} \cdot z \\ 1 \end{pmatrix}

沿z轴旋转:

[cosθsinθ00sinθcosθ0000100001](xyz1)=(cosθxsinθysinθx+cosθyz1)\begin{bmatrix} \color{red}{\cos \theta} & – \color{red}{\sin \theta} & \color{red}0 & \color{red}0 \\ \color{green}{\sin \theta} & \color{green}{\cos \theta} & \color{green}0 & \color{green}0 \\ \color{blue}0 & \color{blue}0 & \color{blue}1 & \color{blue}0 \\ \color{purple}0 & \color{purple}0 & \color{purple}0 & \color{purple}1 \end{bmatrix} \cdot \begin{pmatrix} x \\ y \\ z \\ 1 \end{pmatrix} = \begin{pmatrix} \color{red}{\cos \theta} \cdot x – \color{red}{\sin \theta} \cdot y \\ \color{green}{\sin \theta} \cdot x + \color{green}{\cos \theta} \cdot y \\ z \\ 1 \end{pmatrix}

从3D到屏幕2D显示

前置知识讲完,接着就是进入主题的时候了。如何将3D的物体到渲染到2D屏幕显示呢?这里有一个大家熟悉的非常相似的情景——拍照。

a1d1cc2231ea4c7bcec4125f1314525a.jpeg

现实世界的物体是3维的,最终会被显示在2维的相片中。

照相机工作的基本原理是利用光学成像和感光材料的特性来记录图像。当按下快门时,镜头中的光线被聚焦到感光材料上,形成了一个倒立的实时图像。

d87915a8f9590516707018f01900a85a.jpeg

这是利用小孔成像的原理:物体通过暗箱的小孔后会在投影平面上形成倒像。

所以OpenGL之所以能将3D物体渲染在2D的屏幕上,最根本的原理也是投影。那究竟什么是投影呢,在了解投影之前,先要讲一个东西——坐标系统

坐标系统

首先要知道的是,按照惯例,OpenGL是一个右手坐标系。简单来说,就是正x轴在你的右手边,正y轴朝上,而正z轴是朝向后方的。想象你的屏幕处于三个轴的中心,则正z轴穿过你的屏幕朝向你。坐标系画起来如下:

image.png

要将3D物体渲染在2D的屏幕上,这个过程的本质,其实就是将3D物体在现实世界中的坐标转化为屏幕设备坐标的一个过程,这个过程通常比较复杂,所以一般是通过分步拆为几个转换步骤,这样会容易理解,并且也方便编码。

所以物体的顶点在最终转化为屏幕坐标之前还会被变换到多个坐标系统,将物体的坐标变换到几个过渡坐标系(Intermediate Coordinate System)的优点在于,在这些特定的坐标系统中,一些操作或运算更加方便和容易,比如可以针对某个变换单独处理,层次就更加清晰了。

首先每一个3D物体一般是作为一个独立的模型被渲染的,所以每个物体一开始有自己的坐标系,这个坐标系就叫做局部坐标系,也叫做物体坐标系,比如上图,处在c位的你其实一开始就有属于自己的坐标系:

image.png

(坐标系画得很绰,各位请见谅)

但是其实拍照的时候你并不是唯一,此时需要一个更大的坐标系来表示每个人的位置(不然每个人的坐标位置就重叠了):

image.png

该坐标一般叫做世界坐标,是一个全局坐标。表示当前要渲染的场景需要的最大的坐标系

我们的目标是要将你和你的朋友投影到摄像机,所以关注的是你和你朋友和摄像机的相对位置,所以不如把原点搬到摄像机,以摄像机为为中心出发,即以摄像机为原点建立一个坐标系

image.png

此时得到的坐标系一般叫做观察坐标,即以观察者(摄像机)为中心的坐标系。

此时就需要将物体投影到摄像机的感光材料上了,并且因为摄像机拍摄到的物体范围是有限的,所以每个顶点的xyz坐标都应该在-1.0到1.0之间,超出这个坐标范围的顶点都将不可见,所以此时能够进入摄像机画面的可能是这样的:

image.png

(左下角绿色小企鹅:???)

现在所处的坐标系一般叫做裁剪坐标系,注意此时经过投影之后,已经转为2D空间,不过当前坐标还是归一化的。

最后因为图像终究是要显示在相片(屏幕)中的,所以一定有具体的像素表示,因为上面几个坐标系都还是归一化的坐标系,所以最后还需要转化为具体相片(屏幕)的坐标系(比如1080*720的屏幕)才能显示出来:

image.png

经过这一系列的坐标转化,最终变换到屏幕坐标系上了,3D物体已经成功渲染到2D屏幕上了!

我们再从OpenGL角度来总结下上面的变换步骤:

image.png

  1. 局部坐标是对象相对于局部原点的坐标,也是物体起始的坐标。
  2. 下一步是将局部坐标变换为世界空间坐标,世界空间坐标是处于一个更大的空间范围的。这些坐标相对于世界的全局原点,它们会和其它物体一起相对于世界的原点进行摆放。
  3. 接下来我们将世界坐标变换为观察空间坐标,使得每个坐标都是从摄像机或者说观察者的角度进行观察的。
  4. 坐标到达观察空间之后,我们需要将其投影到裁剪坐标。裁剪坐标会被处理至-1.0到1.0的范围内,并判断哪些顶点将会出现在屏幕上。
  5. 最后,我们将裁剪坐标变换为屏幕坐标,最后变换出来的坐标将会送到光栅器,将其转化为片段。

那么从一个坐标系转换到另一个坐标系是则怎么完成的呢?从上图可以看出,用的正是之前经常提及的工具——矩阵

坐标系之间的转换

从局部坐标系转换到世界坐标系变换

一般来说,从局部坐标转换到世界坐标的过程称为模型变换,比如你本来在自己的坐标里面坐标原点在你的右脚底部处,当你和你的朋友们一起拍照的时候,坐标原点大概率不在你的右脚底部处,此时假如坐标原点放在最左边的朋友的右脚底部处,而你距离最左边的朋友的右脚底部处有a个单位,则相当于要将你平移a个单位,这里的平移a个单位即是一次模型变换。当然模型变化不止是平移,可以缩放或者旋转,根据具体情况而定,总之就是将物体放在世界坐标而需要的一次变换。而模型变换所需要的变换矩阵,就叫做模型矩阵(model matrix)

从世界坐标系转换到观察坐标系变换

从世界坐标系转换到观察坐标系变换的矩阵称为观察矩阵(View Matrix),通过转换为观察坐标系,可以找到一种从摄像机看过去的感觉,也可以很方便地现实物体在摄像机前方做相对运动的效果。当然要明确一点的是,OpenGL本身没有摄像机(Camera)的概念,但我们可以通过把场景中的所有物体往相反方向移动的方式来模拟出摄像机,产生一种我们在移动的感觉,而不是场景在移动。

怎么变换到观察坐标系呢?

首先要知道摄像机是什么样子的:

image.png

上图为一个摄像机,摄像机的位置在(0,0,2)点,它正往(0,0,0)点看去。

要把坐标系从世界坐标转换到摄像机的坐标系,首先要做什么呢?那当然是在摄像机上建立一个坐标系。则怎么建立呢?

因为OpenGL是右手坐标系,所以摄像机一般都是往-z轴看过去的,所以可以以摄像机看过去的方向作为-z轴,即:

image.png

摄像机背后的蓝色轴即为摄像机坐标系的z轴方向,即为摄像机看过去的点为起点,摄像机的位置点为终点的向量,也称为摄像机的方向向量(这名称容易引起误解,其实和摄像机看过去的方向是相反的),它就可以作为摄像机z轴

然后就是要找到摄像机坐标系的x轴了,要知道x轴必然垂直于z轴,所以可以通过摄像机z轴向量和一个和y轴平行方向相同的向量和摄像机的方向向量进行叉乘,即可得到摄像机的x轴:

image.png

图中和世界坐标系的y轴平行的灰色向量和蓝色向量(摄像机的方向向量)叉乘即可得到红色向量,一般称为摄像机右向量,指向的即是摄像机x轴方向

剩下的就是摄像机的y轴了,y轴和x、z轴垂直,所以只要将摄像机的方向向量
和摄像机的x轴叉乘就可以了,如下图:

image.png

图中绿色向量即为摄像机的y轴方向向量,一般称为摄像机的上方向向量

这样,摄像机坐标系就建立完成了。即上图摄像机红色轴为x轴,绿色轴为y轴,蓝色轴为z轴

接下来就是怎么把处于世界坐标系的物体转换为摄像机的坐标系呢?其实就是找出物体相对于摄像机的相对位置,我们的目标就是找出这个转换矩阵,即上面说得到的观察矩阵

因为世界坐标系中的所有物体进行同样的变换,则它们的相对位置不变。所以用同个变换矩阵对所有物体进行变换,它们的相对位置不变

75c12c896928944b02bb501e421c5673.jpeg

于是我们可以这样处理:

1. 将摄像机坐标系原点平移到与世界坐标系的原点重合。
2. 然后将摄像机的三个坐标轴旋转到和世界坐标系的三个坐标轴重合,就可以将所有物体转换到摄像机坐标系了。

我们假设R\color{red}R是右向量即x轴,U\color{green}U是上向量即y轴,D\color{blue}D是方向向量z轴,P\color{purple}P是摄像机位置向量,即从世界坐标原点到摄像机的向量

第一步是将摄像机坐标系原点(就是摄像机本尊)平移到与世界坐标系的原点重合,根据之前所学的平移矩阵,很容易得到矩阵:

image.png

第二步是将摄像机的三个轴旋转到和世界坐标系的三个坐标轴重合,即R\color{red}R和世界坐标系的x轴重合,U\color{green}U和世界坐标系的y轴重合,D\color{blue}D和世界坐标系的z轴重合,则可以得到以下转换矩阵:

image.png

(因为直接求摄像机的三个轴旋转到和世界坐标系的三个坐标轴重合的旋转矩阵不好求,所以这里的数学推导主要思路是先求出逆变换矩阵,即世界坐标系三个轴旋转到和摄像机三个轴重合的旋转矩阵,然后再求出这个旋转矩阵的逆矩阵即可。)

所以最终观察矩阵(View Matrix)就是二者相乘

ViewMatrix=[RxRyRz0UxUyUz0DxDyDz00001][100Px010Py001Pz0001]View Matrix = \begin{bmatrix} \color{red}{R_x} & \color{red}{R_y} & \color{red}{R_z} & 0 \\ \color{green}{U_x} & \color{green}{U_y} & \color{green}{U_z} & 0 \\ \color{blue}{D_x} & \color{blue}{D_y} & \color{blue}{D_z} & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} * \begin{bmatrix} 1 & 0 & 0 & -\color{purple}{P_x} \\ 0 & 1 & 0 & -\color{purple}{P_y} \\ 0 & 0 & 1 & -\color{purple}{P_z} \\ 0 & 0 & 0 & 1 \end{bmatrix}

投影变换

接下来就是从3D变换到2D的关键了,这里的投影和我们平时说的投影很类似:

7fa04d73c2d0fe86faeb30097406e07f.jpeg

本质就是将3维空间中的物体投到一个平面上显示出来

最常见的投影有2种,一种是正交投影,一种是透视投影:

正交投影就是物体本身尺寸多大,投影到投影平面还是多大。而透视投影则是模拟人眼的功能,创造出一种近大远小的感觉,如下图所示:

image.png

下面2个图可以直观地比较2种投影的不同:

image.png

右边的为正交投影,左边的为透视投影。

2种投影都有一个摄像机在一端像眼睛一样看过去,然后距离摄像机近的有一个投影平面,一般叫做近平面,距离摄像机远的有一个平面,叫做远平面

正交投影中,近平面和远平面是一样大的,这两个平面组成了一个长方体,长方体内的物体都可以被投影到近平面上(长方体外的物体则会被裁剪掉),可以看出投影线(假设有)是平行的

透视投影中,远平面比近平面大,将摄像机和近平面和远平面上对应点用分别4条直线连起来,就组成了一个平截头体,平截头体中的物体都会被投影到近平面上(平截头体外的物体则会被裁剪掉),可以看出投影线(假设有)是被投影点和和摄像机的连线,该连线和近平面的交点就是显示该点的位置。总的来看,就是从远平面到近平面,所有点的投影连线最终都聚集在摄像机一点上。

可以明显看出,正交投影物体尺寸保持不变,透视投影物体近大远小

总结

一转眼已经写了上万字了,大伙们估计也看累了,我也写累了哈哈,接下来要讲的投影变换推导原理只能放在下一篇博文了。

今天主要介绍了变换矩阵扩展到3D的场景,以及OpenGL的坐标系统和各个坐标系统之间的转换,下一篇将详细讲解将3D物体渲染到2D平面最重要的投影变换以及投影变换的数学推导,还有最后的视口变换,都是很有意思很重要的内容,大家有兴趣的话可以继续关注,将在近期出炉~

原创不易,如果觉得本文对自己有帮助,别忘了随手点赞和关注,这也是我创作的最大动力~

项目代码

opengl-es-study-demo
不断更新中,欢迎各位来star~

参考:

GAMES101-现代计算机图形学入门-闫令琪
变换
Fundamentals of Computer Graphics, Fourth Edition
计算机图形学系列笔记
坐标系统

系列文章目录

体系化学习系列博文,请看音视频系统学习总目录

实践项目: 介绍一个自己刚出炉的安卓音视频播放录制开源项目 欢迎各位来star~

相关专栏:

C/C++基础与进阶之路

音视频理论基础系列专栏

音视频开发实战系列专栏

一看就懂的OpenGL es教程

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MY3hPPwu' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片