mirror of
https://github.com/LearnOpenGL-CN/LearnOpenGL-CN.git
synced 2025-08-23 04:35:28 +08:00
Fix all the titles
This commit is contained in:
@@ -58,7 +58,7 @@ CMake是一个工程文件生成工具,可以使用预定义好的CMake脚本
|
||||
|
||||
现在,让我们打开Visual Studio,创建一个新的工程。如果提供了多个选项,选择Visual C++,然后选择**空工程(Empty Project)**,别忘了给你的工程起一个合适的名字。现在我们有了一个空的工程去创建我们的OpenGL程序。
|
||||
|
||||
## 链接(Linking)
|
||||
## 链接
|
||||
|
||||
为了使我们的程序使用GLFW,我们需要把GLFW库**链接(Link)**进工程。于是我们需要在链接器的设置里写上**glfw3.lib**。但是我们的工程还不知道在哪寻找这个文件,于是我们首先需要将我们放第三方库的目录添加进设置。
|
||||
|
||||
@@ -138,7 +138,7 @@ GLEW是OpenGL Extension Wrangler Library的缩写,它管理我们上面提到
|
||||
|
||||
我们现在成功编译了GLFW和GLEW库,我们将进入[下一节](http://learnopengl-cn.readthedocs.org/zh/latest/01%20Getting%20started/03%20Hello%20Window/)去使用GLFW和GLEW来设置OpenGL上下文并创建窗口。记住确保你的头文件和库文件的目录设置正确,以及链接器里引用的库文件名正确。如果仍然遇到错误,请参考额外资源中的例子。
|
||||
|
||||
##额外的资源
|
||||
## 额外的资源
|
||||
|
||||
- [Building applications](http://www.opengl-tutorial.org/miscellaneous/building-your-own-c-application/): 提供了很多编译链接相关的信息以及一大批错误的解决方法。
|
||||
- [GLFW with Code::Blocks](http://wiki.codeblocks.org/index.php?title=Using_GLFW_with_Code::Blocks):使用Code::Blocks IDE编译GLFW。
|
||||
|
@@ -63,7 +63,7 @@ glfwMakeContextCurrent(window);
|
||||
|
||||
`glfwCreateWindow`函数需要窗口的宽和高作为它的前两个参数;第三个参数表示只这个窗口的名称(标题),这里我们使用**"LearnOpenGL"**,当然你也可以使用你喜欢的名称;最后两个参数我们暂时忽略,先置为空指针就行。它的返回值`GLFWwindow`对象的指针会在其他的GLFW操作中使用到。创建完窗口我们就可以通知GLFW给我们的窗口在当前的线程中创建我们等待已久的OpenGL上下文了。
|
||||
|
||||
### GLEW
|
||||
## GLEW
|
||||
|
||||
在之前的教程中已经提到过,GLEW是用来管理OpenGL的函数指针的,所以在调用任何OpenGL的函数之前我们需要初始化GLEW。
|
||||
|
||||
@@ -78,7 +78,7 @@ if (glewInit() != GLEW_OK)
|
||||
|
||||
请注意,我们在初始化GLEW之前设置`glewExperimental`变量的值为`GL_TRUE`,这样做能让GLEW在管理OpenGL的函数指针时更多地使用现代化的技术,如果把它设置为`GL_FALSE`的话可能会在使用OpenGL的核心模式(Core-profile)时出现一些问题。
|
||||
|
||||
### 视口(Viewport)
|
||||
## 视口(Viewport)
|
||||
|
||||
在我们绘制之前还有一件重要的事情要做,我们必须告诉OpenGL渲染窗口的尺寸大小,这样OpenGL才只能知道要显示数据的窗口坐标。我们可以通过调用`glViewport`函数来设置这些维度:
|
||||
|
||||
@@ -88,11 +88,11 @@ glViewport(0, 0, 800, 600);
|
||||
|
||||
前两个参数设置窗口左下角的位置。第三个和第四个参数设置渲染窗口的宽度和高度,我们设置成与GLFW的窗口的宽高大小,我们也可以将这个值设置成比窗口小的数值,然后所有的OpenGL渲染将会显示在一个较小的区域。
|
||||
|
||||
!!!Important
|
||||
!!! Important
|
||||
|
||||
OpenGL使用`glViewport`定义的位置和宽高进行位置坐标的转换,将OpenGL中的位置坐标转换为你的屏幕坐标。例如,OpenGL中的坐标(0.5,0.5)有可能被转换为屏幕中的坐标(200,450)。注意,OpenGL只会把-1到1之间的坐标转换为屏幕坐标,因此在此例中(-1,1)转换为屏幕坐标是(0,600)。
|
||||
|
||||
## 准备好你的引擎
|
||||
# 准备好你的引擎
|
||||
|
||||
我们可不希望只绘制一个图像之后我们的应用程序就关闭窗口并立即退出。我们希望程序在我们明确地关闭它之前一直保持运行状态并能够接受用户输入。因此,我们需要在程序中添加一个while循环,我们可以把它称之为游戏循环(Game Loop),这样我们的程序就能在我们让GLFW退出前保持运行了。下面几行的代码就实现了一个简单的游戏循环:
|
||||
|
||||
@@ -115,7 +115,7 @@ while(!glfwWindowShouldClose(window))
|
||||
|
||||
应用程序使用单缓冲区绘图可能会存在图像闪烁的问题。 这是因为生成的图像不是一下子被绘制出来的,而是按照从左到右,由上而下逐像素地绘制而成的。最终图像不是在瞬间显示给用户,而是通过一步一步地计算结果绘制的,这可能会花费一些时间。为了规避这些问题,我们应用双缓冲区渲染窗口应用程序。前面的缓冲区保存着计算后可显示给用户的图像,被显示到屏幕上;所有的的渲染命令被传递到后台的缓冲区进行计算。当所有的渲染命令执行结束后,我们交换前台缓冲和后台缓冲,这样图像就立即呈显出来,之后清空缓冲区。
|
||||
|
||||
### 最后一件事
|
||||
## 最后一件事
|
||||
|
||||
当游戏循环结束后我们需要释放之前的操作分配的资源,我们可以在main函数的最后调用`glfwTerminate`函数来释放GLFW分配的内存。
|
||||
|
||||
@@ -131,7 +131,7 @@ return 0;
|
||||
如果你没有编译通过或者有什么问题的话,首先请检查你程序的的链接选项是否正确
|
||||
。然后对比本教程的代码,检查你的代码是不是哪里写错了,你也可以[点击这里](http://learnopengl.com/code_viewer.php?code=getting-started/hellowindow)获取我的完整代码。
|
||||
|
||||
### 输入
|
||||
## 输入
|
||||
|
||||
我们同样也希望能够在GLFW中实现一些键盘控制,这是通过设置GLFW的**回调函数(Callback Function)**来实现的。回调函数事实上是一个函数指针,当我们为GLFW设置回调函数后,GLWF会在恰当的时候调用它。**按键回调(KeyCallback)**是众多回调函数中的一种,当我们为GLFW设置按键回调之后,GLFW会在用户有键盘交互时调用它。该回调函数的原型如下所示:
|
||||
|
||||
@@ -163,9 +163,9 @@ glfwSetKeyCallback(window, key_callback);
|
||||
除了按键回调函数之外,我们还能为GLFW注册其他的回调函数。例如,我们可以注册一个函数来处理窗口尺寸变化、处理一些错误信息等。我们可以在创建窗口之后到开始游戏循环之前注册各种回调函数。
|
||||
|
||||
|
||||
### 渲染(Rendering)
|
||||
## 渲染
|
||||
|
||||
我们要把所有的渲染操作放到游戏循环中,因为我们想让这些渲染操作在每次游戏循环迭代的时候都能被执行。我们将做如下的操作:
|
||||
我们要把所有的渲染(Rendering)操作放到游戏循环中,因为我们想让这些渲染操作在每次游戏循环迭代的时候都能被执行。我们将做如下的操作:
|
||||
|
||||
```c++
|
||||
// 程序循环
|
||||
|
@@ -6,9 +6,8 @@
|
||||
翻译 | [Django](http://bullteacher.com/)
|
||||
校对 | Geequlim
|
||||
|
||||
## 图形渲染管线(Pipeline)
|
||||
|
||||
在OpenGL中任何事物都在3D空间中,但是屏幕和窗口是一个2D像素阵列,所以OpenGL的大部分工作都是关于如何把3D坐标转变为适应你屏幕的2D像素。3D坐标转为2D坐标的处理过程是由OpenGL的**图形渲染管线**(Pipeline,大多译为管线,实际上指的是一堆原始图形数据途经一个输送管道,期间经过各种变化处理最终出现在屏幕的过程)管理的。图形渲染管线可以被划分为两个主要部分:第一个部分把你的3D坐标转换为2D坐标,第二部分是把2D坐标转变为实际的有颜色的像素。这个教程里,我们会简单地讨论一下图形渲染管线,以及如何使用它创建一些像素,这对我们来说是有好处的。
|
||||
在OpenGL中,任何事物都在3D空间中,但是屏幕和窗口是一个2D像素阵列,所以OpenGL的大部分工作都是关于如何把3D坐标转变为适应你屏幕的2D像素。3D坐标转为2D坐标的处理过程是由OpenGL的**图形渲染管线**(graphics Pipeline,大多译为管线,实际上指的是一堆原始图形数据途经一个输送管道,期间经过各种变化处理最终出现在屏幕的过程)管理的。图形渲染管线可以被划分为两个主要部分:第一个部分把你的3D坐标转换为2D坐标,第二部分是把2D坐标转变为实际的有颜色的像素。这个教程里,我们会简单地讨论一下图形渲染管线,以及如何使用它创建一些像素,这对我们来说是有好处的。
|
||||
|
||||
!!! Important
|
||||
|
||||
@@ -57,9 +56,9 @@
|
||||
|
||||
在现代OpenGL中,我们**必须**定义至少一个顶点着色器和一个片段着色器(因为GPU中没有默认的顶点/片段着色器)。出于这个原因,开始学习现代OpenGL的时候非常困难,因为在你能够渲染自己的第一个三角形之前需要一大堆知识。本节结束就是你可以最终渲染出你的三角形的时候,你也会了解到很多图形编程知识。
|
||||
|
||||
## 顶点输入(Vertex Input)
|
||||
## 顶点输入
|
||||
|
||||
开始绘制一些东西之前,我们必须给OpenGL输入一些顶点数据。OpenGL是一个3D图形库,所以我们在OpenGL中指定的所有坐标都是在3D坐标里(x、y和z)。OpenGL不是简单的把你所有的3D坐标变换为你屏幕上的2D像素;OpenGL只是在当它们的3个轴(x、y和z)在特定的-1.0到1.0的范围内时才处理3D坐标。所有在这个范围内的坐标叫做**标准化设备坐标**(Normalized Device Coordinates,NDC)会最终显示在你的屏幕上(所有出了这个范围的都不会显示)。
|
||||
开始绘制一些东西之前,我们必须给OpenGL输入一些顶点(Vertex)数据。OpenGL是一个3D图形库,所以我们在OpenGL中指定的所有坐标都是在3D坐标里(x、y和z)。OpenGL不是简单的把你所有的3D坐标变换为你屏幕上的2D像素;OpenGL只是在当它们的3个轴(x、y和z)在特定的-1.0到1.0的范围内时才处理3D坐标。所有在这个范围内的坐标叫做**标准化设备坐标**(Normalized Device Coordinates,NDC)会最终显示在你的屏幕上(所有出了这个范围的都不会显示)。
|
||||
|
||||
由于我们希望渲染一个三角形,我们指定所有的这三个顶点都有一个3D位置。我们把它们以`GLfloat`数组的方式定义为标准化设备坐标(也就是在OpenGL的可见区域)中。
|
||||
|
||||
@@ -124,9 +123,9 @@ glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
|
||||
|
||||
现在我们把顶点数据储存在显卡的内存中,用VBO顶点缓冲对象管理。下面我们会创建一个顶点和片段着色器,来处理这些数据。现在我们开始着手创建它们吧。
|
||||
|
||||
## 顶点着色器(Vertex Shader)
|
||||
## 顶点着色器
|
||||
|
||||
顶点着色器是几个着色器中的一个,它是可编程的。现代OpenGL需要我们至少设置一个顶点和一个片段着色器,如果我们打算做渲染的话。我们会简要介绍一下着色器以及配置两个非常简单的着色器来绘制我们第一个三角形。下个教程里我们会更详细的讨论着色器。
|
||||
顶点着色器(Vertex Shader)是几个着色器中的一个,它是可编程的。现代OpenGL需要我们至少设置一个顶点和一个片段着色器,如果我们打算做渲染的话。我们会简要介绍一下着色器以及配置两个非常简单的着色器来绘制我们第一个三角形。下个教程里我们会更详细的讨论着色器。
|
||||
|
||||
我们需要做的第一件事是用着色器语言GLSL写顶点着色器,然后编译这个着色器,这样我们就可以在应用中使用它了。下面你会看到一个非常基础的顶点着色器的源代码,它就是使用GLSL写的:
|
||||
|
||||
@@ -155,7 +154,7 @@ void main()
|
||||
|
||||
这个顶点着色器可能是能想到的最简单的了,因为我们什么都没有处理就把输入数据输出了。在真实的应用里输入数据通常都没有在标准化设备坐标中,所以我们首先就必须把它们放进OpenGL的可视区域内。
|
||||
|
||||
## 编译一个着色器
|
||||
## 编译着色器
|
||||
|
||||
我们已经写了一个顶点着色器源码,但是为了OpenGL能够使用它,我们必须在运行时动态编译它的源码。
|
||||
|
||||
@@ -195,9 +194,9 @@ glCompileShader(vertexShader);
|
||||
|
||||
如果编译的时候没有任何错误,顶点着色器就被编译成功了。
|
||||
|
||||
## 片段着色器(Fragment Shader)
|
||||
## 片段着色器
|
||||
|
||||
片段着色器是第二个也是最终我们打算创建的用于渲染三角形的着色器。片段着色器的全部,都是用来计算你的像素的最后颜色输出。为了让事情比较简单,我们的片段着色器只输出橘黄色。
|
||||
片段着色器(Fragment Shader)是第二个也是最终我们打算创建的用于渲染三角形的着色器。片段着色器的全部,都是用来计算你的像素的最后颜色输出。为了让事情比较简单,我们的片段着色器只输出橘黄色。
|
||||
|
||||
!!! Important
|
||||
|
||||
@@ -327,7 +326,7 @@ someOpenGLFunctionThatDrawsOurTriangle();
|
||||
|
||||
我们绘制一个物体的时候必须重复这件事。这看起来也不多,但是如果有超过5个顶点属性,100多个不同物体呢(这其实并不罕见)。绑定合适的缓冲对象,为每个物体配置所有顶点属性很快就变成一件麻烦事。有没有一些方法可以使我们把所有的配置储存在一个对象中,并且可以通过绑定这个对象来恢复状态?
|
||||
|
||||
### 顶点数组对象(Vertex Array Object, VAO)
|
||||
### 顶点数组对象
|
||||
|
||||
**顶点数组对象(Vertex Array Object, VAO)**可以像顶点缓冲对象一样绑定,任何随后的顶点属性调用都会储存在这个VAO中。这有一个好处,当配置顶点属性指针时,你只用做一次,每次绘制一个物体的时候,我们绑定相应VAO就行了。切换不同顶点数据和属性配置就像绑定一个不同的VAO一样简单。所有状态我们都放到了VAO里。
|
||||
|
||||
@@ -407,9 +406,9 @@ glBindVertexArray(0);
|
||||
|
||||
如果你的输出和这个不一样,你可能做错了什么,去看源码,看看是否遗漏了什么东西或者在评论部分提问。
|
||||
|
||||
## 索引缓冲对象(Element Buffer Objects,EBO)
|
||||
## 索引缓冲对象
|
||||
|
||||
这是我们最后一件在渲染顶点这个问题上要讨论的事——索引缓冲对象简称EBO(或IBO)。解释索引缓冲对象的工作方式最好是举例子:假设我们不再绘制一个三角形而是矩形。我们就可以绘制两个三角形来组成一个矩形(OpenGL主要就是绘制三角形)。这会生成下面的顶点的集合:
|
||||
这是我们最后一件在渲染顶点这个问题上要讨论的事——索引缓冲对象(Element Buffer Objects,EBO)。解释索引缓冲对象的工作方式最好是举例子:假设我们不再绘制一个三角形而是矩形。我们就可以绘制两个三角形来组成一个矩形(OpenGL主要就是绘制三角形)。这会生成下面的顶点的集合:
|
||||
|
||||
```c++
|
||||
GLfloat vertices[] = {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
# 着色器(Shader)
|
||||
# 着色器
|
||||
|
||||
原文 | [Shaders](http://learnopengl.com/#!Getting-started/Shaders)
|
||||
---|---
|
||||
@@ -6,11 +6,11 @@
|
||||
翻译 | [Django](http://bullteacher.com/)
|
||||
校对 | Geequlim
|
||||
|
||||
在[Hello Triangle](http://learnopengl-cn.readthedocs.org/zh/latest/01%20Getting%20started/04%20Hello%20Triangle/)教程中提到,着色器是运行在GPU上的小程序。这些小程序为图形渲染管线的一个特定部分而运行。从基本意义上来说,着色器不是别的,只是一种把输入转化为输出的程序。着色器也是一种相当独立的程序,它们不能相互通信;只能通过输入和输出的方式来进行沟通。
|
||||
在[Hello Triangle](http://learnopengl-cn.readthedocs.org/zh/latest/01%20Getting%20started/04%20Hello%20Triangle/)教程中提到,着色器(Shader)是运行在GPU上的小程序。这些小程序为图形渲染管线的一个特定部分而运行。从基本意义上来说,着色器不是别的,只是一种把输入转化为输出的程序。着色器也是一种相当独立的程序,它们不能相互通信;只能通过输入和输出的方式来进行沟通。
|
||||
|
||||
前面的教程里我们简要地触及了一点着色器的皮毛。了解了如何恰当地使用它们。现在我们会用一种更加通用的方式详细解释着色器,特别是OpenGL着色器语言。
|
||||
|
||||
## GLSL
|
||||
# GLSL
|
||||
|
||||
着色器是使用一种叫GLSL的类C语言写成的。GLSL是为图形计算量身定制的,它包含针对向量和矩阵操作的有用特性。
|
||||
|
||||
@@ -51,9 +51,9 @@ std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << st
|
||||
|
||||
GLSL有像其他编程语言相似的数据类型。GLSL有C风格的默认基础数据类型:`int`、`float`、`double`、`uint`和`bool`。GLSL也有两种容器类型,教程中我们会使用很多,它们是向量(Vector)和矩阵(Matrix),其中矩阵我们会在之后的教程里再讨论。
|
||||
|
||||
## 向量(Vector)
|
||||
### 向量
|
||||
|
||||
GLSL中的向量可以包含有1、2、3或者4个分量,分量类型可以是前面默认基础类型的任意一个。它们可以是下面的形式(n代表元素数量):
|
||||
GLSL中的向量(Vector)可以包含有1、2、3或者4个分量,分量类型可以是前面默认基础类型的任意一个。它们可以是下面的形式(n代表元素数量):
|
||||
|
||||
类型|含义
|
||||
---|---
|
||||
@@ -86,7 +86,7 @@ vec4 otherResult = vec4(result.xyz, 1.0f);
|
||||
|
||||
向量是一种灵活的数据类型,我们可以把用在所有输入和输出上。学完教程你会看到很多如何创造性地管理向量的例子。
|
||||
|
||||
## 输入与输出(in vs out)
|
||||
## 输入与输出
|
||||
|
||||
着色器是各自独立的小程序,但是它们都是一个整体的局部,出于这样的原因,我们希望每个着色器都有输入和输出,这样才能进行数据交流和传递。GLSL定义了`in`和`out`关键字来实现这个目的。每个着色器使用这些关键字定义输入和输出,无论在哪儿,一个输出变量就能与一个下一个阶段的输入变量相匹配。他们在顶点和片段着色器之间有点不同。
|
||||
|
||||
@@ -100,7 +100,7 @@ vec4 otherResult = vec4(result.xyz, 1.0f);
|
||||
|
||||
所以,如果我们打算从一个着色器向另一个着色器发送数据,我们必须**在发送方着色器中声明一个输出,在接收方着色器中声明一个同名输入**。当名字和类型都一样的时候,OpenGL就会把两个变量链接到一起,它们之间就能发送数据了(这是在链接程序(Program)对象时完成的)。为了展示这是这么工作的,我们会改变前面教程里的那个着色器,让顶点着色器为片段着色器决定颜色。
|
||||
|
||||
#### 顶点着色器
|
||||
**顶点着色器**
|
||||
|
||||
```c++
|
||||
#version 330 core
|
||||
@@ -114,7 +114,7 @@ void main()
|
||||
vertexColor = vec4(0.5f, 0.0f, 0.0f, 1.0f); // 把输出颜色设置为暗红色
|
||||
}
|
||||
```
|
||||
#### 片段着色器
|
||||
**片段着色器**
|
||||
|
||||
```c++
|
||||
#version 330 core
|
||||
@@ -296,7 +296,7 @@ glEnableVertexAttribArray(1);
|
||||
|
||||
这正是这个三角形里发生的事。我们有3个顶点,和相应的3个颜色,从这个三角形的像素来看它可能包含50,000左右的像素,片段着色器为这些像素进行插值。如果你仔细看这些颜色,你会发现其中的奥秘:红到紫再到蓝。像素插值会应用到所有片段着色器的输入属性上。
|
||||
|
||||
## 我们自己的着色器类
|
||||
# 我们自己的着色器类
|
||||
|
||||
编写、编译、管理着色器是件麻烦事。在着色器的最后主题里,我们会写一个类来让我们的生活轻松一点,这个类从硬盘读着色器,然后编译和链接它们,对它们进行错误检测,这就变得很好用了。这也会给你一些关于如何把我们目前所学的知识封装到一个抽象的对象里的灵感。
|
||||
|
||||
@@ -335,9 +335,13 @@ public:
|
||||
|
||||
shader类保留了着色器程序的ID。它的构造器需要顶点和片段着色器源代码的文件路径,我们可以把各自的文本文件储存在硬盘上。`Use`函数看似平常,但是能够显示这个自造类如何让我们的生活变轻松(虽然只有一点)。
|
||||
|
||||
### 从文件读取
|
||||
## 从文件读取
|
||||
|
||||
我们使用C++文件流读取着色器内容,储存到几个string对象里([译注1])
|
||||
我们使用C++文件流读取着色器内容,储存到几个string对象里:(译注1)
|
||||
|
||||
!!! note "译注1"
|
||||
|
||||
实际上把着色器代码保存在文件中适合学习OpenGL的时候,实际开发中最好把一个着色器直接储存为多个字符串,这样具有更高的灵活度。
|
||||
|
||||
```c++
|
||||
Shader(const GLchar * vertexPath, const GLchar * fragmentPath)
|
||||
@@ -439,10 +443,8 @@ while(...)
|
||||
|
||||
使用新着色器类的[程序](http://learnopengl.com/code_viewer.php?code=getting-started/shaders-using-object),[着色器类](http://learnopengl.com/code_viewer.php?type=header&code=shader),[顶点着色器](http://learnopengl.com/code_viewer.php?type=vertex&code=getting-started/basic),[片段着色器](http://learnopengl.com/code_viewer.php?type=fragment&code=getting-started/basic)。
|
||||
|
||||
## 练习
|
||||
# 练习
|
||||
|
||||
1. 修改顶点着色器让三角形上下颠倒:[参考解答](http://learnopengl.com/code_viewer.php?code=getting-started/shaders-exercise1)
|
||||
2. 通过使用uniform定义一个水平偏移,在顶点着色器中使用这个偏移量把三角形移动到屏幕右侧:[参考解答](http://learnopengl.com/code_viewer.php?code=getting-started/shaders-exercise2)
|
||||
3. 使用`out`关键字把顶点位置输出到片段着色器,把像素的颜色设置为与顶点位置相等(看看顶点位置值是如何在三角形中进行插值的)。做完这些后,尝试回答下面的问题:为什么在三角形的左下角是黑的?:[参考解答](http://learnopengl.com/code_viewer.php?code=getting-started/shaders-exercise3)
|
||||
|
||||
[译注1]: http://learnopengl-cn.readthedocs.org/zh/latest/01%20Getting%20started/05%20Shaders/#_5 "译者注:实际上把着色器代码保存在文件中适合学习OpenGL的时候,实际开发中最好把一个着色器直接储存为多个字符串,这样具有更高的灵活度。"
|
||||
3. 使用`out`关键字把顶点位置输出到片段着色器,把像素的颜色设置为与顶点位置相等(看看顶点位置值是如何在三角形中进行插值的)。做完这些后,尝试回答下面的问题:为什么在三角形的左下角是黑的?:[参考解答](http://learnopengl.com/code_viewer.php?code=getting-started/shaders-exercise3)
|
@@ -1,4 +1,4 @@
|
||||
# 纹理(Textures)
|
||||
# 纹理
|
||||
|
||||
原文 | [Textures](http://learnopengl.com/#!Getting-started/Textures)
|
||||
---|---
|
||||
@@ -38,11 +38,11 @@ GLfloat texCoords[] = {
|
||||
|
||||
纹理采样有几种不同的插值方式。我们需要自己告诉OpenGL在纹理中采用哪种采样方式。
|
||||
|
||||
### 纹理环绕方式(Texture Wrapping)
|
||||
## 纹理环绕方式
|
||||
|
||||
纹理坐标通常的范围是从(0, 0)到(1, 1),如果我们把纹理坐标设置为范围以外会发生什么?OpenGL默认的行为是重复这个纹理图像(我们简单地忽略浮点纹理坐标的整数部分),但OpenGL提供了更多的选择:
|
||||
|
||||
环绕方式 | 描述
|
||||
环绕方式(Wrapping) | 描述
|
||||
---|---
|
||||
GL_REPEAT | 纹理的默认行为,重复纹理图像
|
||||
GL_MIRRORED_REPEAT | 和`GL_REPEAT`一样,除了重复的图片是镜像放置的
|
||||
@@ -71,11 +71,13 @@ 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,[译注1])。当你有一个很大的物体但是纹理解析度很低的时候这就变得很重要了。你可能已经猜到了,OpenGL也有一个叫做纹理过滤的选项。有多种不同的选项可用,但是现在我们只讨论最重要的两种:`GL_NEAREST`和`GL_LINEAR`。
|
||||
纹理坐标不依赖于解析度,它可以是任何浮点数值,这样OpenGL需要描述出哪个纹理像素对应哪个纹理坐标(Texture Pixel,也叫Texel,译注1)。当你有一个很大的物体但是纹理解析度很低的时候这就变得很重要了。你可能已经猜到了,OpenGL也有一个叫做纹理过滤(Texture Filtering)的选项。有多种不同的选项可用,但是现在我们只讨论最重要的两种:`GL_NEAREST`和`GL_LINEAR`。
|
||||
|
||||
[译注1]: http://learnopengl-cn.readthedocs.org/zh/latest/01%20Getting%20started/06%20Textures/ "Texture Pixel也叫Texel,你可以想象你打开一张.jpg格式图片不断放大你会发现它是由无数像素点组成的,这个点就是纹理像素;注意不要和纹理坐标搞混,纹理坐标是你给模型顶点设置的那个数组,OpenGL以这个顶点的纹理坐标数据去查找纹理图像上的像素,然后进行采样提取纹理像素的颜色"
|
||||
!!! note "译注1"
|
||||
|
||||
Texture Pixel也叫Texel,你可以想象你打开一张.jpg格式图片不断放大你会发现它是由无数像素点组成的,这个点就是纹理像素;注意不要和纹理坐标搞混,纹理坐标是你给模型顶点设置的那个数组,OpenGL以这个顶点的纹理坐标数据去查找纹理图像上的像素,然后进行采样提取纹理像素的颜色
|
||||
|
||||
**GL_NEAREST(Nearest Neighbor Filtering,邻近过滤)** 是一种OpenGL默认的纹理过滤方式。当设置为`GL_NEAREST`的时候,OpenGL选择最接近纹理坐标中心点的那个像素。下图你会看到四个像素,加号代表纹理坐标。左上角的纹理像素是距离纹理坐标最近的那个,这样它就会选择这个作为采样颜色:
|
||||
|
||||
@@ -98,7 +100,7 @@ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
```
|
||||
|
||||
#### 多级渐远纹理(Mipmaps)
|
||||
### 多级渐远纹理
|
||||
|
||||
想象一下,如果我们在一个有着上千物体的大房间,每个物体上都有纹理。距离观察者远的与距离近的物体的纹理的解析度是相同的。由于远处的物体可能只产生很少的片段,OpenGL从高解析度纹理中为这些片段获取正确的颜色值就很困难。这是因为它不得不拾为一个纹理跨度很大的片段取纹理颜色。在小物体上这会产生人工感,更不用说在小物体上使用高解析度纹理浪费内存的问题了。
|
||||
|
||||
@@ -127,13 +129,13 @@ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
|
||||
常见的错误是,为多级渐远纹理过滤选项设置放大过滤。这样没有任何效果,因为多级渐远纹理主要使用在纹理被缩小的情况下的:纹理放大不会使用多级渐远纹理,为多级渐远纹理设置放大过滤选项会产生一个`GL_INVALID_ENUM`错误。
|
||||
|
||||
## 加载和创建纹理
|
||||
# 加载和创建纹理
|
||||
|
||||
使用纹理之前要做的第一件事是把它们加载到应用中。纹理图像可能储存为各种各样的格式,每种都有自己的数据结构和排列,所以我们如何才能把这些图像加载到应用中呢?一个解决方案是写一个我们自己的某种图像格式加载器比如.PNG,用它来把图像转化为byte序列。写自己的图像加载器虽然不难,但是仍然挺烦人的,而且如果要支持更多文件格式呢?你就不得不为每种你希望支持的格式写加载器了。
|
||||
|
||||
另一个解决方案是,也许是更好的一种选择,就是使用一个支持多种流行格式的图像加载库,来为我们解决这个问题。就像SOIL这种库①。
|
||||
|
||||
### SOIL
|
||||
## SOIL
|
||||
|
||||
SOIL是Simple OpenGL Image Library(简易OpenGL图像库)的缩写,它支持大多数流行的图像格式,使用起来也很简单,你可以从他们的主页下载。像大多数其他库一样,你必须自己生成**.lib**。你可以使用**/projects**文件夹里的解决方案(Solution)文件之一(不用担心他们的Visual Studio版本太老,你可以把它们转变为新的版本;这总是可行的。译注:用VS2010的时候,你要用VC8而不是VC9的解决方案,想必更高版本的情况亦是如此),你也可以使用CMake自己生成。你还要添加**src**文件夹里面的文件到你的**includes**文件夹;对了,不要忘记添加**SOIL.lib**到你的连接器选项,并在你代码文件的开头加上`#include <SOIL.h>`。
|
||||
|
||||
@@ -146,7 +148,7 @@ unsigned char* image = SOIL_load_image("container.jpg", &width, &height, 0, SOIL
|
||||
|
||||
函数首先需要输入图片文件的路径。然后需要两个int指针作为第二个和第三个参数,SOIL会返回图片的宽度和高度到其中。之后,我们需要图片的宽度和高度来生成纹理。第四个参数指定图片的通道(Channel)数量,但是这里我们只需留`0`。最后一个参数告诉SOIL如何来加载图片:我们只对图片的RGB感兴趣。结果储存为一个大的char/byte数组。
|
||||
|
||||
### 生成纹理
|
||||
## 生成纹理
|
||||
|
||||
和之前生成的OpenGL对象一样,纹理也是使用ID引用的。
|
||||
|
||||
@@ -204,7 +206,7 @@ SOIL_free_image_data(image);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
```
|
||||
|
||||
### 应用纹理
|
||||
## 应用纹理
|
||||
|
||||
后面的部分我们会使用`glDrawElements`绘制[Hello Triangle](http://learnopengl-cn.readthedocs.org/zh/latest/01%20Getting%20started/04%20Hello%20Triangle/)教程的最后一部分的矩形。我们需要告知OpenGL如何采样纹理,这样我们必须更新顶点纹理坐标数据:
|
||||
|
||||
@@ -290,9 +292,9 @@ color = texture(ourTexture, TexCoord) * vec4(ourColor, 1.0f);
|
||||
|
||||
这个箱子看起来有点70年代迪斯科风格。
|
||||
|
||||
### 纹理单元(Texture Units)
|
||||
## 纹理单元
|
||||
|
||||
你可能感到奇怪为什么`sampler2D`是个uniform变量,你却不用`glUniform`给它赋值,使用`glUniform1i`我们就可以给纹理采样器确定一个位置,这样的话我们能够一次在一个片段着色器中设置多纹理。一个纹理的位置通常称为一个纹理单元。一个纹理的默认纹理单元是0,它是默认激活的纹理单元,所以教程前面部分我们不用给它确定一个位置。
|
||||
你可能感到奇怪为什么`sampler2D`是个uniform变量,你却不用`glUniform`给它赋值,使用`glUniform1i`我们就可以给纹理采样器确定一个位置,这样的话我们能够一次在一个片段着色器中设置多纹理。一个纹理的位置通常称为一个纹理单元(Texture Units)。一个纹理的默认纹理单元是0,它是默认激活的纹理单元,所以教程前面部分我们不用给它确定一个位置。
|
||||
|
||||
纹理单元的主要目的是让我们在着色器中可以使用多于一个的纹理。通过把纹理单元赋值给采样器,我们可以一次绑定多纹理,只要我们首先激活相应的纹理单元。就像`glBindTexture`一样,我们可以使用`glActiveTexture`激活纹理单元,传入我们需要使用的纹理单元:
|
||||
|
||||
@@ -360,7 +362,7 @@ glBindVertexArray(0);
|
||||
|
||||
如果你看到了图片上的笑脸容器,你就做对了。你可以对比[程序源代码](http://learnopengl.com/code_viewer.php?code=getting-started/textures_combined),以及[顶点着色器](http://learnopengl.com/code_viewer.php?type=vertex&code=getting-started/texture)和[片段着色器](http://learnopengl.com/code_viewer.php?type=fragment&code=getting-started/texture)。
|
||||
|
||||
### 练习
|
||||
## 练习
|
||||
|
||||
为了更熟练地使用纹理,建议在继续之后的学习之前做完这些练习:
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
# 变换(Transformations)
|
||||
# 变换
|
||||
|
||||
原文 | [Transformations](http://learnopengl.com/#!Getting-started/Transformations)
|
||||
---|---
|
||||
@@ -12,9 +12,9 @@
|
||||
|
||||
为了深入了解变换,我们首先要在讨论矩阵之前了解一点向量(Vector)。这一节的目标是让你拥有将来需要的最基础的数学背景知识. 如果你发现这节十分困难,尽量尝试去理解它们,当你以后需要它们的时候回过头来复习这些概念。
|
||||
|
||||
## 向量(Vector)
|
||||
# 向量
|
||||
|
||||
向量最最基本的定义就是一个方向。或者更正式的说,向量有一个**方向(Direction)**和**大小(Magnitude,也叫做强度或长度)**。你可以把向量想成一个藏宝图上的指示:“向左走10步,向北走3步,然后向右走5步”;“左”就是方向,“10步”就是向量的长度。你可以发现,这个藏宝图的指示一共有3个向量。向量可以在任意**维度**(Dimension)上,但是我们通常只使用2至4维。如果一个向量有2个维度,它表示一个平面的方向(想象一下2D的图像),当它有3个维度的时候它可以表达一个3D世界的方向。
|
||||
向量(Vector)最最基本的定义就是一个方向。或者更正式的说,向量有一个**方向(Direction)**和**大小(Magnitude,也叫做强度或长度)**。你可以把向量想成一个藏宝图上的指示:“向左走10步,向北走3步,然后向右走5步”;“左”就是方向,“10步”就是向量的长度。你可以发现,这个藏宝图的指示一共有3个向量。向量可以在任意**维度**(Dimension)上,但是我们通常只使用2至4维。如果一个向量有2个维度,它表示一个平面的方向(想象一下2D的图像),当它有3个维度的时候它可以表达一个3D世界的方向。
|
||||
|
||||
下面你会看到3个向量,每个向量在图像中都用一个箭头(x, y)表示。我们在2D图片中展示这些向量,因为这样子会更直观. 你仍然可以把这些2D向量当做z坐标为0的3D向量。由于向量表示的是方向,起始于何处**并不会**改变它的值。下图我们可以看到向量\(\color{red}{\bar{v}}\)和\(\color{blue}{\bar{w}}\)是相等的,尽管他们的起始点不同:
|
||||
|
||||
@@ -30,7 +30,7 @@ $$
|
||||
|
||||
和普通数字一样,我们也可以用向量进行多种运算(其中一些你可能已经知道了)。
|
||||
|
||||
### 向量与标量运算(Scalar Vector Operations)
|
||||
## 向量与标量运算
|
||||
|
||||
**标量(Scalar)**只是一个数字(或者说是仅有一个分量的矢量)。当把一个向量加/减/乘/除一个标量,我们可以简单的把向量的每个分量分别进行该运算。对于加法来说会像这样:
|
||||
|
||||
@@ -40,15 +40,15 @@ $$
|
||||
|
||||
其中的+可以是+,-,·或÷,其中·是乘号。注意-和÷运算时不能颠倒,因为颠倒的运算是没有定义的(标量-/÷矢量)
|
||||
|
||||
### 向量取反(Vector Negation)
|
||||
## 向量取反
|
||||
|
||||
对一个向量取反会将其方向逆转。一个指向东北的向量取反后就指向西南方向了。我们在一个向量的每个分量前加负号就可以实现取反了(或者说用-1数乘该向量):
|
||||
对一个向量取反(Negation)会将其方向逆转。一个指向东北的向量取反后就指向西南方向了。我们在一个向量的每个分量前加负号就可以实现取反了(或者说用-1数乘该向量):
|
||||
|
||||
$$
|
||||
-\bar{v} = -\begin{pmatrix} \color{red}{v_x} \\ \color{blue}{v_y} \\ \color{green}{v_z} \end{pmatrix} = \begin{pmatrix} -\color{red}{v_x} \\ -\color{blue}{v_y} \\ -\color{green}{v_z} \end{pmatrix}
|
||||
$$
|
||||
|
||||
### 向量加减
|
||||
## 向量加减
|
||||
|
||||
向量的加法可以被定义为是**分量的(Component-wise)**相加,即将一个向量中的每一个分量加上另一个向量的对应分量:
|
||||
|
||||
@@ -70,9 +70,9 @@ $$
|
||||
|
||||

|
||||
|
||||
### 长度(Length)
|
||||
## 长度
|
||||
|
||||
我们使用**勾股定理(Pythagoras Theorem)**来获取向量的长度/大小. 如果你把向量的x与y分量画出来,该向量会形成一个以x与y分量为边的三角形:
|
||||
我们使用**勾股定理(Pythagoras Theorem)**来获取向量的长度(Length)/大小(Magnitude). 如果你把向量的x与y分量画出来,该向量会形成一个以x与y分量为边的三角形:
|
||||
|
||||

|
||||
|
||||
@@ -100,13 +100,13 @@ $$
|
||||
|
||||
我们把这种方法叫做一个向量的**标准化(Normalizing)**。单位向量头上有一个^样子的记号,并且它会变得很有用,特别是在我们只关心方向不关系长度的时候(如果我们改变向量的长度,它的方向并不会改变)。
|
||||
|
||||
### 向量相乘(Vector-vector Multiplication)
|
||||
## 向量相乘
|
||||
|
||||
两个向量相乘是一种很奇怪的情况。普通的乘法在向量上是没有定义的,因为它在视觉上是没有意义的,但是有两种特定情境,当需要乘法时我们可以从中选择:一个是**点乘(Dot Product)**,记作\(\bar{v} \cdot \bar{k}\),另一个是**叉乘(Cross Product)**,记作\(\bar{v} \times \bar{k}\)。
|
||||
|
||||
#### 点乘(Dot Product)
|
||||
### 点乘
|
||||
|
||||
两个向量的点乘等于它们的数乘结果乘以两个向量之间夹角的余弦值。听起来有点费解,先看一下公式:
|
||||
两个向量的点乘(Dot Product)等于它们的数乘结果乘以两个向量之间夹角的余弦值。听起来有点费解,先看一下公式:
|
||||
|
||||
$$
|
||||
\bar{v} \cdot \bar{k} = ||\bar{v}|| \cdot ||\bar{k}|| \cdot \cos \theta
|
||||
@@ -138,9 +138,9 @@ $$
|
||||
|
||||
计算两个单位余弦的角度,我们使用反余弦\(cos^{-1}\) ,结果是143.1度。现在我们很快就计算出了两个向量的角度。点乘在计算光照的时候会很有用。
|
||||
|
||||
#### 叉乘(Cross Product)
|
||||
### 叉乘
|
||||
|
||||
叉乘只在3D空间有定义,它需要两个不平行向量作为输入,生成正交于两个输入向量的第三个向量。如果输入的两个向量也是正交的,那么叉乘的结果将会返回3个互相正交的向量。接下来的教程中,这很有用。下面的图片展示了3D空间中叉乘的样子:
|
||||
叉乘(Cross Product)只在3D空间有定义,它需要两个不平行向量作为输入,生成正交于两个输入向量的第三个向量。如果输入的两个向量也是正交的,那么叉乘的结果将会返回3个互相正交的向量。接下来的教程中,这很有用。下面的图片展示了3D空间中叉乘的样子:
|
||||
|
||||

|
||||
|
||||
@@ -152,9 +152,9 @@ $$
|
||||
|
||||
就像你所看到的,看起来毫无头绪。可如果你这么做了,你会得到第三个向量,它正交于你的输入向量。
|
||||
|
||||
## 矩阵(Matrix)
|
||||
# 矩阵
|
||||
|
||||
现在我们已经讨论了向量的全部内容,是时候看看矩阵了!矩阵简单说是一个矩形的数字、符号或表达式数组。矩阵中每一项叫做矩阵的**元素(Element)**。下面是一个2×3矩阵的例子:
|
||||
现在我们已经讨论了向量的全部内容,是时候看看矩阵(Matrix)了!矩阵简单说是一个矩形的数字、符号或表达式数组。矩阵中每一项叫做矩阵的**元素(Element)**。下面是一个2×3矩阵的例子:
|
||||
|
||||
$$
|
||||
\begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \end{bmatrix}
|
||||
@@ -164,7 +164,7 @@ $$
|
||||
|
||||
关于矩阵基本也就是这些了,它就是矩形数学表达式阵列。矩阵也有非常漂亮的数学属性,就跟向量一样。矩阵有几个运算,叫做:矩阵加法、减法和乘法。
|
||||
|
||||
### 矩阵的加减
|
||||
## 矩阵的加减
|
||||
|
||||
矩阵与标量的加减如下所示:
|
||||
|
||||
@@ -190,7 +190,7 @@ $$
|
||||
\begin{bmatrix} \color{red}4 & \color{red}2 \\ \color{green}1 & \color{green}6 \end{bmatrix} - \begin{bmatrix} \color{red}2 & \color{red}4 \\ \color{green}0 & \color{green}1 \end{bmatrix} = \begin{bmatrix} \color{red}4 - \color{red}2 & \color{red}2 - \color{red}4 \\ \color{green}1 - \color{green}0 & \color{green}6 - \color{green}1 \end{bmatrix} = \begin{bmatrix} \color{red}2 & -\color{red}2 \\ \color{green}1 & \color{green}5 \end{bmatrix}
|
||||
$$
|
||||
|
||||
### 矩阵的数乘(Matrix-scalar Products)
|
||||
## 矩阵的数乘
|
||||
|
||||
和矩阵与标量的加减一样,矩阵与标量之间的乘法也是矩阵的每一个元素分别乘以该标量。下面的例子展示了乘法的过程:
|
||||
|
||||
@@ -202,7 +202,7 @@ $$
|
||||
|
||||
到目前为止都还好,我们的例子都不复杂。不过矩阵与矩阵的乘法就不一样了。
|
||||
|
||||
### 矩阵相乘(Matrix-matrix Multiplication)
|
||||
### 矩阵相乘
|
||||
|
||||
矩阵之间的乘法不见得有多复杂,但的确很难让人适应。矩阵乘法基本上意味着遵照规定好的法则进行相乘。当然,相乘还有一些限制:
|
||||
|
||||
@@ -238,13 +238,13 @@ $$
|
||||
|
||||
不管怎样,反正现在我们知道如何进行矩阵相乘了,我们可以开始了解好东西了。
|
||||
|
||||
## 矩阵与向量相乘
|
||||
# 矩阵与向量相乘
|
||||
|
||||
到目前,通过这些教程我们已经相当了解向量了。我们用向量来表示位置、颜色和纹理坐标。让我们进到兔子洞更深处:向量基本上就是一个**N×1**矩阵,N是向量分量的个数(也叫**N维(N-dimensional)**向量)。如果你仔细思考这个问题,会很有意思。向量和矩阵一样都是一个数字序列,但是它只有1列。所以,这个新信息能如何帮助我们?如果我们有一个M×N矩阵,我们可以用这个矩阵乘以我们的N×1向量,因为我们的矩阵的列数等于向量的行数,所以它们就能相乘。
|
||||
|
||||
但是为什么我们关心矩阵是否能够乘以一个向量?有很多有意思的2D/3D变换本质上都是矩阵,而矩阵与我们的向量相乘会变换我们的向量。假如你仍然有些困惑,我们看一些例子,你很快就能明白了。
|
||||
|
||||
### 单位矩阵(Identity Matrix)
|
||||
## 单位矩阵
|
||||
|
||||
在OpenGL中,因为有一些原因我们通常使用4×4的变换矩阵,而其中最重要的原因就是因为每一个向量都有4个分量的。我们能想到的最简单的变换矩阵就是**单位矩阵(Identity Matrix)**。单位矩阵是一个除了对角线以外都是0的N × N矩阵。就像你看到的,这个变换矩阵使一个向量完全不变:
|
||||
|
||||
@@ -258,9 +258,9 @@ $$
|
||||
|
||||
你可能会奇怪一个没变换的变换矩阵有什么用?单位矩阵通常是生成其他变换矩阵的起点,如果我们深挖线性代数,这就是一个对证明定理、解线性方程非常有用的矩阵。
|
||||
|
||||
### 缩放(Scaling)
|
||||
## 缩放
|
||||
|
||||
当我们对一个向量进行缩放的时候就是对向量的长度进行缩放,而它的方向保持不变。如果我们进行2或3维操作,那么我们可以分别定义一个有2或3个缩放变量的向量,每个变量缩放一个轴(x、y或z)。
|
||||
当我们对一个向量进行缩放(Scaling)的时候就是对向量的长度进行缩放,而它的方向保持不变。如果我们进行2或3维操作,那么我们可以分别定义一个有2或3个缩放变量的向量,每个变量缩放一个轴(x、y或z)。
|
||||
|
||||
我们可以尝试去缩放向量\(\color{red}{\bar{v}} = (3,2)\)。我们可以把向量沿着x轴缩放0.5,使它的宽度缩小为原来的二分之一;我们可以沿着y轴把向量的高度缩放为原来的两倍。我们看看把向量缩放(0.5, 2)所获得的\(\color{blue}{\bar{s}}\)是什么样的:
|
||||
|
||||
@@ -276,7 +276,7 @@ $$
|
||||
|
||||
注意,第四个缩放的向量仍然是1,因为不会缩放3D空间中的w分量。w分量另有其他用途,在后面我们会看到。
|
||||
|
||||
### 平移(Translation)
|
||||
## 平移
|
||||
|
||||
**平移(Translation)**是在原来向量的基础上加上另一个的向量从而获得一个在不同位置的新向量的过程,这样就基于平移向量**移动(Move)**了向量。我们已经讨论了向量加法,所以你应该不会陌生。
|
||||
|
||||
@@ -298,9 +298,9 @@ $$
|
||||
|
||||
有了平移矩阵我们就可以在3个方向(x、y、z)上移动物体,它是我们的变换工具箱中非常有用的一个变换矩阵。
|
||||
|
||||
### 旋转(Rotation)
|
||||
### 旋转
|
||||
|
||||
上面几个的变换内容相对容易理解,在2D或3D空间中也容易表示出来,但旋转稍复杂些。如果你想知道旋转矩阵是如何构造出来的,我推荐你去看可汗学院[线性代数](https://www.khanacademy.org/math/linear-algebra/matrix_transformations)视频。
|
||||
上面几个的变换内容相对容易理解,在2D或3D空间中也容易表示出来,但旋转(Rotation)稍复杂些。如果你想知道旋转矩阵是如何构造出来的,我推荐你去看可汗学院[线性代数](https://www.khanacademy.org/math/linear-algebra/matrix_transformations)视频。
|
||||
|
||||
首先我们来定义一个向量的旋转到底是什么。2D或3D空间中点的旋转用**角(Angle)**来表示。角可以是角度制或弧度制的,周角是360度或2 [PI](https://en.wikipedia.org/wiki/Pi)弧度。我个人更喜欢用角度,因为它们看起来更直观。
|
||||
|
||||
@@ -367,15 +367,15 @@ $$
|
||||
|
||||
不错!向量先缩放2倍,然后平移了(1, 2, 3)个单位。
|
||||
|
||||
## 实践
|
||||
# 实践
|
||||
|
||||
现在我们已经解释了所有变换背后的理论,是时候将这些知识利用起来了。OpenGL没有任何自带的矩阵和向量形式,所以我们必须自己定义数学类和方法。在这个教程中我们更愿意抽象所有的数学细节,使用已经做好了的数学库。幸运的是有个使用简单的专门为OpenGL量身定做的数学库,那就是GLM。
|
||||
|
||||
### GLM
|
||||
## GLM
|
||||
|
||||
GLM是Open**GL** **M**athematics的缩写,它是一个只有头文件的库,也就是说我们只需包含合适的头文件就行了;不用链接和编译。GLM可以从他们的[网站](http://glm.g-truc.net/0.9.5/index.html)上下载。把头文件的根目录复制到你的`includes`文件夹,然后你就可以使用这个库了。
|
||||
|
||||

|
||||
<img alt="OpenGL Logo" src="../../img/01/07/glm.png" class="right" />
|
||||
|
||||
我们需要的GLM的大多数功能都可以从下面这3个头文件中找到:
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
# 坐标系统(Coordinate System)
|
||||
# 坐标系统
|
||||
|
||||
原文 | [Coordinate Systems](http://learnopengl.com/#!Getting-started/Coordinate-Systems)
|
||||
---|---
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
在上一个教程中,我们学习了如何有效地利用矩阵变换来对所有顶点进行转换。OpenGL希望在所有顶点着色器运行后,所有我们可见的顶点都变为标准化设备坐标(Normalized Device Coordinate, NDC)。也就是说,每个顶点的x,y,z坐标都应该在-1.0到1.0之间,超出这个坐标范围的顶点都将不可见。我们通常会自己设定一个坐标的范围,之后再在顶点着色器中将这些坐标转换为标准化设备坐标。然后将这些标准化设备坐标传入光栅器(Rasterizer),再将他们转换为屏幕上的二维坐标或像素。
|
||||
|
||||
将坐标转换为标准化设备坐标,接着再转化为屏幕坐标的过程通常是分步,也就是类似于流水线那样子,实现的,在流水线里面我们在将对象转换到屏幕空间之前会先将其转换到多个坐标系统。将对象的坐标转换到几个过渡坐标系(Intermediate Coordinate System)的优点在于,在这些特定的坐标系统中进行一些操作或运算更加方便和容易,这一点很快将会变得很明显。对我们来说比较重要的总共有5个不同的坐标系统:
|
||||
将坐标转换为标准化设备坐标,接着再转化为屏幕坐标的过程通常是分步,也就是类似于流水线那样子,实现的,在流水线里面我们在将对象转换到屏幕空间之前会先将其转换到多个坐标系统(Coordinate System)。将对象的坐标转换到几个过渡坐标系(Intermediate Coordinate System)的优点在于,在这些特定的坐标系统中进行一些操作或运算更加方便和容易,这一点很快将会变得很明显。对我们来说比较重要的总共有5个不同的坐标系统:
|
||||
|
||||
- 局部空间(Local Space,或者称为物体空间(Object Space))
|
||||
- 世界空间(World Space)
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
你现在可能对什么是空间或坐标系到底是什么感到困惑,所以接下来我们将会通过展示完整的图片来解释每一个坐标系实际做了什么。
|
||||
|
||||
### 整体概述
|
||||
## 概述
|
||||
|
||||
为了将坐标从一个坐标系转换到另一个坐标系,我们需要用到几个转换矩阵,最重要的几个分别是**模型(Model)**、**视图(View)**、**投影(Projection)**三个矩阵。首先,顶点坐标开始于**局部空间(Local Space)**,称为**局部坐标(Local Coordinate)**,然后经过**世界坐标(World Coordinate)**,**观察坐标(View Coordinate)**,**裁剪坐标(Clip Coordinate)**,并最后以**屏幕坐标(Screen Coordinate)**结束。下面的图示显示了整个流程及各个转换过程做了什么:
|
||||
|
||||
@@ -36,26 +36,26 @@
|
||||
|
||||
你可能了解了每个单独的坐标空间的作用。我们之所以将顶点转换到各个不同的空间的原因是有些操作在特定的坐标系统中才有意义且更方便。例如,当修改对象时,如果在局部空间中则是有意义的;当对对象做相对于其它对象的位置的操作时,在世界坐标系中则是有意义的;等等这些。如果我们愿意,本可以定义一个直接从局部空间到裁剪空间的转换矩阵,但那样会失去灵活性。接下来我们将要更仔细地讨论各个坐标系。
|
||||
|
||||
### 局部空间(Local Space)
|
||||
## 局部空间
|
||||
|
||||
局部空间是指对象所在的坐标空间,例如,对象最开始所在的地方。想象你在一个模型建造软件(比如说Blender)中创建了一个立方体。你创建的立方体的原点有可能位于(0,0,0),即使有可能在最后的应用中位于完全不同的另外一个位置。甚至有可能你创建的所有模型都以(0,0,0)为初始位置,然而他们会在世界的不同位置。则你的模型的所有顶点都是在**局部**空间:他们相对于你的对象来说都是局部的。
|
||||
局部空间(Local Space)是指对象所在的坐标空间,例如,对象最开始所在的地方。想象你在一个模型建造软件(比如说Blender)中创建了一个立方体。你创建的立方体的原点有可能位于(0,0,0),即使有可能在最后的应用中位于完全不同的另外一个位置。甚至有可能你创建的所有模型都以(0,0,0)为初始位置,然而他们会在世界的不同位置。则你的模型的所有顶点都是在**局部**空间:他们相对于你的对象来说都是局部的。
|
||||
|
||||
我们一直使用的那个箱子的坐标范围为-0.5到0.5,设定(0, 0)为它的原点。这些都是局部坐标。
|
||||
|
||||
|
||||
### 世界空间(World Space)
|
||||
## 世界空间
|
||||
|
||||
如果我们想将我们所有的对象导入到程序当中,它们有可能会全挤在世界的原点上(0,0,0),然而这并不是我们想要的结果。我们想为每一个对象定义一个位置,从而使对象位于更大的世界当中。世界空间中的坐标就如它们听起来那样:是指顶点相对于(游戏)世界的坐标。物体变换到的最终空间就是世界坐标系,并且你会想让这些物体分散开来摆放(从而显得更真实)。对象的坐标将会从局部坐标转换到世界坐标;该转换是由**模型矩阵(Model Matrix)**实现的。
|
||||
如果我们想将我们所有的对象导入到程序当中,它们有可能会全挤在世界的原点上(0,0,0),然而这并不是我们想要的结果。我们想为每一个对象定义一个位置,从而使对象位于更大的世界当中。世界空间(World Space)中的坐标就如它们听起来那样:是指顶点相对于(游戏)世界的坐标。物体变换到的最终空间就是世界坐标系,并且你会想让这些物体分散开来摆放(从而显得更真实)。对象的坐标将会从局部坐标转换到世界坐标;该转换是由**模型矩阵(Model Matrix)**实现的。
|
||||
|
||||
模型矩阵是一种转换矩阵,它能通过对对象进行平移、缩放、旋转来将它置于它本应该在的位置或方向。你可以想象一下,我们需要转换一栋房子,通过将它缩小(因为它在局部坐标系中显得太大了),将它往郊区的方向平移,然后沿着y轴往坐标旋转。经过这样的变换之后,它将恰好能够与邻居的房子重合。你能够想到上一节讲到的利用模型矩阵将各个箱子放置到这个屏幕上;我们能够将箱子中的局部坐标转换为观察坐标或世界坐标。
|
||||
|
||||
### 观察空间(View Space)
|
||||
## 观察空间
|
||||
|
||||
观察空间经常被人们称之OpenGL的**摄像机(Camera)**(所以有时也称为摄像机空间(Camera Space)或视觉空间(Eye Space))。观察空间就是将对象的世界空间的坐标转换为观察者视野前面的坐标。因此观察空间就是从摄像机的角度观察到的空间。而这通常是由一系列的平移和旋转的组合来平移和旋转场景从而使得特定的对象被转换到摄像机前面。这些组合在一起的转换通常存储在一个**观察矩阵(View Matrix)**里,用来将世界坐标转换到观察空间。在下一个教程我们将广泛讨论如何创建一个这样的观察矩阵来模拟一个摄像机。
|
||||
观察空间(View Space)经常被人们称之OpenGL的**摄像机(Camera)**(所以有时也称为摄像机空间(Camera Space)或视觉空间(Eye Space))。观察空间就是将对象的世界空间的坐标转换为观察者视野前面的坐标。因此观察空间就是从摄像机的角度观察到的空间。而这通常是由一系列的平移和旋转的组合来平移和旋转场景从而使得特定的对象被转换到摄像机前面。这些组合在一起的转换通常存储在一个**观察矩阵(View Matrix)**里,用来将世界坐标转换到观察空间。在下一个教程我们将广泛讨论如何创建一个这样的观察矩阵来模拟一个摄像机。
|
||||
|
||||
### 裁剪空间(Clip Space)
|
||||
## 裁剪空间
|
||||
|
||||
在一个顶点着色器运行的最后,OpenGL期望所有的坐标都能落在一个给定的范围内,且任何在这个范围之外的点都应该被裁剪掉(Clipped)。被裁剪掉的坐标就被忽略了,所以剩下的坐标就将变为屏幕上可见的片段。这也就是**裁剪空间**名字的由来。
|
||||
在一个顶点着色器运行的最后,OpenGL期望所有的坐标都能落在一个给定的范围内,且任何在这个范围之外的点都应该被裁剪掉(Clipped)。被裁剪掉的坐标就被忽略了,所以剩下的坐标就将变为屏幕上可见的片段。这也就是**裁剪空间(Clip Space)**名字的由来。
|
||||
|
||||
因为将所有可见的坐标都放置在-1.0到1.0的范围内不是很直观,所以我们会指定自己的坐标集(Coordinate Set)并将它转换回标准化设备坐标系,就像OpenGL期望它做的那样。
|
||||
|
||||
@@ -73,9 +73,9 @@
|
||||
|
||||
投影矩阵将观察坐标转换为裁剪坐标的过程采用两种不同的方式,每种方式分别定义自己的平截头体。我们可以创建一个正射投影矩阵(Orthographic Projection Matrix)或一个透视投影矩阵(Perspective Projection Matrix)。
|
||||
|
||||
#### 正射投影(Orthographic Projection)
|
||||
### 正射投影
|
||||
|
||||
正射投影矩阵定义了一个类似立方体的平截头体,指定了一个裁剪空间,每一个在这空间外面的顶点都会被裁剪。创建一个正射投影矩阵需要指定可见平截头体的宽、高和长度。所有在使用正射投影矩阵转换到裁剪空间后如果还处于这个平截头体里面的坐标就不会被裁剪。它的平截头体看起来像一个容器:
|
||||
正射投影(Orthographic Projection)矩阵定义了一个类似立方体的平截头体,指定了一个裁剪空间,每一个在这空间外面的顶点都会被裁剪。创建一个正射投影矩阵需要指定可见平截头体的宽、高和长度。所有在使用正射投影矩阵转换到裁剪空间后如果还处于这个平截头体里面的坐标就不会被裁剪。它的平截头体看起来像一个容器:
|
||||
|
||||

|
||||
|
||||
@@ -91,13 +91,13 @@ glm::ortho(0.0f, 800.0f, 0.0f, 600.0f, 0.1f, 100.0f);
|
||||
|
||||
正射投影矩阵直接将坐标映射到屏幕的二维平面内,但实际上一个直接的投影矩阵将会产生不真实的结果,因为这个投影没有将**透视(Perspective)**考虑进去。所以我们需要**透视投影**矩阵来解决这个问题。
|
||||
|
||||
#### 透视投影(Perspective Projection)
|
||||
#### 透视投影
|
||||
|
||||
如果你曾经体验过**实际生活**给你带来的景象,你就会注意到离你越远的东西看起来更小。这个神奇的效果我们称之为透视。透视的效果在我们看一条无限长的高速公路或铁路时尤其明显,正如下面图片显示的那样:
|
||||
如果你曾经体验过**实际生活**给你带来的景象,你就会注意到离你越远的东西看起来更小。这个神奇的效果我们称之为透视(Perspective)。透视的效果在我们看一条无限长的高速公路或铁路时尤其明显,正如下面图片显示的那样:
|
||||
|
||||

|
||||
|
||||
正如你看到的那样,由于透视的原因,平行线似乎在很远的地方看起来会相交。这正是透视投影想要模仿的效果,它是使用透视投影矩阵来完成的。这个投影矩阵不仅将给定的平截头体范围映射到裁剪空间,同样还修改了每个顶点坐标的w值,从而使得离观察者越远的顶点坐标w分量越大。被转换到裁剪空间的坐标都会在-w到w的范围之间(任何大于这个范围的对象都会被裁剪掉)。OpenGL要求所有可见的坐标都落在-1.0到1.0范围内从而作为最后的顶点着色器输出,因此一旦坐标在裁剪空间内,透视划分就会被应用到裁剪空间坐标:
|
||||
正如你看到的那样,由于透视的原因,平行线似乎在很远的地方看起来会相交。这正是透视投影(Perspective Projection)想要模仿的效果,它是使用透视投影矩阵来完成的。这个投影矩阵不仅将给定的平截头体范围映射到裁剪空间,同样还修改了每个顶点坐标的w值,从而使得离观察者越远的顶点坐标w分量越大。被转换到裁剪空间的坐标都会在-w到w的范围之间(任何大于这个范围的对象都会被裁剪掉)。OpenGL要求所有可见的坐标都落在-1.0到1.0范围内从而作为最后的顶点着色器输出,因此一旦坐标在裁剪空间内,透视划分就会被应用到裁剪空间坐标:
|
||||
|
||||
$$
|
||||
out = \begin{pmatrix} x /w \\ y / w \\ z / w \end{pmatrix}
|
||||
@@ -145,7 +145,7 @@ $$
|
||||
|
||||
这一章的主题可能会比较难理解,如果你仍然不确定每个空间的作用的话,你也不必太担心。接下来你会看到我们是怎样好好运用这些坐标空间的并且会有足够的展示例子在接下来的教程中。
|
||||
|
||||
## 进入三维
|
||||
# 进入3D
|
||||
|
||||
既然我们知道了如何将三维坐标转换为二维坐标,我们可以开始将我们的对象展示为三维对象而不是目前我们所展示的缺胳膊少腿的二维平面。
|
||||
|
||||
@@ -179,7 +179,7 @@ model = glm::rotate(model, -55.0f, glm::vec3(1.0f, 0.0f, 0.0f));
|
||||
- 使你的食指往上。
|
||||
- 向下90度弯曲你的中指。
|
||||
|
||||
如果你都正确地做了,那么你的大拇指朝着正x轴方向,食指朝着正y轴方向,中指朝着正z轴方向。如果你用左手来做这些动作,你会发现z轴的方向是相反的。这就是有名的左手坐标系,它被DirectX广泛地使用。注意在标准化设备坐标系中OpenGL使用的是左手坐标系(投影矩阵改变了惯用手的习惯)。
|
||||
如果你都正确地做了,那么你的大拇指朝着正x轴方向,食指朝着正y轴方向,中指朝着正z轴方向。如果你用左手来做这些动作,你会发现z轴的方向是相反的。这就是有名的左手坐标系,它被DirectX广泛地使用。注意在标准化设备坐标系中OpenGL使用的是左手坐标系(投影矩阵改变了惯用手的习惯)。
|
||||
|
||||
在下一个教程中我们将会详细讨论如何在场景中移动。目前的观察矩阵是这样的:
|
||||
|
||||
@@ -262,9 +262,9 @@ glDrawArrays(GL_TRIANGLES, 0, 36);
|
||||
|
||||
幸运的是,OpenGL存储深度信息在z缓冲区(Z-buffer)里面,它允许OpenGL决定何时覆盖一个像素何时不覆盖。通过使用z缓冲区我们可以设置OpenGL来进行深度测试。
|
||||
|
||||
### z缓冲区
|
||||
### Z缓冲区
|
||||
|
||||
OpenGL存储它的所有深度信息于z缓冲区中,也被称为深度缓冲区(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`:
|
||||
|
||||
|
@@ -10,13 +10,13 @@
|
||||
|
||||
本节我们将会讨论如何在OpenGL中模拟一个摄像机,将会讨论FPS风格的可自由在3D场景中移动的摄像机。我们也会讨论键盘和鼠标输入,最终完成一个自定义的摄像机类。
|
||||
|
||||
### 摄像机/观察空间(Camera/View Space)
|
||||
## 摄像机/观察空间
|
||||
|
||||
当我们讨论摄像机/观察空间的时候,是我们在讨论以摄像机的透视图作为场景原点时场景中所有可见顶点坐标。观察矩阵把所有的世界坐标变换到观察坐标,这些新坐标是相对于摄像机的位置和方向的。定义一个摄像机,我们需要一个摄像机在世界空间中的位置、观察的方向、一个指向它的右测的向量以及一个指向它上方的向量。细心的读者可能已经注意到我们实际上创建了一个三个单位轴相互垂直的、以摄像机的位置为原点的坐标系。
|
||||
当我们讨论摄像机/观察空间(Camera/View Space)的时候,是我们在讨论以摄像机的透视图作为场景原点时场景中所有可见顶点坐标。观察矩阵把所有的世界坐标变换到观察坐标,这些新坐标是相对于摄像机的位置和方向的。定义一个摄像机,我们需要一个摄像机在世界空间中的位置、观察的方向、一个指向它的右测的向量以及一个指向它上方的向量。细心的读者可能已经注意到我们实际上创建了一个三个单位轴相互垂直的、以摄像机的位置为原点的坐标系。
|
||||
|
||||

|
||||
|
||||
#### 1. 摄像机位置
|
||||
### 1. 摄像机位置
|
||||
|
||||
获取摄像机位置很简单。摄像机位置简单来说就是世界空间中代表摄像机位置的向量。我们把摄像机位置设置为前面教程中的那个相同的位置:
|
||||
|
||||
@@ -28,7 +28,7 @@ glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f);
|
||||
|
||||
不要忘记正z轴是从屏幕指向你的,如果我们希望摄像机向后移动,我们就往z轴正方向移动。
|
||||
|
||||
#### 2. 摄像机方向
|
||||
### 2. 摄像机方向
|
||||
|
||||
下一个需要的向量是摄像机的方向,比如它指向哪个方向。现在我们让摄像机指向场景原点:(0, 0, 0)。用摄像机位置向量减去场景原点向量的结果就是摄像机指向向量。由于我们知道摄像机指向z轴负方向,我们希望方向向量指向摄像机的z轴正方向。如果我们改变相减的顺序,我们就会获得一个指向摄像机正z轴方向的向量(译注:注意看前面的那个图,所说的「方向向量/Direction Vector」是指向z的正方向的,而不是摄像机所注视的那个方向):
|
||||
|
||||
@@ -41,7 +41,7 @@ glm::vec3 cameraDirection = glm::normalize(cameraPos - cameraTarget);
|
||||
|
||||
方向向量(Direction Vector)并不是最好的名字,因为它正好指向从它到目标向量的相反方向。
|
||||
|
||||
#### 3. 右轴(Right axis)
|
||||
### 3. 右轴
|
||||
|
||||
我们需要的另一个向量是一个**右向量(Right Vector)**,它代表摄像机空间的x轴的正方向。为获取右向量我们需要先使用一个小技巧:定义一个**上向量(Up Vector)**。我们把上向量和第二步得到的摄像机方向向量进行叉乘。两个向量叉乘的结果就是同时垂直于两向量的向量,因此我们会得到指向x轴正方向的那个向量(如果我们交换两个向量的顺序就会得到相反的指向x轴负方向的向量):
|
||||
|
||||
@@ -50,7 +50,7 @@ glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f);
|
||||
glm::vec3 cameraRight = glm::normalize(glm::cross(up, cameraDirection));
|
||||
```
|
||||
|
||||
#### 4. 上轴(Up axis)
|
||||
#### 4. 上轴
|
||||
|
||||
现在我们已经有了x轴向量和z轴向量,获取摄像机的正y轴相对简单;我们把右向量和方向向量(Direction Vector)进行叉乘:
|
||||
|
||||
@@ -60,7 +60,7 @@ glm::vec3 cameraUp = glm::cross(cameraDirection, cameraRight);
|
||||
|
||||
在叉乘和一些小技巧的帮助下,我们创建了所有观察/摄像机空间的向量。对于想学到更多数学原理的读者,提示一下,在线性代数中这个处理叫做[Gram-Schmidt(葛兰—施密特)正交](http://en.wikipedia.org/wiki/Gram%E2%80%93Schmidt_process)。使用这些摄像机向量我们就可以创建一个**LookAt**矩阵了,它在创建摄像机的时候非常有用。
|
||||
|
||||
### Look At
|
||||
## Look At
|
||||
|
||||
使用矩阵的好处之一是如果你定义了一个坐标空间,里面有3个相互垂直的轴,你可以用这三个轴外加一个平移向量来创建一个矩阵,你可以用这个矩阵乘以任何向量来变换到那个坐标空间。这正是LookAt矩阵所做的,现在我们有了3个相互垂直的轴和一个定义摄像机空间的位置坐标,我们可以创建我们自己的LookAt矩阵了:
|
||||
|
||||
@@ -100,7 +100,7 @@ view = glm::lookAt(glm::vec3(camX, 0.0, camZ), glm::vec3(0.0, 0.0, 0.0), glm::ve
|
||||
|
||||
这一小段代码中,摄像机围绕场景转动。自己试试改变半径和位置/方向参数,看看LookAt矩阵是如何工作的。同时,这里有[源码](http://learnopengl.com/code_viewer.php?code=getting-started/camera_circle)、[顶点](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)着色器。
|
||||
|
||||
## 自由移动
|
||||
# 自由移动
|
||||
|
||||
让摄像机绕着场景转很有趣,但是让我们自己移动摄像机更有趣!首先我们必须设置一个摄像机系统,在我们的程序前面定义一些摄像机变量很有用:
|
||||
|
||||
@@ -200,7 +200,7 @@ while(!glfwWindowShouldClose(window))
|
||||
|
||||
至此,你可以同时向多个方向移动了,并且当你按下按钮也会立刻运动了。如遇困难查看[源码](http://learnopengl.com/code_viewer.php?code=getting-started/camera_keyboard)。
|
||||
|
||||
### 移动速度
|
||||
## 移动速度
|
||||
|
||||
目前我们的移动速度是个常量。看起来不错,但是实际情况下根据处理器的能力不同,有的人在同一段时间内会比其他人绘制更多帧。也就是调用了更多次`do_movement`函数。每个人的运动速度就都不同了。当你要发布的你应用的时候,你必须确保在所有硬件上移动速度都一样。
|
||||
|
||||
@@ -240,15 +240,15 @@ void Do_Movement()
|
||||
现在我们有了一个在任何系统上移动速度都一样的摄像机。这里是源码。我们可以看到任何移动都会影响返回的`deltaTime`值。
|
||||
|
||||
|
||||
## 自由观看
|
||||
# 视角移动
|
||||
|
||||
只用键盘移动没什么意思。特别是我们还不能转向。是时候使用鼠标了!
|
||||
|
||||
为了能够改变方向,我们必须根据鼠标的输入改变`cameraFront`向量。然而,根据鼠标旋转改变方向向量有点复杂,需要更多的三角学知识。如果你对三角学知之甚少,别担心,你可以跳过这一部分,直接复制粘贴我们的代码;当你想了解更多的时候再回来看。
|
||||
|
||||
### 欧拉角
|
||||
## 欧拉角
|
||||
|
||||
欧拉角是表示3D空间中可以表示任何旋转的三个值,由莱昂哈德·欧拉在18世纪提出。有三种欧拉角:俯仰角(Pitch)、偏航角(Yaw)和滚转角(Roll),下面的图片展示了它们的含义:
|
||||
欧拉角(Euler Angle)是表示3D空间中可以表示任何旋转的三个值,由莱昂哈德·欧拉在18世纪提出。有三种欧拉角:俯仰角(Pitch)、偏航角(Yaw)和滚转角(Roll),下面的图片展示了它们的含义:
|
||||
|
||||

|
||||
|
||||
@@ -293,7 +293,7 @@ direction.z = cos(glm::radians(pitch)) * sin(glm::radians(yaw));
|
||||
|
||||
这样我们就有了一个可以把俯仰角和偏航角转化为用来自由旋转的摄像机的3个维度的方向向量了。你可能会奇怪:我们怎么得到俯仰角和偏航角?
|
||||
|
||||
### 鼠标输入
|
||||
## 鼠标输入
|
||||
|
||||
偏航角和俯仰角是从鼠标移动获得的,鼠标水平移动影响偏航角,鼠标垂直移动影响俯仰角。它的思想是储存上一帧鼠标的位置,在当前帧中我们当前计算鼠标位置和上一帧的位置相差多少。如果差别越大那么俯仰角或偏航角就改变越大。
|
||||
|
||||
@@ -426,7 +426,7 @@ void mouse_callback(GLFWwindow* window, double xpos, double ypos)
|
||||
|
||||
现在我们可以自由的在3D场景中移动了!如果你遇到困难,[这是](http://www.learnopengl.com/code_viewer.php?code=getting-started/camera_mouse)源码。
|
||||
|
||||
### 缩放
|
||||
## 缩放
|
||||
|
||||
我们还要往摄像机系统里加点东西,实现一个缩放接口。前面教程中我们说视野(Field of View或fov)定义了我们可以看到场景中多大的范围。当视野变小时可视区域就会减小,产生放大了的感觉。我们用鼠标滚轮来放大。和鼠标移动、键盘输入一样我们需要一个鼠标滚轮的回调函数:
|
||||
|
||||
@@ -467,7 +467,7 @@ glfwSetScrollCallback(window, scroll_callback);
|
||||
|
||||
注意,使用欧拉角作为摄像机系统并不完美。你仍然可能遇到[万向节死锁](http://en.wikipedia.org/wiki/Gimbal_lock)。最好的摄像机系统是使用四元数的,后面会有讨论。
|
||||
|
||||
## 摄像机类
|
||||
# 摄像机类
|
||||
|
||||
接下来的教程我们会使用一个摄像机来浏览场景,从各个角度观察结果。然而由于一个摄像机会占教程的很大的篇幅,我们会从细节抽象出创建一个自己的摄像机对象。与着色器教程不同我们不会带你一步一步创建摄像机类,如果你想知道怎么工作的的话,只会给你提供一个(有完整注释的)源码。
|
||||
|
||||
|
@@ -10,8 +10,7 @@
|
||||
|
||||
这些就是我们在前几章学习的内容,尝试在教程的基础上进行改动程序,或者实验你自己的想法并解决问题. 一旦你认为你真正熟悉了我们讨论的所有的东西,你就可以进行[下一节](http://learnopengl-cn.readthedocs.org/zh/latest/02%20Lighting/01%20Colors/)的学习.
|
||||
|
||||
词汇表
|
||||
--------
|
||||
## 词汇表
|
||||
|
||||
- **OpenGL**: 一个定义了函数布局和输出的图形API的正式规范.
|
||||
- **GLEW**: 一个拓展加载库用来为我们加载并设定所有OpenGL函数指针从而让我们能够使用所有(现代)OpenGL函数.
|
||||
|
Reference in New Issue
Block a user