1
0
mirror of https://github.com/LearnOpenGL-CN/LearnOpenGL-CN.git synced 2025-08-22 20:25:28 +08:00

Finish 01-08

This commit is contained in:
Meow J
2016-11-16 21:24:01 +08:00
parent bdd326f6d8
commit b35533fa05
2 changed files with 67 additions and 64 deletions

View File

@@ -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)
@@ -22,18 +22,18 @@
## 概述 ## 概述
为了将坐标从一个坐标系换到另一个坐标系,我们需要用到几个换矩阵,最重要的几个分别是<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)的形式结束。下面的这张图展示了整个流程以及各个换过程做了什么: 为了将坐标从一个坐标系换到另一个坐标系,我们需要用到几个换矩阵,最重要的几个分别是<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)的形式结束。下面的这张图展示了整个流程以及各个换过程做了什么:
![coordinate_systems](../img/01/08/coordinate_systems.png) ![coordinate_systems](../img/01/08/coordinate_systems.png)
1. 局部坐标是对象相对于局部原点的坐标,也是物体起始的坐标。 1. 局部坐标是对象相对于局部原点的坐标,也是物体起始的坐标。
2. 下一步是将局部坐标换为世界空间坐标,世界空间坐标是处于一个更大的空间范围的。这些坐标相对于世界的全局原点,它们会和其它物体一起相对于世界的原点进行摆放。 2. 下一步是将局部坐标换为世界空间坐标,世界空间坐标是处于一个更大的空间范围的。这些坐标相对于世界的全局原点,它们会和其它物体一起相对于世界的原点进行摆放。
3. 接下来我们将世界坐标换为观察空间坐标,使得每个坐标都是从摄像机或者说观察者的角度进行观察的。 3. 接下来我们将世界坐标换为观察空间坐标,使得每个坐标都是从摄像机或者说观察者的角度进行观察的。
4. 坐标到达观察空间之后,我们需要将其投影到裁剪坐标。裁剪坐标会被处理至-1.0到1.0的范围内,并判断哪些顶点将会出现在屏幕上。 4. 坐标到达观察空间之后,我们需要将其投影到裁剪坐标。裁剪坐标会被处理至-1.0到1.0的范围内,并判断哪些顶点将会出现在屏幕上。
5. 最后,我们将裁剪坐标换为屏幕坐标,我们将使用一个叫做<def>视口变换</def>(Viewport Transform)的过程。视口变换将位于-1.0到1.0范围的坐标换到由<fun>glViewport</fun>函数所定义的坐标范围内。最后换出来的坐标将会送到光栅器,将其转化为片段。 5. 最后,我们将裁剪坐标换为屏幕坐标,我们将使用一个叫做<def>视口变换</def>(Viewport Transform)的过程。视口变换将位于-1.0到1.0范围的坐标换到由<fun>glViewport</fun>函数所定义的坐标范围内。最后换出来的坐标将会送到光栅器,将其转化为片段。
你可能已经大致了解了每个坐标空间的作用。我们之所以将顶点换到各个不同的空间的原因是有些操作在特定的坐标系统中才有意义且更方便。例如,当需要对物体进行修改的时候,在局部空间中来操作会更说得通;如果要对一个物体做出一个相对于其它物体位置的操作时,在世界坐标系中来做这个才更说得通,等等。如果我们愿意,我们也可以定义一个直接从局部空间换到裁剪空间的换矩阵,但那样会失去很多灵活性。 你可能已经大致了解了每个坐标空间的作用。我们之所以将顶点换到各个不同的空间的原因是有些操作在特定的坐标系统中才有意义且更方便。例如,当需要对物体进行修改的时候,在局部空间中来操作会更说得通;如果要对一个物体做出一个相对于其它物体位置的操作时,在世界坐标系中来做这个才更说得通,等等。如果我们愿意,我们也可以定义一个直接从局部空间换到裁剪空间的换矩阵,但那样会失去很多灵活性。
接下来我们将要更仔细地讨论各个坐标系统。 接下来我们将要更仔细地讨论各个坐标系统。
@@ -45,21 +45,21 @@
## 世界空间 ## 世界空间
如果我们将我们所有的物体导入到程序当中,它们有可能会全挤在世界的原点(0, 0, 0)上,这并不是我们想要的结果。我们想为每一个物体定义一个位置,从而能在更大的世界当中放置它们。世界空间中的坐标正如其名:是指顶点相对于(游戏)世界的坐标。如果你希望将物体分散在世界上摆放(特别是非常真实的那样),这就是你希望物体变换到的空间。物体的坐标将会从局部换到世界空间;该换是由<def>模型</def>矩阵(Model Matrix)实现的。 如果我们将我们所有的物体导入到程序当中,它们有可能会全挤在世界的原点(0, 0, 0)上,这并不是我们想要的结果。我们想为每一个物体定义一个位置,从而能在更大的世界当中放置它们。世界空间中的坐标正如其名:是指顶点相对于(游戏)世界的坐标。如果你希望将物体分散在世界上摆放(特别是非常真实的那样),这就是你希望物体变换到的空间。物体的坐标将会从局部换到世界空间;该换是由<def>模型</def>矩阵(Model Matrix)实现的。
模型矩阵是一种换矩阵它能通过对物体进行位移、缩放、旋转来将它置于它本应该在的位置或朝向。你可以将它想像为变换一个房子你需要先将它缩小它在局部空间中太大了并将其位移至郊区的一个小镇然后在y轴上往左旋转一点以搭配附近的房子。你也可以把上一节将箱子到处摆放在场景中用的那个矩阵大致看作一个模型矩阵我们将箱子的局部坐标变换到场景/世界中的不同位置。 模型矩阵是一种换矩阵它能通过对物体进行位移、缩放、旋转来将它置于它本应该在的位置或朝向。你可以将它想像为变换一个房子你需要先将它缩小它在局部空间中太大了并将其位移至郊区的一个小镇然后在y轴上往左旋转一点以搭配附近的房子。你也可以把上一节将箱子到处摆放在场景中用的那个矩阵大致看作一个模型矩阵我们将箱子的局部坐标变换到场景/世界中的不同位置。
## 观察空间 ## 观察空间
观察空间经常被人们称之OpenGL的<def>摄像机</def>(Camera)(所以有时也称为<def>摄像机空间</def>(Camera Space)或<def>视觉空间</def>(Eye Space))。观察空间是将世界空间坐标转化为用户视野前方的坐标而产生的结果。因此观察空间就是从摄像机的视角所观察到的空间。而这通常是由一系列的位移和旋转的组合来完成,平移/旋转场景从而使得特定的对象被换到摄像机的前方。这些组合在一起的换通常存储在一个<def>观察矩阵</def>(View Matrix)里,它被用来将世界坐标换到观察空间。在下一节中我们将深入讨论如何创建一个这样的观察矩阵来模拟一个摄像机。 观察空间经常被人们称之OpenGL的<def>摄像机</def>(Camera)(所以有时也称为<def>摄像机空间</def>(Camera Space)或<def>视觉空间</def>(Eye Space))。观察空间是将世界空间坐标转化为用户视野前方的坐标而产生的结果。因此观察空间就是从摄像机的视角所观察到的空间。而这通常是由一系列的位移和旋转的组合来完成,平移/旋转场景从而使得特定的对象被换到摄像机的前方。这些组合在一起的换通常存储在一个<def>观察矩阵</def>(View Matrix)里,它被用来将世界坐标换到观察空间。在下一节中我们将深入讨论如何创建一个这样的观察矩阵来模拟一个摄像机。
## 裁剪空间 ## 裁剪空间
在一个顶点着色器运行的最后OpenGL期望所有的坐标都能落在一个特定的范围内且任何在这个范围之外的点都应该被<def>裁剪掉</def>(Clipped)。被裁剪掉的坐标就会被忽略,所以剩下的坐标就将变为屏幕上可见的片段。这也就是<def>裁剪空间</def>(Clip Space)名字的由来。 在一个顶点着色器运行的最后OpenGL期望所有的坐标都能落在一个特定的范围内且任何在这个范围之外的点都应该被<def>裁剪掉</def>(Clipped)。被裁剪掉的坐标就会被忽略,所以剩下的坐标就将变为屏幕上可见的片段。这也就是<def>裁剪空间</def>(Clip Space)名字的由来。
因为将所有可见的坐标都指定在-1.0到1.0的范围内不是很直观,所以我们会指定自己的坐标集(Coordinate Set)并将它换回标准化设备坐标系就像OpenGL期望的那样。 因为将所有可见的坐标都指定在-1.0到1.0的范围内不是很直观,所以我们会指定自己的坐标集(Coordinate Set)并将它换回标准化设备坐标系就像OpenGL期望的那样。
为了将顶点坐标从观察变换到裁剪空间,我们需要定义一个<def>投影矩阵</def>(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
@@ -67,15 +67,15 @@
由投影矩阵创建的**观察箱**(Viewing Box)被称为<def>平截头体</def>(Frustum)每个出现在平截头体范围内的坐标都会最终出现在用户的屏幕上。将特定范围内的坐标转化到标准化设备坐标系的过程而且它很容易被映射到2D观察空间坐标被称之为<def>投影</def>(Projection)因为使用投影矩阵能将3D坐标<def>投影</def>(Project)到很容易映射到2D的标准化设备坐标系中。 由投影矩阵创建的**观察箱**(Viewing Box)被称为<def>平截头体</def>(Frustum)每个出现在平截头体范围内的坐标都会最终出现在用户的屏幕上。将特定范围内的坐标转化到标准化设备坐标系的过程而且它很容易被映射到2D观察空间坐标被称之为<def>投影</def>(Projection)因为使用投影矩阵能将3D坐标<def>投影</def>(Project)到很容易映射到2D的标准化设备坐标系中。
一旦所有顶点被换到裁剪空间,最终的操作——<def>透视除法</def>(Perspective Division)将会执行在这个过程中我们将位置向量的xyz分量分别除以向量的齐次w分量透视除法是将4D裁剪空间坐标换为3D标准化设备坐标的过程。这一步会在每一个顶点着色器运行的最后被自动执行。 一旦所有顶点被换到裁剪空间,最终的操作——<def>透视除法</def>(Perspective Division)将会执行在这个过程中我们将位置向量的xyz分量分别除以向量的齐次w分量透视除法是将4D裁剪空间坐标换为3D标准化设备坐标的过程。这一步会在每一个顶点着色器运行的最后被自动执行。
在这一阶段之后,最终的坐标将会被映射到屏幕空间中(使用<fun>glViewport</fun>中的设定),并被换成片段。 在这一阶段之后,最终的坐标将会被映射到屏幕空间中(使用<fun>glViewport</fun>中的设定),并被换成片段。
将观察坐标换为裁剪坐标的投影矩阵可以为两种不同的形式,每种形式都定义了不同的平截头体。我们可以选择创建一个<def>正射</def>投影矩阵(Orthographic Projection Matrix)或一个<def>透视</def>投影矩阵(Perspective Projection Matrix)。 将观察坐标换为裁剪坐标的投影矩阵可以为两种不同的形式,每种形式都定义了不同的平截头体。我们可以选择创建一个<def>正射</def>投影矩阵(Orthographic Projection Matrix)或一个<def>透视</def>投影矩阵(Perspective Projection Matrix)。
### 正射投影 ### 正射投影
正射投影矩阵定义了一个类似立方体的平截头箱,它定义了一个裁剪空间,在这空间之外的顶点都会被裁剪掉。创建一个正射投影矩阵需要指定可见平截头体的宽、高和长度。在使用正射投影矩阵换至裁剪空间之后处于这个平截头体内的所有坐标将不会被裁剪掉。它的平截头体看起来像一个容器: 正射投影矩阵定义了一个类似立方体的平截头箱,它定义了一个裁剪空间,在这空间之外的顶点都会被裁剪掉。创建一个正射投影矩阵需要指定可见平截头体的宽、高和长度。在使用正射投影矩阵换至裁剪空间之后处于这个平截头体内的所有坐标将不会被裁剪掉。它的平截头体看起来像一个容器:
![orthographic projection frustum](../img/01/08/orthographic_frustum.png) ![orthographic projection frustum](../img/01/08/orthographic_frustum.png)
@@ -97,7 +97,7 @@ glm::ortho(0.0f, 800.0f, 0.0f, 600.0f, 0.1f, 100.0f);
![perspective](../img/01/08/perspective.png) ![perspective](../img/01/08/perspective.png)
正如你看到的那样,由于透视,这两条线在很远的地方看起来会相交。这正是透视投影想要模仿的效果,它是使用<def>透视投影矩阵</def>来完成的。这个投影矩阵将给定的平截头体范围映射到裁剪空间除此之外还修改了每个顶点坐标的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}
@@ -129,78 +129,78 @@ glm::mat4 proj = glm::perspective(45.0f, (float)width/(float)height, 0.1f, 100.0
## 把它们都组合到一起 ## 把它们都组合到一起
我们为上述的每一个步骤都创建了一个换矩阵:模型矩阵、观察矩阵和投影矩阵。一个顶点坐标将会根据以下过程被换到裁剪坐标: 我们为上述的每一个步骤都创建了一个换矩阵:模型矩阵、观察矩阵和投影矩阵。一个顶点坐标将会根据以下过程被换到裁剪坐标:
$$ $$
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将会自动进行透视除法和裁剪。 注意矩阵运算的顺序是相反的记住我们需要从右往左阅读矩阵的乘法)。最后的顶点应该被赋值到顶点着色器中的<var>gl_Position</var>OpenGL将会自动进行透视除法和裁剪。
!!! Important !!! important
**然后呢?** **然后呢?**
顶点着色器的输出要所有的顶点都在裁剪空间内,这是我们的转换矩阵所做的。OpenGL然后在裁剪空间中执行透视除法从而将它们换到标准化设备坐标。OpenGL会使用`glViewPort`内部的参数来将标准化设备坐标映射到屏幕坐标,每个坐标都关联了一个屏幕上的点(在我们的例子中屏幕是800 *600)。这个过程称为视口换。 顶点着色器的输出要所有的顶点都在裁剪空间内,这是我们刚才使用变换矩阵所做的。OpenGL然后对**裁剪坐标**执行**透视除法**从而将它们换到**标准化设备坐标**。OpenGL会使用<fun>glViewPort</fun>内部的参数来将标准化设备坐标映射到**屏幕坐标**,每个坐标都关联了一个屏幕上的点在我们的例子中是一个800x600的屏幕)。这个过程称为视口换。
这一章的主题可能会比较难理解,如果你仍然不确定每个空间的作用的话,你也不必太担心。接下来你会看到我们是怎样好好运用这些坐标空间的并且会有足够的展示例子在接下来的教程中 这一章的主题可能会比较难理解,如果你仍然不确定每个空间的作用的话,你也不必太担心。接下来你会看到我们是怎样运用这些坐标空间的,而且之后也会有足够多的例子
# 进入3D # 进入3D
既然我们知道了如何将三维坐标换为二维坐标,我们可以开始将我们的对象展示为三维对象而不是目前我们所展示的缺胳膊少腿的二维平面。 既然我们知道了如何将3D坐标换为2D坐标,我们可以开始使用真正的3D物体而不是枯燥的2D平面
在开始进行三维画图时,我们首先创建一个模型矩阵。这个模型矩阵包含了移、缩放与旋转,我们将会运用它来将对象的顶点转换到全局世界空间。让我们平移一下我们的平面,通过将其绕着x轴旋转使它看起来像放在地上一样。这个模型矩阵看起来是这样的 在开始进行3D绘图时,我们首先创建一个模型矩阵。这个模型矩阵包含了移、缩放与旋转操作,它们会被应用到所有物体的顶点上,以**变换**它们到全局世界空间。让我们变换一下我们的平面将其绕着x轴旋转使它看起来像放在地上一样。这个模型矩阵看起来是这样的:
```c++ ```c++
glm::mat4 model; glm::mat4 model;
model = glm::rotate(model, -55.0f, glm::vec3(1.0f, 0.0f, 0.0f)); model = glm::rotate(model, -55.0f, glm::vec3(1.0f, 0.0f, 0.0f));
``` ```
通过将顶点坐标乘以这个模型矩阵我们将该顶点坐标换到世界坐标。我们的平面看起来就是在地板上的因此可以代表真实世界的平面。 通过将顶点坐标乘以这个模型矩阵我们将该顶点坐标换到世界坐标。我们的平面看起来就是在地板上,代表全局世界的平面。
接下来我们需要创建一个观察矩阵。我们想要在场景里面稍微往后移动以使得对象变成可见的(当在世界空间时,我们位于原点(0,0,0))。要想在场景里面移动,思考下面的问题: 接下来我们需要创建一个观察矩阵。我们想要在场景里面稍微往后移动以使得物体变成可见的当在世界空间时,我们位于原点(0,0,0)。要想在场景里面移动,先想一想下面这个问题:
- 将摄像机后移动将整个场景前移是一样的。 - 将摄像机后移动,和将整个场景前移是一样的。
是观察空间所做的,我们以相反于移动摄像机的方向移动整个场景。因为我们想要往后移动并且OpenGL是一个右手坐标系(Right-handed System)所以我们沿着z轴的正方向移动。我们会通过将场景沿着z轴负方向平移来实现这个。它会给我们一种我们在往后移动的感觉。 是观察矩阵所做的,我们以相反于摄像机移动的方向移动整个场景。因为我们想要往后移动并且OpenGL是一个右手坐标系(Right-handed System)所以我们需要沿着z轴的正方向移动。我们会通过将场景沿着z轴负方向平移来实现。它会给我们一种我们在往后移动的感觉。
!!! Important !!! important
**右手坐标系(Right-handed System)** **右手坐标系(Right-handed System)**
按照约定OpenGL是一个右手坐标系。最基本的就是正x轴在你的右手边正y轴往上而正z轴是往后的。想象你的屏幕处于三个轴的中心正z轴穿过你的屏幕朝向你。坐标系画起来如下 按照惯例OpenGL是一个右手坐标系。简单来说,就是正x轴在你的右手边正y轴朝上,而正z轴是朝向后方的。想象你的屏幕处于三个轴的中心,则正z轴穿过你的屏幕朝向你。坐标系画起来如下
![coordinate_systems_right_handed](../img/01/08/coordinate_systems_right_handed.png) ![coordinate_systems_right_handed](../img/01/08/coordinate_systems_right_handed.png)
为了理解为什么被称为右手坐标系,按如下的步骤做: 为了理解为什么被称为右手坐标系,按如下的步骤做:
- 张开你的右手使正y轴沿着你的手往上 - 沿着正y轴方向伸出你的右臂手指着上方
- 使你的大拇指往右 - 大拇指指向右方
- 使你的食指往上 - 食指指向上方
- 向下90度弯曲你的中指。 - 中指向下弯曲90度
如果你都正确地做了,那么你的大拇指朝着正x轴方向食指朝着正y轴方向中指朝着正z轴方向。如果你用左来做这些动作你会发现z轴的方向是相反的。这就是有名的左手坐标系它被DirectX广泛地使用。注意在标准化设备坐标系中OpenGL使用的是左手坐标系(投影矩阵改变了惯用手的习惯) 如果你的动作正确,那么你的大拇指指向正x轴方向食指指向正y轴方向中指指向正z轴方向。如果你用左来做这些动作你会发现z轴的方向是相反的。这个叫做左手坐标系它被DirectX广泛地使用。注意在标准化设备坐标系中OpenGL实际上使用的是左手坐标系投影矩阵交换了左右手)
在下一个教程中我们将会详细讨论如何在场景中移动。目前观察矩阵是这样的: 在下一个教程中我们将会详细讨论如何在场景中移动。目前来说,观察矩阵是这样的:
```c++ ```c++
glm::mat4 view; glm::mat4 view;
// 注意,我们将矩阵向我们要进行移动场景的反向移动。 // 注意,我们将矩阵向我们要进行移动场景的反向移动。
view = glm::translate(view, glm::vec3(0.0f, 0.0f, -3.0f)); view = glm::translate(view, glm::vec3(0.0f, 0.0f, -3.0f));
``` ```
最后我们需要做的是定义一个投影矩阵。我们想要在我们的场景中使用透视投影所以我们声明的投影矩阵是像这样的 最后我们需要做的是定义一个投影矩阵。我们希望在场景中使用透视投影所以像这样声明一个投影矩阵
```c++ ```c++
glm::mat4 projection; glm::mat4 projection;
projection = glm::perspective(45.0f, screenWidth / screenHeight, 0.1f, 100.0f); projection = glm::perspective(45.0f, screenWidth / screenHeight, 0.1f, 100.0f);
``` ```
!!! Attention !!! attention
再重复一遍在glm指定角度的时候要注意。这里我们将参数fov设置为45但有些GLM的实现是将fov当成弧度在这种情况你需要使用`glm::radians(45.0)`来设置。 再重复一遍在glm指定角度的时候要注意。这里我们将参数fov设置为**45**但有些GLM的实现是将fov当成弧度在这种情况你需要使用`glm::radians(45.0)`来设置。
既然我们创建了换矩阵,我们应该将它们传入着色器。首先,让我们在顶点着色器中声明一个单位转换矩阵然后将它乘以顶点坐标: 既然我们已经创建了换矩阵,我们应该将它们传入着色器。首先,让我们在顶点着色器中声明一个uniform变换矩阵然后将它乘以顶点坐标:
```c++ ```c++
#version 330 core #version 330 core
@@ -218,7 +218,7 @@ void main()
} }
``` ```
我们应该将矩阵传入着色器(这通常在每次渲染的时候即转换矩阵将要改变的时候完成) 我们应该将矩阵传入着色器这通常在每次渲染迭代中进行,因为变换矩阵会经常变动)
```c++ ```c++
GLint modelLoc = glGetUniformLocation(ourShader.Program, "model"); GLint modelLoc = glGetUniformLocation(ourShader.Program, "model");
@@ -226,53 +226,53 @@ glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
... // 观察矩阵和投影矩阵与之类似 ... // 观察矩阵和投影矩阵与之类似
``` ```
现在我们的顶点坐标通过模型、观察和投影矩阵来转换,最后的对象应该 我们的顶点坐标已经使用模型、观察和投影矩阵进行变换了,最终的物体应该
- 往后向地板倾斜 - 稍微向后倾斜至地板方向
- 离我们有距离。 - 离我们有一些距离。
- 透视展示(顶点越远,变得越小) - 透视效果(顶点越远,变得越小
让我们检查一下结果是否满足这些要求: 让我们检查一下结果是否满足这些要求:
![coordinate_systems_result](../img/01/08/coordinate_systems_result.png) ![coordinate_systems_result](../img/01/08/coordinate_systems_result.png)
它看起来就像是一个三维的平面,静止在一虚构的地板上。如果你不是得到相同的结果,请检查下完整的[源代码](http://learnopengl.com/code_viewer.php?code=getting-started/coordinate_systems) 以及[顶点](http://learnopengl.com/code_viewer.php?code=getting-started/transform&type=vertex)和[片段](http://learnopengl.com/code_viewer.php?code=getting-started/transform&type=fragment)着色器。 它看起来就像是一个3D的平面,静止在一虚构的地板上。如果你得到的不是相同的结果,请检查下完整的[源代码](http://learnopengl.com/code_viewer.php?code=getting-started/coordinate_systems) 以及[顶点](http://learnopengl.com/code_viewer.php?code=getting-started/transform&type=vertex)和[片段](http://learnopengl.com/code_viewer.php?code=getting-started/transform&type=fragment)着色器。
## 更多的3D ## 更多的3D
到目前为止,我们在二维平面甚至在三维空间中画图,所以让我们采取大胆的方式来将我们的二维平面扩展为三维立方体。要渲染一个立方体我们一共需要36个顶点(6个面 x 每个面有2个三角形组成 x 每个三角形有3个顶点)这36个顶点的位置你可以[从这里获取](http://learnopengl.com/code_viewer.php?code=getting-started/cube_vertices)。注意,这一次我们省略了颜色值,因为我们只使用纹理得到最终的颜色值。 到目前为止,我们一直都在使用一个2D平面而且甚至是在3D空间里所以让我们大胆地拓展我们的2D平面为一个3D立方体。要渲染一个立方体我们一共需要36个顶点6个面 x 每个面有2个三角形组成 x 每个三角形有3个顶点这36个顶点的位置你可以从[这里](http://learnopengl.com/code_viewer.php?code=getting-started/cube_vertices)获取。注意,这一次我们省略了颜色值,因为我们只通过纹理来获取最终的颜色值。
为了好玩,我们将让立方体随着时间旋转: 为了有趣一点,我们将让立方体随着时间旋转:
```c++ ```c++
model = glm::rotate(model, (GLfloat)glfwGetTime() * 50.0f, glm::vec3(0.5f, 1.0f, 0.0f)); model = glm::rotate(model, (GLfloat)glfwGetTime() * 50.0f, glm::vec3(0.5f, 1.0f, 0.0f));
``` ```
然后我们使用`glDrawArrays`来画立方体这一次总共有36个顶点。 然后我们使用<fun>glDrawArrays</fun>来绘制立方体,这一次总共有36个顶点。
```c++ ```c++
glDrawArrays(GL_TRIANGLES, 0, 36); glDrawArrays(GL_TRIANGLES, 0, 36);
``` ```
如果一切顺利的话绘制效果将与下面的类似 如果一切顺利的话你应该能得到下面这样的效果
<video src="../../img/01/08/coordinate_system_no_depth.mp4" controls="controls"></video> <video src="../../img/01/08/coordinate_system_no_depth.mp4" controls="controls"></video>
这有点像一个立方体,但又有种说不出的奇怪。立方体的某些本应被遮挡住的面被绘制在了这个立方体其他面的上面。之所以这样是因为OpenGL是通过画一个一个三角形来画你的立方体的,所以它将会覆盖之前已经画在那里的像素。因为这个原因,有些三角形会在其它三角形上面,虽然它们本不应该是被覆盖的。 的确有点像一个立方体,但又有种说不出的奇怪。立方体的某些本应被遮挡住的面被绘制在了这个立方体其他面之上。之所以这样是因为OpenGL是一个三角形一个三角形地来绘制你的立方体的,所以即便之前那里有东西它也会覆盖之前的像素。因为这个原因,有些三角形会被绘制在其它三角形上面,虽然它们本不应该是被覆盖的。
幸运的是OpenGL存储深度信息在Z缓冲(Z-buffer)里面它允许OpenGL决定何时覆盖一个像素何时不覆盖。通过使用Z缓冲我们可以置OpenGL来进行深度测试。 幸运的是OpenGL存储深度信息在一个叫做<def>Z缓冲</def>(Z-buffer)的缓冲中它允许OpenGL决定何时覆盖一个像素何时不覆盖。通过使用Z缓冲我们可以置OpenGL来进行深度测试。
### Z缓冲 ### Z缓冲
OpenGL存储它的所有深度信息于Z缓冲(Z-buffer)中,也被称为深度缓冲区(Depth Buffer)。GLFW会自动为你生成这样一个缓冲区 (就如它有一个颜色缓冲来存储输出图像的颜色)。深度存储在每个片段里面(作为片段的z值)当片段想要输出它的颜色时OpenGL会将它的深度值和z缓冲进行比较然后如果当前的片段在其它片段之后它将会被丢弃,否则重写。这个过程称为**深度测试(Depth Testing)**并且它是由OpenGL自动完成的。 OpenGL存储它的所有深度信息于一个Z缓冲(Z-buffer)中,也被称为<def>深度缓冲</def>(Depth Buffer)。GLFW会自动为你生成这样一个缓冲(就像它也有一个颜色缓冲来存储输出图像的颜色。深度存储在每个片段里面作为片段的**z**值),当片段想要输出它的颜色时OpenGL会将它的深度值和z缓冲进行比较如果当前的片段在其它片段之后它将会被丢弃,否则将会覆盖。这个过程称为<def>深度测试</def>(Depth Testing)它是由OpenGL自动完成的。
然而如果我们想要确定OpenGL是否真的执行深度测试首先我们要告诉OpenGL我们想要启深度测试;而这通常是默认关闭的。我们通过`glEnable`函数来开启深度测试。`glEnable`和`glDisable`函数允许我们开启或关闭某一个OpenGL功能。功能会一直是开启或关闭的状态直到另一个调用来关闭或开启它。现在我们想启深度测试需要开启`GL_DEPTH_TEST` 然而如果我们想要确定OpenGL真的执行深度测试首先我们要告诉OpenGL我们想要启深度测试;默认关闭的。我们可以通过<fun>glEnable</fun>函数来开启深度测试。<fun>glEnable</fun>和<fun>glDisable</fun>函数允许我们启用或禁用某个OpenGL功能。这个功能会一直保持启用/禁用状态直到另一个调用来禁用/启用它。现在我们想启深度测试需要开启<var>GL_DEPTH_TEST</var>
```c++ ```c++
glEnable(GL_DEPTH_TEST); glEnable(GL_DEPTH_TEST);
``` ```
既然我们使用了深度测试我们也想要在每次重复渲染之前清除深度缓冲区(否则前一个片段的深度信息仍然保存在缓冲区中)。就像清除颜色缓冲一样,我们可以通过在`glclear`函数中指定`DEPTH_BUFFER_BIT`位来清除深度缓冲 因为我们使用了深度测试我们也想要在每次渲染迭代之前清除深度缓冲否则前一的深度信息仍然保存在缓冲中)。就像清除颜色缓冲一样,我们可以通过在<fun>glClear</fun>函数中指定<var>DEPTH_BUFFER_BIT</var>位来清除深度缓冲:
```c++ ```c++
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
@@ -286,9 +286,9 @@ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
### 更多的立方体! ### 更多的立方体!
现在我们想在屏幕上显示10个立方体。每个立方体看起来都是一样的区别在于它们在世界的位置及旋转角度不同。立方体的图形布局已经定义好了所以当渲染更多物体的时候我们不需要改变我们的缓冲数组和属性数组我们唯一需要做的只是改变每个对象的模型矩阵来将立方体换到世界坐标系中。 现在我们想在屏幕上显示10个立方体。每个立方体看起来都是一样的区别在于它们在世界的位置及旋转角度不同。立方体的图形布局已经定义好了所以当渲染更多物体的时候我们不需要改变我们的缓冲数组和属性数组我们唯一需要做的只是改变每个对象的模型矩阵来将立方体换到世界坐标系中。
首先,让我们为每个立方体定义一个转换向量来指定它在世界空间的位置。我们将在`glm::vec3`数组中定义10个立方体位置向量。 首先,让我们为每个立方体定义一个位移向量来指定它在世界空间的位置。我们将在一个`glm::vec3`数组中定义10个立方体位置
```c++ ```c++
glm::vec3 cubePositions[] = { glm::vec3 cubePositions[] = {
@@ -305,7 +305,7 @@ glm::vec3 cubePositions[] = {
}; };
``` ```
现在,在循环中,我们调用`glDrawArrays`10次在我们开始渲染之前每次传入一个不同的模型矩阵到顶点着色器中。我们将会创建一个小的循环来通过一个不同的模型矩阵重复渲染我们的对象10次。注意我们也传入了一旋转参数到每个箱子中 现在,在游戏循环中,我们调用<fun>glDrawArrays</fun> 10次但这次在我们渲染之前每次传入一个不同的模型矩阵到顶点着色器中。我们将会在游戏循环中创建一个小的循环不同的模型矩阵渲染我们的物体10次。注意我们也对每个箱子加了一旋转:
```c++ ```c++
glBindVertexArray(VAO); glBindVertexArray(VAO);
@@ -322,15 +322,15 @@ for(GLuint i = 0; i < 10; i++)
glBindVertexArray(0); glBindVertexArray(0);
``` ```
代码将会每次都更新模型矩阵然后画出新的立方体如此总共重复10次。然后我们应该就能看到一个拥有10个正在奇葩旋转着的立方体的世界。 代码将会每次新立方体绘制出来的时候更新模型矩阵如此总共重复10次。然后我们应该就能看到一个拥有10个正在奇葩旋转着的立方体的世界。
![coordinate_systems_multiple_objects](../img/01/08/coordinate_systems_multiple_objects.png) ![coordinate_systems_multiple_objects](../img/01/08/coordinate_systems_multiple_objects.png)
完美!这就像我们的箱子找到了志同道合的小伙伴一样。如果你在这里卡住了,你可以对照一下[代码](http://learnopengl.com/code_viewer.php?code=getting-started/coordinate_systems_multiple_objects) 以及[顶点着色器](http://learnopengl.com/code_viewer.php?code=getting-started/coordinate_systems&type=vertex)和[片段着色器](http://learnopengl.com/code_viewer.php?code=getting-started/coordinate_systems&type=fragment) 。 完美!这就像我们的箱子找到了志同道合的小伙伴一样。如果你在这里卡住了,你可以对照一下[代码](http://learnopengl.com/code_viewer.php?code=getting-started/coordinate_systems_multiple_objects) 以及[顶点](http://learnopengl.com/code_viewer.php?code=getting-started/coordinate_systems&type=vertex)和[片段](http://learnopengl.com/code_viewer.php?code=getting-started/coordinate_systems&type=fragment) 着色器
## 练习 ## 练习
- 对GLM的`projection`函数中的`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)。

View File

@@ -155,6 +155,9 @@
- Viewing Box观察箱 - Viewing Box观察箱
- Perspective Division透视除法 - Perspective Division透视除法
- Z-bufferZ缓冲 - Z-bufferZ缓冲
- Draw绘制
- Depth Testing深度测试
- Depth Buffer深度缓冲
## 06-01 ## 06-01