LearnOpenGL学习笔记(四) - 摄像机与入门知识点总结

本文最后更新于 2024年7月15日 晚上

逐渐理解一切…

摄像机

定义一个摄像机,需要获取其世界坐标、观察方向、指向其右侧与上方的方向向量。

  • 世界坐标:与观察矩阵所使用的变换相同。

    • glm::vec3 cameraPos = glm::vec3(0.0f,0.0f,3.0f)
  • 观察方向:借助矢量相减,获取摄像机位置与世界原点之间的方向向量。

    • glm::vec3 cameraDir = glm::normalize(cameraPos - vec3(0.0f,0.0f,0.0f))
    • 这样得到的实际上是观察方向的反方向。
  • 右轴:将上向量与观察方向叉乘。

    1
    2
    glm::vec3 up = glm::vec3(0.0f,1.0f,0.0f);
    glm::vec3 cameraRight = glm::normalize(glm::cross(up,cameraDir));
  • 上轴:将右轴与观察方向叉乘。

glm::vec3 cameraUp = glm::normalize(glm::cross(cameraRight,cameraDir))

上面的操作定义了一个额外的坐标空间。

这个操作被称为格拉姆-施密特正交化(Gram-Schmidt Process)

LookAt矩阵

坐标空间被定义后,便可以创建一个矩阵。把这个矩阵乘以任意向量,便可以把这个向量变换到我们定义的坐标空间。在摄像机空间中,这个矩阵被称为LookAt矩阵。

image-20240714112018261

其中,R为相机右轴,U为相机上轴,D为摄像机方向向量(相反,由原点指向相机),P为摄像机位置。

GLM提供了快速创建LookAt矩阵的方法。

1
2
3
4
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)); //参数三:世界空间的上向量。

一般,我们会把三个参数以全局变量的形式定义。

1
2
3
4
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);
view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);

对于任何仅表示方向的向量,都应该做正交化处理。

Deltatime

Deltatime变量存储了渲染上一帧需要的时间。将该变量引入移动速度的计算,就可以做到每帧的移动速度在各种设备上相对平衡。

1
2
3
float currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;

视角旋转

欧拉角分为三种:

  • 俯仰角(Pitch):如何往上或往下看的角,绕x轴旋转。
  • 偏航角(Yaw):往左和往右看的程度,绕y轴旋转。
  • 滚转角(Roll):翻滚摄像机的程序,绕z轴旋转。

在Unity和其他引擎中,一般不关心滚转角。

1
2
3
direction.x = cos(glm::radians(pitch)) * cos(glm::radians(yaw)); // 译注:direction代表摄像机的前轴(Front),这个前轴是和本文第一幅图片的第二个摄像机的方向向量是相反的
direction.y = sin(glm::radians(pitch));
direction.z = cos(glm::radians(pitch)) * sin(glm::radians(yaw));

理解上面的算法:

img

在x/z平面,可以计算水平距离和垂直距离。水平距离是cos(pitch),垂直距离(即最终的y分量)是sin(pitch)。

完成水平距离计算后,还要计算x分量和z分量。从上往下看x/z平面:

img

可以知道x分量是cos(yaw),z分量是sin(yaw)

对于鼠标输入,其水平移动影响Yaw角,竖直移动影响Pitch角。通过计算每一帧鼠标在垂直和水平方向与上一帧的插值,就可以得到具体的pitch和raw角。

glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED)让当前拥有焦点的窗口隐藏并捕捉鼠标。

捕捉(Capture)意味着,无论鼠标如何移动,都不会离开窗口范围。

定义鼠标移动回调函数:void mouse_callback(GLFWwindow* window, double xpos, double ypos),并将其注册到glfwSetCursorPosCallback(window, mouse_callback)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
void mouse_callback(GLFWwindow* window, double xpos, double ypos)
{
//判断鼠标是否第一次进入窗口。如果不设置,会发生鼠标第一次进入窗口时突然跳一下的情况。
if(firstMouse)
{
lastX = xpos;
lastY = ypos;
firstMouse = false;
}

//计算鼠标x、y方向的增量
float xoffset = xpos - lastX;
//注意:这里要反过来加,否则是空战游戏的操作模式。
float yoffset = lastY - ypos;
lastX = xpos;
lastY = ypos;

//灵敏度配置
float sensitivity = 0.05;
xoffset *= sensitivity;
yoffset *= sensitivity;

yaw += xoffset;
pitch += yoffset;

//限制pitch,防止视角偏转。空战类游戏可能不需要限制
if(pitch > 89.0f)
pitch = 89.0f;
if(pitch < -89.0f)
pitch = -89.0f;

//根据三角学原理,修改相机朝向。要注意弧度角度转换
glm::vec3 front;
front.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch));
front.y = sin(glm::radians(pitch));
front.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch));
cameraFront = glm::normalize(front);
}

