1. 相机/视空间
-
要定义一个相机我们需要相机在世界空间的位置(也称位置矢量),相机的指向(也称方向矢量),一个指向相机右侧和一个指向相机上方的矢量。这相当于创建以相机位置为原点,由三条相互垂直的单位坐标轴构成的坐标系统。(图片取自书中)
相机定义
-
注意:z轴的正向是从屏幕指向你,所以如果我们想要向后移动相机,我们需要沿着正向z轴移动。
-
相机位置:世界空间中指向相机位置的矢量。设置相机位置代码如下:
glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f);
- 因为我们让相机指向世界空间的原点,所以我们用世界空间原点矢量减去相机位置矢量就得到相机指向矢量。代码如下:
glm::vec3 cameraTarget = glm::vec3(0.0f, 0.0f, 0.0f);
glm::vec3 cameraDirection = glm::normalize(cameraPos - cameraTarget);
- 相机右向矢量:代表相机空间中的正向x轴。要获取右向矢量,我们先在世界空间中指定一个垂直向上的矢量,然后将该矢量与相机的方向矢量叉积。(变换中讲过,叉积的结果是与输入矢量都垂直的矢量)
glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f);
glm::vec3 cameraRight = glm::normalize(glm::cross(up, cameraDirection));
- 相机向上矢量:由相机指向矢量和右向矢量叉积获得。
glm::vec3 cameraUp = glm::cross(cameraDirection, cameraRight);
2. Look At
- 如果使用相互垂直的3条坐标轴定义一个坐标空间,我们可以使用这3条坐标轴外加一个平移矢量创建一个矩阵,通过与该矩阵相乘可以将任何矢量转换到我们定义的坐标空间。这个矩阵就叫做LookAt矩阵。通过上面对相机空间的讲述,我们可以利用相机的3条相互垂直轴和相机位置矢量来定义这样一个LookAt矩阵:
其中,是相机右向矢量,是相机向上矢量,是相机方向矢量而是相机位置矢量。 - 注意:LookAt矩阵中的旋转(左侧矩阵)与平移(右侧矩阵)是颠倒的(分别是转置和负的),因为相机的旋转和移动相当于世界空间中物体在反方旋转和移动。
- 使用相机空间的LookAt矩阵,我们就可以将世界空间坐标转换到我们定义的视空间。
- 使用GLM的
glm::lookAt
函数创建LookAt矩阵,该函数分别需要一个位置,目标和向上矢量。下面创建一个与坐标系统章节一样的视矩阵。
glm::mat4 view;
view = glm::lookAt(glm::vec3(0.0f, 0.0f, 3.0f),
glm::vec3(0.0f, 0.0f, 0.0f),
glm::vec3(0.0f, 1.0f, 0.0f));
- 修改相机位置(在x-z平面上按一定半径随时间旋转)
const float radius = 10.0f;
float camX = sin(glfwGetTime()) * radius;
float camZ = cos(glfwGetTime()) * radius;
glm::mat4 view;
view = glm::lookAt(glm::vec3(camX, 0.0f, camZ), glm::vec3(0.0f, 0.0f, 0.0f),
glm::vec3(0.0f, 1.0f, 0.0f));
-
渲染效果
移动相机位置1
移动相机位置2
3. 用键盘控制相机位置
- 3.1 设置相机系统,定义与相机相关的变量
glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f);
glm::vec3 cameraFront = glm::vec3(0.0f, 0.0f, -1.0f);
glm::vec3 cameraUp = glm::vec3(0.0f, 1.0f, 0.0f);
- 3.2 调整LookAt矩阵,其中相机方向矢量为位置矢量与前向矢量相加保证无论我们如何移动相机,相机始终指向目标方向。
view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);
- 3.3 处理键盘输入
void processInput(GLFWwindow* window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
const float cameraSpeed = 2.5f * deltaTime; // 根据需要调整
if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
cameraPos += cameraSpeed * cameraFront;
if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
cameraPos -= cameraSpeed * cameraFront;
if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
cameraPos -= glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed;
if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
cameraPos += glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed;
}
- 最终效果
- 按下
w
键:向前移动物体(看着像放大) - 按下
s
键:向后移动物体 - 按下
a
键:向右侧移动物体 - 按下
d
键:向左侧移动物体
- 按下
4. 改进相机移动速度
- 问题:现实中,不同人的计算机有不同的处理能力,这导致某些计算机在一秒内会比另一些计算机渲染更多的帧。渲染更多帧同时也会处理更多次键盘输入,这产生一些计算机相机移动很快而有些很慢的差异。
- 增量时间(deltatime):渲染上一帧的时间。
- 解决不同计算机相机移动速度问题:用deltatime乘以速度对相机速度进行平衡。
- 计算deltatime值
// 定义两个追踪变量(全局)
float deltaTime = 0.0f; // 当前帧与上一帧的时间
float lastFrame = 0.0f; // 上一帧的渲染时间
// 计算(渲染循环中)
float currentTime = glfwGetTime();
deltaTime = currentTime - lastFrame;
lastFrame = currentTime;
- 调整相机速度计算
const float cameraSpeed = 2.5f * deltaTime;
5. 鼠标控制相机
5.1 欧拉角与相机的方向矢量
-
欧拉角(Euler angles):是代表3D空间任意旋转的3个值,有3种欧拉角:pitch, yaw和roll。(图片取自书中)
- pitch:描述向上下看的角度。
- yaw:表示向左右看的大小。
-
roll:代表滚动了多少(经常用于航天相机)。
欧拉角
- 基本的三角几何知识:当斜边(hypotenuse)长度为1时,(图片取自书中)
邻边长度为:
(上述公式取自书中,个人感觉其实就是:,其中为邻边长度。)
对边长度为
(上述公式取自书中,个人感觉其实就是:,其中为对边长度。)
三角几何 - 根据
yaw
值计算方向矢量的x和z分部:从欧拉角的图示中我们可以知道,yaw
角度是绕y轴旋转的角度值。如下图所示,我们按逆时针方向旋转yaw角度,根据三角几何知识我们就可以计算出方向矢量x和z分部的值。(图片取自书中)
yaw值计算
glm::vec3 direction;
direction.x = cos(glm::radians(yaw));
direction.z = sin(glm::radians(yaw));
- 根据
pitch
值计算方向矢量的x和z分部:从欧拉角的图示中我们可以知道,pitch
角度是绕x轴旋转的角度值。与yaw
值计算相似,我们可以计算出方向矢量y分部的值。(图片取自书中)
Pitch值计算
direction.y = sin(glm::radians(pitch));
- 从
pitch
的图示中我们可以看到x和z分部都受到的影响,结合yaw
和pitch
值,我们可以计算出方向矢量的各个分部:
direction.x = cos(glm::radians(yaw) * cos(glm::radians(pitch)));
direction.y = sin(glm::radians(pitch));
direction.z = sin(glm::radians(yaw) * cos(glm::radians(pitch)));
- 从
yaw
值图示中我们可以看到,当将yaw
值设置为-90度时,正好指向z轴的负向。
// yaw初始值
yaw = -90.0f;
5.2 通过鼠标设置相机方向矢量
下面通过获取鼠标的水平移动设置yaw
值,通过鼠标的垂直移动设置pitch
值。
- 设置捕获和隐藏光标
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
- 创建鼠标移动回调函数
// 函数原型
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
- 注册鼠标移动回调函数
glfwSetCursorPosCallback(window, mouse_callback);
-
- 计算相机方向矢量
- 4.1. 计算与上一帧的鼠标偏移量。
- 4.2. 将偏移量添加到
yaw
和pitch
值。 - 4.3. 控制
pitch
的最小和最大值。 - 4.4. 计算方向矢量。
// 定义全局变量
float lastX = 400, lastY = 300; // 初始化为屏幕中心位置
bool firstMouse = true;
float yaw = -90.0f;
float pitch = 0.0f;
//---------------------回调函数
// 放置初次进入出现跳跃
if (firstMouse)
{
lastX = xpos;
lastY = ypos;
firstMouse = false;
}
// 4.1 计算偏移量
float xoffset = xpos - lastX;
float yoffset = lastY - ypos; // 相反,y是从底到上
lastX = xpos;
lastY = ypos;
// 放置鼠标移动太过距离
const float sensitivity = 0.1f;
xoffset *= sensitivity;
yoffset *= sensitivity;
// 4.2
yaw += xoffset;
pitch += yoffset;
// 4.3
if (pitch > 89.0f)
pitch = 89.0f;
if (pitch < -89.0f)
pitch = -89.0f;
// 4.4
glm::vec3 direction;
direction.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch));
direction.y = sin(glm::radians(pitch));
direction.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch));
cameraFront = glm::normalize(direction);
5.3 鼠标滚轮进行变焦(zoom)
- 坐标系统章节中我们讲到了视野(fov: field of view) 定义了我们所看到的场景的大小,当视野变小则场景变小,但是变小的场景被投影到相同的NDC上,会产生一种放大(zoom in)的错觉。
- 鼠标滚轮回调函数注册与鼠标移动相似
glfwSetScrollCallback(window, scroll_callback);
- 通过滚轮回调函数设置视野值大小
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
fov -= (float)yoffset;
// 将视野限制在1.0到45.0之间
if (fov < 1.0f)
fov = 1.0f;
if (fov > 45.0f)
fov = 45.0f;
}
- 将视野值应用到投影矩阵
glm::mat4 projection = glm::mat4(1.0f);
projection = glm::perspective(glm::radians(fov), 800.0f / 600.0f, 0.1f, 100.0f);
网友评论