1
0
mirror of https://github.com/LearnOpenGL-CN/LearnOpenGL-CN.git synced 2025-08-23 12:45:29 +08:00
Files
LearnOpenGL-CN/docs/01 Getting started/08 Coordinate Systems.md
2016-11-15 21:46:54 +08:00

337 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 坐标系统
原文 | [Coordinate Systems](http://learnopengl.com/#!Getting-started/Coordinate-Systems)
---|---
作者 | JoeyDeVries
翻译 | linkoln
校对 | Geequlim, Meow J, [BLumia](https://github.com/blumia/)
在上一个教程中我们学习了如何有效地利用矩阵的变换来对所有顶点进行转换。OpenGL希望在每次顶点着色器运行后我们可见的所有顶点都为标准化设备坐标(Normalized Device Coordinate, NDC)。也就是说,每个顶点的**x****y****z**坐标都应该在**-1.0**到**1.0**之间,超出这个坐标范围的顶点都将不可见。我们通常会自己设定一个坐标的范围,之后再在顶点着色器中将这些坐标转换为标准化设备坐标。然后将这些标准化设备坐标传入光栅器(Rasterizer),将它们转换为屏幕上的二维坐标或像素。
将坐标转换为标准化设备坐标,接着再转化为屏幕坐标的过程通常是分步进行的,也就是类似于流水线那样子。在流水线中,物体的顶点在最终转化为屏幕坐标之前还会被转换到多个坐标系统(Coordinate System)。将物体的坐标转换到几个**过渡**坐标系(Intermediate Coordinate System)的优点在于在这些特定的坐标系统中一些操作或运算更加方便和容易这一点很快就会变得很明显。对我们来说比较重要的总共有5个不同的坐标系统
- 局部空间(Local Space或者称为物体空间(Object Space))
- 世界空间(World Space)
- 观察空间(View Space或者称为视觉空间(Eye Space))
- 裁剪空间(Clip Space)
- 屏幕空间(Screen Space)
这就是一个顶点在最终被转化为片段之前需要经历的所有不同状态。
你现在可能会对什么是坐标空间,什么是坐标系统感到非常困惑,所以我们将用一种更加通俗的方式来解释它们。下面,我们将显示一个整体的图片,之后我们会讲解每个空间的具体功能。
## 概述
为了将坐标从一个坐标系转换到另一个坐标系,我们需要用到几个转换矩阵,最重要的几个分别是<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)
1. 局部坐标是对象相对于局部原点的坐标,也是物体起始的坐标。
2. 下一步是将局部坐标转换为世界空间坐标,世界空间坐标是处于一个更大的空间范围的。这些坐标相对于世界的全局原点,它们会和其它物体一起相对于世界的原点进行摆放。
3. 接下来我们将世界坐标转换为观察空间坐标,使得每个坐标都是从摄像机或者说观察者的角度进行观察的。
4. 坐标到达观察空间之后,我们需要将其投影到裁剪坐标。裁剪坐标会被处理至-1.0到1.0的范围内,并判断哪些顶点将会出现在屏幕上。
5. 最后,我们将裁剪坐标转换为屏幕坐标,我们将使用一个叫做<def>视口变换</def>(Viewport Transform)的过程。视口变换将位于-1.0到1.0范围的坐标转换到由<fun>glViewport</fun>函数所定义的坐标范围内。最后转换出来的坐标将会送到光栅器,将其转化为片段。
你可能已经大致了解了每个坐标空间的作用。我们之所以将顶点转换到各个不同的空间的原因是有些操作在特定的坐标系统中才有意义且更方便。例如,当需要对物体进行修改的时候,在局部空间中来操作会更说得通;如果要对一个物体做出一个相对于其它物体位置的操作时,在世界坐标系中来做这个才更说得通,等等。如果我们愿意,我们也可以定义一个直接从局部空间转换到裁剪空间的转换矩阵,但那样会失去很多灵活性。
接下来我们将要更仔细地讨论各个坐标系统。
## 局部空间
局部空间是指物体所在的坐标空间即对象最开始所在的地方。想象你在一个建模软件比如说Blender中创建了一个立方体。你创建的立方体的原点有可能位于(0, 0, 0),即便它有可能最后在程序中处于完全不同的位置。甚至有可能你创建的所有模型都以(0, 0, 0)为初始位置(译注:然而它们会最终出现在世界的不同位置)。所以,你的模型的所有顶点都是在**局部**空间中:它们相对于你的物体来说都是局部的。
我们一直使用的那个箱子的顶点是被设定在-0.5到0.5的坐标范围中,(0, 0)是它的原点。这些都是局部坐标。
## 世界空间
如果我们将我们所有的物体导入到程序当中,它们有可能会全挤在世界的原点(0, 0, 0)上,这并不是我们想要的结果。我们想为每一个物体定义一个位置,从而能在更大的世界当中放置它们。世界空间中的坐标正如其名:是指顶点相对于(游戏)世界的坐标。如果你希望将物体分散在世界上摆放(特别是非常真实的那样),这就是你希望物体变换到的空间。物体的坐标将会从局部转换到世界空间;该转换是由<def>模型</def>矩阵(Model Matrix)实现的。
模型矩阵是一种转换矩阵它能通过对物体进行位移、缩放、旋转来将它置于它本应该在的位置或朝向。你可以将它想像为变换一个房子你需要先将它缩小它在局部空间中太大了并将其位移至郊区的一个小镇然后在y轴上往左旋转一点以搭配附近的房子。你也可以把上一节将箱子到处摆放在场景中用的那个矩阵大致看作一个模型矩阵我们将箱子的局部坐标变换到场景/世界中的不同位置。
## 观察空间
观察空间经常被人们称之OpenGL的<def>摄像机</def>(Camera)(所以有时也称为<def>摄像机空间</def>(Camera Space)或<def>视觉空间</def>(Eye Space))。观察空间是将世界空间坐标转化为用户视野前方的坐标而产生的结果。因此观察空间就是从摄像机的视角所观察到的空间。而这通常是由一系列的位移和旋转的组合来完成,平移/旋转场景从而使得特定的对象被转换到摄像机的前方。这些组合在一起的转换通常存储在一个<def>观察矩阵</def>(View Matrix)里,它被用来将世界坐标转换到观察空间。在下一节中我们将深入讨论如何创建一个这样的观察矩阵来模拟一个摄像机。
## 裁剪空间
在一个顶点着色器运行的最后OpenGL期望所有的坐标都能落在一个特定的范围内且任何在这个范围之外的点都应该被<def>裁剪掉</def>(Clipped)。被裁剪掉的坐标就会被忽略,所以剩下的坐标就将变为屏幕上可见的片段。这也就是<def>裁剪空间</def>(Clip Space)名字的由来。
因为将所有可见的坐标都指定在-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的标准化设备坐标,所以被裁剪掉了。
!!! important
如果只是图元(Primitive),例如三角形,的一部分超出了<def>裁剪体积</def>(Clipping Volume)则OpenGL会重新构建这个三角形为一个或多个三角形让其能够适合这个裁剪范围。
由投影矩阵创建的**观察箱**(Viewing Box)被称为<def>平截头体</def>(Frustum)每个出现在平截头体范围内的坐标都会最终出现在用户的屏幕上。将特定范围内的坐标转化到标准化设备坐标系的过程而且它很容易被映射到2D观察空间坐标被称之为<def>投影</def>(Projection)因为使用投影矩阵能将3D坐标<def>投影<def>(Project)到很容易映射到2D的标准化设备坐标系中。
一旦所有顶点被转换到裁剪空间,最终的操作——<def>透视除法</def>(Perspective Division)将会执行在这个过程中我们将位置向量的xyz分量分别除以向量的齐次w分量透视除法是将4D裁剪空间坐标转换为3D标准化设备坐标的过程。这一步会在每一个顶点着色器运行的最后被自动执行。
在这一阶段之后,最终的坐标将会被映射到屏幕空间中(使用<fun>glViewport</fun>中的设定),并被转换成片段。
将观察坐标转换为裁剪坐标的投影矩阵可以为两种不同的形式,每种形式都定义了不同的平截头体。我们可以选择创建一个<def>正射</def>投影矩阵(Orthographic Projection Matrix)或一个<def>透视</def>投影矩阵(Perspective Projection Matrix)。
### 正射投影
正射投影矩阵定义了一个类似立方体的平截头箱,它定义了一个裁剪空间,在这空间之外的顶点都会被裁剪掉。创建一个正射投影矩阵需要指定可见平截头体的宽、高和长度。在使用正射投影矩阵转换至裁剪空间之后处于这个平截头体内的所有坐标将不会被裁剪掉。它的平截头体看起来像一个容器:
![orthographic projection frustum](../img/01/08/orthographic_frustum.png)
上面的平截头体定义了可见的坐标,它由由宽、高、<def></def>(Near)平面和<def></def>(Far)平面所指定。任何出现在近平面之前或远平面之后的坐标都会被裁剪掉。正射平截头体直接将平截头体内部的所有坐标映射为标准化设备坐标因为每个向量的w分量都没有进行改变如果w分量等于1.0,透视除法则不会改变这个坐标。
要创建一个正射投影矩阵我们可以使用GLM的内置函数`glm::ortho`
```c++
glm::ortho(0.0f, 800.0f, 0.0f, 600.0f, 0.1f, 100.0f);
```
前两个参数指定了平截头体的左右坐标第三和第四参数指定了平截头体的底部和顶部。通过这四个参数我们定义了近平面和远平面的大小然后第五和第六个参数则定义了近平面和远平面的距离。这个投影矩阵会将处于这些xyz值范围内的坐标变换为标准化设备坐标。
正射投影矩阵直接将坐标映射到2D平面中即你的屏幕但实际上一个直接的投影矩阵会产生不真实的结果因为这个投影没有将<def>透视</def>(Perspective)考虑进去。所以我们需要<def>透视投影</def>矩阵来解决这个问题。
### 透视投影
如果你曾经体验过**实际生活**给你带来的景象,你就会注意到离你越远的东西看起来更小。这个奇怪的效果称之为<def>透视</def>(Perspective)。透视的效果在我们看一条无限长的高速公路或铁路时尤其明显,正如下面图片显示的那样:
![perspective](../img/01/08/perspective.png)
正如你看到的那样,由于透视,这两条线在很远的地方看起来会相交。这正是透视投影想要模仿的效果,它是使用<def>透视投影矩阵</def>来完成的。这个投影矩阵将给定的平截头体范围映射到裁剪空间除此之外还修改了每个顶点坐标的w值从而使得离观察者越远的顶点坐标w分量越大。被转换到裁剪空间的坐标都会在-w到w的范围之间任何大于这个范围的坐标都会被裁剪掉。OpenGL要求所有可见的坐标都落在-1.0到1.0范围内,作为顶点着色器最后的输出,因此,一旦坐标在裁剪空间内之后,透视除法就会被应用到裁剪空间坐标上:
$$
out = \begin{pmatrix} x /w \\ y / w \\ z / w \end{pmatrix}
$$
顶点坐标的每个分量都会除以它的w分量距离观察者越远顶点坐标就会越小。这是也是w分量非常重要的另一个原因它能够帮助我们进行透视投影。最后的结果坐标就是处于标准化设备空间中的。如果你对正射投影矩阵和透视投影矩阵是如何计算的很感兴趣且不会对数学感到恐惧的话我推荐这篇由Songho写的[]文章](http://www.songho.ca/opengl/gl_projectionmatrix.html)。
在GLM中可以这样创建一个透视投影矩阵
```c++
glm::mat4 proj = glm::perspective(45.0f, (float)width/(float)height, 0.1f, 100.0f);
```
同样,`glm::perspective`所做的其实就是创建了一个定义了可视空间的大**平截头体**,任何在这个平截头体以外的东西最后都不会出现在裁剪空间体积内,并且将会受到裁剪。一个透视平截头体可以被看作一个不均匀形状的箱子,在这个箱子内部的每个坐标都会被映射到裁剪空间上的一个点。下面是一张透视平截头体的图片:
![ perspective_frustum](../img/01/08/perspective_frustum.png)
它的第一个参数定义了<def>fov</def>的值,它表示的是<def>视野</def>(Field of View)并且设置了观察空间的大小。如果想要一个真实的观察效果它的值通常设置为45.0f,但想要一个末日风格的结果你可以将其设置一个更大的值。第二个参数设置了宽高比,由视口的宽除以高所得。第三和第四个参数设置了平截头体的**近**和**远**平面。我们通常设置近距离为0.1f而远距离设为100.0f。所有在近平面和远平面内且处于平截头体内的顶点都会被渲染。
!!! important
当你把透视矩阵的 *near* 值设置太大时如10.0fOpenGL会将靠近摄像机的坐标在0.0f和10.0f之间)都裁剪掉,这会导致一个你在游戏中很熟悉的视觉效果:在太过靠近一个物体的时候你的视线会直接穿过去。
当使用正射投影时每一个顶点坐标都会直接映射到裁剪空间中而不经过任何精细的透视除法它仍然会进行透视除法只是w分量没有被改变它保持为1因此没有起作用。因为正射投影没有使用透视远处的物体不会显得更小所以产生奇怪的视觉效果。由于这个原因正射投影主要用于二维渲染以及一些建筑或工程的程序在这些场景中我们更希望顶点不会被透视所干扰。某些如 *Blender* 等进行三维建模的软件有时在建模时也会使用正射投影因为它在各个维度下都更准确地描绘了每个物体。下面你能够看到在Blender里面使用两种投影方式的对比
![perspective_orthographic](../img/01/08/perspective_orthographic.png)
你可以看到,使用透视投影的话,远处的顶点看起来比较小,而在正射投影中每个顶点距离观察者的距离都是一样的。
## 把它们都组合到一起
我们为上述的每一个步骤都创建了一个转换矩阵:模型矩阵、观察矩阵和投影矩阵。一个顶点的坐标将会根据以下过程被转换到裁剪坐标:
$$
V_{clip} = M_{projection} \cdot M_{view} \cdot M_{model} \cdot V_{local}
$$
注意每个矩阵被运算的顺序是相反的(记住我们需要从右往左乘上每个矩阵)。最后的顶点应该被赋予顶点着色器中的`gl_Position`且OpenGL将会自动进行透视除法和裁剪。
!!! Important
**然后呢?**
顶点着色器的输出需要所有的顶点都在裁剪空间内而这是我们的转换矩阵所做的。OpenGL然后在裁剪空间中执行透视除法从而将它们转换到标准化设备坐标。OpenGL会使用`glViewPort`内部的参数来将标准化设备坐标映射到屏幕坐标,每个坐标都关联了一个屏幕上的点(在我们的例子中屏幕是800 *600)。这个过程称为视口转换。
这一章的主题可能会比较难理解,如果你仍然不确定每个空间的作用的话,你也不必太担心。接下来你会看到我们是怎样好好运用这些坐标空间的并且会有足够的展示例子在接下来的教程中。
# 进入3D
既然我们知道了如何将三维坐标转换为二维坐标,我们可以开始将我们的对象展示为三维对象而不是目前我们所展示的缺胳膊少腿的二维平面。
在开始进行三维画图时我们首先创建一个模型矩阵。这个模型矩阵包含了平移、缩放与旋转我们将会运用它来将对象的顶点转换到全局世界空间。让我们平移一下我们的平面通过将其绕着x轴旋转使它看起来像放在地上一样。这个模型矩阵看起来是这样的
```c++
glm::mat4 model;
model = glm::rotate(model, -55.0f, glm::vec3(1.0f, 0.0f, 0.0f));
```
通过将顶点坐标乘以这个模型矩阵我们将该顶点坐标转换到世界坐标。我们的平面看起来就是在地板上的因此可以代表真实世界的平面。
接下来我们需要创建一个观察矩阵。我们想要在场景里面稍微往后移动以使得对象变成可见的(当在世界空间时,我们位于原点(0,0,0))。要想在场景里面移动,思考下面的问题:
- 将摄像机往后移动跟将整个场景往前移是一样的。
这就是观察空间所做的我们以相反于移动摄像机的方向移动整个场景。因为我们想要往后移动并且OpenGL是一个右手坐标系(Right-handed System)所以我们沿着z轴的正方向移动。我们会通过将场景沿着z轴负方向平移来实现这个。它会给我们一种我们在往后移动的感觉。
!!! Important
**右手坐标系(Right-handed System)**
按照约定OpenGL是一个右手坐标系。最基本的就是说正x轴在你的右手边正y轴往上而正z轴是往后的。想象你的屏幕处于三个轴的中心且正z轴穿过你的屏幕朝向你。坐标系画起来如下
![coordinate_systems_right_handed](../img/01/08/coordinate_systems_right_handed.png)
为了理解为什么被称为右手坐标系,按如下的步骤做:
- 张开你的右手使正y轴沿着你的手往上。
- 使你的大拇指往右。
- 使你的食指往上。
- 向下90度弯曲你的中指。
如果你都正确地做了那么你的大拇指朝着正x轴方向食指朝着正y轴方向中指朝着正z轴方向。如果你用左手来做这些动作你会发现z轴的方向是相反的。这就是有名的左手坐标系它被DirectX广泛地使用。注意在标准化设备坐标系中OpenGL使用的是左手坐标系(投影矩阵改变了惯用手的习惯)。
在下一个教程中我们将会详细讨论如何在场景中移动。目前的观察矩阵是这样的:
```c++
glm::mat4 view;
// 注意,我们将矩阵向我们要进行移动场景的反向移动。
view = glm::translate(view, glm::vec3(0.0f, 0.0f, -3.0f));
```
最后我们需要做的是定义一个投影矩阵。我们想要在我们的场景中使用透视投影所以我们声明的投影矩阵是像这样的:
```c++
glm::mat4 projection;
projection = glm::perspective(45.0f, screenWidth / screenHeight, 0.1f, 100.0f);
```
!!! Attention
再重复一遍在glm指定角度的时候要注意。这里我们将参数fov设置为45度但有些GLM的实现是将fov当成弧度在这种情况你需要使用`glm::radians(45.0)`来设置。
既然我们创建了转换矩阵,我们应该将它们传入着色器。首先,让我们在顶点着色器中声明一个单位转换矩阵然后将它乘以顶点坐标:
```c++
#version 330 core
layout (location = 0) in vec3 position;
...
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
// 注意从右向左读
gl_Position = projection * view * model * vec4(position, 1.0f);
...
}
```
我们应该将矩阵传入着色器(这通常在每次渲染的时候即转换矩阵将要改变的时候完成)
```c++
GLint modelLoc = glGetUniformLocation(ourShader.Program, "model");
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
... // 观察矩阵和投影矩阵与之类似
```
现在我们的顶点坐标通过模型、观察和投影矩阵来转换,最后的对象应该是:
- 往后向地板倾斜。
- 离我们有点距离。
- 由透视展示(顶点越远,变得越小)。
让我们检查一下结果是否满足这些要求:
![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
到目前为止我们在二维平面甚至在三维空间中画图所以让我们采取大胆的方式来将我们的二维平面扩展为三维立方体。要渲染一个立方体我们一共需要36个顶点(6个面 x 每个面有2个三角形组成 x 每个三角形有3个顶点)这36个顶点的位置你可以[从这里获取](http://learnopengl.com/code_viewer.php?code=getting-started/cube_vertices)。注意,这一次我们省略了颜色值,因为我们只使用纹理得到最终的颜色值。
为了好玩,我们将让立方体随着时间旋转:
```c++
model = glm::rotate(model, (GLfloat)glfwGetTime() * 50.0f, glm::vec3(0.5f, 1.0f, 0.0f));
```
然后我们使用`glDrawArrays`来画立方体这一次总共有36个顶点。
```c++
glDrawArrays(GL_TRIANGLES, 0, 36);
```
如果一切顺利的话绘制效果将与下面的类似:
<video src="../../img/01/08/coordinate_system_no_depth.mp4" controls="controls"></video>
这有点像一个立方体但又有种说不出的奇怪。立方体的某些本应被遮挡住的面被绘制在了这个立方体的其他面的上面。之所以这样是因为OpenGL是通过画一个一个三角形来画你的立方体的所以它将会覆盖之前已经画在那里的像素。因为这个原因有些三角形会画在其它三角形上面虽然它们本不应该是被覆盖的。
幸运的是OpenGL存储深度信息在Z缓冲(Z-buffer)里面它允许OpenGL决定何时覆盖一个像素何时不覆盖。通过使用Z缓冲我们可以设置OpenGL来进行深度测试。
### Z缓冲
OpenGL存储它的所有深度信息于Z缓冲(Z-buffer)中,也被称为深度缓冲区(Depth Buffer)。GLFW会自动为你生成这样一个缓冲区 (就如它有一个颜色缓冲区来存储输出图像的颜色)。深度存储在每个片段里面(作为片段的z值)当片段想要输出它的颜色时OpenGL会将它的深度值和z缓冲进行比较然后如果当前的片段在其它片段之后它将会被丢弃否则重写。这个过程称为**深度测试(Depth Testing)**并且它是由OpenGL自动完成的。
然而如果我们想要确定OpenGL是否真的执行深度测试首先我们要告诉OpenGL我们想要开启深度测试而这通常是默认关闭的。我们通过`glEnable`函数来开启深度测试。`glEnable`和`glDisable`函数允许我们开启或关闭某一个OpenGL的功能。该功能会一直是开启或关闭的状态直到另一个调用来关闭或开启它。现在我们想开启深度测试就需要开启`GL_DEPTH_TEST`
```c++
glEnable(GL_DEPTH_TEST);
```
既然我们使用了深度测试我们也想要在每次重复渲染之前清除深度缓冲区(否则前一个片段的深度信息仍然保存在缓冲区中)。就像清除颜色缓冲区一样,我们可以通过在`glclear`函数中指定`DEPTH_BUFFER_BIT`位来清除深度缓冲区:
```c++
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
```
我们来重新运行下程序看看OpenGL是否执行了深度测试
<video src="../../img/01/08/coordinate_system_depth.mp4" controls="controls"></video>
就是这样!一个开启了深度测试,各个面都是纹理,并且还在旋转的立方体!如果你的程序有问题可以到[这里](http://learnopengl.com/code_viewer.php?code=getting-started/coordinate_systems_with_depth)下载源码进行比对。
### 更多的立方体!
现在我们想在屏幕上显示10个立方体。每个立方体看起来都是一样的区别在于它们在世界的位置及旋转角度不同。立方体的图形布局已经定义好了所以当渲染更多物体的时候我们不需要改变我们的缓冲数组和属性数组我们唯一需要做的只是改变每个对象的模型矩阵来将立方体转换到世界坐标系中。
首先,让我们为每个立方体定义一个转换向量来指定它在世界空间的位置。我们将要在`glm::vec3`数组中定义10个立方体位置向量。
```c++
glm::vec3 cubePositions[] = {
glm::vec3( 0.0f, 0.0f, 0.0f),
glm::vec3( 2.0f, 5.0f, -15.0f),
glm::vec3(-1.5f, -2.2f, -2.5f),
glm::vec3(-3.8f, -2.0f, -12.3f),
glm::vec3( 2.4f, -0.4f, -3.5f),
glm::vec3(-1.7f, 3.0f, -7.5f),
glm::vec3( 1.3f, -2.0f, -2.5f),
glm::vec3( 1.5f, 2.0f, -2.5f),
glm::vec3( 1.5f, 0.2f, -1.5f),
glm::vec3(-1.3f, 1.0f, -1.5f)
};
```
现在,在循环中,我们调用`glDrawArrays`10次在我们开始渲染之前每次传入一个不同的模型矩阵到顶点着色器中。我们将会创建一个小的循环来通过一个不同的模型矩阵重复渲染我们的对象10次。注意我们也传入了一个旋转参数到每个箱子中
```c++
glBindVertexArray(VAO);
for(GLuint i = 0; i < 10; i++)
{
glm::mat4 model;
model = glm::translate(model, cubePositions[i]);
GLfloat angle = 20.0f * i;
model = glm::rotate(model, angle, glm::vec3(1.0f, 0.3f, 0.5f));
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
glDrawArrays(GL_TRIANGLES, 0, 36);
}
glBindVertexArray(0);
```
这个代码将会每次都更新模型矩阵然后画出新的立方体如此总共重复10次。然后我们应该就能看到一个拥有10个正在奇葩旋转着的立方体的世界。
![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) 。
## 练习
- 对GLM的`projection`函数中的`FoV`和`aspect-ratio`参数进行实验。看能否搞懂它们是如何影响透视平截头体的。
- 将观察矩阵在各个方向上进行平移,来看看场景是如何改变的。注意把观察矩阵当成摄像机对象。
- 只使用模型矩阵每次只让3个箱子旋转(包括第1个)而让剩下的箱子保持静止。[参考解答](http://learnopengl.com/code_viewer.php?code=getting-started/coordinate_systems-exercise3)。