视角缩放

缩放作用于FOV,FOV是投影矩阵范畴的概念,所以缩放是改变的投影矩阵。

1
2
3
4
5
6
7
8
9
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
if(fov >= 1.0f && fov <= 45.0f)
fov -= yoffset;
if(fov <= 1.0f)
fov = 1.0f;
if(fov >= 45.0f)
fov = 45.0f;
}

projection = glm::perspective(glm::radians(fov), 800.0f / 600.0f, 0.1f, 100.0f);

glfwSetScrollCallback(window, scroll_callback);

手动计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
glm::mat4 GetViewMatrix()
{
// 重新计算摄像机的 Right 和 Up 向量
Right = glm::normalize(glm::cross(Front, WorldUp));
Up = glm::normalize(glm::cross(Right, Front));

// 计算LookAt矩阵
glm::mat4 rotation(1.0f);
rotation[0][0] = Right.x;
rotation[1][0] = Right.y;
rotation[2][0] = Right.z;
rotation[0][1] = Up.x;
rotation[1][1] = Up.y;
rotation[2][1] = Up.z;
//负的是因为,实际上变的是物体而不是摄像机
rotation[0][2] = -Front.x;
rotation[1][2] = -Front.y;
rotation[2][2] = -Front.z;

glm::mat4 translation(1.0f);
translation[3][0] = -Position.x;
translation[3][1] = -Position.y;
translation[3][2] = -Position.z;

return rotation * translation;
}

知识点

  • 相机空间的施密特正交化:相机位置、方向向量(目标指向相机)、右轴、上轴
  • LookAt矩阵与观察矩阵:view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp)
    • 此处cameraFront为相机指向目标的向量
  • Deltatime控制速度
1
2
3
direction.x = cos(glm::radians(pitch)) * cos(glm::radians(yaw));
direction.y = sin(glm::radians(pitch));
direction.z = cos(glm::radians(pitch)) * sin(glm::radians(yaw));
  • 鼠标输入
    • glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED)
    • glfwSetCursorPosCallback(window, mouse_callback)
    • pitch角的限制,注意弧度转换
    • 初次鼠标进入时的判断
  • 滚轮、FOV与投影矩阵

复习

  • OpenGL: 一个定义了函数布局和输出的图形API的正式规范。

除OpenGL外,还有DirectX 11,DirectX 12,Metal,Vulkan等图形API。在教程中,我们使用的OpenGL版本是3.3。

我们使用GLFW管理窗口相关的API。

  • GLAD: 一个扩展加载库,用来为我们加载并设定所有OpenGL函数指针,从而让我们能够使用所有(现代)OpenGL函数。

在OpenGL中,所有函数都是运行时动态确定的,因此,IDE没办法给我们提供编译时语法检查和代码补全。为了解决这一问题,我们引入GLAD库,让上述功能成为可能。

gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)用来加载所有OpenGL函数指针。

  • 视口(Viewport): 我们需要渲染的窗口。

视口与窗口是不同的概念。视口指的是渲染范围,是最终裁剪变换处理的区域。使用glViewport(x,y,width,height)初始化并配置视口。

  • 图形管线(Graphics Pipeline): 一个顶点在呈现为像素之前经过的全部过程。

基本的图形管线可以定义为:顶点数据->顶点着色器->几何着色器->形状装配->光栅化->片段着色器->测试与混合->输出

  • 着色器(Shader): 一个运行在显卡上的小型程序。很多阶段的图形管道都可以使用自定义的着色器来代替原有的功能。
  • 标准化设备坐标(Normalized Device Coordinates, NDC): 顶点在通过在剪裁坐标系中剪裁与透视除法后最终呈现在的坐标系。所有位置在NDC下-1.0到1.0的顶点将不会被丢弃并且可见。

经过顶点着色器处理的坐标必定处于NDC范围内。

  • 顶点缓冲对象(Vertex Buffer Object): 一个调用显存并存储所有顶点数据供显卡使用的缓冲对象。

