mirror of
https://github.com/LearnOpenGL-CN/LearnOpenGL-CN.git
synced 2025-08-23 04:35:28 +08:00
01-06, 07
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
# 纹理
|
||||
# 纹理(Textures)
|
||||
|
||||
原文 | [Shaders](http://learnopengl.com/#!Getting-started/Textures)
|
||||
---|---
|
||||
@@ -6,25 +6,25 @@
|
||||
翻译 | [Django](http://bullteacher.com/)
|
||||
校对 | Geequlim
|
||||
|
||||
我们已经了解到,可以为每个顶点使用颜色来增加图形的细节,从而创建出有趣的图像。但是,为了获得足够的显示我们就必须有足够多的顶点,才能指定足够多的颜色。这会花费很多额外开销,因为每个模型都会需求更多的顶点和顶点颜色。
|
||||
我们已经了解到,我们可以为每个顶点使用颜色来增加图形的细节,从而创建出有趣的图像。但是,如果想让图形看起来更真实我们就必须有足够多的顶点,从而指定足够多的颜色。这将会产生很多额外开销,因为每个模型都会需求更多的顶点和顶点颜色。
|
||||
|
||||
艺术家和程序员更喜欢使用纹理(texture)。纹理是一个2D图片(也有1D和3D),它用来添加物体的细节;这就像有一张绘有砖块的图片贴到你的3D的房子上,你的房子看起来就像一堵砖墙。因为我们可以在一张图片上插入足够多的细节,这样物体就会拥有很多细节而不用增加额外的顶点。
|
||||
艺术家和程序员更喜欢使用**纹理(Texture)**。纹理是一个2D图片(也有1D和3D),它用来添加物体的细节;这就像有一张绘有砖块的图片贴到你的3D的房子上,你的房子看起来就像一堵砖墙。因为我们可以在一张图片上插入足够多的细节,这样物体就会拥有很多细节而不用增加额外的顶点。
|
||||
|
||||
!!! Important
|
||||
|
||||
除了图像以外,纹理也可以储存数据,这些数据用来发送到着色器上,但是我们会留在其他主题来讨论这个问题。
|
||||
除了图像以外,纹理也可以储存大量的数据,这些数据用来发送到着色器上,但是这不是我们现在的主题。
|
||||
|
||||
下面你会看到前面的那个三角形贴上了一张砖墙图片。
|
||||
下面你会看到之前教程的那个三角形贴上了一张[砖墙](http://learnopengl.com/img/textures/wall.jpg)图片。
|
||||
|
||||

|
||||
|
||||
为了能够把纹理映射到三角形上,我们需要说明三角形的每个顶点各自对应纹理的哪个部分。这样每个顶点就会有一个纹理坐标(texture coordinate),它指明从纹理图像的哪个地方采样(采集像素颜色)。之后在所有的其他的像素上进行像素插值。
|
||||
为了能够把纹理映射到三角形上,我们需要指定三角形的每个顶点各自对应纹理的哪个部分。这样每个顶点就会有一个**纹理坐标(Texture Coordinate)**,它指明从纹理图像的哪个地方采样(采集像素颜色)。之后在所有的其他的片段上进行片段插值(Fragment Interpolation)。
|
||||
|
||||
纹理坐标是x和y轴上0到1之间的范围(我们使用的是2D纹理图片)。使用纹理坐标获取纹理颜色叫做采样(sampling)。纹理坐标起始于(0,0)也就是纹理图片的左下角,终结于纹理图片的右上角(1,1)。下面的图片展示了我们是如何把纹理坐标映射到三角形上的。
|
||||
纹理坐标是x和y轴上0到1之间的范围(注意我们使用的是2D纹理图片)。使用纹理坐标获取纹理颜色叫做**采样(Sampling)**。纹理坐标起始于(0,0)也就是纹理图片的左下角,终结于纹理图片的右上角(1,1)。下面的图片展示了我们是如何把纹理坐标映射到三角形上的。
|
||||
|
||||

|
||||
|
||||
我们为三角形准备了3个纹理坐标点。如上图所示,我们希望三角形的左下角对应纹理的左下角,因此我们把三角左下角的顶点的纹理坐标设置为(0,0);三角形的上顶点对应于图片的中间所以我们把它的纹理坐标设置为(0.5,1.0);同理右下方的顶点设置为(1.0,0)。我们只要传递这三个纹理坐标给顶点着色器就行了,接着片段着色器会为每个像素生成纹理坐标的插值。
|
||||
我们为三角形准备了3个纹理坐标点。如上图所示,我们希望三角形的左下角对应纹理的左下角,因此我们把三角左下角的顶点的纹理坐标设置为(0,0);三角形的上顶点对应于图片的中间所以我们把它的纹理坐标设置为(0.5,1.0);同理右下方的顶点设置为(1.0,0)。我们只要传递这三个纹理坐标给顶点着色器就行了,接着片段着色器会为每个片段生成纹理坐标的插值。
|
||||
|
||||
纹理坐标看起来就像这样:
|
||||
|
||||
@@ -38,22 +38,22 @@ GLfloat texCoords[] = {
|
||||
|
||||
纹理采样有几种不同的插值方式。我们需要自己告诉OpenGL在纹理中采用哪种采样方式。
|
||||
|
||||
### 纹理环绕方式(Texture Wrapping)
|
||||
### 纹理环绕方式(Texture Wrapping)
|
||||
|
||||
纹理坐标通常的范围是从(0, 0)到(1, 1),如果我们把纹理坐标设置为范围以外会发生什么?OpenGL默认的行为是重复这个纹理图像(我们简单地忽略浮点纹理坐标的整数部分),但OpenGL提供了更多的选择:
|
||||
纹理坐标通常的范围是从(0, 0)到(1, 1),如果我们把纹理坐标设置为范围以外会发生什么?OpenGL默认的行为是重复这个纹理图像(我们简单地忽略浮点纹理坐标的整数部分),但OpenGL提供了更多的选择:
|
||||
|
||||
环绕方式 | 描述
|
||||
---|---
|
||||
GL_REPEAT | 纹理的默认行为,重复纹理图像
|
||||
GL_MIRRORED_REPEAET |和GL_REPEAT一样,除了重复的图片是镜像放置的
|
||||
GL_MIRRORED_REPEAET |和`GL_REPEAT`一样,除了重复的图片是镜像放置的
|
||||
GL_CLAMP_TO_EDGE | 纹理坐标会在0到1之间,超出的部分会重复纹理坐标的边缘,就是边缘被拉伸
|
||||
GL_CLAMP_TO_BORDER | 超出的部分是用户指定的边缘的颜色
|
||||
|
||||
当纹理坐标超出默认范围时,每个选项都有一个不同的视觉效果输出。我们来看看这些纹理图像的例子:
|
||||
当纹理坐标超出默认范围时,每个值都有不同的视觉效果输出。我们来看看这些纹理图像的例子:
|
||||
|
||||

|
||||
|
||||
前面提到的选项都可以使用`glTexParameter`函数单独设置每个坐标轴`s`、`t`(如果是使用3D纹理那么也有一个`r`)它们和`x`、`y`(`z`)是相等的:
|
||||
前面提到的选项都可以使用`glTexParameter`函数单独设置每个坐标轴`s`、`t`(如果是使用3D纹理那么还有一个`r`)它们和`x`、`y`(`z`)是相等的:
|
||||
|
||||
`glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);`
|
||||
`glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);`
|
||||
@@ -70,61 +70,64 @@ GL_CLAMP_TO_BORDER | 超出的部分是用户指定的边缘的颜色
|
||||
float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
|
||||
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
|
||||
```
|
||||
### 纹理过滤(Texture Filtering)
|
||||
|
||||
纹理坐标不依赖于解析度,它可以是任何浮点数值,这样OpenGL需要描述出哪个纹理像素对应哪个纹理坐标(译注:texture pixel也叫texel,你可以想象你打开一张.jpg格式图片不断放大你会发现它是由无数像素点组成的,这个点就是纹理像素;注意不要和纹理坐标搞混,纹理坐标是你给模型顶点设置的那个数组,OpenGL以这个顶点的纹理坐标数据去查找纹理图像上的像素,然后进行采样提取纹理像素的颜色)。当你有一个很大的物体但是纹理解析度很低的时候这就变得很重要了。你可能已经猜到了,OpenGL也有一个叫做纹理过滤的选项。有多种不同的选项可用,但是现在我们只讨论最重要的两种:`GL_NEAREST`和`GL_LINEAR`。
|
||||
### 纹理过滤(Texture Filtering)
|
||||
|
||||
**GL_NEAREST(邻近过滤)** 是一种OpenGL默认的纹理过滤方式。当设置为`GL_NEAREST`的时候,OpenGL选择最接近纹理坐标中心点的那个像素。下图你会看到四个像素,加号代表纹理坐标。左上角的纹理像素是距离纹理坐标最近的那个,这样它就会选择这个作为采样颜色:
|
||||
纹理坐标不依赖于解析度,它可以是任何浮点数值,这样OpenGL需要描述出哪个纹理像素对应哪个纹理坐标(Texture Pixel,也叫Texel,[译注1])。当你有一个很大的物体但是纹理解析度很低的时候这就变得很重要了。你可能已经猜到了,OpenGL也有一个叫做纹理过滤的选项。有多种不同的选项可用,但是现在我们只讨论最重要的两种:`GL_NEAREST`和`GL_LINEAR`。
|
||||
|
||||
[译注1]: http://learnopengl-cn.readthedocs.org/zh/latest/01%20Getting%20started/06%20Textures/ "Texture Pixel也叫Texel,你可以想象你打开一张.jpg格式图片不断放大你会发现它是由无数像素点组成的,这个点就是纹理像素;注意不要和纹理坐标搞混,纹理坐标是你给模型顶点设置的那个数组,OpenGL以这个顶点的纹理坐标数据去查找纹理图像上的像素,然后进行采样提取纹理像素的颜色"
|
||||
|
||||
**GL_NEAREST(Nearest Neighbor Filtering,邻近过滤)** 是一种OpenGL默认的纹理过滤方式。当设置为`GL_NEAREST`的时候,OpenGL选择最接近纹理坐标中心点的那个像素。下图你会看到四个像素,加号代表纹理坐标。左上角的纹理像素是距离纹理坐标最近的那个,这样它就会选择这个作为采样颜色:
|
||||
|
||||

|
||||
|
||||
**GL_LINEAR(线性过滤)** 它会从纹理坐标的临近纹理像素进行计算,返回一个多个纹理像素的近似值。一个纹理像素距离纹理坐标越近,那么这个纹理像素对最终的采样颜色的影响越大。下面你会看到临近像素返回的混合颜色:
|
||||
**GL_LINEAR((Bi)linear Filtering,线性过滤)** 它会从纹理坐标的临近纹理像素进行计算,返回一个多个纹理像素的近似值。一个纹理像素距离纹理坐标越近,那么这个纹理像素对最终的采样颜色的影响越大。下面你会看到临近像素返回的混合颜色:
|
||||
|
||||

|
||||
|
||||
不同的纹理过滤方式有怎样的视觉效果呢?让我们看看当在一个很大的物体上应用一张地解析度的纹理会发生什么吧(纹理被放大了,纹理像素也能看到):
|
||||
不同的纹理过滤方式有怎样的视觉效果呢?让我们看看当在一个很大的物体上应用一张地解析度的纹理会发生什么吧(纹理被放大了,纹理像素也能看到):
|
||||
|
||||

|
||||
|
||||
如上面两张图片所示,`GL_NEAREST`返回了格子一样的样式,我们能够清晰看到纹理由像素组成,而`GL_LINEAR`产生出更平滑的样式,看不出纹理像素。`GL_LINEAR`是一种更真实的输出,但有些开发者更喜欢8-bit风格,所以他们还是用`GL_NEAREST`选项。
|
||||
|
||||
纹理过滤可以为放大和缩小设置不同的选项,这样你可以在纹理被缩小的时候使用最临近过滤,被放大时使用线性过滤。我们必须通过`glTexParameter`为放大和缩小指定过滤方式。这段代码看起来和纹理环绕方式(Texture Wrapping)的设置相似:
|
||||
纹理过滤可以为放大和缩小设置不同的选项,这样你可以在纹理被缩小的时候使用最临近过滤,被放大时使用线性过滤。我们必须通过`glTexParameter`为放大和缩小指定过滤方式。这段代码看起来和纹理环绕方式(Texture Wrapping)的设置相似:
|
||||
|
||||
```c++
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
```
|
||||
|
||||
### Mipmap
|
||||
#### 多级渐远纹理(Mipmaps)
|
||||
|
||||
想象一下,如果我们在一个有着上千物体的大房间,每个物体上都有纹理。距离观察者远的与距离近的物体的纹理的解析度是相同的。由于远处的物体可能只产生很少的片段,OpenGL从高解析度纹理中为这些片段获取正确的颜色值就很困难。这是因为它不得不拾为一个纹理跨度很大的片段取纹理颜色。在小物体上这会产生人工感,更不用说在小物体上使用高解析度纹理浪费内存的问题了。
|
||||
|
||||
OpenGL使用一种叫做 **Mipmap** 的概念解决这个问题,大概来说就是一系列纹理,每个后面的一个纹理是前一个的二分之一。Mipmap背后的思想很简单:距离观察者更远的距离的一段确定的阈值,OpenGL会把最适合这个距离的物体的不同的Mipmap纹理应用其上。由于距离远,解析度不高也不会被使用者注意到。同时,Mipmap另一加分之处是,执行效率不错。让我们近距离看一看Mipmap纹理:
|
||||
OpenGL使用一种叫做 **多级渐远纹理(Mipmap)** 的概念解决这个问题,大概来说就是一系列纹理,每个后面的一个纹理是前一个的二分之一。多级渐远纹理背后的思想很简单:距离观察者更远的距离的一段确定的阈值,OpenGL会把最适合这个距离的物体的不同的多级渐远纹理纹理应用其上。由于距离远,解析度不高也不会被使用者注意到。同时,多级渐远纹理另一加分之处是,执行效率不错。让我们近距离看一看多级渐远纹理纹理:
|
||||
|
||||

|
||||
|
||||
手工为每个纹理图像创建一系列mipmap纹理很麻烦,幸好OpenGL有一个`glGenerateMipmaps`函数,它可以在我们创建完一个纹理后帮我们做所有的mipmap创建工作。后面的纹理教程中你会看到如何使用它。
|
||||
手工为每个纹理图像创建一系列多级渐远纹理很麻烦,幸好OpenGL有一个`glGenerateMipmaps`函数,它可以在我们创建完一个纹理后帮我们做所有的多级渐远纹理创建工作。后面的纹理教程中你会看到如何使用它。
|
||||
|
||||
OpenGL渲染的时候,两个不同级别的mipmap之间会产生不真实感的生硬的边界。就像普通的纹理过滤一样,也可以在两个不同mipmap级别之间使用`NEAREST`和`LINEAR`过滤。指定不同mipmap级别之间的过滤方式可以使用下面四种选项代替原来的过滤方式:
|
||||
OpenGL渲染的时候,两个不同级别的多级渐远纹理之间会产生不真实感的生硬的边界。就像普通的纹理过滤一样,也可以在两个不同多级渐远纹理级别之间使用`NEAREST`和`LINEAR`过滤。指定不同多级渐远纹理级别之间的过滤方式可以使用下面四种选项代替原来的过滤方式:
|
||||
|
||||
|
||||
过滤方式 | 描述
|
||||
---|---
|
||||
GL_NEAREST_MIPMAP_NEAREST | 接收最近的mipmap来匹配像素大小,并使用最临近插值进行纹理采样
|
||||
GL_LINEAR_MIPMAP_NEAREST | 接收最近的mipmap级别,并使用线性插值采样
|
||||
GL_NEAREST_MIPMAP_LINEAR | 在两个mipmap之间进行线性插值,通过最邻近插值采样
|
||||
GL_LINEAR_MIPMAP_LINEAR | 在两个相邻的mipmap进行线性插值,并通过线性插值进行采样
|
||||
GL_NEAREST_MIPMAP_NEAREST | 接收最近的多级渐远纹理来匹配像素大小,并使用最临近插值进行纹理采样
|
||||
GL_LINEAR_MIPMAP_NEAREST | 接收最近的多级渐远纹理级别,并使用线性插值采样
|
||||
GL_NEAREST_MIPMAP_LINEAR | 在两个多级渐远纹理之间进行线性插值,通过最邻近插值采样
|
||||
GL_LINEAR_MIPMAP_LINEAR | 在两个相邻的多级渐远纹理进行线性插值,并通过线性插值进行采样
|
||||
|
||||
就像纹理过滤一样,前面提到的四种方法也可以使用glTexParameteri设置过滤方式:
|
||||
就像纹理过滤一样,前面提到的四种方法也可以使用`glTexParameteri`设置过滤方式:
|
||||
|
||||
```c++
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
```
|
||||
|
||||
常见的错误是,为mipmap过滤选项设置放大过滤。这样没有任何效果,因为mipmap主要使用在纹理被缩小的情况下的:纹理放大不会使用mipmap,为mipmap设置放大过滤选项会产生一个`GL_INVALID_ENUM`错误。
|
||||
常见的错误是,为多级渐远纹理过滤选项设置放大过滤。这样没有任何效果,因为多级渐远纹理主要使用在纹理被缩小的情况下的:纹理放大不会使用多级渐远纹理,为多级渐远纹理设置放大过滤选项会产生一个`GL_INVALID_ENUM`错误。
|
||||
|
||||
### 加载和创建纹理
|
||||
## 加载和创建纹理
|
||||
|
||||
使用纹理之前要做的第一件事是把它们加载到应用中。纹理图像可能储存为各种各样的格式,每种都有自己的数据结构和排列,所以我们如何才能把这些图像加载到应用中呢?一个解决方案是写一个我们自己的某种图像格式加载器比如.PNG,用它来把图像转化为byte序列。写自己的图像加载器虽然不难,但是仍然挺烦人的,而且如果要支持更多文件格式呢?你就不得不为每种你希望支持的格式写加载器了。
|
||||
|
||||
@@ -132,25 +135,27 @@ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
|
||||
### SOIL
|
||||
|
||||
SOIL代表Simple OpenGL Image Library(简易OpenGL图像库)的缩写,它支持大多数流行的图像格式,使用起来也很简单,可以从他们的主页下载。像大多数其他库一样,你必须自己生成.lib。可以使用/projects文件夹里的solution文件(不用担心他们的visual studio版本太老,你可以把它们转变为新的版本;这总是可行的。译注:用VS2010的时候,你要用VC8而不是VC9的solution,想必更高版本的情况亦是如此),你也可以使用CMake自己生成。还要添加src文件夹到你的includes文件夹;不要忘记添加SOIL.lib到你的连接器选项,添加#include 到你的代码上部。
|
||||
SOIL是Simple OpenGL Image Library(简易OpenGL图像库)的缩写,它支持大多数流行的图像格式,使用起来也很简单,你可以从他们的主页下载。像大多数其他库一样,你必须自己生成**.lib**。你可以使用**/projects**文件夹里的解决方案(Solution)文件之一(不用担心他们的Visual Studio版本太老,你可以把它们转变为新的版本;这总是可行的。译注:用VS2010的时候,你要用VC8而不是VC9的解决方案,想必更高版本的情况亦是如此),你也可以使用CMake自己生成。你还要添加**src**文件夹里面的文件到你的**includes**文件夹;对了,不要忘记添加**SOIL.lib**到你的连接器选项,并在你代码文件的开头加上`#include <SOIL.h>`。
|
||||
|
||||
下面的纹理部分,我们会使用一张木箱的图片。使用SOIL加载图片,我们使用它的`SOIL_load_image`函数:
|
||||
下面的纹理部分,我们会使用一张木箱的图片。使用SOIL加载图片,我们会使用它的`SOIL_load_image`函数:
|
||||
|
||||
```c++
|
||||
int width, height;
|
||||
unsigned char* image = SOIL_load_image(“container..jpg”, &width, &height, 0, SOIL_LOAD_RGB);
|
||||
```
|
||||
|
||||
函数首先需要输入图片文件的路径。然后需要两个int指针作为第二个和第三个参数,SOIL会返回图片的宽度和高度到其中。之后,我们需要图片的宽度和高度来生成纹理。第四个参数指定图片的通道(channel)数量,但是这里我们只需留`0`。最后一个参数告诉SOIL如何来加载图片:我们只对图片的RGB感兴趣。结果储存为一个大的char/byte数组。
|
||||
函数首先需要输入图片文件的路径。然后需要两个int指针作为第二个和第三个参数,SOIL会返回图片的宽度和高度到其中。之后,我们需要图片的宽度和高度来生成纹理。第四个参数指定图片的通道(Channel)数量,但是这里我们只需留`0`。最后一个参数告诉SOIL如何来加载图片:我们只对图片的RGB感兴趣。结果储存为一个大的char/byte数组。
|
||||
|
||||
生成一个纹理和之前生成的OpenGL对象一样,纹理也是使用ID引用。
|
||||
### 生成纹理
|
||||
|
||||
和之前生成的OpenGL对象一样,纹理也是使用ID引用的。
|
||||
|
||||
```c++
|
||||
GLuint texture;
|
||||
glGenTextures(1, &texture);
|
||||
```
|
||||
|
||||
`glGenTextures`函数首先需要输入纹理生成的数量,然后把它们储存在第二个参数的`GLuint`数组中(我们的例子里只有一个`GLuint`),就像其他对象一样,我们需要绑定它,所以下面的纹理命令会配置当前绑定的纹理:
|
||||
`glGenTextures`函数首先需要输入纹理生成的数量,然后把它们储存在第二个参数的`GLuint`数组中(我们的例子里只有一个`GLuint`),就像其他对象一样,我们需要绑定它,所以下面的纹理命令会配置当前绑定的纹理:
|
||||
|
||||
```c++
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
@@ -165,17 +170,17 @@ glGenerateMipmap(GL_TEXTURE_2D);
|
||||
|
||||
函数很长,参数也不少,所以我们一个一个地讲解:
|
||||
|
||||
* 第一个参数指定纹理目标(环境);设置为`GL_TEXTURE_2D`意味着会生成与当前绑定的纹理对象在同一个目标(target)上的纹理(任何绑定到`GL_TEXTURE_1D`和`GL_TEXTURE_3D`的纹理不会受到影响)。
|
||||
* 第二个参数为我们打算创建的纹理指定mipmap层级,如果你希望为每个mimap层级单独手工设置的话。这里我们填0基本级。
|
||||
* 第三个参数告诉OpenGL,我们希望把纹理储存为何种格式。我们的图像只有RGB值,因此我们把纹理储存为`GL_RGB`值。
|
||||
* 第四个和第五个参数设置最终的纹理的宽度和高度。我们加载图像的时候提前储存它们这样我们就能使用相应变量了。
|
||||
下个参数应该一直被设为`0`(遗留问题)。
|
||||
* 第七第八个参数定义了源图的格式和数据类型。我们使用RGB值加载这个图像,并把它们储存在char(byte),我们将会传入相应值。
|
||||
* 最后一个参数是真实的图像数据。
|
||||
- 第一个参数指定纹理目标(环境);设置为`GL_TEXTURE_2D`意味着会生成与当前绑定的纹理对象在同一个目标(Target)上的纹理(任何绑定到`GL_TEXTURE_1D`和`GL_TEXTURE_3D`的纹理不会受到影响)。
|
||||
- 第二个参数为我们打算创建的纹理指定多级渐远纹理的层级,如果你希望单独手工设置每个多级渐远纹理的层级的话。这里我们填0基本级。
|
||||
- 第三个参数告诉OpenGL,我们希望把纹理储存为何种格式。我们的图像只有RGB值,因此我们把纹理储存为`GL_RGB`值。
|
||||
- 第四个和第五个参数设置最终的纹理的宽度和高度。我们加载图像的时候提前储存它们这样我们就能使用相应变量了。
|
||||
下个参数应该一直被设为`0`(遗留问题)。
|
||||
- 第七第八个参数定义了源图的格式和数据类型。我们使用RGB值加载这个图像,并把它们储存在char(byte),我们将会传入相应值。
|
||||
- 最后一个参数是真实的图像数据。
|
||||
|
||||
当调用`glTexImage2D`,当前绑定的纹理对象就会被附加上纹理图像。然而,当前只有基本级别(base-level)纹理图像加载了,如果要使用mipmap,我们必须手工设置不同的图像(通过不断把第二个参数增加的方式)或者,在生成纹理之后调用`glGenerateMipmap`。这会为当前绑定的纹理自动生成所有需要的mipmap。
|
||||
当调用`glTexImage2D`,当前绑定的纹理对象就会被附加上纹理图像。然而,当前只有基本级别(Base-level)纹理图像加载了,如果要使用多级渐远纹理,我们必须手工设置不同的图像(通过不断把第二个参数增加的方式)或者,在生成纹理之后调用`glGenerateMipmap`。这会为当前绑定的纹理自动生成所有需要的多级渐远纹理。
|
||||
|
||||
生成了纹理和相应的mipmap后,解绑纹理对象、释放图像的内存很重要。
|
||||
生成了纹理和相应的多级渐远纹理后,解绑纹理对象、释放图像的内存很重要。
|
||||
|
||||
```c++
|
||||
SOIL_free_image_data(image);
|
||||
@@ -222,7 +227,7 @@ glVertexAttribPointer(2, 2, GL_FLOAT,GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)(6
|
||||
glEnableVertexAttribArray(2);
|
||||
```
|
||||
|
||||
注意,我们必须修正前面两个顶点属性的步长参数为`8*sizeof(GLfloat)`。
|
||||
注意,我们必须修正前面两个顶点属性的步长参数为`8 * sizeof(GLfloat)`。
|
||||
|
||||
接着我们需要让顶点着色器把纹理坐标作为一个顶点属性,把坐标传给片段着色器:
|
||||
|
||||
@@ -240,9 +245,9 @@ void main()
|
||||
TexCoord = texCoord;
|
||||
}
|
||||
```
|
||||
片段色器应该把输出变量`TexCoord`作为输入变量。
|
||||
片段着色器应该把输出变量`TexCoord`作为输入变量。
|
||||
|
||||
片段着色器应该也获取纹理对象,但是我们怎样把纹理对象传给片段着色器?GLSL有一个内建数据类型,供纹理对象使用,叫做采样器(sampler),它以纹理类型作为后缀,比如sampler1D、sampler3D,在我们的例子中它是sampler2D。我们可以简单的声明一个uniform sampler2D把一个纹理传到片段着色器中,稍后我们把我们的纹理赋值给这个uniform。
|
||||
片段着色器应该也获取纹理对象,但是我们怎样把纹理对象传给片段着色器?GLSL有一个内建数据类型,供纹理对象使用,叫做采样器(Sampler),它以纹理类型作为后缀,比如`sampler1D`、`sampler3D`,在我们的例子中它是`sampler2D`。我们可以简单的声明一个`uniform sampler2D`把一个纹理传到片段着色器中,稍后我们把我们的纹理赋值给这个uniform。
|
||||
|
||||
```c++
|
||||
#version 330 core
|
||||
@@ -256,7 +261,7 @@ void main()
|
||||
}
|
||||
```
|
||||
|
||||
我们使用GLSL的内建texture函数来采样纹理的颜色,它第一个参数是纹理采样器,第二个参数是相应的纹理坐标。texture函数使用前面设置的纹理参数对相应颜色值进行采样。这个片段着色器的输出就是纹理的(插值)纹理坐标上的(过滤)颜色。
|
||||
我们使用GLSL的内建`texture`函数来采样纹理的颜色,它第一个参数是纹理采样器,第二个参数是相应的纹理坐标。`texture`函数使用前面设置的纹理参数对相应颜色值进行采样。这个片段着色器的输出就是纹理的(插值)纹理坐标上的(过滤)颜色。
|
||||
|
||||
现在要做的就是在调用`glDrawElements`之前绑定纹理,它会自动把纹理赋值给片段着色器的采样器:
|
||||
|
||||
@@ -267,11 +272,11 @@ glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
|
||||
glBindVertexArray(0);
|
||||
```
|
||||
|
||||
如果你每件事都做对了,你会看到下面的图像:
|
||||
如果你跟着这个教程正确的做完了,你会看到下面的图像:
|
||||
|
||||

|
||||
|
||||
如果你的矩形是全黑或全白的你可能在哪儿做错了什么。检查你的着色器日志,尝试对比[应用源码](http://learnopengl.com/code_viewer.php?code=getting-started/textures)。
|
||||
如果你的矩形是全黑或全白的你可能在哪儿做错了什么。检查你的着色器日志,或者尝试对比一下[源码](http://learnopengl.com/code_viewer.php?code=getting-started/textures)。
|
||||
|
||||
我们还可以把纹理颜色和顶点颜色混合,来获得有趣的效果。我们简单的把纹理颜色与顶点颜色在片段着色器中相乘来混合二者的颜色:
|
||||
|
||||
@@ -285,7 +290,7 @@ color = texture(ourTexture, TexCoord) * vec4(ourColor, 1.0f);
|
||||
|
||||
这个箱子看起来有点70年代迪斯科风格。
|
||||
|
||||
### 纹理单元
|
||||
### 纹理单元(Texture Units)
|
||||
|
||||
你可能感到奇怪为什么`sampler2D`是个uniform变量,你却不用`glUniform`给它赋值,使用`glUniform1i`我们就可以给纹理采样器确定一个位置,这样的话我们能够一次在一个片段着色器中设置多纹理。一个纹理的位置通常称为一个纹理单元。一个纹理的默认纹理单元是0,它是默认激活的纹理单元,所以教程前面部分我们不用给它确定一个位置。
|
||||
|
||||
@@ -315,11 +320,11 @@ void main()
|
||||
}
|
||||
```
|
||||
|
||||
最终输出颜色现在结合了两个纹理查找。GLSL的内建mix函数需要两个参数将根据第三个参数为前两者作为输入,并在之间进行线性插值。如果第三个值是0.0,它返回第一个输入;如果是1.0,就返回第二个输入值。0.2返回80%的第一个输入颜色和20%的第二个输入颜色,返回两个纹理的混合。
|
||||
最终输出颜色现在结合了两个纹理查找。GLSL的内建`mix`函数需要两个参数将根据第三个参数为前两者作为输入,并在之间进行线性插值。如果第三个值是0.0,它返回第一个输入;如果是1.0,就返回第二个输入值。0.2返回80%的第一个输入颜色和20%的第二个输入颜色,返回两个纹理的混合。
|
||||
|
||||
我们现在需要载入和创建另一个纹理;我们应该对这些步骤感到熟悉了。确保创建另一个纹理对象,载入图片,使用`glTexImage2D`生成最终纹理。对于第二个纹理我们使用一张你学习OpenGL时的表情图片。
|
||||
|
||||
为了使用第二个纹理(也包括第一个),我们必须改变一点渲染流程,先绑定两个纹理到相应的纹理单元,然后定义哪个uniform采样器对应哪个纹理单元:
|
||||
为了使用第二个纹理(也包括第一个),我们必须改变一点渲染流程,先绑定两个纹理到相应的纹理单元,然后定义哪个uniform采样器对应哪个纹理单元:
|
||||
|
||||
```c++
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
@@ -342,7 +347,7 @@ glBindVertexArray(0);
|
||||
|
||||
所以修复我们的小问题,有两个选择:
|
||||
|
||||
1. 我们切换顶点数据的纹理坐标,翻转`y`值(用1减去y坐标)。
|
||||
1. 我们切换顶点数据的纹理坐标,翻转`y`值(用1减去y坐标)。
|
||||
2. 我们可以编辑顶点着色器来翻转`y`坐标,自动替换`TexCoord`赋值:`TexCoord = vec2(texCoord.x, 1 – texCoord.y);`
|
||||
|
||||
!!! Attention
|
||||
@@ -355,12 +360,9 @@ glBindVertexArray(0);
|
||||
|
||||
### 练习
|
||||
|
||||
纹理更熟练地使用纹理,建议在继续下面的学习之间做完这些练习:
|
||||
为了更熟练地使用纹理,建议在继续之后的学习之前做完这些练习:
|
||||
|
||||
使用片段着色器仅对笑脸图案进行翻转,[解决方案](http://learnopengl.com/code_viewer.php?code=getting-started/textures-exercise1)
|
||||
|
||||
尝试用不同的纹理环绕方式并将纹理坐标的范围从`0.0f`到`2.0f`。在木箱子上放4个笑脸,并观察它的边缘:[解决方案](http://learnopengl.com/code_viewer.php?code=getting-started/textures-exercise2),[结果](http://learnopengl.com/img/getting-started/textures_exercise2.png)。
|
||||
|
||||
尝试在矩形范围内只显示纹理图的中间一部分,并通过修改纹理坐标来设置显示效果。尝试使用`GL_NEAREST`的纹理过滤方式让图像显示得更清晰:[参考答案](http://learnopengl.com/code_viewer.php?code=getting-started/textures-exercise3)
|
||||
|
||||
使用uniform变量作为`mix`函数的第三个参数来改变两个纹理清晰可见,使用向上和向下键来改变笑脸的多少和可见:[解决方案](http://learnopengl.com/code_viewer.php?code=getting-started/textures-exercise4),[片段着色器](http://learnopengl.com/code_viewer.php?code=getting-started/textures-exercise4_fragment)。
|
||||
- 使用片段着色器**仅**对笑脸图案进行翻转,[参考解答](http://learnopengl.com/code_viewer.php?code=getting-started/textures-exercise1)
|
||||
- 尝试用不同的纹理环绕方式,并将纹理坐标的范围设定为从`0.0f`到`2.0f`而不是原来的`0.0f`到`1.0f`,在木箱子的角落放置4个笑脸:[参考解答](http://learnopengl.com/code_viewer.php?code=getting-started/textures-exercise2),[结果](http://learnopengl.com/img/getting-started/textures_exercise2.png)。记得一定要试试其他的环绕方式。
|
||||
- 尝试在矩形范围内只显示纹理图的中间一部分,并通过修改纹理坐标来设置显示效果。尝试使用`GL_NEAREST`的纹理过滤方式让图像显示得更清晰:[参考解答](http://learnopengl.com/code_viewer.php?code=getting-started/textures-exercise3)
|
||||
- 使用一个uniform变量作为`mix`函数的第三个参数来改变两个纹理可见度,使用上和下键来改变容器的大小和笑脸是否可见:[参考解答](http://learnopengl.com/code_viewer.php?code=getting-started/textures-exercise4),[片段着色器](http://learnopengl.com/code_viewer.php?code=getting-started/textures-exercise4_fragment)。
|
||||
|
@@ -1,18 +1,24 @@
|
||||
本文作者JoeyDeVries,由[Django](http://bullteacher.com/8-transformations.html)翻译自[http://learnopengl.com](http://learnopengl.com/#!Getting-started/Transformations)
|
||||
|
||||
# 变换(Transformations)
|
||||
|
||||
我们现在已经知道了如何创建一个物体,为它们上色,使用纹理给它们一些细节表现,但是它们仍然还是不够有趣,因为它们都是静态的物体。我们可以尝试通过在每一帧中重新配置它们的缓冲,移动它们的顶点来使它们动起来,但这样就复杂了,而且会耗费处理能力。使用(多个)矩阵(matrix)对象可以更好的变换(transform)一个物体。这并不意味着我们会去讨论武术和数字虚拟世界(译注:Matrix是电影「黑客帝国」的英文名,电影中人类生活在数字虚拟世界,主角会武术)。
|
||||
// TODO 校对过程中有对原来的翻译大段大段重写,变动比较大(特别前半部分),可能还需要重新校对一遍
|
||||
|
||||
矩阵是非常强大的数学概念,开始看起来可怕,但是当你了解它们后,它们会非常有用。讨论矩阵之前,我们需要先了解一些数学,对于一些愿意多了解一些的读者我会附加一些资源给你们之后阅读。
|
||||
原文 | [Transformations](http://learnopengl.com/#!Getting-started/Transformations)
|
||||
---|---
|
||||
作者 | JoeyDeVries
|
||||
翻译 | Django
|
||||
校对 | Meow J
|
||||
|
||||
然而, 要完全理解变换,我们首先要在讨论矩阵之前了解一点向量(vector)。本章的目标是给你一个后面内容需要的基本的数学背景知识。如果这个主题很难,尝试尽量理解它们,当你需要它们的时候回过头来复习这些概念。
|
||||
尽管我们现在已经知道了如何创建一个物体、着色、加入纹理从而给它们一些细节的表现,但是它们仍然还是不够有趣,因为它们都还是静态的物体。我们可以尝试着在每一帧改变物体的顶点并且重设缓冲区从而使他们移动,但这太繁琐了,而且会消耗很多的处理时间。然而,我们现在有一个更好的解决方案,使用(多个)矩阵(Matrix)对象可以更好的变换(Transform)一个物体。当然,这并不是说我们会去讨论武术和数字虚拟世界(译注:Matrix同样也是电影「黑客帝国」的英文名,电影中人类生活在数字虚拟世界,主角会武术)。
|
||||
|
||||
# 向量
|
||||
**矩阵**(Matrix)是一种非常有用的数学工具,尽管听起来可能有些吓人,不过一旦你理解了它们后,它们会非常有用。在讨论矩阵的过程中,我们需要使用到一些数学知识。对于一些愿意多了解这些知识的读者,我会附加一些资源给你们阅读。
|
||||
|
||||
向量最近本的定义是它是一个方向。向量有一个方向和大小(也叫做强度或长度)。你可以把向量想成一个藏宝图上的方向:“向左走10步,向北走3步,然后向右走5步”;这里的“左”是方向,“10步”是向量的长度。藏宝图上的方向包含3个向量。向量可以有任何维度,但是我们通常只是用2至4维。如果一个向量有2个维度,它表示一个平面的方向(想象2D图像),当它有3个维度的时候它可以表达一个3D世界的方向。
|
||||
为了深入了解变换,我们首先要在讨论矩阵之前了解一点向量(Vector)。这一节的目标是让你拥有将来需要的最基础的数学背景知识. 如果你发现这节十分困难,尽量尝试去理解它们,当你以后需要它们的时候回过头来复习这些概念。
|
||||
|
||||
下面你会看到3个向量,每个向量在2D图像中用一个箭头(x, y)表示。因为在2D中表示向量很容易,你可以想象2D向量有一个z坐标0,这样它就是一个3D向量了。由于向量表示的是方向,起始于何处并不会改变它的值。下图我们可以看到向量和是相等的,尽管他们的起始点不同:
|
||||
## 向量(Vector)
|
||||
|
||||
向量最最基本的定义就是一个方向。或者更正式的说,向量有一个方向(Direction)和大小(Magnitude,也叫做强度或长度)。你可以把向量想成一个藏宝图上的指示:“向左走10步,向北走3步,然后向右走5步”;这里的“左”是方向,“10步”是向量的长度。藏宝图上的方向包含3个向量。向量可以在任意**维度**(Dimension)上,但是我们通常只使用2至4维。如果一个向量有2个维度,它表示一个平面的方向(想象一下2D的图像),当它有3个维度的时候它可以表达一个3D世界的方向。
|
||||
|
||||
下面你会看到3个向量,每个向量在图像中都用一个箭头(x, y)表示。我们在2D图片中展示这些向量,因为这样子会更直观. 你仍然可以把这些2D向量当做z坐标为0的3D向量。由于向量表示的是方向,起始于何处**并不会**改变它的值。下图我们可以看到向量和是相等的,尽管他们的起始点不同:
|
||||
|
||||

|
||||
|
||||
@@ -20,27 +26,27 @@
|
||||
|
||||

|
||||
|
||||
由于向量是一个方向,因此此有时候很难把它们显示为一个位置。我们简单的把方向的起点设置为(0, 0, 0)然后指向某个方向,它的终点就是一个位置向量(position vector)了(你也可以把起点设置为其他的点,然后说:这个向量从这个点起始指向另一个点)。位置向量(3, 5)的起点是(0, 0)指向(3, 5)。我们可以这样描述2D和3D空间中的方向和位置。
|
||||
由于向量是一个方向,所以有些时候会很难形象地将它们用位置(Position)表示出来。我们通常设定这个方向的原点为(0,0,0),然后指向对应坐标的点,使其变为位置向量(Position Vector)来表示(你也可以把起点设置为其他的点,然后说:这个向量从这个点起始指向另一个点)。位置向量(3, 5)的在图像中起点是(0, 0),指向(3, 5)。我们可以使用向量在2D或3D空间中表示方向**与**位置.
|
||||
|
||||
和普通数字一样,我们也可以用向量进行多种操作(其中一些你已经看到了)。
|
||||
和普通数字一样,我们也可以用向量进行多种运算(其中一些你可能已经知道了)。
|
||||
|
||||
## 向量和标量运算
|
||||
### 向量和标量运算
|
||||
|
||||
标量只是一个数字(如果你想用向量表示,那么这个向量只能有一个元素)。当把一个向量加减乘除一个标量,我们可以简单的把向量的每个元素分别加减乘除标量。就像这样:
|
||||
**标量(Scalar)**只是一个数字(或者说是仅有一个分量的矢量)。当把一个向量加/减/乘/除一个标量,我们可以简单的把向量的每个分量分别进行该运算。对于加法来说会像这样:
|
||||
|
||||

|
||||
|
||||
+可以是+和-,·,÷,其中·是乘号。记住,-和÷的逆序操作不存在。
|
||||
其中的+可以是+,-,·或÷,其中·是乘号。注意-和÷运算时不能颠倒,颠倒的运算是没有定义的(标量-/÷矢量)
|
||||
|
||||
## 向量取反
|
||||
### 向量取反
|
||||
|
||||
向量的相反数是一个有着相反方向的向量。一个指向东北的向量的相反数是指向西南的向量。向量取相反数是在向量的每个元素前加上一个减号(你也可以用一个标量-1乘以这个向量来表示):
|
||||
对一个向量取反会将其方向逆转。一个指向东北的向量的相反数是指向西南的向量。我们在一个向量的每个分量前加负号从而实现取反(或者说用-1数乘该向量):
|
||||
|
||||

|
||||
|
||||
## 向量加减
|
||||
### 向量加减
|
||||
|
||||
两个向量相加被定义为元素逐个相加,一个向量的元素与另一个向量相同的元素相加,就像这样:
|
||||
向量的加法可以被定义为是分量的相加,即将一个向量中的每一个分量加上另一个向量的对应分量:
|
||||
|
||||

|
||||
|
||||
@@ -52,21 +58,21 @@
|
||||
|
||||

|
||||
|
||||
两个向量的差的结果是两个向量所指的位置的向量。在我们需要获得两点之间的差的向量这种情境这很有用。
|
||||
两个向量的相减会得到这两个向量指向位置的差. 这在我们想要获取两点的差会非常有用.
|
||||
|
||||

|
||||
|
||||
## 长度
|
||||
### 长度(Length)
|
||||
|
||||
我们使用勾股定理获取向量的长度/大小,你可能想起来数学课上学过了。一个向量的x和y元素可以看作是三角形的两条边,这个向量可以表示成一个三角形:
|
||||
我们使用勾股定理(Pythagoras theorem)来获取向量的长度(大小). 如果你把向量的x与y分量画出来,该向量会形成一个以x与y分量为边的三角形:
|
||||
|
||||

|
||||
|
||||
因为两条边(x和y)是已知的,我们希望知道斜边 ,我们可以通过勾股定理来计算出它:
|
||||
因为两条边(x和y)是已知的,而且我们希望知道斜边的长度,所以我们可以通过勾股定理来计算出它:
|
||||
|
||||

|
||||
|
||||
表示向量的长度,通过添加很容易能扩展到3D空间。
|
||||
表示向量的大小,我们也可以很容易加上把这个公式拓展到三维空间
|
||||
|
||||
例子中向量(4, 2)的长度等于:
|
||||
|
||||
@@ -74,71 +80,71 @@
|
||||
|
||||
结果是4.47。
|
||||
|
||||
有一个特殊类型向量叫做单位向量(unit vector)。单位向量有一个特别的性质——它的长度是1。我们可以用任意向量的每个元素除以向量的长度得到单位向量:
|
||||
有一个特殊类型向量叫做**单位向量(Unit Vector)**。单位向量有一个特别的性质——它的长度是1。我们可以用任意向量的每个分量除以向量的长度得到单位向量:
|
||||
|
||||

|
||||
|
||||
我们把这种方法叫做一个向量的标准化(normalizing)。单位向量头上有一个小屋顶,特别是在我们只关心方向不关系长度的时候,它变得很有用(如果我们改变向量的长度,它的方向并不会改变)。
|
||||
我们把这种方法叫做一个向量的**标准化(Normalizing)**。单位向量头上有一个小屋顶,特别是在我们只关心方向不关系长度的时候,它变得很有用(如果我们改变向量的长度,它的方向并不会改变)。
|
||||
|
||||
## 向量与向量相乘
|
||||
### 向量与向量相乘
|
||||
|
||||
两个向量相乘有点怪异。在向量中没有普通乘法的定义,这是因为它没有视觉意义,但是有两种特定情境,当需要乘法时我们可以从中选择:一个是点乘(dot product)写作另一个是叉乘(cross product)表示为。
|
||||
两个向量相乘是一种很奇怪的情况。普通的乘法在向量上是没有定义的,因为它在视觉上是没有意义的,但是有两种特定情境,当需要乘法时我们可以从中选择:一个是**点乘(Dot Product)**,记作,另一个是**叉乘(Cross Product)**,记作。
|
||||
|
||||
### 点乘(Dot Product)
|
||||
#### 点乘(Dot Product)
|
||||
|
||||
两个向量的点乘等于它们的模乘以两个向量之间夹角的余弦。听起来有点费解,先看一下公式:
|
||||
两个向量的点乘等于它们的数乘结果乘以两个向量之间夹角的余弦值。听起来有点费解,先看一下公式:
|
||||
|
||||

|
||||
|
||||
它们之间的夹角是。为什么这很有趣?想象如果和是单位向量,它们的长度等于1。公式会有效简化:
|
||||
它们之间的夹角我们记作。为什么这很有用?想象如果和都是单位向量,它们的长度等于1。公式会有效简化成:
|
||||
|
||||

|
||||
|
||||
现在点乘只和两个向量的角度有关。你也许记得当90度的余弦是0,0度的余弦是1。使用点乘可以很容易测试两个向量是否垂直或平行(垂直意味着两个向量互为直角)。你可能想要了解更多的关于正弦或余弦的知识,我推荐你看[可汗学院](https://www.khanacademy.org/math/trigonometry/basic-trigonometry/basic_trig_ratios/v/basic-trigonometry)的基础三角学视频。
|
||||
现在点乘**只**和两个向量的角度有关。你也许记得当90度的余弦是0,0度的余弦是1。使用点乘可以很容易测试两个向量是否正交(Orthogonal)或平行(正交意味着两个向量互为直角)。你可能想要了解更多的关于正弦或余弦的知识,我推荐你看[可汗学院](https://www.khanacademy.org/math/trigonometry/basic-trigonometry/basic_trig_ratios/v/basic-trigonometry)的基础三角学视频。
|
||||
|
||||
!!! Important
|
||||
|
||||
你可以计算两个非单位向量的角度,但是你必须除把结果以两个向量的长度,再除以。
|
||||
|
||||
所以,我们如何计算点乘?点乘是按元素逐个相乘,然后再把结果相加。两个单位向量点乘就像这样(你可以用两个长度为1的验证):
|
||||
所以,我们如何计算点乘?点乘是按分量逐个相乘,然后再把结果相加。两个单位向量点乘就像这样(你可以用两个长度为1的验证):
|
||||
|
||||

|
||||
|
||||
计算两个单位余弦的角度,我们使用反余弦 ,结果是143.1度。现在我们很快就计算出了两个向量的角度。当计算光照的时候点乘被证明很有用。
|
||||
计算两个单位余弦的角度,我们使用反余弦 ,结果是143.1度。现在我们很快就计算出了两个向量的角度。点乘在计算光照的时候会很有用。
|
||||
|
||||
### 叉乘(Cross Product)
|
||||
#### 叉乘(Cross Product)
|
||||
|
||||
叉乘只在3D空间才被定义,它需要两个不平行向量作为输入,生成垂直于两个向量的第三个向量。如果输入的两个向量也垂直,那么叉乘的结果返回3个垂直向量。接下来的教程中,这很有用。下面的图片展示了3D空间中叉乘的样子:
|
||||
叉乘只在3D空间有定义,它需要两个不平行向量作为输入,生成正交于两个输入向量的第三个向量。如果输入的两个向量也是正交的,那么叉乘的结果将会返回3个互相正交的向量。接下来的教程中,这很有用。下面的图片展示了3D空间中叉乘的样子:
|
||||
|
||||

|
||||
|
||||
不同于其他操作,如果没有钻研过线性代数,叉乘很饭直觉,所以最好记住公式,就没问题(记不住也没问题)。下面你会看到两个垂直向量A和B叉乘结果:
|
||||
不同于其他运算,如果你没有钻研过线性代数,会觉得叉乘很反直觉,所以最好记住公式,就没问题(记不住也没问题)。下面你会看到两个正交向量A和B叉乘结果:
|
||||
|
||||

|
||||
|
||||
就像你所看到的,看起来毫无头绪。可如果你这么做了,你会得到第三个向量,它垂直于你的输入向量。
|
||||
就像你所看到的,看起来毫无头绪。可如果你这么做了,你会得到第三个向量,它正交于你的输入向量。
|
||||
|
||||
# 矩阵
|
||||
## 矩阵(Matrix)
|
||||
|
||||
现在我们已经讨论了向量的全部内容,是时候看看矩阵了!矩阵简单说是一个矩形的数字、符号或表达式阵列。矩阵中每个单独内容叫做矩阵的元素(element)。下面是一个2×3矩阵的例子:
|
||||
现在我们已经讨论了向量的全部内容,是时候看看矩阵了!矩阵简单说是一个矩形的数字、符号或表达式数组。矩阵中每一项叫做矩阵的**元素(Element)**。下面是一个2×3矩阵的例子:
|
||||
|
||||

|
||||
|
||||
矩阵可以通过(i, j)进行索引,i是行,j是列,这就是上面的矩阵叫做2×3矩阵的原因(3列、2行,也叫做矩阵的维度)。与你在索引2D图像(x, y)相反,获取4的索引是(2, 1)(第二行,第一列)(译注:如果是图像索引应该是(1, 2),先算列,再算行)。
|
||||
矩阵可以通过(i, j)进行索引,i是行,j是列,这就是上面的矩阵叫做2×3矩阵的原因(3列2行,也叫做矩阵的**维度(Dimension)**)。这与你在索引2D图像时的(x, y)相反,获取4的索引是(2, 1)(第二行,第一列)(译注:如果是图像索引应该是(1, 2),先算列,再算行)。
|
||||
|
||||
关于矩阵基本也就是这些了,它就是矩形数学表达式阵列。矩阵也有非常漂亮的数学属性,就跟向量一样。矩阵有几个操作,叫做:矩阵加法、减法和乘法。
|
||||
关于矩阵基本也就是这些了,它就是矩形数学表达式阵列。矩阵也有非常漂亮的数学属性,就跟向量一样。矩阵有几个运算,叫做:矩阵加法、减法和乘法。
|
||||
|
||||
## 矩阵加减
|
||||
### 矩阵的加减
|
||||
|
||||
矩阵与标量的加减如下所示:
|
||||
|
||||

|
||||
|
||||
标量值要加到矩阵的每个独立元素上。同样的方式也能应用到矩阵与标量的减法上:
|
||||
标量值要加到矩阵的每一个元素上。矩阵与标量的减法也是同样的:
|
||||
|
||||

|
||||
|
||||
矩阵之间的加减可以用元素相加减。和普通数字适用同样的法则,但是只能在矩阵间的相同索引的元素上进行。这意味着加法和减法只能在同样维度的矩阵之间进行。一个3×2矩阵和一个2×3矩阵(或一个3×3矩阵与4×4矩阵)是不能进行加减的。我们看看两个2×2矩阵是怎样加减的:
|
||||
矩阵与矩阵之间的加减就是两个矩阵对应元素的加减运算,所以总体的规则和与标量运算是差不多的,只不过在相同索引下的元素才能进行运算。这也就是说加法和减法只在同维度的矩阵中是有定义的。一个3×2矩阵和一个2×3矩阵(或一个3×3矩阵与4×4矩阵)是不能进行加减的。我们看看两个2×2矩阵是怎样加减的:
|
||||
|
||||

|
||||
|
||||
@@ -146,120 +152,122 @@
|
||||
|
||||

|
||||
|
||||
## 矩阵与标量相乘
|
||||
### 矩阵与标量相乘
|
||||
|
||||
和加减一样,矩阵与标量之间的乘法也是矩阵的元素分别乘以标量。下面的例子描述了乘法:
|
||||
和矩阵与标量的加减一样,矩阵与标量之间的乘法也是矩阵的每一个元素分别乘以该标量。下面的例子展示了乘法的过程:
|
||||
|
||||

|
||||
|
||||
现在我们也能明白为什么一个单独数字叫做标量(Scalar)了。简单来说,标量就是用它的值缩放(Scale)矩阵的所有元素(译注:注意Scalar是由Scale + -ar演变过来的)。前面的例子里,所有的元素都被放大2。
|
||||
现在我们也就能明白为什么一个单独的数字要叫做标量(Scalar)了。简单来说,标量就是用它的值缩放(Scale)矩阵的所有元素(译注:注意Scalar是由Scale + -ar演变过来的)。前面的例子里,所有的元素都被放大2。
|
||||
|
||||
到目前为止都还好,我们的例子都不复杂。不过矩阵与矩阵的乘法就不一样了。
|
||||
|
||||
## 矩阵与矩阵相乘
|
||||
### 矩阵与矩阵相乘
|
||||
|
||||
矩阵之间的乘法不见得多难,但的确不太让人适应。矩阵乘法基本上意味着遵照预定好的法则进行相乘。有一些限制存在:
|
||||
矩阵之间的乘法不见得有多复杂,但的确很难让人适应。矩阵乘法基本上意味着遵照规定好的法则进行相乘。当然,相乘还有一些限制:
|
||||
|
||||
1. 只有当左侧的矩阵列数等于右侧矩阵的行数,两个矩阵才能相乘。
|
||||
2. 矩阵间相乘不能交换,。
|
||||
1. 只有当左侧矩阵的列数与右侧矩阵的行数相等,两个矩阵才能相乘。
|
||||
2. 矩阵相乘不遵守**交换律(Commutative)**,。
|
||||
|
||||
我们先看一个两个2×2矩阵相乘的例子:
|
||||
|
||||

|
||||
|
||||
现在你可能正尝试看出到底发生了什么?矩阵乘法结合了普通的乘法和用左侧矩阵行加上右侧矩阵的列。我们可以看下面的图片:
|
||||
现在你可能会在想了:我勒个去,刚刚到底发生了什么? 矩阵的乘法是一系列乘法和加法组合的结果,它使用到了左侧矩阵的行和右侧矩阵的列。我们可以看下面的图片:
|
||||
|
||||

|
||||
|
||||
我们先把左侧矩阵的行和右侧矩阵的列拿出来。这些我们挑出来行和列决定着作为结果的2×2矩阵的输出值。如果我们拿出来的是左矩阵的第一行,最终的值就会出现在作为结果的矩阵的第一行,如果我们拿出来的是右矩阵的第一列,最终值会出现在作为结果的矩阵的第一列。这正是红框框住的情况。计算结果矩阵右下的值,我们要用第一个矩阵的第二行和第二个矩阵的第二列(译注:简单来说就是结果矩阵的元素的行取决于第一个矩阵,列取决于第二个矩阵)。
|
||||
我们先把左侧矩阵的行和右侧矩阵的列拿出来。这些我们挑出来行和列决定着作为结果的2×2矩阵的输出值。如果我们拿出来的是左矩阵的第一行,最终的值就会出现在作为结果的矩阵的第一行,如果我们拿出来的是右矩阵的第一列,最终值会出现在作为结果的矩阵的第一列。这正是红框里的情况。如果想计算结果矩阵右下角的值,我们要用第一个矩阵的第二行和第二个矩阵的第二列(译注:简单来说就是结果矩阵的元素的行取决于第一个矩阵,列取决于第二个矩阵)。
|
||||
|
||||
计算结果值的方式是先计算行和列的第一个元素之间的积,然后是第二个,第三个,第四个等等。然后把所有的乘积相加。现在就能解释为什么左矩阵的列数必须和右矩阵的行数想等了,否则就不能进行操作了!
|
||||
计算一项的结果值的方式是先计算左侧矩阵对应行和右侧矩阵对应列的第一个元素之积,然后是第二个,第三个,第四个等等,然后把所有的乘积相加,这就是结果了。现在我们就能解释为什么左侧矩阵的列数必须和右侧矩阵的行数相等了,如果不相等这一步的操作我们就无法完成了!
|
||||
|
||||
结果的矩阵的维度是(n, m),n等于左矩阵的行数,m等于右矩阵的列数。
|
||||
结果的矩阵的维度是(n, m),n等于左侧矩阵的行数,m等于右侧矩阵的列数。
|
||||
|
||||
如果你在脑子里想象出乘法有困难别担心。用笔写下来,如果遇到困难回头看这页的内容。随着时间流逝,矩阵乘法对你来说会成为自然的事。
|
||||
如果你在脑子里想象出乘法有困难别担心。用笔写下来,如果遇到困难回头看这页的内容。随着时间流逝,矩阵乘法对你来说会变成很自然的事。
|
||||
|
||||
我们用一个更大的例子来结束矩阵与矩阵乘法的讨论。尝试使用颜色来视觉化公式。作为一个有用的练习你可以自己回答这个乘法问题然后对比结果矩阵(如果你用笔计算,你很快就能掌握它们)。
|
||||
我们用一个更大的例子来结束矩阵与矩阵乘法的讨论。尝试使用颜色来让这个公式更容易理解。作为一个有用的练习,你可以自己回答这个乘法问题然后对比你的结果和图中的这个(如果你用笔计算,你很快就能掌握它们)。
|
||||
|
||||

|
||||
|
||||
就像你所看到的那样,矩阵与矩阵相乘复杂而容易犯错(这就是我们通常让计算机做这件事的原因),而且当矩阵变大以后很快就会出现问题。如果你仍然希望了解更多,对矩阵的数学属性感到好奇,我强烈推荐你看看[可汗学院](https://www.khanacademy.org/math/algebra2/algebra-matrices)的矩阵内容视频。
|
||||
就像你所看到的那样,矩阵与矩阵相乘复杂而容易犯错(这就是我们通常让计算机做这件事的原因),而且当矩阵变大以后很快就会出现问题。如果你仍然希望了解更多,对矩阵的数学属性感到好奇,我强烈推荐你看看[可汗学院](https://www.khanacademy.org/math/algebra2/algebra-matrices)的矩阵内容视频。
|
||||
|
||||
不管怎样,反正现在我们知道如何进行矩阵相乘了,我们可以开始了解好东西了。
|
||||
|
||||
# 矩阵与向量相乘
|
||||
## 矩阵与向量相乘
|
||||
|
||||
到目前,通过这些教程我们已经相当了解向量了。我们用向量来表示位置、颜色和纹理坐标。让我们进到兔子洞更深处:向量基本上就是一个N×1矩阵,N是向量元素的个数(也叫N维向量)。如果你仔细思考这个问题,会很有意思。向量和矩阵一样都是一个数字序列,但是它只有1列。所以,这个新信息能如何帮助我们?如果我们有一个M×N矩阵,我们可以用这个矩阵乘以我们的N×1向量,因为我们的矩阵的列数等于向量的行数,所以它们就能相乘。
|
||||
到目前,通过这些教程我们已经相当了解向量了。我们用向量来表示位置、颜色和纹理坐标。让我们进到兔子洞更深处:向量基本上就是一个N×1矩阵,N是向量分量的个数(也叫N维向量)。如果你仔细思考这个问题,会很有意思。向量和矩阵一样都是一个数字序列,但是它只有1列。所以,这个新信息能如何帮助我们?如果我们有一个M×N矩阵,我们可以用这个矩阵乘以我们的N×1向量,因为我们的矩阵的列数等于向量的行数,所以它们就能相乘。
|
||||
|
||||
但是为什么我们关心矩阵是否能够乘以一个向量?有很多有意思的2D/3D变换本质上都是矩阵,而矩阵与我们的向量相乘会变换我们的向量。假如你仍然有些困惑,我们看一些例子,你很快就能明白了。
|
||||
|
||||
## 单位矩阵
|
||||
### 单位矩阵(Identity Matrix)
|
||||
|
||||
因为一些缘故,在OpenGL中,我们通常使用4 × 4变换矩阵,每一个元素都是4元素向量。我们能想到的最简单的变换矩阵是单位矩阵。单位矩阵是一个除了对角线以外都是0的N × N矩阵。就像你看到的,这个变换矩阵使一个向量完全不变:
|
||||
在OpenGL中,因为有一些原因我们通常使用4×4的变换矩阵,而其中最重要的原因就是因为每一个向量都有4个分量的。我们能想到的最简单的变换矩阵就是**单位矩阵(Identity Matrix)**。单位矩阵是一个除了对角线以外都是0的N × N矩阵。就像你看到的,这个变换矩阵使一个向量完全不变:
|
||||
|
||||

|
||||
|
||||
向量看起来完全没动。从乘法法则来看很明显:第一个结果元素是矩阵的第一行的每个对立元素乘以向量的每一个元素。因为每行的元素除了第一个都是0,可得: ,这对向量的其他3个元素同样适用。
|
||||
向量看起来完全没动。从乘法法则来看很明显:第一个结果分量是矩阵的第一行的每个对应分量乘以向量的每一个分量。因为每行的分量除了第一个都是0,可得: ,这对向量的其他3个分量同样适用。
|
||||
|
||||
!!! Important
|
||||
|
||||
你可能会奇怪一个没变换的变换矩阵有什么用?单位矩阵通常是生成其他变换矩阵的起点,如果我们深挖线性代数,这就是一个对证明定理、解线性方程非常有用的矩阵。
|
||||
|
||||
## 缩放
|
||||
### 缩放(Scaling)
|
||||
|
||||
当我们对一个向量进行缩放的时候就是对向量的长度进行缩放,而它的方向保持不变。如果我们进行2或3维操作,那么我们可以定义一个2或3个的缩放向量,每个缩放一个轴(x、y或z)。
|
||||
当我们对一个向量进行缩放的时候就是对向量的长度进行缩放,而它的方向保持不变。如果我们进行2或3维操作,那么我们可以分别定义一个有2或3个缩放变量的向量,每个变量缩放一个轴(x、y或z)。
|
||||
|
||||
我们可以尝试去缩放向量。我们可以把向量沿着x轴缩放0.5,使它的宽度缩小为原来的二分之一;我们可以沿着y轴把向量的高度缩放为原来的两倍。我们看看把向量缩放(0.5, 2)所获得的是什么样的:
|
||||
我们可以尝试去缩放向量。我们可以把向量沿着x轴缩放0.5,使它的宽度缩小为原来的二分之一;我们可以沿着y轴把向量的高度缩放为原来的两倍。我们看看把向量缩放(0.5, 2)所获得的是什么样的:
|
||||
|
||||

|
||||
|
||||
记住,OpenGL通常是在3D空间操作的,对于2D的情况我们可以把z轴缩放1这样就无害了。我们刚刚的缩放操作是不均匀(non-uniform)缩放,因为每个轴的缩放因子都不一样。如果每个轴的缩放都一样那么就叫均匀(uniform)缩放。
|
||||
记住,OpenGL通常是在3D空间操作的,对于2D的情况我们可以把z轴缩放1这样z轴的值就不变了。我们刚刚的缩放操作是**不均匀(Non-uniform)**缩放,因为每个轴的缩放因子(Scaling Factor)都不一样。如果每个轴的缩放都一样那么就叫**均匀缩放(Uniform Scale)**。
|
||||
|
||||
我们下面设置一个变换矩阵来为我们提供缩放功能。我们从单位矩阵了解到,每个对角线元素曾以对应的向量元素。如果我们把1变为3会怎样?这种情况,我们就把向量的每个元素乘以3了,这事实上就把向量缩放3。如果我们把缩放变量表示为我们可以为任意向量(x, y, z)定义一个缩放矩阵:
|
||||
我们下面设置一个变换矩阵来为我们提供缩放功能。我们从单位矩阵了解到,每个对角线元素乘以对应的向量分量。如果我们把1变为3会怎样?这种情况,我们就把向量的每个分量乘以3了,这事实上就把向量缩放3。如果我们把缩放变量表示为我们可以为任意向量(x, y, z)定义一个缩放矩阵:
|
||||
|
||||

|
||||
|
||||
注意,第四个缩放的向量仍然是1,因为不会缩放3D空间中的w元素。w元素另有其他用途,后面我们会看到。
|
||||
注意,第四个缩放的向量仍然是1,因为不会缩放3D空间中的w分量。w分量另有其他用途,在后面我们会看到。
|
||||
|
||||
## 平移(Translation)
|
||||
### 平移(Translation)
|
||||
|
||||
平移是把一个新向量放在原向量的上面来返回一个具有不用位置的新向量,这样就基于平移向量移动了向量。我们已经讨论了向量加法,所以这不是什么新东西。
|
||||
如同缩放矩阵,有几个4×4矩阵的位置,用来执行特定的操作,对于平移它们是第四列前三个值。如果我们把缩放向量表示为我们就能把平移矩阵定义为:
|
||||
平移是在原来向量的基础上加上另一个的向量从而获得一个在不同位置的新向量的过程,这样就基于平移向量**移动(Move)**了向量。我们已经讨论了向量加法,所以你应该不会陌生。
|
||||
|
||||
和缩放矩阵一样,在4×4矩阵上有几个特别的位置用来执行特定的操作,对于平移来说它们是第四列最上面的3个值。如果我们把缩放向量表示为我们就能把平移矩阵定义为:
|
||||
|
||||

|
||||
|
||||
因为所有的平移值都要乘以向量的w列,然后加到向量的起点值上(想想矩阵乘法法则)。用3×3矩阵是不行的。
|
||||
这样是能工作的,因为所有的平移值都要乘以向量的w列,所以平移值会加到向量的原始坐标上(想想矩阵乘法法则)。而如果你用3x3矩阵我们的平移值就没地方放也没地方乘了,所以是不行的。
|
||||
|
||||
!!! Important
|
||||
|
||||
**齐次坐标(Homogeneous coordinates)**
|
||||
**齐次坐标(Homogeneous coordinates)**
|
||||
|
||||
向量的w元素也叫齐次坐标。从齐次坐标得到3D坐标,我们可以把x、y和z坐标除以w坐标。我们通常不会注意这个问题,因为w元素通常是1.0。使用齐次坐标有几点好处:它允许我们在3D向量上进行平移(如果没有w元素我们是不能尽心平移向量的),下一章我们会用w值创建3D图像。
|
||||
如果一个向量的齐次坐标是0,这个坐标就是方向向量(direction vector),因为w坐标是0,这个向量就不能平移。
|
||||
向量的w分量也叫齐次坐标。从齐次坐标得到3D坐标,我们可以把x、y和z坐标除以w坐标。我们通常不会注意这个问题,因为w分量通常是1.0。使用齐次坐标有几点好处:它允许我们在3D向量上进行平移(如果没有w分量我们是不能平移向量的),下一章我们会用w值创建3D图像。
|
||||
如果一个向量的齐次坐标是0,这个坐标就是方向向量(Direction Vector),因为w坐标是0,这个向量就不能平移(译注:这也就是我们说的不能平移一个方向)。
|
||||
|
||||
有了平移矩阵我们就可以在3个方向(x、y、z)上移动物体,它是我们的变换工具箱中非常有用的一个变换矩阵。
|
||||
有了平移矩阵我们就可以在3个方向(x、y、z)上移动物体,它是我们的变换工具箱中非常有用的一个变换矩阵。
|
||||
|
||||
## 旋转
|
||||
### 旋转(Rotation)
|
||||
|
||||
上面几个的变换内容相对容易理解,在2D或3D空间中也容易表示出来,但旋转稍复杂些。如果你想知道矩阵是如何构造出来的,我推荐你去看可汗学院线性代数视频。
|
||||
上面几个的变换内容相对容易理解,在2D或3D空间中也容易表示出来,但旋转稍复杂些。如果你想知道旋转矩阵是如何构造出来的,我推荐你去看可汗学院[线性代数](https://www.khanacademy.org/math/linear-algebra/matrix_transformations)视频。
|
||||
|
||||
首先我们来定义一个向量的旋转到底是什么。2D或3D空间中点的旋转用角来表示。角可以是角度或弧度,周角是360度或2PI弧度。我个人更喜欢用角度,因为它们看起来更直观。
|
||||
首先我们来定义一个向量的旋转到底是什么。2D或3D空间中点的旋转用**角(Angle)**来表示。角可以是角度制或弧度制的,周角是360度或2 [PI](https://en.wikipedia.org/wiki/Pi)弧度。我个人更喜欢用角度,因为它们看起来更直观。
|
||||
|
||||
!!! Important
|
||||
|
||||
大多数旋转函数需要用弧度制的角,但是角度制的角也可以很容易地转化为弧度制:
|
||||
弧度转角度:角度 = 弧度 * (180.0f / PI)
|
||||
角度转弧度:弧度 = 角度 * (PI / 180.0f)
|
||||
弧度转角度:角度 = 弧度 * (180.0f / PI)
|
||||
角度转弧度:弧度 = 角度 * (PI / 180.0f)
|
||||
PI约等于3.14159265359。
|
||||
|
||||
平角会向右旋转向右360/2 = 180度,向右旋转1/5表示向右360/5 = 72度。这表明2D空间的向量是向右旋转72度得到的:
|
||||
转半圈会向右旋转360/2 = 180度,向右旋转1/5圈表示向右旋转360/5 = 72度。这表明2D空间的向量是由向右旋转72度得到的:
|
||||
|
||||

|
||||
|
||||
在3D空间中旋转需要一个角和一个旋转轴。物体会沿着给定的旋转轴旋转特定角度。你可以尝试慢慢向下看,头部向下旋转一定角度,这就会沿着一个旋转轴旋转。2D向量在3D空间中旋转,我们把旋转轴设为z轴(尝试想象)。
|
||||
在3D空间中旋转需要一个角**和**一个旋转轴(Rotation Axis)。物体会沿着给定的旋转轴旋转特定角度。如果你想要更形象化的描述,可以试试向下看着一个特定的旋转轴,同时将你的头部旋转一定角度。比如2D向量在3D空间中旋转时,我们把旋转轴设为z轴(尝试想象这种情况)。
|
||||
|
||||
使用三角学就能把一个向量变换为一个经过旋转给定角度的新向量。这通常是通过把正弦和余弦函数结合得到的(一般简称sin和cos)。讨论如何生成变换矩阵超出了这个教程的范围。
|
||||
使用三角学就能把一个向量变换为一个经过旋转特定角度的新向量。这通常是使用一系列正弦和余弦各种巧妙的组合得到的(一般简称sin和cos)。当然,讨论如何生成变换矩阵超出了这个教程的范围。
|
||||
|
||||
旋转矩阵在3D空间中每个轴都有不同定义,这个角度表示为。
|
||||
旋转矩阵在3D空间中每个单位轴都有不同定义,这个角度表示为:
|
||||
|
||||
沿x轴旋转:
|
||||
|
||||
@@ -273,37 +281,37 @@
|
||||
|
||||

|
||||
|
||||
利用旋转矩阵我们可以把我们的位置矩阵(position vectors)沿一个或多个轴进行旋转。也可以把多个矩阵结合起来,比如第一个沿X轴旋转第二个沿Y轴旋转。然而,这会很快导致一个问题——Gimbal lock(译注:万向节死锁,这个视频描述了这个问题)。我们不会讨论它的细节,但是更好的解决方案是沿着任意轴比如(0.662, 0.2, 0.7222)旋转(注意,它是个单位向量上),而不再使用旋转矩阵相结合。这样一个(难看)的矩阵是存在的,下面代表任意旋转轴:
|
||||
利用旋转矩阵我们可以把我们的位置向量(Position Vectors)沿一个或多个轴进行旋转。也可以把多个矩阵结合起来,比如先沿着X轴旋转再沿着Y轴旋转。但是这会很快导致一个问题——**万向节死锁(Gimbal Lock,可以看看[这个视频](https://www.youtube.com/watch?v=zc8b2Jo7mno)来了解)**。我们不会讨论它的细节,但是一个更好的解决方案是沿着任意轴比如(0.662, 0.2, 0.7222)(注意,这是个单位向量)旋转,而不是使用一系列旋转矩阵的组合。这样一个(超级麻烦)的矩阵是存在的,下面代表任意旋转轴:
|
||||
|
||||

|
||||
|
||||
在数学上讨论如何生成这样的矩阵超出了本节内容。但是记住,即使这样一个矩阵也不能完全解决万向节死锁问题(尽管会极大地避免)。避免万向节死锁的真正解决方案是使用四元数(quaternions),它不仅安全,而且计算更友好。有关四元数会在后面的教程中讨论。
|
||||
在数学上讨论如何生成这样的矩阵仍然超出了本节内容。但是记住,即使这样一个矩阵也不能完全解决万向节死锁问题(尽管会极大地避免)。避免万向节死锁的真正解决方案是使用四元数(Quaternion),它不仅安全,而且计算更加友好。有关四元数会在后面的教程中讨论。
|
||||
|
||||
## 矩阵的组合
|
||||
### 矩阵的组合
|
||||
|
||||
感谢矩阵与矩阵乘法,使用矩阵变换的真正力量在于,我们可以把多个变换结合到一个矩阵中。让我们看看我们是否能生成一个多个变换相结合而成的变换矩阵。我们有一个顶点(x, y, z),我们希望用2对它进行缩放,然后平移(1, 2, 3)。为了必不可少的步骤,我们需要一个平移和缩放矩阵。结果的变换矩阵看起来像这样:
|
||||
使用矩阵变换的真正力量在于,根据矩阵之前的乘法,我们可以把多个变换组合到一个矩阵中。让我们看看我们是否能生成一个多个变换相结合而成的变换矩阵。我们有一个顶点(x, y, z),我们希望将其缩放2倍,然后用位移(1, 2, 3)来平移它。我们需要一个平移和缩放矩阵来完成这些变换。结果的变换矩阵看起来像这样:
|
||||
|
||||

|
||||
|
||||
注意,当矩阵相乘时我们先写平移,然后写缩放变换。矩阵乘法是不可交换的,这意味着它们的顺序很重要。当矩阵相乘,最右边的矩阵是第一个乘以向量的,所以你应该从右向左读这个乘法。建议当在结合矩阵时,先做缩放操作,然后旋转,最后平移,否则它们会(消极地)影响其他变换。比如,如果你先平移然后缩放,平移向量也会缩放(译注:比如向某方向移动2米,2米也许会被缩放成1米)!
|
||||
注意,当矩阵相乘时我们先写平移再写缩放变换的。矩阵乘法是不可交换的,这意味着它们的顺序很重要。当矩阵相乘时,在最右边的矩阵是第一个乘以向量的,所以你应该从右向左读这个乘法。我们建议您在组合矩阵时,先进行缩放操作,然后是旋转,最后才是平移,否则它们会(消极地)互相影响。比如,如果你先平移然后缩放,平移的向量也会同样被缩放(译注:比如向某方向移动2米,2米也许会被缩放成1米)!
|
||||
|
||||
我们的顶点运行最终的变换矩阵,最为结果的顶点是:
|
||||
将我们的矢量左乘最终的变换矩阵会得到以下结果:
|
||||
|
||||

|
||||
|
||||
不错!向量先缩放2,然后平移(1, 2, 3)。
|
||||
不错!向量先缩放2倍,然后平移了(1, 2, 3)个单位。
|
||||
|
||||
# 实践
|
||||
## 实践
|
||||
|
||||
现在我们已经解释了所有变换背后的理论,是时候白这些知识利用起来了。OpenGL没有任何内建的矩阵和向量形式,所以我们必须自己定义数学类和方法。在这个教程中我们更愿意使用已抽象出来的、做好了的数学库。幸运的是有个使用简单的专门为OpenGL量身定做的数学库,它就是GLM。
|
||||
现在我们已经解释了所有变换背后的理论,是时候将这些知识利用起来了。OpenGL没有任何自带的矩阵和向量形式,所以我们必须自己定义数学类和方法。在这个教程中我们更愿意抽象所有的数学细节,使用已经做好了的数学库。幸运的是有个使用简单的专门为OpenGL量身定做的数学库,那就是GLM。
|
||||
|
||||
## GLM
|
||||
### GLM
|
||||
|
||||
GLM代表OpenGL Mathematics,它是一个只有头文件的库,这意味着我们只需包含合适的头文件就行了;不用链接和编译。GLM可以从他们的[网站](http://glm.g-truc.net/0.9.5/index.html)上下载。把头文件的根目录复制到你的`includes`文件夹,让我们开始摇滚吧。
|
||||
GLM是Open**GL** **M**athematics的缩写,它是一个只有头文件的库,也就是说我们只需包含合适的头文件就行了;不用链接和编译。GLM可以从他们的[网站](http://glm.g-truc.net/0.9.5/index.html)上下载。把头文件的根目录复制到你的`includes`文件夹,然后你就可以使用这个库了。
|
||||
|
||||

|
||||
|
||||
GLM的大多数功能都可以从3个头文件中找到,这里我们会包含:
|
||||
我们需要的GLM的大多数功能都可以从下面这3个头文件中找到:
|
||||
|
||||
```c++
|
||||
#include <glm/glm.hpp>
|
||||
@@ -311,7 +319,7 @@ GLM的大多数功能都可以从3个头文件中找到,这里我们会包含
|
||||
#include <glm/gtc/type_ptr.hpp>
|
||||
```
|
||||
|
||||
我们来看看是否可以利用变换知识把一个向量(1, 0, 0)平移(1, 1, 0)(注意,我们把它定义成一个齐次坐标为1.0的。
|
||||
我们来看看是否可以利用我们刚学的变换知识把一个向量(1, 0, 0)平移(1, 1, 0)个单位(注意,我们把它定义为一个glm::vec4类型的值,其中齐次坐标我们设定为1.0):
|
||||
|
||||
```c++
|
||||
glm::vec4 vec(1.0f, 0.0f, 0.0f, 1.0f);
|
||||
@@ -321,11 +329,11 @@ vec = trans * vec;
|
||||
std::cout << vec.x << vec.y << vec.z << std::endl;
|
||||
```
|
||||
|
||||
我们先用GLM的内建vec类定义一个叫做vec的向量。下一步使我们定义一个默认mat4的4×4单位矩阵。接下来把单位矩阵传递给glm::translate函数创建一个变换矩阵,用来与平移向量结合(用给定的矩阵乘以平移矩阵就能获得需要的最后的矩阵)。
|
||||
我们先用GLM内建的向量类定义一个叫做`vec`的向量。接下来我们定义一个`mat4`类型的`trans`,默认是4×4单位矩阵。接下来我们创建一个变换矩阵,我们是把单位矩阵和一个平移向量传递给`glm::translate`函数来完成这个工作的(然后用给定的矩阵乘以平移矩阵就能获得最后需要的矩阵)。
|
||||
|
||||
我们把向量乘以平移矩阵就能获得最后的结果。如果我们仍然记得平移矩阵是如何工作的话,得到的向量应该是(1 + 1, 0 + 1, 0 + 0)就是(2, 1, 0)。这段代码用平移矩阵输出210。
|
||||
之后我们把向量乘以平移矩阵并且输出最后的结果。如果我们仍然记得平移矩阵是如何工作的话,得到的向量应该是(1 + 1, 0 + 1, 0 + 0),也就是(2, 1, 0)。这个代码片段将会输出210,所以这个平移矩阵是正确的。
|
||||
|
||||
我们来做些更有意思的是,旋转和缩放前面教程中的那个箱子。首先我们把箱子逆时针旋转90度。然后缩放0.5,使它变成原来的二分之一。我们先来创建变换矩阵:
|
||||
我们来做些更有意思的事情,让我们来旋转和缩放之前教程中的那个箱子。首先我们把箱子逆时针旋转90度。然后缩放0.5倍,使它变成原来的二分之一。我们先来创建变换矩阵:
|
||||
|
||||
```c++
|
||||
glm::mat4 trans;
|
||||
@@ -333,13 +341,13 @@ trans = glm::rotate(trans, 90.0f, glm::vec3(0.0, 0.0, 1.0));
|
||||
trans = glm::scale(trans, glm::vec3(0.5, 0.5, 0.5));
|
||||
```
|
||||
|
||||
我们先把箱子在每个轴缩放到0.5然后沿Z轴旋转90度。因为我们把矩阵传递给了GLM的每个函数,GLM会自动乘以矩阵,返回的结果是一个变换矩阵,它结合了多个变换。
|
||||
首先,我们把箱子在每个轴缩放到0.5倍,然后沿Z轴旋转90度。注意有纹理的那面矩形是在XY平面上的,我们需要把它绕着z轴旋转。因为我们把这个矩阵传递给了GLM的每个函数,GLM会自动将矩阵相乘,返回的结果是一个包括了多个变换的变换矩阵。
|
||||
|
||||
!!! Attention
|
||||
|
||||
有些GLM版本接收的是弧度而不是角度,这种情况下你可以用`glm::radians(90.0f)`转换为弧度。
|
||||
有些GLM版本接收的是弧度而不是角度,这种情况下你可以用`glm::radians(90.0f)`将角度转换为弧度。
|
||||
|
||||
下一个大问题是:如何把矩阵传递给着色器?我们在前面简单提到过GLSL里的mat4类型。所以我们改写顶点着色器来接收一个mat4的uniform变量,然后用矩阵uniform乘以位置向量:
|
||||
下一个大问题是:如何把矩阵传递给着色器?我们在前面简单提到过GLSL里的`mat4`类型。所以我们改写顶点着色器来接收一个`mat4`的uniform变量,然后再用矩阵uniform乘以位置向量:
|
||||
|
||||
```c++
|
||||
#version 330 core
|
||||
@@ -362,22 +370,22 @@ void main()
|
||||
|
||||
!!! Attention
|
||||
|
||||
GLSL也有mat2和mat3类型,允许如同向量一样的混合运算。前面提到的所有数学运算(比如标量-矩阵乘法,矩阵-向量乘法和矩阵-矩阵乘法)都被允许使用矩阵类型。无论在哪里只要指定矩阵运算我们都会解释发生了什么。
|
||||
GLSL也有`mat2`和`mat3`类型从而允许了像向量一样的混合运算。前面提到的所有数学运算(比如标量-矩阵乘法,矩阵-向量乘法和矩阵-矩阵乘法)在矩阵类型里都可以使用。当出现特殊的矩阵运算的时候我们会特别说明发生了什么的。
|
||||
|
||||
我们添加一个uniform,在把它传给gl_Position之前用变换矩阵乘以位置向量。我们的箱子现在应该是原来的二分之一大小并旋转了90度(向左倾斜)。我们仍需要把变换矩阵传给着色器:
|
||||
在把位置向量传给`gl_Position`之前,我们添加一个uniform,并且用变换矩阵乘以它。我们的箱子现在应该是原来的二分之一大小并旋转了90度(向左倾斜)。当然,我们仍需要把变换矩阵传递给着色器:
|
||||
|
||||
```c++
|
||||
GLuint transformLoc = glGetUniformLocation(ourShader.Program, "transform");
|
||||
glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(trans));
|
||||
```
|
||||
|
||||
首先请求uniform变量的地址,然后用glUniform函数把矩阵数据发送给着色器,这个函数有一个Matrix4fv的后缀。第一个参数现在应该很熟悉了,它是uniform的地址。第二个参数告诉OpenGL多少个矩阵会被发送过去,目前是1。第三个参数询问我们我们是否希望对我们的矩阵进行置换,这样会把矩阵的行转换为列。OpenGL使用的内部的矩阵布局叫做以列为主顺序(column-major ordering)布局。GLM已经是用以列为主顺序定义了它的矩阵,所以并不需要置换矩阵,我们可以填为GL_FALSE、最后一个参数是实际的矩阵数据,但是GLM并不是把它们的矩阵储存为OpenGL所希望的那种,因此我们要先用GLM的内建函数value_ptr来变换这些数据。
|
||||
我们首先请求uniform变量的地址,然后用有`Matrix4fv`后缀的`glUniform`函数把矩阵数据发送给着色器。第一个参数你现在应该很熟悉了,它是uniform的地址(Location)。第二个参数告诉OpenGL我们将要发送多少个矩阵,目前是1。第三个参数询问我们我们是否希望对我们的矩阵进行置换(Transpose),也就是说交换我们矩阵的行和列。OpenGL开发者通常使用一种内部矩阵布局叫做以列为主顺序的(Column-major Ordering)布局。GLM已经是用以列为主顺序定义了它的矩阵,所以并不需要置换矩阵,我们填`GL_FALSE`、最后一个参数是实际的矩阵数据,但是GLM并不是把它们的矩阵储存为OpenGL所希望的那种,因此我们要先用GLM的自带的函数`value_ptr`来变换这些数据。
|
||||
|
||||
我们创建了一个变换矩阵,在顶点着色器中声明了一个uniform,并把矩阵发送给了着色器,着色器会变换我们的顶点坐标。最后的结果应该看起来像这样:
|
||||
|
||||

|
||||
|
||||
完美!我们的箱子向左侧倾斜,是原来的二分之一大小,看来变换成功了。我们现在做些更有意思的,看看我们是否可以让箱子随着时间旋转,我们还会把箱子重置在窗口的左下角。要随着时间旋转箱子我们必须在游戏循环中更新变换矩阵,因为每一次渲染循环都要更新它。我们使用GLFW的时间函数来推导出不同时间的不同角度:
|
||||
完美!我们的箱子向左侧倾斜,是原来的二分之一大小,看来变换成功了。我们现在做些更有意思的,看看我们是否可以让箱子随着时间旋转,我们还会重新把箱子放在窗口的左下角。要让箱子随着时间推移旋转,我们必须在游戏循环中更新变换矩阵,因为它需要在每一次渲染迭代中被更新。我们使用GLFW的时间函数来获取不同时间的角度:
|
||||
|
||||
```c++
|
||||
glm::mat4 trans;
|
||||
@@ -385,21 +393,22 @@ trans = glm::translate(trans, glm::vec3(0.5f, -0.5f, 0.0f));
|
||||
trans = glm::rotate(trans,(GLfloat)glfwGetTime() * 50.0f, glm::vec3(0.0f, 0.0f, 1.0f));
|
||||
```
|
||||
|
||||
要记住的是前面的例子中我们可以在任何地方声明变换矩阵,但是现在我们必须在每一帧中创建它,所以才能持续更新旋转。这意味着我们不得不在每次循环中重新创建变换矩阵。通常在渲染场景的时候,我们有多个变换矩阵用来重新创建每次渲染的新值。
|
||||
要记住的是前面的例子中我们可以在任何地方声明变换矩阵,但是现在我们必须在每一次迭代中创建它,从而保证我们能够更新旋转矩阵。这也就意味着我们不得不在每次迭代中中重新创建变换矩阵。通常在渲染场景的时候,我们也会有多个在每次渲染迭代中都用新的值重新创建的变换矩阵
|
||||
|
||||
在这里我们先把箱子围绕原点(0, 0, 0)旋转,之后,我们把旋转过后的箱子平移到屏幕的右下角。记住,实际的变换顺序应该从下向上阅读:尽管在代码中我们先平移再旋转,实际的变换却是先应用旋转然后平移的。明白所有这些变换的组合,并且知道它们是如何应用到物体上的并不简单。只有尝试和实验这些变换你才能快速地掌握它们。
|
||||
|
||||
在这儿我们先把箱子围绕原点(0, 0, 0)旋转,之后,我们把它平移到屏幕的右下角。记住,实际的变换顺序应该从后往前阅读:尽管在代码中我们先平移再旋转,实际的变换确实先应用旋转然后平移的。明白所有这些变换结合,并知道它们是如何应用到物体上的并不简单。尝试和实验才能快速掌握它。
|
||||
如果你做对了,你将看到下面的结果:
|
||||
|
||||
<video src="http://learnopengl.com/video/getting-started/transformations.mp4" controls="controls">
|
||||
</video>
|
||||
|
||||
这就是你所得到的!一个平移过的箱子,它会一直转,一个变换矩阵就做到了!现在你可以明白为什么矩阵在图形领域是一个如此重要的工具了。我们可以定义一个无限数量的变换,把它们结合为一个单独矩阵,如果愿意的话我们可以重复使用。在着色器中使用矩阵可以省区重新定义顶点数据的时间,也会节约处理资源,因为我们没有总是重新发送数据(这很慢)。
|
||||
这就是我们刚刚做到的!一个平移过的箱子,它会一直转,一个变换矩阵就做到了!现在你可以明白为什么矩阵在图形领域是一个如此重要的工具了。我们可以定义一个无限数量的变换,把它们组合为一个单独的矩阵,如果愿意的话我们还可以重复使用它。在着色器中使用矩阵可以省去重新定义顶点数据的力气,它也能够节省处理时间,因为我们没有一直重新发送我们的数据(这是个非常慢的过程)。
|
||||
|
||||
如果你没有得到正确的结果,或者你有哪儿不清楚的地方。可以看[源码](http://learnopengl.com/code_viewer.php?code=getting-started/transformations)和[顶点](http://learnopengl.com/code_viewer.php?code=getting-started/transformations&type=vertex)、[片段](http://learnopengl.com/code_viewer.php?code=getting-started/transformations&type=fragment)着色器。
|
||||
|
||||
下个教程中,我们会讨论怎样使用矩阵为顶点定义不同的坐标空间。这将是我们进入实时3D图像的第一步!
|
||||
|
||||
# 练习
|
||||
## 练习
|
||||
|
||||
- 使用应用在箱子上的最后的变换,尝试将其改变成先旋转,后平移。看看发生了什么,试着想想为什么会发生这样的事情: [参考解答](http://learnopengl.com/code_viewer.php?code=getting-started/transformations-exercise1)
|
||||
- 尝试着再次调用`glDrawElements`画出第二个箱子,但是**只能**使用变换将其摆放在不同的位置。保证这个箱子被摆放在窗口的左上角,并且会不断的缩放(而不是旋转)。使用sin函数在这里会很有用;注意使用sin函数取到负值时会导致物体被翻转: [参考解答](http://learnopengl.com/code_viewer.php?code=getting-started/transformations-exercise2)
|
Reference in New Issue
Block a user