mirror of
https://github.com/LearnOpenGL-CN/LearnOpenGL-CN.git
synced 2025-08-22 20:25:28 +08:00
01-08 First half
This commit is contained in:
@@ -6,9 +6,9 @@
|
|||||||
翻译 | linkoln
|
翻译 | linkoln
|
||||||
校对 | Geequlim, Meow J, [BLumia](https://github.com/blumia/)
|
校对 | Geequlim, Meow J, [BLumia](https://github.com/blumia/)
|
||||||
|
|
||||||
在上一个教程中,我们学习了如何有效地利用矩阵变换来对所有顶点进行转换。OpenGL希望在所有顶点着色器运行后,所有我们可见的顶点都变为标准化设备坐标(Normalized Device Coordinate, NDC)。也就是说,每个顶点的x,y,z坐标都应该在-1.0到1.0之间,超出这个坐标范围的顶点都将不可见。我们通常会自己设定一个坐标的范围,之后再在顶点着色器中将这些坐标转换为标准化设备坐标。然后将这些标准化设备坐标传入光栅器(Rasterizer),再将他们转换为屏幕上的二维坐标或像素。
|
在上一个教程中,我们学习了如何有效地利用矩阵的变换来对所有顶点进行转换。OpenGL希望在每次顶点着色器运行后,我们可见的所有顶点都为标准化设备坐标(Normalized Device Coordinate, NDC)。也就是说,每个顶点的**x**,**y**,**z**坐标都应该在**-1.0**到**1.0**之间,超出这个坐标范围的顶点都将不可见。我们通常会自己设定一个坐标的范围,之后再在顶点着色器中将这些坐标转换为标准化设备坐标。然后将这些标准化设备坐标传入光栅器(Rasterizer),将它们转换为屏幕上的二维坐标或像素。
|
||||||
|
|
||||||
将坐标转换为标准化设备坐标,接着再转化为屏幕坐标的过程通常是分步,也就是类似于流水线那样子,实现的,在流水线里面我们在将对象转换到屏幕空间之前会先将其转换到多个坐标系统(Coordinate System)。将对象的坐标转换到几个过渡坐标系(Intermediate Coordinate System)的优点在于,在这些特定的坐标系统中进行一些操作或运算更加方便和容易,这一点很快将会变得很明显。对我们来说比较重要的总共有5个不同的坐标系统:
|
将坐标转换为标准化设备坐标,接着再转化为屏幕坐标的过程通常是分步进行的,也就是类似于流水线那样子。在流水线中,物体的顶点在最终转化为屏幕坐标之前还会被转换到多个坐标系统(Coordinate System)。将物体的坐标转换到几个**过渡**坐标系(Intermediate Coordinate System)的优点在于,在这些特定的坐标系统中,一些操作或运算更加方便和容易,这一点很快就会变得很明显。对我们来说比较重要的总共有5个不同的坐标系统:
|
||||||
|
|
||||||
- 局部空间(Local Space,或者称为物体空间(Object Space))
|
- 局部空间(Local Space,或者称为物体空间(Object Space))
|
||||||
- 世界空间(World Space)
|
- 世界空间(World Space)
|
||||||
@@ -16,94 +16,94 @@
|
|||||||
- 裁剪空间(Clip Space)
|
- 裁剪空间(Clip Space)
|
||||||
- 屏幕空间(Screen Space)
|
- 屏幕空间(Screen Space)
|
||||||
|
|
||||||
这些就是我们将所有顶点转换为片段之前,顶点需要处于的不同的状态。
|
这就是一个顶点在最终被转化为片段之前需要经历的所有不同状态。
|
||||||
|
|
||||||
你现在可能对什么是空间或坐标系到底是什么感到困惑,所以接下来我们将会通过展示完整的图片来解释每一个坐标系实际做了什么。
|
你现在可能会对什么是坐标空间,什么是坐标系统感到非常困惑,所以我们将用一种更加通俗的方式来解释它们。下面,我们将显示一个整体的图片,之后我们会讲解每个空间的具体功能。
|
||||||
|
|
||||||
## 概述
|
## 概述
|
||||||
|
|
||||||
为了将坐标从一个坐标系转换到另一个坐标系,我们需要用到几个转换矩阵,最重要的几个分别是**模型(Model)**、**视图(View)**、**投影(Projection)**三个矩阵。首先,顶点坐标开始于**局部空间(Local Space)**,称为**局部坐标(Local Coordinate)**,然后经过**世界坐标(World Coordinate)**,**观察坐标(View Coordinate)**,**裁剪坐标(Clip Coordinate)**,并最后以**屏幕坐标(Screen Coordinate)**结束。下面的图示显示了整个流程及各个转换过程做了什么:
|
为了将坐标从一个坐标系转换到另一个坐标系,我们需要用到几个转换矩阵,最重要的几个分别是<def>模型</def>(Model)、<def>观察</def>(View)、<def>投影</def>(Projection)三个矩阵。我们的顶点坐标起始于<def>局部空间</def>(Local Space),在这里它称为<def>局部坐标</def>(Local Coordinate),它在之后会变为<def>世界坐标</def>(World Coordinate),<def>观察坐标</def>(View Coordinate),<def>裁剪坐标</def>(Clip Coordinate),并最后以<def>屏幕坐标</def>(Screen Coordinate)的形式结束。下面的这张图展示了整个流程以及各个转换过程做了什么:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
1. 局部坐标是对象相对于局部原点的坐标;也是对象开始的坐标。
|
1. 局部坐标是对象相对于局部原点的坐标,也是物体起始的坐标。
|
||||||
2. 将局部坐标转换为世界坐标,世界坐标是作为一个更大空间范围的坐标系统。这些坐标是相对于世界的原点的。
|
2. 下一步是将局部坐标转换为世界空间坐标,世界空间坐标是处于一个更大的空间范围的。这些坐标相对于世界的全局原点,它们会和其它物体一起相对于世界的原点进行摆放。
|
||||||
3. 接下来我们将世界坐标转换为观察坐标,观察坐标是指以摄像机或观察者的角度观察的坐标。
|
3. 接下来我们将世界坐标转换为观察空间坐标,使得每个坐标都是从摄像机或者说观察者的角度进行观察的。
|
||||||
4. 在将坐标处理到观察空间之后,我们需要将其投影到裁剪坐标。裁剪坐标是处理-1.0到1.0范围内并判断哪些顶点将会出现在屏幕上。
|
4. 坐标到达观察空间之后,我们需要将其投影到裁剪坐标。裁剪坐标会被处理至-1.0到1.0的范围内,并判断哪些顶点将会出现在屏幕上。
|
||||||
5. 最后,我们需要将裁剪坐标转换为屏幕坐标,我们将这一过程称为**视口变换(Viewport Transform)**。视口变换将位于-1.0到1.0范围的坐标转换到由`glViewport`函数所定义的坐标范围内。最后转换的坐标将会送到光栅器,由光栅器将其转化为片段。
|
5. 最后,我们将裁剪坐标转换为屏幕坐标,我们将使用一个叫做<def>视口变换</def>(Viewport Transform)的过程。视口变换将位于-1.0到1.0范围的坐标转换到由<fun>glViewport</fun>函数所定义的坐标范围内。最后转换出来的坐标将会送到光栅器,将其转化为片段。
|
||||||
|
|
||||||
|
你可能已经大致了解了每个坐标空间的作用。我们之所以将顶点转换到各个不同的空间的原因是有些操作在特定的坐标系统中才有意义且更方便。例如,当需要对物体进行修改的时候,在局部空间中来操作会更说得通;如果要对一个物体做出一个相对于其它物体位置的操作时,在世界坐标系中来做这个才更说得通,等等。如果我们愿意,我们也可以定义一个直接从局部空间转换到裁剪空间的转换矩阵,但那样会失去很多灵活性。
|
||||||
|
|
||||||
你可能了解了每个单独的坐标空间的作用。我们之所以将顶点转换到各个不同的空间的原因是有些操作在特定的坐标系统中才有意义且更方便。例如,当修改对象时,如果在局部空间中则是有意义的;当对对象做相对于其它对象的位置的操作时,在世界坐标系中则是有意义的;等等这些。如果我们愿意,本可以定义一个直接从局部空间到裁剪空间的转换矩阵,但那样会失去灵活性。接下来我们将要更仔细地讨论各个坐标系。
|
接下来我们将要更仔细地讨论各个坐标系统。
|
||||||
|
|
||||||
## 局部空间
|
## 局部空间
|
||||||
|
|
||||||
局部空间(Local Space)是指对象所在的坐标空间,例如,对象最开始所在的地方。想象你在一个模型建造软件(比如说Blender)中创建了一个立方体。你创建的立方体的原点有可能位于(0,0,0),即使有可能在最后的应用中位于完全不同的另外一个位置。甚至有可能你创建的所有模型都以(0,0,0)为初始位置,然而他们会在世界的不同位置。则你的模型的所有顶点都是在**局部**空间:他们相对于你的对象来说都是局部的。
|
局部空间是指物体所在的坐标空间,即对象最开始所在的地方。想象你在一个建模软件(比如说Blender)中创建了一个立方体。你创建的立方体的原点有可能位于(0, 0, 0),即便它有可能最后在程序中处于完全不同的位置。甚至有可能你创建的所有模型都以(0, 0, 0)为初始位置(译注:然而它们会最终出现在世界的不同位置)。所以,你的模型的所有顶点都是在**局部**空间中:它们相对于你的物体来说都是局部的。
|
||||||
|
|
||||||
我们一直使用的那个箱子的坐标范围为-0.5到0.5,设定(0, 0)为它的原点。这些都是局部坐标。
|
|
||||||
|
|
||||||
|
我们一直使用的那个箱子的顶点是被设定在-0.5到0.5的坐标范围中,(0, 0)是它的原点。这些都是局部坐标。
|
||||||
|
|
||||||
## 世界空间
|
## 世界空间
|
||||||
|
|
||||||
如果我们想将我们所有的对象导入到程序当中,它们有可能会全挤在世界的原点上(0,0,0),然而这并不是我们想要的结果。我们想为每一个对象定义一个位置,从而使对象位于更大的世界当中。世界空间(World Space)中的坐标就如它们听起来那样:是指顶点相对于(游戏)世界的坐标。物体变换到的最终空间就是世界坐标系,并且你会想让这些物体分散开来摆放(从而显得更真实)。对象的坐标将会从局部坐标转换到世界坐标;该转换是由**模型矩阵(Model Matrix)**实现的。
|
如果我们将我们所有的物体导入到程序当中,它们有可能会全挤在世界的原点(0, 0, 0)上,这并不是我们想要的结果。我们想为每一个物体定义一个位置,从而能在更大的世界当中放置它们。世界空间中的坐标正如其名:是指顶点相对于(游戏)世界的坐标。如果你希望将物体分散在世界上摆放(特别是非常真实的那样),这就是你希望物体变换到的空间。物体的坐标将会从局部转换到世界空间;该转换是由<def>模型</def>矩阵(Model Matrix)实现的。
|
||||||
|
|
||||||
模型矩阵是一种转换矩阵,它能通过对对象进行平移、缩放、旋转来将它置于它本应该在的位置或方向。你可以想象一下,我们需要转换一栋房子,通过将它缩小(因为它在局部坐标系中显得太大了),将它往郊区的方向平移,然后沿着y轴往左旋转一点。经过这样的变换之后,它将恰好能够与邻居的房子重合。你能够想到上一节讲到的利用模型矩阵将各个箱子放置到这个屏幕上;我们能够将箱子中的局部坐标转换为观察坐标或世界坐标。
|
模型矩阵是一种转换矩阵,它能通过对物体进行位移、缩放、旋转来将它置于它本应该在的位置或朝向。你可以将它想像为变换一个房子,你需要先将它缩小(它在局部空间中太大了),并将其位移至郊区的一个小镇,然后在y轴上往左旋转一点以搭配附近的房子。你也可以把上一节将箱子到处摆放在场景中用的那个矩阵大致看作一个模型矩阵;我们将箱子的局部坐标变换到场景/世界中的不同位置。
|
||||||
|
|
||||||
## 观察空间
|
## 观察空间
|
||||||
|
|
||||||
观察空间(View Space)经常被人们称之OpenGL的**摄像机(Camera)**(所以有时也称为摄像机空间(Camera Space)或视觉空间(Eye Space))。观察空间就是将对象的世界空间的坐标转换为观察者视野前面的坐标。因此观察空间就是从摄像机的角度观察到的空间。而这通常是由一系列的平移和旋转的组合来平移和旋转场景从而使得特定的对象被转换到摄像机前面。这些组合在一起的转换通常存储在一个**观察矩阵(View Matrix)**里,用来将世界坐标转换到观察空间。在下一个教程我们将广泛讨论如何创建一个这样的观察矩阵来模拟一个摄像机。
|
观察空间经常被人们称之OpenGL的<def>摄像机</def>(Camera)(所以有时也称为<def>摄像机空间</def>(Camera Space)或<def>视觉空间</def>(Eye Space))。观察空间是将世界空间坐标转化为用户视野前方的坐标而产生的结果。因此观察空间就是从摄像机的视角所观察到的空间。而这通常是由一系列的位移和旋转的组合来完成,平移/旋转场景从而使得特定的对象被转换到摄像机的前方。这些组合在一起的转换通常存储在一个<def>观察矩阵</def>(View Matrix)里,它被用来将世界坐标转换到观察空间。在下一节中我们将深入讨论如何创建一个这样的观察矩阵来模拟一个摄像机。
|
||||||
|
|
||||||
## 裁剪空间
|
## 裁剪空间
|
||||||
|
|
||||||
在一个顶点着色器运行的最后,OpenGL期望所有的坐标都能落在一个给定的范围内,且任何在这个范围之外的点都应该被裁剪掉(Clipped)。被裁剪掉的坐标就被忽略了,所以剩下的坐标就将变为屏幕上可见的片段。这也就是**裁剪空间(Clip Space)**名字的由来。
|
在一个顶点着色器运行的最后,OpenGL期望所有的坐标都能落在一个特定的范围内,且任何在这个范围之外的点都应该被<def>裁剪掉</def>(Clipped)。被裁剪掉的坐标就会被忽略,所以剩下的坐标就将变为屏幕上可见的片段。这也就是<def>裁剪空间</def>(Clip Space)名字的由来。
|
||||||
|
|
||||||
因为将所有可见的坐标都放置在-1.0到1.0的范围内不是很直观,所以我们会指定自己的坐标集(Coordinate Set)并将它转换回标准化设备坐标系,就像OpenGL期望它做的那样。
|
因为将所有可见的坐标都指定在-1.0到1.0的范围内不是很直观,所以我们会指定自己的坐标集(Coordinate Set)并将它转换回标准化设备坐标系,就像OpenGL期望的那样。
|
||||||
|
|
||||||
为了将顶点坐标从观察空间转换到裁剪空间,我们需要定义一个**投影矩阵(Projection Matrix)**,它指定了坐标的范围,例如,每个维度都是从-1000到1000。投影矩阵接着会将在它指定的范围内的坐标转换到标准化设备坐标系中(-1.0,1.0)。所有在范围外的坐标在-1.0到1.0之间都不会被绘制出来并且会被裁剪。在投影矩阵所指定的范围内,坐标(1250,500,750)将是不可见的,这是由于它的x坐标超出了范围,随后被转化为在标准化设备坐标中坐标值大于1.0的值并且被裁剪掉。
|
为了将顶点坐标从观察变换到裁剪空间,我们需要定义一个<def>投影矩阵</def>(Projection Matrix),它指定了一个范围的坐标,比如在每个维度上的-1000到1000。投影矩阵接着会将在这个指定的范围内的坐标转换为标准化设备坐标的范围(-1.0, 1.0)。所有在范围外的坐标不会被映射到在-1.0到1.0的范围之间,所以会被裁剪掉。在上面这个投影矩阵所指定的范围内,坐标(1250, 500, 750)将是不可见的,这是由于它的x坐标超出了范围,它被转化为一个大于1.0的标准化设备坐标,所以被裁剪掉了。
|
||||||
|
|
||||||
!!! Important
|
!!! important
|
||||||
|
|
||||||
如果只是片段的一部分例如三角形,超出了裁剪体积(Clipping Volume),则OpenGL会重新构建三角形以使一个或多个三角形能适应在裁剪范围内。
|
如果只是图元(Primitive),例如三角形,的一部分超出了<def>裁剪体积</def>(Clipping Volume),则OpenGL会重新构建这个三角形为一个或多个三角形让其能够适合这个裁剪范围。
|
||||||
|
|
||||||
由投影矩阵创建的**观察区域(Viewing Box)**被称为**平截头体(Frustum)**,且每个出现在平截头体范围内的坐标都会最终出现在用户的屏幕上。将一定范围内的坐标转化到标准化设备坐标系的过程(而且它很容易被映射到2D观察空间坐标)被称之为**投影(Projection)**,因为使用投影矩阵能将3维坐标**投影(Project)**到很容易映射的2D标准化设备坐标系中。
|
由投影矩阵创建的**观察箱**(Viewing Box)被称为<def>平截头体</def>(Frustum),每个出现在平截头体范围内的坐标都会最终出现在用户的屏幕上。将特定范围内的坐标转化到标准化设备坐标系的过程(而且它很容易被映射到2D观察空间坐标)被称之为<def>投影</def>(Projection),因为使用投影矩阵能将3D坐标<def>投影<def>(Project)到很容易映射到2D的标准化设备坐标系中。
|
||||||
|
|
||||||
一旦所有顶点被转换到裁剪空间,最终的操作——**透视划分(Perspective Division)**将会执行,在这个过程中我们将位置向量的x,y,z分量分别除以向量的齐次w分量;透视划分是将4维裁剪空间坐标转换为3维标准化设备坐标。这一步会在每一个顶点着色器运行的最后被自动执行。
|
一旦所有顶点被转换到裁剪空间,最终的操作——<def>透视除法</def>(Perspective Division)将会执行,在这个过程中我们将位置向量的x,y,z分量分别除以向量的齐次w分量;透视除法是将4D裁剪空间坐标转换为3D标准化设备坐标的过程。这一步会在每一个顶点着色器运行的最后被自动执行。
|
||||||
|
|
||||||
在这一阶段之后,坐标经过转换的结果将会被映射到屏幕空间(由`glViewport`设置)且被转换成片段。
|
在这一阶段之后,最终的坐标将会被映射到屏幕空间中(使用<fun>glViewport</fun>中的设定),并被转换成片段。
|
||||||
|
|
||||||
投影矩阵将观察坐标转换为裁剪坐标的过程采用两种不同的方式,每种方式分别定义自己的平截头体。我们可以创建一个正射投影矩阵(Orthographic Projection Matrix)或一个透视投影矩阵(Perspective Projection Matrix)。
|
将观察坐标转换为裁剪坐标的投影矩阵可以为两种不同的形式,每种形式都定义了不同的平截头体。我们可以选择创建一个<def>正射</def>投影矩阵(Orthographic Projection Matrix)或一个<def>透视</def>投影矩阵(Perspective Projection Matrix)。
|
||||||
|
|
||||||
### 正射投影
|
### 正射投影
|
||||||
|
|
||||||
正射投影(Orthographic Projection)矩阵定义了一个类似立方体的平截头体,指定了一个裁剪空间,每一个在这空间外面的顶点都会被裁剪。创建一个正射投影矩阵需要指定可见平截头体的宽、高和长度。所有在使用正射投影矩阵转换到裁剪空间后如果还处于这个平截头体里面的坐标就不会被裁剪。它的平截头体看起来像一个容器:
|
正射投影矩阵定义了一个类似立方体的平截头箱,它定义了一个裁剪空间,在这空间之外的顶点都会被裁剪掉。创建一个正射投影矩阵需要指定可见平截头体的宽、高和长度。在使用正射投影矩阵转换至裁剪空间之后处于这个平截头体内的所有坐标将不会被裁剪掉。它的平截头体看起来像一个容器:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
上面的平截头体定义了由宽、高、**近**平面和**远**平面决定的可视的坐标系。任何出现在近平面前面或远平面后面的坐标都会被裁剪掉。正视平截头体直接将平截头体内部的顶点映射到标准化设备坐标系中,因为每个向量的w分量都是不变的;如果w分量等于1.0,则透视划分不会改变坐标的值。
|
上面的平截头体定义了可见的坐标,它由由宽、高、<def>近</def>(Near)平面和<def>远</def>(Far)平面所指定。任何出现在近平面之前或远平面之后的坐标都会被裁剪掉。正射平截头体直接将平截头体内部的所有坐标映射为标准化设备坐标,因为每个向量的w分量都没有进行改变;如果w分量等于1.0,透视除法则不会改变这个坐标。
|
||||||
|
|
||||||
为了创建一个正射投影矩阵,我们利用GLM的构建函数`glm::ortho`:
|
要创建一个正射投影矩阵,我们可以使用GLM的内置函数`glm::ortho`:
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
glm::ortho(0.0f, 800.0f, 0.0f, 600.0f, 0.1f, 100.0f);
|
glm::ortho(0.0f, 800.0f, 0.0f, 600.0f, 0.1f, 100.0f);
|
||||||
```
|
```
|
||||||
|
|
||||||
前两个参数指定了平截头体的左右坐标,第三和第四参数指定了平截头体的底部和上部。通过这四个参数我们定义了近平面和远平面的大小,然后第五和第六个参数则定义了近平面和远平面的距离。这个指定的投影矩阵将处于这些x,y,z范围之间的坐标转换到标准化设备坐标系中。
|
前两个参数指定了平截头体的左右坐标,第三和第四参数指定了平截头体的底部和顶部。通过这四个参数我们定义了近平面和远平面的大小,然后第五和第六个参数则定义了近平面和远平面的距离。这个投影矩阵会将处于这些x,y,z值范围内的坐标变换为标准化设备坐标。
|
||||||
|
|
||||||
正射投影矩阵直接将坐标映射到屏幕的二维平面内,但实际上一个直接的投影矩阵将会产生不真实的结果,因为这个投影没有将**透视(Perspective)**考虑进去。所以我们需要**透视投影**矩阵来解决这个问题。
|
正射投影矩阵直接将坐标映射到2D平面中,即你的屏幕,但实际上一个直接的投影矩阵会产生不真实的结果,因为这个投影没有将<def>透视</def>(Perspective)考虑进去。所以我们需要<def>透视投影</def>矩阵来解决这个问题。
|
||||||
|
|
||||||
#### 透视投影
|
### 透视投影
|
||||||
|
|
||||||
如果你曾经体验过**实际生活**给你带来的景象,你就会注意到离你越远的东西看起来更小。这个神奇的效果我们称之为透视(Perspective)。透视的效果在我们看一条无限长的高速公路或铁路时尤其明显,正如下面图片显示的那样:
|
如果你曾经体验过**实际生活**给你带来的景象,你就会注意到离你越远的东西看起来更小。这个奇怪的效果称之为<def>透视</def>(Perspective)。透视的效果在我们看一条无限长的高速公路或铁路时尤其明显,正如下面图片显示的那样:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
正如你看到的那样,由于透视的原因,平行线似乎在很远的地方看起来会相交。这正是透视投影(Perspective Projection)想要模仿的效果,它是使用透视投影矩阵来完成的。这个投影矩阵不仅将给定的平截头体范围映射到裁剪空间,同样还修改了每个顶点坐标的w值,从而使得离观察者越远的顶点坐标w分量越大。被转换到裁剪空间的坐标都会在-w到w的范围之间(任何大于这个范围的对象都会被裁剪掉)。OpenGL要求所有可见的坐标都落在-1.0到1.0范围内从而作为最后的顶点着色器输出,因此一旦坐标在裁剪空间内,透视划分就会被应用到裁剪空间坐标:
|
正如你看到的那样,由于透视,这两条线在很远的地方看起来会相交。这正是透视投影想要模仿的效果,它是使用<def>透视投影矩阵</def>来完成的。这个投影矩阵将给定的平截头体范围映射到裁剪空间,除此之外还修改了每个顶点坐标的w值,从而使得离观察者越远的顶点坐标w分量越大。被转换到裁剪空间的坐标都会在-w到w的范围之间(任何大于这个范围的坐标都会被裁剪掉)。OpenGL要求所有可见的坐标都落在-1.0到1.0范围内,作为顶点着色器最后的输出,因此,一旦坐标在裁剪空间内之后,透视除法就会被应用到裁剪空间坐标上:
|
||||||
|
|
||||||
$$
|
$$
|
||||||
out = \begin{pmatrix} x /w \\ y / w \\ z / w \end{pmatrix}
|
out = \begin{pmatrix} x /w \\ y / w \\ z / w \end{pmatrix}
|
||||||
$$
|
$$
|
||||||
|
|
||||||
每个顶点坐标的分量都会除以它的w分量,得到一个距离观察者的较小的顶点坐标。这是也是另一个w分量很重要的原因,因为它能够帮助我们进行透视投影。最后的结果坐标就是处于标准化设备空间内的。如果你对研究正射投影矩阵和透视投影矩阵是如何计算的很感兴趣(且不会对数学感到恐惧的话)我推荐[这篇由Songho写的文章](http://www.songho.ca/opengl/gl_projectionmatrix.html)。
|
顶点坐标的每个分量都会除以它的w分量,距离观察者越远顶点坐标就会越小。这是也是w分量非常重要的另一个原因,它能够帮助我们进行透视投影。最后的结果坐标就是处于标准化设备空间中的。如果你对正射投影矩阵和透视投影矩阵是如何计算的很感兴趣(且不会对数学感到恐惧的话)我推荐这篇由Songho写的[]文章](http://www.songho.ca/opengl/gl_projectionmatrix.html)。
|
||||||
|
|
||||||
在GLM中可以这样创建一个透视投影矩阵:
|
在GLM中可以这样创建一个透视投影矩阵:
|
||||||
|
|
||||||
@@ -111,23 +111,23 @@ $$
|
|||||||
glm::mat4 proj = glm::perspective(45.0f, (float)width/(float)height, 0.1f, 100.0f);
|
glm::mat4 proj = glm::perspective(45.0f, (float)width/(float)height, 0.1f, 100.0f);
|
||||||
```
|
```
|
||||||
|
|
||||||
`glm::perspective`所做的其实就是再次创建了一个定义了可视空间的大的**平截头体**,任何在这个平截头体以外的对象最后都不会出现在裁剪空间体积内,并且将会受到裁剪。一个透视平截头体可以被可视化为一个不均匀形状的盒子,在这个盒子内部的每个坐标都会被映射到裁剪空间的点。一张透视平截头体的照片如下所示:
|
同样,`glm::perspective`所做的其实就是创建了一个定义了可视空间的大**平截头体**,任何在这个平截头体以外的东西最后都不会出现在裁剪空间体积内,并且将会受到裁剪。一个透视平截头体可以被看作一个不均匀形状的箱子,在这个箱子内部的每个坐标都会被映射到裁剪空间上的一个点。下面是一张透视平截头体的图片:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
它的第一个参数定义了**fov**的值,它表示的是**视野(Field of View)**,并且设置了观察空间的大小。对于一个真实的观察效果,它的值经常设置为45.0,但想要看到更多结果你可以设置一个更大的值。第二个参数设置了宽高比,由视口的高除以宽。第三和第四个参数设置了平截头体的近和远平面。我们经常设置近距离为0.1而远距离设为100.0。所有在近平面和远平面的顶点且处于平截头体内的顶点都会被渲染。
|
它的第一个参数定义了<def>fov</def>的值,它表示的是<def>视野</def>(Field of View),并且设置了观察空间的大小。如果想要一个真实的观察效果,它的值通常设置为45.0f,但想要一个末日风格的结果你可以将其设置一个更大的值。第二个参数设置了宽高比,由视口的宽除以高所得。第三和第四个参数设置了平截头体的**近**和**远**平面。我们通常设置近距离为0.1f,而远距离设为100.0f。所有在近平面和远平面内且处于平截头体内的顶点都会被渲染。
|
||||||
|
|
||||||
!!! Important
|
!!! important
|
||||||
|
|
||||||
当你把透视矩阵的*near*值设置太大时(如10.0),OpenGL会将靠近摄像机的坐标都裁剪掉(在0.0和10.0之间),这会导致一个你很熟悉的视觉效果:在太过靠近一个物体的时候视线会直接穿过去。
|
当你把透视矩阵的 *near* 值设置太大时(如10.0f),OpenGL会将靠近摄像机的坐标(在0.0f和10.0f之间)都裁剪掉,这会导致一个你在游戏中很熟悉的视觉效果:在太过靠近一个物体的时候你的视线会直接穿过去。
|
||||||
|
|
||||||
当使用正射投影时,每一个顶点坐标都会直接映射到裁剪空间中而不经过任何精细的透视划分(它仍然会进行透视划分,只是w分量没有被操作(它保持为1)因此没有起作用)。因为正射投影没有使用透视,远处的对象不会显得小以产生神奇的视觉输出。由于这个原因,正射投影主要用于二维渲染以及一些建筑或工程的应用,或者是那些我们不需要使用投影来转换顶点的情况下。某些如Blender的进行三维建模的软件有时在建模时会使用正射投影,因为它在各个维度下都更准确地描绘了每个物体。下面你能够看到在Blender里面使用两种投影方式的对比:
|
当使用正射投影时,每一个顶点坐标都会直接映射到裁剪空间中而不经过任何精细的透视除法(它仍然会进行透视除法,只是w分量没有被改变(它保持为1),因此没有起作用)。因为正射投影没有使用透视,远处的物体不会显得更小,所以产生奇怪的视觉效果。由于这个原因,正射投影主要用于二维渲染以及一些建筑或工程的程序,在这些场景中我们更希望顶点不会被透视所干扰。某些如 *Blender* 等进行三维建模的软件有时在建模时也会使用正射投影,因为它在各个维度下都更准确地描绘了每个物体。下面你能够看到在Blender里面使用两种投影方式的对比:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
你可以看到使用透视投影的话,远处的顶点看起来比较小,而在正射投影中每个顶点距离观察者的距离都是一样的。
|
你可以看到,使用透视投影的话,远处的顶点看起来比较小,而在正射投影中每个顶点距离观察者的距离都是一样的。
|
||||||
|
|
||||||
### 把它们都组合到一起
|
## 把它们都组合到一起
|
||||||
|
|
||||||
我们为上述的每一个步骤都创建了一个转换矩阵:模型矩阵、观察矩阵和投影矩阵。一个顶点的坐标将会根据以下过程被转换到裁剪坐标:
|
我们为上述的每一个步骤都创建了一个转换矩阵:模型矩阵、观察矩阵和投影矩阵。一个顶点的坐标将会根据以下过程被转换到裁剪坐标:
|
||||||
|
|
||||||
@@ -135,13 +135,13 @@ $$
|
|||||||
V_{clip} = M_{projection} \cdot M_{view} \cdot M_{model} \cdot V_{local}
|
V_{clip} = M_{projection} \cdot M_{view} \cdot M_{model} \cdot V_{local}
|
||||||
$$
|
$$
|
||||||
|
|
||||||
注意每个矩阵被运算的顺序是相反的(记住我们需要从右往左乘上每个矩阵)。最后的顶点应该被赋予顶点着色器中的`gl_Position`且OpenGL将会自动进行透视划分和裁剪。
|
注意每个矩阵被运算的顺序是相反的(记住我们需要从右往左乘上每个矩阵)。最后的顶点应该被赋予顶点着色器中的`gl_Position`且OpenGL将会自动进行透视除法和裁剪。
|
||||||
|
|
||||||
!!! Important
|
!!! Important
|
||||||
|
|
||||||
**然后呢?**
|
**然后呢?**
|
||||||
|
|
||||||
顶点着色器的输出需要所有的顶点都在裁剪空间内,而这是我们的转换矩阵所做的。OpenGL然后在裁剪空间中执行透视划分从而将它们转换到标准化设备坐标。OpenGL会使用`glViewPort`内部的参数来将标准化设备坐标映射到屏幕坐标,每个坐标都关联了一个屏幕上的点(在我们的例子中屏幕是800 *600)。这个过程称为视口转换。
|
顶点着色器的输出需要所有的顶点都在裁剪空间内,而这是我们的转换矩阵所做的。OpenGL然后在裁剪空间中执行透视除法从而将它们转换到标准化设备坐标。OpenGL会使用`glViewPort`内部的参数来将标准化设备坐标映射到屏幕坐标,每个坐标都关联了一个屏幕上的点(在我们的例子中屏幕是800 *600)。这个过程称为视口转换。
|
||||||
|
|
||||||
这一章的主题可能会比较难理解,如果你仍然不确定每个空间的作用的话,你也不必太担心。接下来你会看到我们是怎样好好运用这些坐标空间的并且会有足够的展示例子在接下来的教程中。
|
这一章的主题可能会比较难理解,如果你仍然不确定每个空间的作用的话,你也不必太担心。接下来你会看到我们是怎样好好运用这些坐标空间的并且会有足够的展示例子在接下来的教程中。
|
||||||
|
|
||||||
@@ -260,11 +260,11 @@ glDrawArrays(GL_TRIANGLES, 0, 36);
|
|||||||
|
|
||||||
这有点像一个立方体,但又有种说不出的奇怪。立方体的某些本应被遮挡住的面被绘制在了这个立方体的其他面的上面。之所以这样是因为OpenGL是通过画一个一个三角形来画你的立方体的,所以它将会覆盖之前已经画在那里的像素。因为这个原因,有些三角形会画在其它三角形上面,虽然它们本不应该是被覆盖的。
|
这有点像一个立方体,但又有种说不出的奇怪。立方体的某些本应被遮挡住的面被绘制在了这个立方体的其他面的上面。之所以这样是因为OpenGL是通过画一个一个三角形来画你的立方体的,所以它将会覆盖之前已经画在那里的像素。因为这个原因,有些三角形会画在其它三角形上面,虽然它们本不应该是被覆盖的。
|
||||||
|
|
||||||
幸运的是,OpenGL存储深度信息在z缓冲区(Z-buffer)里面,它允许OpenGL决定何时覆盖一个像素何时不覆盖。通过使用z缓冲区我们可以设置OpenGL来进行深度测试。
|
幸运的是,OpenGL存储深度信息在Z缓冲(Z-buffer)里面,它允许OpenGL决定何时覆盖一个像素何时不覆盖。通过使用Z缓冲我们可以设置OpenGL来进行深度测试。
|
||||||
|
|
||||||
### Z缓冲区
|
### Z缓冲
|
||||||
|
|
||||||
OpenGL存储它的所有深度信息于Z缓冲区(Z-buffer)中,也被称为深度缓冲区(Depth Buffer)。GLFW会自动为你生成这样一个缓冲区 (就如它有一个颜色缓冲区来存储输出图像的颜色)。深度存储在每个片段里面(作为片段的z值)当片段想要输出它的颜色时,OpenGL会将它的深度值和z缓冲进行比较然后如果当前的片段在其它片段之后它将会被丢弃,否则重写。这个过程称为**深度测试(Depth Testing)**并且它是由OpenGL自动完成的。
|
OpenGL存储它的所有深度信息于Z缓冲(Z-buffer)中,也被称为深度缓冲区(Depth Buffer)。GLFW会自动为你生成这样一个缓冲区 (就如它有一个颜色缓冲区来存储输出图像的颜色)。深度存储在每个片段里面(作为片段的z值)当片段想要输出它的颜色时,OpenGL会将它的深度值和z缓冲进行比较然后如果当前的片段在其它片段之后它将会被丢弃,否则重写。这个过程称为**深度测试(Depth Testing)**并且它是由OpenGL自动完成的。
|
||||||
|
|
||||||
然而,如果我们想要确定OpenGL是否真的执行深度测试,首先我们要告诉OpenGL我们想要开启深度测试;而这通常是默认关闭的。我们通过`glEnable`函数来开启深度测试。`glEnable`和`glDisable`函数允许我们开启或关闭某一个OpenGL的功能。该功能会一直是开启或关闭的状态直到另一个调用来关闭或开启它。现在我们想开启深度测试就需要开启`GL_DEPTH_TEST`:
|
然而,如果我们想要确定OpenGL是否真的执行深度测试,首先我们要告诉OpenGL我们想要开启深度测试;而这通常是默认关闭的。我们通过`glEnable`函数来开启深度测试。`glEnable`和`glDisable`函数允许我们开启或关闭某一个OpenGL的功能。该功能会一直是开启或关闭的状态直到另一个调用来关闭或开启它。现在我们想开启深度测试就需要开启`GL_DEPTH_TEST`:
|
||||||
|
|
||||||
@@ -284,7 +284,7 @@ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
|||||||
|
|
||||||
就是这样!一个开启了深度测试,各个面都是纹理,并且还在旋转的立方体!如果你的程序有问题可以到[这里](http://learnopengl.com/code_viewer.php?code=getting-started/coordinate_systems_with_depth)下载源码进行比对。
|
就是这样!一个开启了深度测试,各个面都是纹理,并且还在旋转的立方体!如果你的程序有问题可以到[这里](http://learnopengl.com/code_viewer.php?code=getting-started/coordinate_systems_with_depth)下载源码进行比对。
|
||||||
|
|
||||||
### 更多的立方体
|
### 更多的立方体!
|
||||||
|
|
||||||
现在我们想在屏幕上显示10个立方体。每个立方体看起来都是一样的,区别在于它们在世界的位置及旋转角度不同。立方体的图形布局已经定义好了,所以当渲染更多物体的时候我们不需要改变我们的缓冲数组和属性数组,我们唯一需要做的只是改变每个对象的模型矩阵来将立方体转换到世界坐标系中。
|
现在我们想在屏幕上显示10个立方体。每个立方体看起来都是一样的,区别在于它们在世界的位置及旋转角度不同。立方体的图形布局已经定义好了,所以当渲染更多物体的时候我们不需要改变我们的缓冲数组和属性数组,我们唯一需要做的只是改变每个对象的模型矩阵来将立方体转换到世界坐标系中。
|
||||||
|
|
||||||
@@ -330,7 +330,7 @@ glBindVertexArray(0);
|
|||||||
|
|
||||||
## 练习
|
## 练习
|
||||||
|
|
||||||
- 对GLM的投影函数中的`FoV`和`aspect-ratio`参数进行试验。看能否搞懂它们是如何影响透视平截头体的。
|
- 对GLM的`projection`函数中的`FoV`和`aspect-ratio`参数进行实验。看能否搞懂它们是如何影响透视平截头体的。
|
||||||
- 将观察矩阵在各个方向上进行平移,来看看场景是如何改变的。注意把观察矩阵当成摄像机对象。
|
- 将观察矩阵在各个方向上进行平移,来看看场景是如何改变的。注意把观察矩阵当成摄像机对象。
|
||||||
- 只使用模型矩阵每次只让3个箱子旋转(包括第1个)而让剩下的箱子保持静止。[参考解答](http://learnopengl.com/code_viewer.php?code=getting-started/coordinate_systems-exercise3)。
|
- 只使用模型矩阵每次只让3个箱子旋转(包括第1个)而让剩下的箱子保持静止。[参考解答](http://learnopengl.com/code_viewer.php?code=getting-started/coordinate_systems-exercise3)。
|
||||||
|
|
||||||
|
18
glossary.md
18
glossary.md
@@ -138,6 +138,24 @@
|
|||||||
- Rotation Axis:旋转轴
|
- Rotation Axis:旋转轴
|
||||||
- Column-major Ordering:列主序
|
- Column-major Ordering:列主序
|
||||||
|
|
||||||
|
## 01-08
|
||||||
|
|
||||||
|
- Local Space:局部空间
|
||||||
|
- Object Space:物体空间
|
||||||
|
- World Space:世界空间
|
||||||
|
- View Space:观察空间
|
||||||
|
- Eye Space:视觉空间
|
||||||
|
- Clip Space:裁剪空间
|
||||||
|
- Screen Space:屏幕空间
|
||||||
|
- Model Matrix:模型矩阵
|
||||||
|
- View Matrix:观察矩阵
|
||||||
|
- Projection Matrix:投影矩阵
|
||||||
|
- Viewport Transform:视口变换
|
||||||
|
- Clipping Volume:裁剪体积
|
||||||
|
- Viewing Box:观察箱
|
||||||
|
- Perspective Division:透视除法
|
||||||
|
- Z-buffer:Z缓冲
|
||||||
|
|
||||||
## 06-01
|
## 06-01
|
||||||
|
|
||||||
- Debugging:调试
|
- Debugging:调试
|
||||||
|
Reference in New Issue
Block a user