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

01-09 First half

This commit is contained in:
Meow J
2016-11-17 21:49:59 +08:00
parent 00a515ce63
commit c3ba06745e
3 changed files with 68 additions and 55 deletions

View File

@@ -1,4 +1,4 @@
# 摄像机(Camera) # 摄像机
原文 | [Camera](http://learnopengl.com/#!Getting-started/Camera) 原文 | [Camera](http://learnopengl.com/#!Getting-started/Camera)
---|--- ---|---
@@ -6,31 +6,31 @@
翻译 | [Django](http://bullteacher.com/) 翻译 | [Django](http://bullteacher.com/)
校对 | Geequlim, [BLumia](https://github.com/blumia/) 校对 | Geequlim, [BLumia](https://github.com/blumia/)
前面的教程中我们讨论了观察矩阵以及如何使用观察矩阵移动场景。OpenGL本身没有摄像机的概念但我们可以通过把场景中的所有物体往相反方向移动的方式来模拟出摄像机这样感觉就像我们在移动,而不是场景在移动。 前面的教程中我们讨论了观察矩阵以及如何使用观察矩阵移动场景(我们向后移动了一点)。OpenGL本身没有**摄像机**(Camera)的概念,但我们可以通过把场景中的所有物体往相反方向移动的方式来模拟出摄像机,产生一种**我们**在移动的感觉,而不是场景在移动。
本节我们将会讨论如何在OpenGL中模拟一个摄像机将会讨论FPS风格的可自由在3D场景中移动的摄像机。我们也会讨论键盘和鼠标输入,最终完成一个自定义的摄像机类。 本节我们将会讨论如何在OpenGL中配置一个摄像机,并且将会讨论FPS风格的摄像机让你能够在3D场景中自由移动。我们也会讨论键盘和鼠标输入,最终完成一个自定义的摄像机类。
## 摄像机/观察空间 ## 摄像机/观察空间
当我们讨论摄像机/观察空间(Camera/View Space)的时候,是我们在讨论以摄像机的透视图作为场景原点时场景中所有可见顶点坐标观察矩阵把所有的世界坐标变换到观察坐标,这些新坐标是相对于摄像机位置方向的定义一个摄像机,我们需要一个摄像机在世界空间中的位置、观察的方向、一个指向它右测的向量以及一个指向它上方的向量。细心的读者可能已经注意到我们实际上创建了一个三个单位轴相互垂直的、以摄像机的位置为原点的坐标系。 当我们讨论摄像机/观察空间(Camera/View Space)的时候,是在讨论以摄像机的视角作为场景原点时场景中所有顶点坐标观察矩阵把所有的世界坐标变换相对于摄像机位置方向的观察坐标。要定义一个摄像机,我们需要在世界空间中的位置、观察的方向、一个指向它右测的向量以及一个指向它上方的向量。细心的读者可能已经注意到我们实际上创建了一个三个单位轴相互垂直的、以摄像机的位置为原点的坐标系。
![](../img/01/09/camera_axes.png) ![](../img/01/09/camera_axes.png)
### 1. 摄像机位置 ### 1. 摄像机位置
获取摄像机位置很简单。摄像机位置简单来说就是世界空间中代表摄像机位置的向量。我们把摄像机位置设置为前面教程中的那个相同的位置: 获取摄像机位置很简单。摄像机位置简单来说就是世界空间中一个指向摄像机位置的向量。我们把摄像机位置设置为上一节中的那个相同的位置:
```c++ ```c++
glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f); glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f);
``` ```
!!! Important !!! important
不要忘记正z轴是从屏幕指向你的如果我们希望摄像机向后移动我们就z轴正方向移动。 不要忘记正z轴是从屏幕指向你的如果我们希望摄像机向后移动我们就沿着z轴正方向移动。
### 2. 摄像机方向 ### 2. 摄像机方向
下一个需要的向量是摄像机的方向,比如它指向哪个方向。现在我们让摄像机指向场景原点:(0, 0, 0)。用摄像机位置向量减去场景原点向量的结果就是摄像机指向向量。由于我们知道摄像机指向z轴负方向我们希望方向向量指向摄像机的z轴正方向。如果我们改变相减的顺序我们就会获得一个指向摄像机正z轴方向的向量(译注:注意看前面的那个图,所说的「方向向量/Direction Vector」是指向z的正方向的而不是摄像机所注视的那个方向) 下一个需要的向量是摄像机的方向,比如它指向哪个方向。现在我们让摄像机指向场景原点:(0, 0, 0)。还记得两个矢量相减,我们能得到这两个矢量的差吗?用场景原点向量减去摄像机位置向量的结果就是摄像机指向向量。由于我们知道摄像机指向z轴负方向我们希望方向向量(Direction Vector)指向摄像机的z轴正方向。如果我们交换相减的顺序我们就会获得一个指向摄像机正z轴方向的向量
```c++ ```c++
glm::vec3 cameraTarget = glm::vec3(0.0f, 0.0f, 0.0f); glm::vec3 cameraTarget = glm::vec3(0.0f, 0.0f, 0.0f);
@@ -39,38 +39,38 @@ glm::vec3 cameraDirection = glm::normalize(cameraPos - cameraTarget);
!!! Attention !!! Attention
方向向量(Direction Vector)并不是最好的名字,因为它正好指向从它到目标向量的相反方向。 **方向向量**(Direction Vector)并不是最好的名字,因为它正好指向从它到目标向量的相反方向译注注意看前面的那个图所说的“方向向量”是指向z轴正方向的而不是摄像机所注视的那个方向
### 3. 右轴 ### 3. 右轴
我们需要的另一个向量是一个**右向量(Right Vector)**它代表摄像机空间的x轴的正方向。为获取右向量我们需要先使用一个小技巧定义一个**上向量(Up Vector)**。我们把上向量和第二步得到的摄像机方向向量进行叉乘。两个向量叉乘的结果就是同时垂直于两向量的向量因此我们会得到指向x轴正方向的那个向量(如果我们交换两个向量的顺序就会得到相反的指向x轴负方向的向量) 我们需要的另一个向量是一个**右向量**(Right Vector)它代表摄像机空间的x轴的正方向。为获取右向量我们需要先使用一个小技巧定义一个**上向量**(Up Vector)。接下来把上向量和第二步得到的方向向量进行叉乘。两个向量叉乘的结果同时垂直于两向量因此我们会得到指向x轴正方向的那个向量如果我们交换两个向量叉乘的顺序就会得到相反的指向x轴负方向的向量
```c++ ```c++
glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f); glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f);
glm::vec3 cameraRight = glm::normalize(glm::cross(up, cameraDirection)); glm::vec3 cameraRight = glm::normalize(glm::cross(up, cameraDirection));
``` ```
#### 4. 上轴 ### 4. 上轴
现在我们已经有了x轴向量和z轴向量获取摄像机的正y轴相对简单我们把右向量和方向向量(Direction Vector)进行叉乘: 现在我们已经有了x轴向量和z轴向量获取一个指向摄像机的正y轴向量就相对简单了:我们把右向量和方向向量进行叉乘:
```c++ ```c++
glm::vec3 cameraUp = glm::cross(cameraDirection, cameraRight); glm::vec3 cameraUp = glm::cross(cameraDirection, cameraRight);
``` ```
在叉乘和一些小技巧的帮助下,我们创建了所有观察/摄像机空间的向量。对于想学到更多数学原理的读者,提示一下,在线性代数中这个处理叫做[Gram-Schmidt(葛兰—施密特)正交](http://en.wikipedia.org/wiki/Gram%E2%80%93Schmidt_process)。使用这些摄像机向量我们就可以创建一个**LookAt**矩阵了,它在创建摄像机的时候非常有用。 在叉乘和一些小技巧的帮助下,我们创建了所有构成观察/摄像机空间的向量。对于想学到更多数学原理的读者,提示一下,在线性代数中这个处理叫做[格拉姆—施密特正交](http://en.wikipedia.org/wiki/Gram%E2%80%93Schmidt_process)(Gram-Schmidt Process)。使用这些摄像机向量我们就可以创建一个<def>LookAt</def>矩阵了,它在创建摄像机的时候非常有用。
## Look At ## Look At
使用矩阵的好处之一是如果你定义了一个坐标空间里面有3个相互垂直的轴,你可以用这个轴外加一个平移向量来创建一个矩阵你可以用这个矩阵乘以任何向量来变换到那个坐标空间。这正是LookAt矩阵所做的现在我们有了3个相互垂直的轴和一个定义摄像机空间的位置坐标我们可以创建我们自己的LookAt矩阵了 使用矩阵的好处之一是如果你使用3个相互垂直或非线性的轴定义了一个坐标空间,你可以用这3个轴外加一个平移向量来创建一个矩阵,并且你可以用这个矩阵乘以任何向量来将其变换到那个坐标空间。这正是**LookAt**矩阵所做的现在我们有了3个相互垂直的轴和一个定义摄像机空间的位置坐标我们可以创建我们自己的LookAt矩阵了
$$ $$
LookAt = \begin{bmatrix} \color{red}{R_x} & \color{red}{R_y} & \color{red}{R_z} & 0 \\ \color{green}{U_x} & \color{green}{U_y} & \color{green}{U_z} & 0 \\ \color{blue}{D_x} & \color{blue}{D_y} & \color{blue}{D_z} & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} * \begin{bmatrix} 1 & 0 & 0 & -\color{purple}{P_x} \\ 0 & 1 & 0 & -\color{purple}{P_y} \\ 0 & 0 & 1 & -\color{purple}{P_z} \\ 0 & 0 & 0 & 1 \end{bmatrix} LookAt = \begin{bmatrix} \color{red}{R_x} & \color{red}{R_y} & \color{red}{R_z} & 0 \\ \color{green}{U_x} & \color{green}{U_y} & \color{green}{U_z} & 0 \\ \color{blue}{D_x} & \color{blue}{D_y} & \color{blue}{D_z} & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} * \begin{bmatrix} 1 & 0 & 0 & -\color{purple}{P_x} \\ 0 & 1 & 0 & -\color{purple}{P_y} \\ 0 & 0 & 1 & -\color{purple}{P_z} \\ 0 & 0 & 0 & 1 \end{bmatrix}
$$ $$
\(\color{red}R\)是右向量,\(\color{green}U\)是上向量,\(\color{blue}D\)是方向向量\(\color{purple}P\)是摄像机位置向量。注意,位置向量是相反的,因为我们最终希望把世界平移到与我们自身移动的相反方向。使用这个LookAt矩阵坐标观察矩阵可以很高效地把所有世界坐标变换为观察坐标LookAt矩阵就像它的名字表达的那样它会创建一个观察矩阵looks at(看着)一个给定目标 其中\(\color{red}R\)是右向量,\(\color{green}U\)是上向量,\(\color{blue}D\)是方向向量\(\color{purple}P\)是摄像机位置向量。注意,位置向量是相反的,因为我们最终希望把世界平移到与我们自身移动的相反方向。这个LookAt矩阵作为观察矩阵可以很高效地把所有世界坐标变换到刚刚定义的观察空间。LookAt矩阵就像它的名字表达的那样它会创建一个看着(Look at)给定目标的观察矩阵
幸运的是GLM已经提供了这些支持。我们要做的只是定义一个摄像机位置一个目标位置和一个表示上向量的世界空间中的向量(我们使用上向量计算右向量)。接着GLM就会创建一个LookAt矩阵我们可以把它当作我们的观察矩阵 幸运的是GLM已经提供了这些支持。我们要做的只是定义一个摄像机位置一个目标位置和一个表示世界空间中的向量的向量(我们计算右向量使用的那个上向量)。接着GLM就会创建一个LookAt矩阵我们可以把它当作我们的观察矩阵
```c++ ```c++
glm::mat4 view; glm::mat4 view;
@@ -79,11 +79,11 @@ view = glm::lookAt(glm::vec3(0.0f, 0.0f, 3.0f),
glm::vec3(0.0f, 1.0f, 0.0f)); glm::vec3(0.0f, 1.0f, 0.0f));
``` ```
`glm::LookAt`函数需要一个位置、目标和上向量。它可以创建一个和前面所说的同样的观察矩阵。 <fun>glm::LookAt</fun>函数需要一个位置、目标和上向量。它创建一个和上一节一样的观察矩阵。
开始做用户输入之前,我们来做些有意思的事,把我们的摄像机在场景中旋转。我们的注视点保持在(0, 0, 0)。 讨论用户输入之前,我们来做些有意思的事,把我们的摄像机在场景中旋转。我们会将摄像机的注视点保持在(0, 0, 0)。
我们在每一帧创建x和z坐标这要使用一点三角学知识。x和z表示一个在一个圆圈上的一点,我们会使用它作为摄像机的位置。通过重计算x和y坐标遍历所有圆圈上的点,这样摄像机就会绕着场景旋转了。我们预先定义这个圆的半径,使用`glfwGetTime`函数不断增加它的值,在每次渲染迭代创建一个新的观察矩阵 我们需要用到一点三角学的知识来在每一帧创建一个x和z坐标它会代表圆上的一点,我们会使用它作为摄像机的位置。通过重计算x和y坐标我们会遍历圆上的所有点,这样摄像机就会绕着场景旋转了。我们预先定义这个圆的半径<var>radius</var>在每次渲染迭代中使用GLFW的<fun>glfwGetTime</fun>函数重新创建观察矩阵,来扩大这个圆
```c++ ```c++
GLfloat radius = 10.0f; GLfloat radius = 10.0f;
@@ -93,16 +93,16 @@ glm::mat4 view;
view = glm::lookAt(glm::vec3(camX, 0.0, camZ), glm::vec3(0.0, 0.0, 0.0), glm::vec3(0.0, 1.0, 0.0)); view = glm::lookAt(glm::vec3(camX, 0.0, camZ), glm::vec3(0.0, 0.0, 0.0), glm::vec3(0.0, 1.0, 0.0));
``` ```
如果你运行代码会得到下面的东西 如果你运行代码,应该会得到下面的结果
<video src="../../img/01/09/camera_circle.mp4" controls="controls"> <video src="../../img/01/09/camera_circle.mp4" controls="controls">
</video> </video>
这一小段代码,摄像机围绕场景转动。自己试试改变半径和位置/方向参数看看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)着色器。 通过这一小段代码,摄像机现在会随着时间流逝围绕场景转动。自己试试改变半径和位置/方向参数,看看**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)着色器。
# 自由移动 # 自由移动
让摄像机绕着场景转很有趣,但是让我们自己移动摄像机更有趣!首先我们必须设置一个摄像机系统,在我们的程序前面定义一些摄像机变量很有用: 让摄像机绕着场景转的确很有趣,但是让我们自己移动摄像机更有趣!首先我们必须设置一个摄像机系统,所以在我们的程序前面定义一些摄像机变量很有用:
```c++ ```c++
glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f); glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f);
@@ -110,15 +110,15 @@ glm::vec3 cameraFront = glm::vec3(0.0f, 0.0f, -1.0f);
glm::vec3 cameraUp = glm::vec3(0.0f, 1.0f, 0.0f); glm::vec3 cameraUp = glm::vec3(0.0f, 1.0f, 0.0f);
``` ```
LookAt函数现在成了 `LookAt`函数现在成了:
```c++ ```c++
view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp); view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);
``` ```
我们首先设置之前定义的`cameraPos`为摄像机位置。方向(Direction)是当前的位置加上我们刚刚定义的方向向量。这样能保证无论我们怎么移动,摄像机都会注视目标。我们在按下某按钮时更新`cameraPos`向量。 我们首先将摄像机位置设置之前定义的<var>cameraPos</var>。方向是当前的位置加上我们刚刚定义的方向向量。这样能保证无论我们怎么移动,摄像机都会注视目标方向。让我们摆弄一下这些向量,在按下某按钮时更新<var>cameraPos</var>向量。
我们已经为GLFW的键盘输入定义了一个`key_callback`函数,我们来添加几个按键命令: 我们已经为GLFW的键盘输入定义了一个<fun>key_callback</fun>函数,我们来添加几个需要检查的按键命令:
```c++ ```c++
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode) void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode)
@@ -136,26 +136,26 @@ void key_callback(GLFWwindow* window, int key, int scancode, int action, int mod
} }
``` ```
当我们按下WASD,摄像机的位置都会相应更新。如果我们希望向前或向后移动,我们就把位置向量加上或减去方向向量。如果我们希望向旁边移动,我们做一个叉乘来创建一个右向量沿着它移动就可以了。这样就创建了类似使用摄像机横向、前后移动的效果。 当我们按下**WASD**键的任意一个,摄像机的位置都会相应更新。如果我们希望向前或向后移动,我们就把位置向量加上或减去方向向量。如果我们希望向左右移动,我们使用叉乘来创建一个**右向量**(Right Vector),并沿着它相应移动就可以了。这样就创建了使用摄像机时熟悉的<def>扫射</def>(Strafe)效果。
!!! Important !!! important
注意,我们对右向量进行了标准化。如果我们没对这个向量进行标准化,最后的叉乘结果会根据`cameraFront`变量的大小返回不同的大小。如果我们不对向量进行标准化,我们就得根据摄像机的方位加速或减速移动了,但假如进行了标准化移动就是匀速的。 注意,我们对**右向量**进行了标准化。如果我们没对这个向量进行标准化,最后的叉乘结果会根据<var>cameraFront</var>变量返回大小不同的向量。如果我们不对向量进行标准化,我们就得根据摄像机的朝向不同加速或减速移动了,但假如进行了标准化移动就是匀速的。
如果你用这段代码更新`key_callback`函数,你就可以在场景中自由的前后左右移动了。 如果你用这段代码更新<fun>key_callback</fun>函数,你就可以在场景中自由的前后左右移动了。
<video src="../../img/01/09/camera_inside.mp4" controls="controls"> <video src="../../img/01/09/camera_inside.mp4" controls="controls">
</video> </video>
你可能会注意到这个摄像机系统不能同时朝两个方向移动当你按下一个按键时它会先顿一下才开始移动。这是因为大多数事件输入系统一次只能处理一个键盘输入它们的函数只有当我们激活了一个按键时才被调用。大多数GUI系统都是这样的,它对摄像机来说并不合理。我们可以用一些小技巧解决这个问题。 在摆弄这个基础的摄像机系统之后你可能会注意到这个摄像机系统不能同时朝两个方向移动(对角线移动),而且当你按下一个按键时,它会先顿一下才开始移动。这是因为大多数事件输入系统一次只能处理一个键盘输入,它们的函数只有当我们激活了一个按键时才被调用。虽然这对大多数GUI系统都没什么问题,它对摄像机来说并不合理。我们可以用一些小技巧解决这个问题。
这个技巧是在回调函数中跟踪哪个键被按下/释放。在游戏循环中我们读取这些值,检查个按键被激活了,然后做出相应反应。我们只储存哪个键被按下/释放的状态信息,在游戏循环中对状态做出反应,我们来创建一个布尔数组代表按下/释放的键: 这个技巧是在回调函数中跟踪哪个键被按下/释放。在游戏循环中我们读取这些值,检查个按键是**激活的**,然后做出相应反应。我们只储存哪个键被按下/释放的状态信息,在游戏循环中对状态做出反应。首先,我们来创建一个boolean数组代表按下/释放的键:
```c++ ```c++
bool keys[1024]; bool keys[1024];
``` ```
然后我们必须在`key_callback`函数中设置按下/释放键为`true`或`false` 然后我们需要在<fun>key_callback</fun>函数中设置按下/释放的按键为`true`或`false`
```c++ ```c++
if(action == GLFW_PRESS) if(action == GLFW_PRESS)
@@ -164,7 +164,7 @@ else if(action == GLFW_RELEASE)
keys[key] = false; keys[key] = false;
``` ```
我们创建一个新的叫做`do_movement`的函数,用它根据按下的按键更新摄像机的值: 并且我们创建一个新的叫做<fun>do_movement</fun>的函数,在这个函数中,我们将根据正在被按下的按键更新摄像机的值
```c++ ```c++
void do_movement() void do_movement()
@@ -182,7 +182,7 @@ void do_movement()
} }
``` ```
之前的代码移动到了`do_movement`函数中。由于所有GLFW的按键枚举都是整数我们可以把它们当数组索引使用。 之前的代码现在被移动到<fun>do_movement</fun>函数中。由于所有GLFW的按键枚举值本质上都是整数,我们可以把它们当数组索引使用。
最后,我们需要在游戏循环中添加新函数的调用: 最后,我们需要在游戏循环中添加新函数的调用:
@@ -198,22 +198,22 @@ while(!glfwWindowShouldClose(window))
} }
``` ```
至此,你可以同时向多个方向移动了,并且当你按下按钮也会立刻动了。如遇困难查看[源码](http://learnopengl.com/code_viewer.php?code=getting-started/camera_keyboard)。 至此,你应该可以同时向多个方向移动了,并且当你按下按钮也会立刻动了。如遇困难,可以查看[源码](http://learnopengl.com/code_viewer.php?code=getting-started/camera_keyboard)。
## 移动速度 ## 移动速度
目前我们的移动速度是个常量。看起来不错,但是实际情况下根据处理器的能力不同,有的人在同一段时间内会比其他人绘制更多帧也就是调用了更多次`do_movement`函数。每个人的运动速度就都不同了。当你发布的你应用的时候,你必须确保在所有硬件上移动速度都一样。 目前我们的移动速度是个常量。理论上没什么问题,但是实际情况下根据处理器的能力不同,有些人可能会比其他人每秒绘制更多帧也就是以更高的频率调用<fun>do_movement</fun>函数。结果就是,根据配置的不同,有些人可能移动很快,而有些人会移动很慢。当你发布你的程序的时候,你必须确保在所有硬件上移动速度都一样。
图形和游戏应用通常有回跟踪一个`deltaTime`变量,它储存渲染上一帧所用的时间。我们把所有速度都去乘以`deltaTime`值。当我们的`deltaTime`变大时意味着上一帧渲染花了更多时间,所以这一帧使用这个更大的`deltaTime`的值乘以速度,会获得更高的速度,这样就与上一帧平衡了。使用这种方法时,无论你的机器快还是慢,摄像机的速度都会保持一致,这样每个用户的体验就都一样了。 图形程序和游戏通常会跟踪一个<def>时间差</def>(Deltatime)变量,它储存渲染上一帧所用的时间。我们把所有速度都去乘以<var>deltaTime</var>值。结果就是,如果我们的<var>deltaTime</var>很大,就意味着上一帧渲染花了更多时间,所以这一帧的速度需要变得更高,来平衡渲染所花去的时间。使用这种方法时,无论你的电脑快还是慢,摄像机的速度都会相应平衡,这样每个用户的体验就都一样了。
我们要用两个全局变量来计算出`deltaTime`值: 我们跟踪两个全局变量来计算出<var>deltaTime</var>值:
```c++ ```c++
GLfloat deltaTime = 0.0f; // 当前帧与上一帧的时间差 GLfloat deltaTime = 0.0f; // 当前帧与上一帧的时间差
GLfloat lastFrame = 0.0f; // 上一帧的时间 GLfloat lastFrame = 0.0f; // 上一帧的时间
``` ```
在每一帧中我们计算出新的`deltaTime`以备后用 在每一帧中我们计算出新的<var>deltaTime</var>以备后用
```c++ ```c++
GLfloat currentFrame = glfwGetTime(); GLfloat currentFrame = glfwGetTime();
@@ -221,10 +221,10 @@ deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame; lastFrame = currentFrame;
``` ```
现在我们有了`deltaTime`在计算速度的使用可以使用了: 现在我们有了<var>deltaTime</var>,在计算速度的时候可以将其考虑进去了:
```c++ ```c++
void Do_Movement() void do_movement()
{ {
GLfloat cameraSpeed = 5.0f * deltaTime; GLfloat cameraSpeed = 5.0f * deltaTime;
... ...
@@ -237,61 +237,61 @@ void Do_Movement()
<video src="../../img/01/09/camera_smooth.mp4" controls="controls"> <video src="../../img/01/09/camera_smooth.mp4" controls="controls">
</video> </video>
现在我们有了一个在任何系统上移动速度都一样的摄像机。这里是源码。我们可以看到任何移动都会影响返回的`deltaTime`值。 现在我们有了一个在任何系统上移动速度都一样的摄像机。同样,如果你卡住了,查看一下[源码](http://learnopengl.com/code_viewer.php?code=getting-started/camera_keyboard_dt)。我们可以看到任何移动都会影响返回的<var>deltaTime</var>值。
# 视角移动 # 视角移动
只用键盘移动没什么意思。特别是我们还不能转向。是时候使用鼠标了! 只用键盘移动没什么意思。特别是我们还不能转向,移动很受限制。是时候加入鼠标了!
为了能够改变方向,我们必须根据鼠标的输入改变`cameraFront`向量。然而,根据鼠标旋转改变方向向量有点复杂,需要更多的三角学知识。如果你对三角学知之甚少,别担心,你可以跳过这一部分,直接复制粘贴我们的代码;当你想了解更多的时候再回来看。 为了能够改变视角,我们需要根据鼠标的输入改变<var>cameraFront</var>向量。然而,根据鼠标移动改变方向向量有点复杂,需要一些三角学知识。如果你对三角学知之甚少,别担心,你可以跳过这一部分,直接复制粘贴我们的代码;当你想了解更多的时候再回来看。
## 欧拉角 ## 欧拉角
欧拉角(Euler Angle)是表示3D空间中可以表示任何旋转的个值由莱昂哈德·欧拉在18世纪提出。有三种欧拉角:俯仰角(Pitch)、偏航角(Yaw)和滚转角(Roll),下面的图片展示了它们的含义: 欧拉角(Euler Angle)是可以表示3D空间中任何旋转的3个值,由莱昂哈德·欧拉(Leonhard Euler)在18世纪提出。一共有3种欧拉角:俯仰角(Pitch)、偏航角(Yaw)和滚转角(Roll),下面的图片展示了它们的含义:
![](../img/01/09/camera_pitch_yaw_roll.png) ![](../img/01/09/camera_pitch_yaw_roll.png)
**俯仰角**是描述我们如何往上往下看的角,在第一张图中表示。第二张图示了**偏航角**,偏航角表示我们往左和往右看的大小。**滚转角**代表我们如何翻滚摄像机。每个欧拉角都有一个值来表示把三个角结合起来我们就能够计算3D空间中任何的旋转了。 <def>俯仰角</def>是描述我们如何往上往下看的角,可以在第一张图中看到。第二张图示了<def>偏航角</def>,偏航角表示我们往左和往右看的程度。<def>滚转角</def>代表我们如何**翻滚**摄像机,通常在太空飞船的摄像机中使用。每个欧拉角都有一个值来表示把三个角结合起来我们就能够计算3D空间中任何的旋转向量了。
对于我们的摄像机系统来说,我们只关心俯仰角和偏航角,所以我们不会讨论滚转角。用一个给定的俯仰角和偏航角我们可以把它们转换为一个代表新的方向向量的3D向量。俯仰角和偏航角转换为方向向量的处理需要一些三角学知识我们最基本的情况开始: 对于我们的摄像机系统来说,我们只关心俯仰角和偏航角,所以我们不会讨论滚转角。给定一个俯仰角和偏航角我们可以把它们转换为一个代表新的方向向量的3D向量。俯仰角和偏航角转换为方向向量的处理需要一些三角学知识我们先从最基本的情况开始:
![](../img/01/09/camera_triangle.png) ![](../img/01/09/camera_triangle.png)
如果我们把斜边边长定义为1我们就能知道邻边的长度是\(\cos \ \color{red}x/\color{purple}h = \cos \ \color{red}x/\color{purple}1 = \cos\ \color{red}x\),它的对边是\(\sin \ \color{green}y/\color{purple}h = \sin \ \color{green}y/\color{purple}1 = \sin\ \color{green}y\)。这样我们获得了能够得到x和y方向的长度的公式它们取决于所给的角度。我们使用它来计算方向向量的元素 如果我们把斜边边长定义为1我们就能知道邻边的长度是\(\cos \ \color{red}x/\color{purple}h = \cos \ \color{red}x/\color{purple}1 = \cos\ \color{red}x\),它的对边是\(\sin \ \color{green}y/\color{purple}h = \sin \ \color{green}y/\color{purple}1 = \sin\ \color{green}y\)。这样我们获得了能够得到x和y方向的长度的通用公式,它们取决于所给的角度。我们使用它来计算方向向量的分量
![](../img/01/09/camera_pitch.png) ![](../img/01/09/camera_pitch.png)
这个三角形看起来和前面的三角形很像所以如果我们想象自己在xz平面上正望向y轴我们可以基于第一个三角形计算长度/y方向的强度(我们往上或往下看多少)。从图中我们可以看到一个给定俯仰角的y值等于sinθ: 这个三角形看起来和前面的三角形很像所以如果我们想象自己在xz平面上向y轴我们可以基于第一个三角形计算来计算它的长度/y方向的强度(Strength)我们往上或往下看多少。从图中我们可以看到对于一个给定俯仰角的y值等于\(\sin\ \theta\)
```c++ ```c++
direction.y = sin(glm::radians(pitch)); // 注意我们先把角度转为弧度 direction.y = sin(glm::radians(pitch)); // 注意我们先把角度转为弧度
``` ```
这里我们只更新了y值仔细观察x和z元素也被影响了。从三角形中我们可以看到它们的值等于: 这里我们只更新了y值仔细观察x和z分量也被影响了。从三角形中我们可以看到它们的值等于:
```c++ ```c++
direction.x = cos(glm::radians(pitch)); direction.x = cos(glm::radians(pitch));
direction.z = cos(glm::radians(pitch)); direction.z = cos(glm::radians(pitch));
``` ```
看看我们是否能够为偏航角找到需要的元素 看看我们是否能够为偏航角找到需要的分量
![](../img/01/09/camera_yaw.png) ![](../img/01/09/camera_yaw.png)
就像俯仰角一样我们可以看到x元素取决于cos(偏航角)的值z值同样取决于偏航角的正弦值。把这个加到前面的值中会得到基于俯仰角和偏航角的方向向量 就像俯仰角的三角形一样我们可以看到x分量取决于`cos(yaw)`的值z值同样取决于偏航角的正弦值。把这个加到前面的值中会得到基于俯仰角和偏航角的方向向量
!!! note "译注" !!! note "译注"
这里的球坐标与笛卡尔坐标的转换把x和z弄反了如果你去看最后的源码会发现作者在摄像机源码那里写了`yaw = yaw 90`实际上在这里x就应该是`sin(glm::radians(yaw))`z也是同样处理当然也可以认为是这个诡异的坐标系但是在这里使用球坐标转笛卡尔坐标有个大问题就是在初始渲染时无法指定摄像机的初始朝向还要花一些功夫自己实现这个此外这只能实现像第一人称游戏一样的简易摄像机类似Maya、Unity3D编辑器窗口的那种摄像机还是最好自己设置摄像机的位置、上、右、前轴在旋转时用四元数对这四个变量进行调整才能获得更好的效果而不是仅仅调整摄像机前轴。 这里的球坐标与笛卡尔坐标的转换把x和z弄反了如果你去看最后的源码会发现作者在摄像机源码那里写了`yaw = yaw 90`实际上在这里x就应该是`sin(glm::radians(yaw))`z也是同样处理当然也可以认为是这个诡异的坐标系但是在这里使用球坐标转笛卡尔坐标有个大问题就是在初始渲染时无法指定摄像机的初始朝向还要花一些功夫自己实现这个此外这只能实现像第一人称游戏一样的简易摄像机类似Maya、Unity3D编辑器窗口的那种摄像机还是最好自己设置摄像机的位置、上、右、前轴在旋转时用四元数对这四个变量进行调整才能获得更好的效果而不是仅仅调整摄像机前轴。
```c++ ```c++
direction.x = cos(glm::radians(pitch)) * cos(glm::radians(yaw));//译注direction代表摄像机的“前”轴,但此前轴是和本文第一幅图片的第二个摄像机的direction是相反的 direction.x = cos(glm::radians(pitch)) * cos(glm::radians(yaw)); // 译注direction代表摄像机的前轴(Front),这个前轴是和本文第一幅图片的第二个摄像机的方向向量是相反的
direction.y = sin(glm::radians(pitch)); direction.y = sin(glm::radians(pitch));
direction.z = cos(glm::radians(pitch)) * sin(glm::radians(yaw)); direction.z = cos(glm::radians(pitch)) * sin(glm::radians(yaw));
``` ```
这样我们就有了一个可以把俯仰角和偏航角转化为用来自由旋转的摄像机的3个维度的方向向量了。你可能会奇怪:我们怎么得到俯仰角和偏航角? 这样我们就有了一个可以把俯仰角和偏航角转化为用来自由旋转视角的摄像机的3方向向量了。你可能会奇怪:我们怎么得到俯仰角和偏航角?
## 鼠标输入 ## 鼠标输入

View File

@@ -37,4 +37,4 @@
- **裁剪空间(Clip Space)** 所有的坐标都是从摄像机视角观察的但是该空间应用了投影。这个空间应该是一个顶点坐标最终的空间作为顶点着色器的输出。OpenGL负责处理剩下的事情裁剪/透视除法)。 - **裁剪空间(Clip Space)** 所有的坐标都是从摄像机视角观察的但是该空间应用了投影。这个空间应该是一个顶点坐标最终的空间作为顶点着色器的输出。OpenGL负责处理剩下的事情裁剪/透视除法)。
- **屏幕空间(Screen Space)** 所有的坐标都由屏幕视角来观察。坐标的范围是从0到屏幕的宽/高。 - **屏幕空间(Screen Space)** 所有的坐标都由屏幕视角来观察。坐标的范围是从0到屏幕的宽/高。
- **LookAt矩阵** 一种特殊类型的观察矩阵,它创建了一个坐标系,其中所有坐标都根据从一个位置正在观察目标的用户旋转或者平移。 - **LookAt矩阵** 一种特殊类型的观察矩阵,它创建了一个坐标系,其中所有坐标都根据从一个位置正在观察目标的用户旋转或者平移。
- **欧拉角(Euler Angles)** 被定义为偏航角(yaw),俯仰角(pitch),和滚角(roll)从而允许我们通过这三个值构造任何3D方向。 - **欧拉角(Euler Angles)** 被定义为偏航角(Yaw),俯仰角(Pitch),和滚角(Roll)从而允许我们通过这三个值构造任何3D方向。

View File

@@ -159,6 +159,19 @@
- Depth Testing深度测试 - Depth Testing深度测试
- Depth Buffer深度缓冲 - Depth Buffer深度缓冲
## 01-09
- Direction Vector方向向量
- Camera Space摄像机空间
- Gram-Schmidt Process格拉姆—施密特正交化
- LookAt MatrixLookAt矩阵
- Strafe(?)扫射
- Deltatime时间差
- Euler Angles欧拉角
- Pitch俯仰角
- Yaw偏航角
- Roll滚转角
## 06-01 ## 06-01
- Debugging调试 - Debugging调试