众所周知,OpenGL是一个3D图形库,在终端设备上普遍使用。可是咱们的显示设备都是2D平面,那么OpenGL怎么把3D图形映射到2D屏幕那?这就是OpenGL坐标变换所要完成的工做。 通常状况下,咱们老是经过一个2D屏幕,观察3D世界。所以,咱们实际看到的是3D世界在2D屏幕上的一个投影。经过OpenGL坐标变换,咱们能够在一个给定的观察视角下,把3D物体投影到2D屏幕上,再通过后面的光栅化和片元着色,整个3D物体就映射成了2D屏幕上的像素。 OpenGL的坐标变换流程以下所示: html
- 第一行和第二行的模型变换、视变换和投影变换是顶点着色器负责完成的,它决定了一个图元在3D空间中的位置。
- 第三行的透视除法和视口变换是图元装配阶段完成的,它决定了一个图元在屏幕上的位置。
咱们先简单看下整个流程:c++
Model
矩阵完成的。模型矩阵能够实现多种变换:平移(translation)、缩放(scale)、旋转(rotation)、镜像(reflection)、错切(shear)等。例如:经过平移操做,咱们能够在世界坐标系的不一样位置绘制同一个3D模型;View
矩阵完成的。视图矩阵定义了摄像头的位置、方向向量和上向量等构成摄像头坐标系的基础信息。View
矩阵左乘世界坐标系中顶点A的坐标,就把顶点A变换到了摄像头坐标系。同一个3D物体,在世界坐标系中,拥有一个世界坐标;在摄像头坐标系中,拥有一个摄像头坐标,View
变换就是负责把物体的坐标从世界坐标系变换到摄像头坐标系。Projection
矩阵,能够把物体从摄像头坐标系变换到裁剪坐标系。在裁剪坐标下,X、Y、Z各个坐标轴上会指定一个可见范围,超过可见范围的顶点(vertex)都会被裁剪掉。W
,获得NDC坐标:
glViewport
指定绘制区域的坐标和宽高,系统会帮咱们自动完成视口变换。通过视口变换,咱们就获得了2D屏幕上的屏幕坐标。须要注意的是:屏幕坐标与屏幕的像素位置是不同的,屏幕坐标是屏幕上任意一个顶点的精确位置,能够是任意小数。可是像素位置只能是整数(具体的某个像素)。这里的视口变换是从NDC坐标变换到屏幕坐标,尚未生成最终的像素位置。从屏幕坐标映射到对应的像素位置,是后面光栅化完成的。在OpenGL中,本地坐标系、世界坐标系和摄像头坐标系都属于右手坐标系,而最终的裁剪坐标系和标准化设备坐标系属于左手坐标系。 左右手坐标系的示意图以下所示,其中大拇指、食指、其他手指分别指向x,y,z轴的正方向。 git
![]()
下面咱们分别来看下模型变换、视图变换、投影变换和视口变换的推导和使用。github
模型变换经过对3D模型执行平移、缩放、旋转、镜像、错切等操做,来调整模型在世界坐标系中的位置。模型变换是经过模型矩阵来完成的,咱们看下每种模型矩阵的推导过程。app
平移就是将一个顶点A = (x,y,z),移动到另外一个位置 =(
,
,
),移动距离D =
- A = (
- x ,
- y ,
- z) = (
,
,
),因此
能够用顶点A来表示:ide
经过平移矩阵来表示以下所示:wordpress
其中就是平移变换矩阵,
表示X轴上的位移,
表示Y轴上的位移,
表示Z轴上的位移。 虽然看上去很繁琐,可是在OpenGL中,咱们能够经过GLM库来实现平移变换。函数
glm::mat4 model; // 定义单位矩阵
model = glm::translate(model, glm::vec3(1.0f, 1.0f, 1.0f));
复制代码
上述代码定义了平移模型矩阵,表示在X、Y、Z轴上同时位移1。学习
能够在X、Y和Z轴上对物体进行缩放,3个坐标轴相互独立。对于以原点为中心的缩放,假设顶点A(x,y,z)在X、Y和Z轴上分别放大、
、
倍,那么能够获得放大后的顶点
=(
* x ,
* y ,
* z),经过缩放矩阵来表示以下所示:spa
其中就是缩放变换矩阵。 默认状况下,缩放的中心点是坐标原点,若是咱们要以指定顶点P(
,
,
)为中心对物体进行缩放。那么能够按照以下步骤操做:
在OpenGL中,咱们能够经过GLM库来实现缩放变换:
glm::mat4 model; // 定义单位矩阵
model = glm::scale(model, glm::vec3(2.0f, 0.5f, 1.0f);
复制代码
上述代码定义了缩放模型矩阵,表示在X轴上fa2倍,Y轴上缩小0.5倍、Z轴上保持不变。
在3D空间中,旋转须要定义一个旋转轴和一个角度。物体会沿着给定的旋转轴旋转指定角度。 咱们首先看下,沿着Z轴旋转的旋转矩阵是怎样的? 假设有一个顶点P,原始坐标为 ( ,
,
),离原点的距离是
,沿着Z轴顺时针旋转
度,新的坐标为(
,
,
),由于旋转先后,z坐标不变,因此暂时忽略,那么能够获得:
根据上述公式,能够获得围绕Z轴的旋转矩阵:
同理,能够获得围绕X轴的旋转矩阵:
同理,能够获得围绕Y轴的旋转矩阵:
在OpenGL中,咱们能够经过GLM库来实现旋转变换:
glm::mat4 model; // 定义单位矩阵
model = glm::rotate(model, glm::radians(-45.0f), glm::vec3(0.4f, 0.6f, 0.8f));
复制代码
上述代码表示:围绕向量(0.4f, 0.6f, 0.8f),顺时针旋转45度。
在进行旋转操做时,常常有一个困惑:顺时针是正方向,仍是逆时针是正方向? 其实,存在一个左手规则和右手规则,能够用于判断物体绕轴旋转时的正方向。
在OpenGl中,咱们使用右手规则,大拇指指向旋转轴的正方向,其他手指的弯曲方向即为旋转正方向。因此上面的-45度是顺时针旋转。 ![]()
由于矩阵不知足交换律,因此平移、旋转和缩放的顺序十分重要, 通常是先缩放、再旋转、最后平移。固然最终仍是要考虑实际状况。 还有一点须要注意,GLM操做矩阵的顺序和实际效果是相反的。以下所示,虽然书写顺序是:平移、旋转和缩放,可是实际最终的模型矩阵是:先缩放、再旋转、最后平移。
glm::mat4 model; // 定义单位矩阵
model = glm::translate(model, glm::vec3(1.0f, 1.0f, 1.0f));
model = glm::rotate(model, glm::radians(-45.0f), glm::vec3(0.4f, 0.6f, 0.8f));
model = glm::scale(model, glm::vec3(2.0f, 0.5f, 1.0f);
复制代码
通过模型变换,都有的坐标都处于世界坐标系中,本节就是以摄像头的角度观察整个世界空间。首先须要定义一个摄像头坐标系。 通常状况下,定义一个坐标系须要如下参数:
基向量
,基向量通常都是正交的。坐标系中的全部顶点都是经过基向量表示的。坐标系=(基向量,原点O)
同一个顶点,在不一样的坐标系中拥有不一样的坐标,那怎么才能把世界坐标系中的顶点坐标,变换到摄像头坐标系那? 要实现不一样坐标系之间的坐标转换,须要计算一个变换矩阵。这个矩阵就是坐标系A中的原点和基向量在另外一个坐标系B下的坐标表示。假设存在A坐标系和B坐标系以及顶点V,那么顶点V在A和B坐标系下的坐标变换公式以下所示:
简单解释一下:
顶点V在A坐标系的坐标 = B坐标系的基向量和原点在A坐标系下的坐标表示构成的变换矩阵 * 顶点V在B坐标系的坐标;
顶点V在B坐标系的坐标 = A坐标系的基向量和原点在B坐标系下的坐标表示构成的变换矩阵 * 顶点V在A坐标系的坐标
复制代码
其中,和
互为逆矩阵。因此坐标系之间的切换,关键就是求出坐标系之间互相表示的变换矩阵。那么
矩阵应该怎么计算那?假设坐标系A的三个基向量和原点在B坐标空间的单位坐标向量分别是
、
、
和
,那么
矩阵以下所示:
矩阵的计算方式也相似,此处再也不赘述。
下面咱们看下OpenGL的视图变换矩阵是怎么计算出来的? 如今存在两个坐标系:世界坐标系W
和摄像头坐标系E
,还有一个顶点V,而且知道顶点V在世界坐标系的坐标 = (,
,
),那么顶点V在摄像头坐标系下的坐标是多少那?根据上面的公式可知,咱们首先须要计算出
矩阵。
众所周知,世界坐标系的原点O = (0,0,0),三个基向量分别是,X轴:(1,0,0)、Y轴:(0,1,0)、Z轴:(0,0,1)。 理论上,定义一个摄像头坐标系,须要4个参数:
经过上述4个参数,咱们实际上建立了一个三个单位轴相互垂直的,以摄像机位置为原点的坐标系。
在使用过程当中,咱们只须要指定3个参数:
接下来是根据上面3个参数,推导出摄像头坐标系单位基向量的步骤:
而后计算出单位方向向量
而后计算出单位右向量
这样,就肯定了摄像头坐标系的三个单位基向量:、
和
以及摄像头的位置向量
。这四个参数一块儿肯定了摄像头坐标系:摄像头位置是坐标原点,单位右向量指向正X轴,单位上向量指向正Y轴,单位方向向量指向正Z轴。
如今咱们已经定义了一个摄像头坐标系,下一步就是把世界坐标系中的顶点V = (,
,
),变换到这个摄像头坐标系。根据上文可知,顶点V在摄像头坐标系
E
的坐标计算过程以下所示:
因此关键是计算变换矩阵,而根据摄像头坐标系的基向量和原点在世界空间中的坐标表示,咱们能够获得
:
那么最终的变换矩阵以下所示:
其中,dot
函数表示向量的点积,是一个标量。最终,顶点V在摄像头坐标系下的坐标以下所示:
上面的矩阵就是
View
变换矩阵。
下面看一个案例:假设摄像头的坐标是(0, 0, 3),摄像头的观察方向是世界坐标系的原点(0,0,0),上向量是(0,1,0),顶点V在世界坐标系的坐标为(1,1,0),那么能够计算出摄像头坐标系的基向量和原点以下所示:
View
变换矩阵就是:最后,顶点V在摄像头坐标系的坐标就是:
虽然上述流程很复杂,但在OpenGL中,咱们能够经过GLM库定义View
矩阵。针对上述案例,经过lookAt
函数就能够获得View
矩阵。
glm::mat4 view;
view = glm::lookAt(glm::vec3(0.0f, 0.0f, 3.0f),glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
复制代码
通过验证,经过lookAt
函数获得View
矩阵为:
很显然,经过lookAt
函数获得View
矩阵和上面咱们推导的View
矩阵是一致的。
前面通过模型变换和视图变换后,3D模型已经处于摄像头坐标系中。本节的投影变换将物体从摄像头坐标系变换到裁剪坐标系,为下一步的视口变换作好准备。 投影变换经过指定视见体来决定场景中哪些物体能够呈如今屏幕上。在视见体中的物体会出如今投影平面上,而在视见体以外的物体不会出如今投影平面上。在OpenGL中,咱们主要考虑透视投影和正交投影,二者的区别以下所示:
无论透视投影,仍是正交投影,均可以经过指定(GLdouble left, GLdouble right, GLdouble bottom, GLdouble top, GLdouble near, GLdouble far)6个参数来指定视见体。(left,bottom)指定了近裁剪面左下角的坐标,(right,top)指定了近裁剪面右上角的坐标,-near
表示近裁剪面,−far
表示远裁剪面。下面须要利用这6个参数,推导投影矩阵。
在摄像头坐标系下,摄像头指向-z轴,因此近裁剪面z=−near,远裁剪面z=−far。而且OpenGL是在近平面上成像的。
经过上述6个参数指定的透视投影变换以下所示:
经过上述6个参数指定的正交投影变换以下所示:
投影变换和透视除法后,摄像头坐标系中的顶点被映射到一个标准立方体中,即NDC坐标系。其中X轴上:[left,right]映射到[−1,1],Y轴上:[bottom,top]映射到[-1,1]中,Z轴上:[near,far]映射到[−1,1],下面的矩阵推导会利用这里的映射关系。下面咱们分别看下两种投影矩阵的推导过程。
透视投影和透视除法的坐标映射以下所示:
上图中,摄像头坐标系是右手坐标系,NDC是左手坐标系,NDC坐标系的Z轴指向摄像头坐标系的-Z轴方向。
假设顶点V在摄像头坐标系的坐标 = ( ,
,
,
),变换到裁剪坐标系的坐标 = (
,
,
,
),透视除法到NDC坐标系的坐标 = (
,
,
,
)。咱们的目标是计算出投影矩阵
,使得:
同时,可获得透视除法的变换:
首先,咱们看下投影矩阵对X轴和Y轴的变换。顶点P投影到近平面后,获得顶点
= (
,
, −near)。具体示意图以下所示:
因此,能够获得X轴上的投影值:
同理,经过右图,能够获得Y轴上的投影值:
由(1)(2)公式能够发现,他们都除以了份量,而且与之成反比。这能够做为透视除法的一个线索,所以咱们的矩阵
以下所示:
也就是说。
接下来,咱们根据、
与NDC坐标的映射关系,推导出
的前两行。
知足[left,right]映射到[-1,1],以下所示:
K
和常量
P
。
经过代入[left,right]到[-1,1]的映射关系,能够获得线性方程:
将上面的公式(1)代入公式(3),可得:
又由于,因此能够进一步简化公式:
根据公式(5),能够进一步获得矩阵:
OK,继续看下的映射关系:知足[bottom,top]映射到[-1,1],以下所示:
又由于,因此能够进一步简化公式:
根据公式(7),能够进一步获得矩阵:
接下来须要计算的系数,这和
、
的计算方式不一样,由于摄像头坐标系的坐标
投影到近平面后老是-near。同时咱们知道
与x和y份量无关,所以,可进一步获得矩阵
:
由于,因此能够获得:
又由于摄像头坐标系中 = 1,因此进一步获得:
一样的,代入与
的映射关系:[-near,-far]映射到[-1,1],可获得:
又由于,能够进一步简化获得
和
的关系:
由公式(9)就能够知道A和B了,所以,最终的矩阵:
通常状况下,投影的视见体都是对称的,即知足left=−right,bottom=−top,那么能够获得:
则矩阵能够简化为:
除了能够经过(left,right,bottom,top,near,far)指定透视投影矩阵外,还能够经过函数glm::perspective
指定视角(Fov)、宽高比(Aspect)、近平面(Near)、远平面(Far)来生成透视投影矩阵,以下所示,指定了45度视角,近平面和远平面分别是0.1f和100.0f:
glm::mat4 proj = glm::perspective(glm::radians(45.0f), width/height, 0.1f, 100.0f);
复制代码
观察视角的示意图以下所示:
由于视见体是对称的,因此把公式(10)(11)代入已有的矩阵,能够获得由视角Fov表示的
矩阵,以下所示:
经过矩阵左乘摄像头坐标系中的顶点,就把这些顶点变换到了裁剪坐标系。而后再通过透视除法,就变换到了NDC坐标系。
相比于透视投影矩阵,正交投影矩阵要简单一些,以下所示:
对于正交投影变换,投影到近平面的坐标( ,
) = (
,
),所以能够直接利用
与
、
与
、
与
的线性映射关系,求出线性方程系数。X、Y、Z轴的映射关系以下所示:
映射关系 | 映射值 | 示意图 |
---|---|---|
![]() ![]() |
[left , right] ![]() |
![]() |
![]() ![]() |
[bottom , top] ![]() |
![]() |
![]() ![]() |
[near , far] ![]() |
![]() |
根据上述的映射关系,同时摄像头坐标系的 = 1,能够获得三个线性方程,以下所示:
根据上述3个线性方程,能够获得正交投影矩阵:
若是视见体是对称的,即知足left=−right,bottom=−top,那么能够获得:
则正交投影矩阵能够进一步简化为:
通过投影变换和透视除法后,咱们裁减掉了不可见物体,获得了NDC坐标。最后一步是把NDC坐标映射到屏幕坐标( ,
,
)。以下所示:
//指定窗口的位置和宽高
glViewport(GLint x , GLint y , GLsizei width , GLsizei height);
//指定窗口的深度
glDepthRangef(GLclampf near , GLclampf far);
复制代码
那么能够NDC坐标和屏幕坐标的线性映射关系:
映射关系 | 映射值 |
---|---|
![]() ![]() |
[-1 , 1] ![]() |
![]() ![]() |
[-1 , 1] ![]() |
![]() ![]() |
[-1 , 1] ![]() |
所以,能够设置线性方程,求出系数K
和常量P
。
把上述映射关系代入线性方程,能够获得各个份量的参数值。
坐标份量 | 线性方程的系数K |
线性方程的常量P |
---|---|---|
X份量线性方程 | ![]() |
x + ![]() |
Y份量线性方程 | ![]() |
y + ![]() |
Z份量线性方程 | ![]() |
![]() |
经过上述各个坐标份量值,能够获得视口变换矩阵:
所以,经过ViewPort矩阵左乘NDC坐标,就获得了屏幕坐标。
对于2D屏幕,
near
和far
通常为0。所以ViewPort
矩阵的第三行都是0。因此通过视口变换后,屏幕坐标的Z值都是0。
至此,OpenGL的整个坐标变换过程都介绍完了,关键仍是要多实践、实践、实践!!!