This commit is contained in:
Mouse
2018-06-27 23:04:50 +08:00
parent e3f8df0b56
commit 1cad6db4fb
2 changed files with 44 additions and 48 deletions

View File

@@ -1,40 +1,40 @@
# 摄像机Camera
这个章节我们将学如何渲染3D场景的画面这个能力就像一个摄像机可以在3D世界穿梭,然而实际上是用来说明他的一种编程语言
本章中,我们将学如何在三维场景中移动,就像一个可以在三维世界穿梭的摄像机,然而实际上这就是描述它的专用术语
但是如果你尝试在OpenGL寻找中这些特定的摄像机功能,你会发现这根本不是摄像机,换句话说摄像机一直是固定住的,以屏幕\(0, 0, 0\)的位置为中心点
但是如果你尝试在OpenGL寻找中的摄像机功能你会发现这根本不是摄像机换句话说摄像机一直是固定以屏幕(0, 0, 0)的中心点的位置。
之所以这样,我们应该模拟出一个摄像机可以在三维空间中移动的摄像机。但是如何做到呢?但是摄像机是不能移动的我们必须要移动全部的实体在我们的3D世界中。换句话说,如果移动不了摄像机我们得移动整个世界。
因此,我们需要模拟出一个可以在三维空间中移动的摄像机。但是怎么做呢?摄像机是不能移动的,因此我们必须要移动世界中的全部的实体。换句话说,如果移动不了摄像机移动整个世界。
因此,假设我们沿着Z轴移动摄像机从\(Cx, Cy, Cz\)到\(Cx, Cy, Cz+dz\),从而靠近在\(Ox, Oy, Oz\)放置的目标
因此,假设摄像机从(Cx, Cy, Cz)沿着Z轴移动到(Cx, Cy, Cz+dz),从而靠近在(Ox, Oy, Oz)放置的物体。
![摄像机的运动](_static/08/camera_movement.png)
我们要做的是如何精确的移动摄像机移动到相反的方向\(在我们的3D空间中的所有物体\)。想想看,其实就像物体在跑步机上跑步一样。
我们要做的是如何向相反的方向精确地移动物体(在三维空间中的所有物体)。换句话说,其实物体就像在跑步机上跑步一样。
![实际的运动](_static/08/actual_movement.png)
摄像机可以沿着三个轴\(x, y and z\),也可以沿着他们旋转\(滚动, 俯视和偏斜"yaw"\).
摄像机可以沿着三个轴(x, y和z)移动,也可以绕着它们旋转(翻滚(`roll`)、俯仰(`pitch`)和偏航(`yaw`))。
![侧倾和偏](_static/08/roll_pitch_yaw.png)
![俯仰和偏](_static/08/roll_pitch_yaw.png)
所以从基本上我们必须做的就是让移动和旋转对于我们所设置的3D世界全部体。我们应该怎么做?答案是用另外一种转化方法,把他变化所有在摄像机运动方向上相反的顶点,从而根据摄像机的旋转进而旋转们。当然,这将要用另外一个矩阵,所谓的视图矩阵来完成。这个矩阵首先行平移,然后沿着轴线进行旋转。
所以从基本上我们做的就是让移动和旋转应用于所设置的三维世界全部体。应该怎么做?答案是用另外一种变换,让它变换所有在摄像机运动方向上相反的顶点,根据摄像机的旋转旋转们。当然,这将要用另外一个矩阵,所谓的观察矩阵(`View Matrix`)来完成。这个矩阵首先行平移,然后沿着轴线进行旋转。
让我们看看如何构造这个矩阵。如果你记得变章节第6章转换方程式这样的:
看看如何构造这个矩阵。如果你记得变章节第6章变换矩阵:
$$Transf = \lbrack ProjMatrix \rbrack \cdot \lbrack TranslationMatrix \rbrack \cdot \lbrack RotationMatrix \rbrack \cdot \lbrack ScaleMatrix \rbrack = \lbrack ProjMatrix \rbrack \cdot \lbrack WorldMatrix \rbrack$$
这投影的相乘矩阵之前,应该先应用视图矩阵,之后我们的方程式应该是这样的:
使用观察矩阵之前,应将它与投影矩阵相乘,之后矩阵应该是这样的:
$$Transf = \lbrack ProjMatrix \rbrack \cdot \lbrack ViewMatrix \rbrack \cdot \lbrack TranslationMatrix \rbrack \cdot \lbrack RotationMatrix \rbrack \cdot \lbrack ScaleMatrix \rbrack = \lbrack ProjMatrix \rbrack \cdot \lbrack ViewMatrix \rbrack \cdot \lbrack WorldMatrix \rbrack $$
现在有三个矩阵,我们应该思考一下这些矩阵的生命周期。在我们的游戏运行的时候,投影矩阵不应该改变的太多,在情况最不好的时候,每渲染都要调用才可能改变一次。如果摄像机移动,则视图矩阵可以在每次渲染改变一次。视图矩阵每`GameItem`改变一次,所以每次渲染调用都会改变许多次。
现在已经有三个矩阵,我们应该思考一下这些矩阵的生命周期。在游戏运行的时候,投影矩阵不应该更改太多,最坏的情况,每渲染可能改变一次。如果摄像机移动,则观察矩阵可以在每次渲染改变一次。世界矩阵每渲染一个`GameItem`改变一次,所以每次渲染调用都会改变许多次。
因此,如何把每一个矩阵到顶点着色器呢?可能会看到一些代码,使用三个统一的每一个矩阵,但原则上,最有效的方法是合投影和视图矩阵,我们称之`PV`矩阵,并推动`world``PV`矩阵到我们的着色器。通过这种方法,我们将有可以与世界坐标一起进行并且可以避免一些额外的运算。
因此,如何把每一个矩阵传递到顶点着色器呢?可能会看到一些代码,为三个矩阵分别定义一个Uniform但理论上,最有效的方法是合投影和观察矩阵,将其称`PV`矩阵,并传递`world``PV`矩阵到着色器。这样,将可以与世界坐标一起运算,并且可以避免一些额外的运算。
实际上,最方便的方法是将视图与世界矩阵相合。为什么会这样?因为要记住整个摄像机的概念就是戏法,要做的是动整个世界来模拟世界的位移和只显示一小部分的3D世界。因此,如果直接联合世界坐标一起工作,这样可能会引起远离中心点的世界坐标系,会遇到一些精度问题。如果在所谓的摄像机空间中工作利用点的性质,虽然远离世界的中心点,但也靠近摄像机。可以将视图和世界矩阵相结合的矩阵通常被称为模型视图矩阵
实际上,最方便的方法是将观察矩阵与世界矩阵相合。为什么会这样?因为要记住整个摄像机的概念就是戏法,要做的是动整个世界来模拟摄像机的位移和只显示一小部分的三维世界。因此,如果直接世界坐标一起处理,可能会远离中心点的世界坐标遇到一些精度问题。如果在所谓的摄像机空间中工作利用点的性质,虽然远离世界的中心点,但也靠近摄像机。可以将观察和世界矩阵相结合的矩阵称为模型观察矩阵(`Model View Matrix`)
让我们开始修改代码来支持摄像机。首先,先创建一个新的类,称`Camera`,它将确保持相机的位置与旋转的方向。该类将提供位置或旋转方向\(`setPosition` or `setRotation`\)的方法,或在当前状态\(`movePosition` and `moveRotation`\)用偏移量更新这些值。
开始修改代码实现摄像机。首先,先创建一个`Camera`的类,它将储存摄像机的位置与旋转的方向。该类将提供设置位置或旋转方向\(`setPosition` `setRotation`\)的方法,或在当前状态\(`movePosition` `moveRotation`\)用偏移量更新这些值。
```java
@@ -98,13 +98,13 @@ public class Camera {
}
```
下一步 `Transformation` 到类中,将一个新矩阵来保存视图矩阵的数值
接下来在`Transformation`中,将添加一个新矩阵来储存观察矩阵
```java
private final Matrix4f viewMatrix;
```
我们要提供一更新这个值的方法。与投影矩阵一样,这个矩阵对于渲染周期中要渲染的所面对的对象都是相同的。
我们要提供一更新这个值的方法。与投影矩阵一样,这个矩阵对于渲染周期中所面对的对象都是相同的。
```java
public Matrix4f getViewMatrix(Camera camera) {
@@ -121,15 +121,11 @@ public Matrix4f getViewMatrix(Camera camera) {
}
```
正如你所看到的,我们首先需要做旋转,然后翻译。如果我们做相反的事情,我们不会沿着摄像机位置旋转,而是沿着坐标原点旋转。请注意,在`Camera`类的`movePosition`方法中,我们不只是简单地增加机位置的偏移量。我们还考虑了沿Y轴的旋转偏航以便计算最终位置。如果我们只是通过偏移来增加机的位置,机就不会朝着它的方向移动。
正如你所的,我们首先需要做旋转,然后变换。如果操作顺序相反,我们不会沿着摄像机位置旋转,而是沿着坐标原点旋转。请注意,在`Camera`类的`movePosition`方法中,我们不只是简单地增加摄像机位置的偏移量。我们还考虑了沿Y轴的旋转偏航以便计算最终位置。如果我们只是通过偏移来增加摄像机的位置,摄像机就不会朝着它的方向移动。
除了上面提到的,我们现在还没有得到一个可以完全自由移动的摄像机\(例如如果我们沿着X轴旋转当我们向前移动时摄像机不会在空中向上或向下移动\)。这将在之后的章节中完成,因为这有点复杂。
正如你所看到的我们首先需要做旋转然后翻译。如果我们做相反的事情我们不会沿着摄像机位置旋转而是沿着坐标原点旋转。请注意在“摄像机”类的“移动位置”方法中我们不只是简单地增加相机位置的偏移量。我们还考虑了沿Y轴的旋转偏航以便计算最终位置。如果我们只是通过偏移来增加相机的位置相机就不会朝着它的方向移动。
除了上面提到的,我们这里没有一个完全自由移动的摄像机\(例如如果我们沿着X轴旋转当我们向前移动时摄像机不会在空中向上或向下移动\)。这将在后面的章节中完成,因为这有点复杂。
最后,我们将删除以前的方法`getWorldMatrix`,并添加一个新的名为`getModelViewMatrix`的方法。
最后,我们将移除之前的`getWorldMatrix`方法,并添加一个名为`getModelViewMatrix`的新方法。
```java
public Matrix4f getModelViewMatrix(GameItem gameItem, Matrix4f viewMatrix) {
@@ -144,11 +140,9 @@ public Matrix4f getModelViewMatrix(GameItem gameItem, Matrix4f viewMatrix) {
}
```
`getModelViewMatrix`方法将在每个`GameItem`实例中调用,因此我们必须对观察矩阵的副本进行处理,因此在每次调用中都不会积累转换\(记住`Matrix4f`类不是不可变的\)。
这个`getModelViewMatrix`方法将在每个`GameItem`实例中调用,因此我们必须对视图矩阵的副本进行处理,因此在每次调用中都不会积累转换\(记住`Matrix4f`类不是不可变的\).
`Renderer`类的`render`方法中,在投影矩阵更新之后,我们只需要根据摄像机的更新视图矩阵的值,。
`Renderer`类的`render`方法中,在投影矩阵更新之后只需要根据摄像机的值更新观察矩阵。
```java
// Update projection Matrix
@@ -169,15 +163,15 @@ for(GameItem gameItem : gameItems) {
}
```
就是这样对于基本代码支持摄像机的概念。现在我们需要用它。这样可以改变输入处理和更新机的方式。我们将设置以下控件
* 键“A”和“D”到移动摄像机的左边和右边\(x axis\)。
* 键“W”和“S”到移动摄像机的前面和后面\(z axis\)。
* 键“Z”和“X”到移动摄像机的上面和下面的\(y axis\)。
就是我们实现摄像机的基本代码,现在我们需要用它。我们Key更改输入处理和更新摄像机的方式。我们将设置以下按键
* “A”和“D”键使摄像机左右\(x轴\)移动。
* “W”和“S”键使摄像机前后\(z轴\)移动。
* “Z”和“X”键使摄像机上下\(y轴\)移动。
当鼠标按下右键时我们可以使用鼠标位置沿X和Y轴旋转摄像机。
正如你所看到的,我们将首次使用鼠标。我们将创建一个名为`MouseInput`的新类,该类将封装鼠标访问的代码。
正如你所看到的,我们将首次使用鼠标。创建一个名为`MouseInput`的新类,该类将封装鼠标处理的代码。
```java
package org.lwjglb.engine;
@@ -253,18 +247,15 @@ public class MouseInput {
}
```
`MouseInput`类提供了一个初始化过程中应该调用的`init`方法,并注册一组回调来处理鼠标事件:
`MouseInput`类提供了一个初始化过程中应该调用的`init`方法,并注册一组回调来处理鼠标事件:
* `glfwSetCursorPosCallback`:注册一个回调,该回调将在鼠标移动时被调用。
* `glfwSetCursorEnterCallback`:注册一个回调,该回调将在鼠标进入窗口时被调用。即使鼠标不在窗口内,我们也会收到鼠标事件。当鼠标在窗口内时,使用这个回调来确认鼠标进入窗口。
* `glfwSetMouseButtonCallback`:注册一个回调,该回调在按下鼠标按钮时被调用。
* `glfwSetCursorPosCallback`:注册一个监听器,该监听器将在鼠标移动时调用。
`MouseInput`类提供了一个`input`方法,在处理游戏输入时应调用该方法。该方法计算鼠标从先前位置的位移,并将其存储到`Vector2f`类型的`displVec`变量中,以便它被游戏使用。
* `glfwSetCursorEnterCallback`:注册一个监听器,该监听器将在鼠标进入我们的窗口时调用。即使鼠标不在我们的窗口,我们也会收到鼠标事件。当鼠标在我们的窗口中时,我们使用这个监听器来进行跟踪。
* `glfwSetMouseButtonCallback`:注册监听器在按下鼠标按钮时将调用。
`MouseInput`类提供了一种输入方法,在处理游戏输入时应该调用该方法。该方法计算鼠标从先前位置的位移,并将其存储到 `Vector2f` `displVec`变量中,以便它可以被我们的游戏使用。
`MouseInput`类将被实例化在我们的`GameEngine`类中,并且将作为游戏实现的`init``update`方法中的参数传递(因此我们需要相应地更改接口)。
`MouseInput`类将在`GameEngine`类中被实例化,并且将作为游戏实现的`init``update`方法中的参数传递(因此需要相应地更改接口)。
```java
void input(Window window, MouseInput mouseInput);
@@ -272,7 +263,7 @@ void input(Window window, MouseInput mouseInput);
void update(float interval, MouseInput mouseInput);
```
鼠标输入将在`GameEngine`类的输入方法并传送控制到游戏执行前被处理
鼠标输入将在`GameEngine`类的`input`方法中被处理,而最终的控制将交由游戏实现
```java
protected void input() {
@@ -281,7 +272,7 @@ protected void input() {
}
```
现在已经准备好更新我们的`DummyGame`类处理键盘和鼠标输入。 该类的输入方法将如下所示:
现在已经准备好更`DummyGame`处理键盘和鼠标输入。该类的输入方法将如下所示:
```java
@Override
@@ -305,7 +296,9 @@ public void input(Window window, MouseInput mouseInput) {
}
```
现在,已经准备好更新我们的`DummyGame`类处理键盘和鼠标输入。 该类的输入方法将如下所示:
这将更新一个名为`cameraInc``Vector3f`变量,它储存了摄像机应有的位移。
`DummyGame`类的`update`方法将根据处理键盘和鼠标事件,来修改摄像机的位置和旋转。
```java
@Override
@@ -322,7 +315,8 @@ public void update(float interval, MouseInput mouseInput) {
}
}
```
现在我们可以为我们的世界添加更多的立方体,将它们放置在特定位置并使用我们的新相机进行播放。 正如你可以看到所有的立方体共享相同的网格。
现在可以向世界添加更多的立方体,将它们放在特定位置并使用新摄像机进行观察。正如你所见的,所有的立方体共享相同的`Mesh`实例。
```java
GameItem gameItem1 = new GameItem(mesh);
@@ -344,7 +338,7 @@ gameItem4.setPosition(0.5f, 0, -2.5f);
gameItems = new GameItem[]{gameItem1, gameItem2, gameItem3, gameItem4};
```
你会得到这样的东西
你会得到如下所示的东西
![方块](_static/08/cubes.png)

View File

@@ -11,3 +11,5 @@
04-rendering
05-more-on-rendering
06-transformations
08-camera
10-let-there-be-light