glDrawArray函数直接使用VBO内的数据进行绘制。

  • 顶点数组对象(Vertex Array Object): 存储缓冲区和顶点属性状态

在不使用VAO时,我们每次想绘制一个物体,都需要手动绑定对应的VBO,设置顶点属性指针。

VAO可以把这些操作保存,并生成一个VAO对象。之后每次想要绘制这个物体,绑定对应的VAO即可。

  • 元素缓冲对象(Element Buffer Object,EBO),也叫索引缓冲对象(Index Buffer Object,IBO): 一个存储元素索引供索引化绘制使用的缓冲对象。

通常,输入的顶点数据不会包含重复的顶点。但对于一些图元来说,它们是共用顶点的。为了简化输入顶点数据,我们使用EBO记录绘制对象所使用的顶点序号。

  • Uniform: 一个特殊类型的GLSL变量。它是全局的(在一个着色器程序中每一个着色器都能够访问uniform变量),并且只需要被设定一次。

使用glUniformnx(Location, value)向某着色器的某个uniform传递值。

location使用glGetUniformLocation(shaderProgram, nameofuniform)获取。

n代表参数个数,x代表数据类型,可以是fv(float数组)、4i(ivec4)等。

向Uniform传值之前,对应的着色器程序必须被use。

  • 纹理(Texture): 一种包裹着物体的特殊类型图像,给物体精细的视觉效果。

glGenTexture(cnt, addr)用于创建纹理对象。

glBindTexture(GL_TEXTURE_2D, textureObj)用于绑定对象到纹理上下文。

glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_X,GL_X)用于设置纹理参数。参数有GL_TEXTURE_WARP和GL_TEXTURE_MIN_FILTER等。

glTexImage2D(GL_TEXTURE_2D,0,IMAGETYPE,width,height,0,IMAGETYPE,GL_UNSIGNED_BYTE,data)用于将纹理数据载入上下文。

glGenerateMipmap(GL_TEXTURE_2D)用于生成当前上下文存储纹理的多级渐远纹理。

glActiveTexture(GL_TEXTUREX)用于激活X号纹理单元。类似于,让GL_TEXTURE_2D目标换到X号槽。

Active操作以后要Bind。

  • 纹理环绕(Texture Wrapping): 定义了一种当纹理顶点超出范围(0, 1)时指定OpenGL如何采样纹理的模式。

  • 纹理过滤(Texture Filtering): 定义了一种当有多种纹素选择时指定OpenGL如何采样纹理的模式。这通常在纹理被放大情况下发生。

  • 多级渐远纹理(Mipmaps): 被存储的材质的一些缩小版本,根据距观察者的距离会使用材质的合适大小。

  • stb_image.h: 图像加载库。

stbi_load(path, &width, &height,&channels,0)

  • 纹理单元(Texture Units): 通过绑定纹理到不同纹理单元从而允许多个纹理在同一对象上渲染。
  • 向量(Vector): 一个定义了在空间中方向和/或位置的数学实体。
  • 矩阵(Matrix): 一个矩形阵列的数学表达式。
  • GLM: 一个为OpenGL打造的数学库。
  • 局部空间(Local Space): 一个物体的初始空间。所有的坐标都是相对于物体的原点的。

局部空间类似于Unity的Transform属性的参数。

  • 世界空间(World Space): 所有的坐标都相对于全局原点。

  • 观察空间(View Space): 所有的坐标都是从摄像机的视角观察的。

  • 裁剪空间(Clip Space): 所有的坐标都是从摄像机视角观察的,但是该空间应用了投影。这个空间应该是一个顶点坐标最终的空间,作为顶点着色器的输出。OpenGL负责处理剩下的事情(裁剪/透视除法)。

  • 屏幕空间(Screen Space): 所有的坐标都由屏幕视角来观察。坐标的范围是从0到屏幕的宽/高。

  • LookAt矩阵: 一种特殊类型的观察矩阵,它创建了一个坐标系,其中所有坐标都根据从一个位置正在观察目标的用户旋转或者平移。

  • 欧拉角(Euler Angles): 被定义为偏航角(Yaw),俯仰角(Pitch),和滚转角(Roll)从而允许我们通过这三个值构造任何3D方向。


LearnOpenGL学习笔记(四) - 摄像机与入门知识点总结
http://example.com/2024/07/14/LearnOpenGL学习笔记(四) - 摄像机与入门知识点总结/
作者
Yoi
发布于
2024年7月14日
许可协议