1. 数学基础
1.1 向量
一个有方向(Direction)和大小(Magnitude)的量,向量的每一项叫做分量,默认向量的起点是原点,因此只需要指定向量的终点就可以指示一个方向
| 向量运算 | 描述 |
|---|
| 标量运算 | 一个向量加/减/乘/除一个标量,相当于对向量的每个分量分别进行该运算 |
| 取反 | 将向量的每个分量取反 |
| 加减 | 两个向量的对应分量进行加减 |
| 长度 | 对分量进行平方和后开根号 |
| 归一 | 每个分量除以向量的长度得到单位向量 |
| 点积 | 将对应分量逐个相乘 |
| 叉积 | 生成一个正交于两个输入向量的第三个向量 |
2. 矩阵
一个二维数组,矩阵中每一项叫做矩阵的元素(element),矩阵可以通过(i, j)进行索引,i是行,j是列,矩阵的行数和列数分别叫做矩阵的维度(dimension)
| 矩阵运算 | 描述 |
|---|
| 加减 | 两个矩阵对应位置的元素进行加减 |
| 数乘 | 矩阵的每个元素乘以一个标量 |
| 相乘 | 新矩阵每一个元素是对应行和对应列的线性组合 |
3. GLM
GLM(OpenGL Mathematics):专门为OpenGL量身定做的数学库,它提供了许多数学函数和数据类型,用于处理图形学中的各种数学运算
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| #include <glm/glm.hpp> #include <glm/gtc/matrix_transform.hpp> #include <glm/gtc/type_ptr.hpp>
glm::vec3 vector1 = glm::vec3(1.0f, 1.0f, 1.0f); glm::vec4 vector2 = glm::vec4(1.0f, 1.0f, 1.0f, 1,0f); glm::mat3 matrix1 = glm::mat3(1.0f); glm::mat4 matrix2 = glm::mat4(0.0f);
float mult_matrix = matrix1 * matrix2; float glm::dot(const glm::vec3 &x, const glm::vec3 &y); glm::vec3 glm::cross(const glm::vec3 &x, const glm::vec3 &y); glm::vec3 glm::normalize(const glm::vec3 &v); glm::mat4 glm::transpose(const glm::mat4 &m); glm::mat4 glm::inverse(const glm::mat4 &m);
glm::mat4 glm::scale(const glm::mat4 &m, const glm::vec3 &scale); glm::mat4 glm::translate(const glm::mat4 &m, const glm::vec3 &offset); glm::mat4 glm::rotate(const glm::mat4 &m, float angle, const glm::vec3 &axis);
|
2. 变换
2.1 缩放
缩放(Scale):对向量的不同分量大小进行倍增或倍减
缩放矩阵:左对角线上每一个值是对应分量的缩放倍数

2.2 位移
位移(Translate):对向量的不同分量加上一个值进行位移
位移矩阵:最后一列的每个值是对应分量加上的值

2.3 旋转
旋转(Rotate):指定一个旋转轴和一个旋转角度
旋转矩阵

2.4 组合变换
之所以利用矩阵表示变换,就是因为可以通过矩阵乘法可以将多个变换矩阵合并到一个变换矩阵,但由于矩阵乘法不满足交换律但满足结合律且变换顺序不同会导致结果不同,所以变换一般遵从以下顺序:缩放-旋转-位移
由于是利用矩阵左乘向量,所以计算式要从右往左读,即Transform=Translate∗Rotate∗Scale

3. 坐标系统
3.1 五个坐标系统
| 名称 | 描述 | 作用 |
|---|
| 局部空间(Local)/物体空间(Object) | 相对于物体自身原点的坐标系统 | 用于定义和操控物体自身的几何形状和运动 |
| 世界空间(World) | 相对于固定的世界原点的坐标 | 用于将多个物体放在一个世界场景 |
| 观察空间(View)/视觉空间(Eye) | 相对于相机/观察者的坐标 | 确定从相机视角看到的物体位置,从而进行投影 |
| 裁剪空间(Clip) | 标准化设备坐标 | 用于确定哪些物体可以被看到,从而决定渲染哪些部分 |
| 屏幕空间(Screen) | 相对于屏幕上窗口的坐标 | 用于最终渲染图像的坐标系统,使得物体能够正确地显示在屏幕上 |
3.2 三个变换矩阵
| 矩阵 | 变换 | 作用 |
|---|
| 观察(View) | 世界到观察,3D->3D | 用于确定在相机视角下的物体坐标 |
| 投影(Projection) | 观察到裁剪,3D->2D | 用于确定在相机视角下能被看到的物体和渲染后的物体 |
| 视口(ViewPort) | 裁剪到屏幕,2D->2D | 用于确定显示在相机屏幕上的物体 |
3.3 右手坐标系
食指向上表示y轴正方向,大拇指向右表示x轴正方向,中指向内表示z轴正方向
4. 观察
4.1 摄像机
如何定义摄像机
| 元素 | 定义 | 计算 | 注意 |
|---|
| 摄像机位置 | 摄像机位于世界空间中的位置 | 由用户自定义 | z轴的正方向是指出屏幕,因此为了将镜头拉远,需要将摄像机位置沿着z轴的正方向移动 |
| 前轴 | 观察空间的z轴正方向 | 前轴向量 = 摄像机的位置向量 - 拍摄场景的原点向量 | 从拍摄场景指向摄像机的 |
| 右轴 | 观察空间的x轴正方向 | 右轴向量 = 上向量 x 前向向量 | 上向量是世界空间中指向y轴正方向的(0,1,0) |
| 上轴 | 观察空间的y轴正方向 | 上轴向量 = 前向向量 x 右轴向量 | 上轴是观察空间的y轴,上向量是世界空间的y轴 |

LookAt矩阵:即观察矩阵,记位置向量为P,前向向量为F,右轴向量为R,上轴向量为U,则有
RxUxFx0RyUyFy0RzUzFz00001×100001000010−Px−Py−Pz1
glm提供的lookAt方法
1
| glm::mat4 viewMatrix = glm::lookAt(cameraPosition, targetPosition, upDirection);
|
4.2 基于摄像机的旋转
欧拉角:是可以表示3D空间中任何旋转的3个值
- 俯仰角(Pitch):绕X轴旋转的角度
- 偏航角(Yaw):绕Y轴旋转的角度
- 滚转角(Roll):绕Z轴旋转的角度
可以这样理解,一个飞机目标沿着z轴正方向的航线飞行,遵循右手定则
- 飞机绕着x轴旋转,相当于机头翘起或垂落,也就是飞机俯仰姿态
- 飞机绕着y轴旋转,相当于机头向左或向右,也就是飞机偏离航线
- 飞机绕着z轴旋转,相当于机头顺指针或逆时针,也就是飞机滚转机身

摄像机系统只关心俯仰角和偏航角,满足
- forward.x=cos(pitch)∗cos(yaw)
- forward.y=sin(pitch)
- forward.z=cos(pitch)∗cos(yaw)


4.2 一些摄像机操作
4.2.1 摄像机按照圆形轨迹移动
假设摄像机的圆形轨迹是位于X-Z平面的,因此要先定义距离世界坐标中心的半径radius,然后利用三角函数算出X坐标和Z坐标,最后传递给lookAt矩阵即可
1 2 3 4
| float radius = 10.0f; float camX = sin(glfwGetTime()) * radius; float camZ = cos(glfwGetTime()) * radius; glm::mat4 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));
|
4.2.2 摄像机根据键盘方向键移动
假设保持方向向量不变和移动速度不变
- 摄像机向前或向后移动,就将位置向量加上或减去方向向量
- 摄像机向左或向右移动,就将位置向量加上或减去方向向量叉乘上向量得到的右轴向量
1 2 3 4 5 6 7 8 9 10 11
| void processInput(GLFWwindow *window) { float cameraSpeed = 0.05f; 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; }
|
4.2.3 鼠标控制视角
鼠标水平移动影响偏航角,鼠标竖直移动影响俯仰角,可以通过存储上一帧鼠标的位置并获取当前帧鼠标的位置来计算
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
| glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
glfwSetCursorPosCallback(window, mouse_callback);
void mouse_callback(GLFWwindow* window, double xpos, double ypos) { if (firstMouse) { lastX = xpos; lastY = ypos; firstMouse = false; } float xoffset = xpos - lastX; float yoffset = lastY - ypos; lastX = xpos; lastY = ypos; float sensitivity = 0.05; xoffset *= sensitivity; yoffset *= sensitivity; yaw += xoffset; pitch += yoffset; 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); }
|
4.2.4 鼠标控制缩放
缩放是通过投影矩阵的fov参数实现的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| glfwSetScrollCallback(window, scroll_callback);
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);
|
5. 投影
5.1 正射投影
正射(Orthographic):所有的投影线都是平行的,意味着在投影过程中物体的大小和形状不会发生变化
正射投影:定义了一个方体,由宽、高、近平面和远平面所指定,在方体内,所有的物体都被投影到一个二维平面上,在方体外,所有的物体都被裁剪掉
1
| mat4 orthoMatrix = ortho(left, right, bottom, top, near, far);
|

2.2 透视投影
透视(Perspective):物体的大小和形状在投影过程中会发生变化,越靠近观察者的物体越大,越远离观察者的物体越小,依据透视除法有最后输出的顶点坐标为(x/w,y/w,z/w)
透视投影:定义了一个锥体,由视野(Fov)、宽高比、近平面和远平面所指定,在锥体内,所有的物体都被投影到一个二维平面上,在锥体外,所有的物体都被裁剪掉
2.3 深度缓冲
深度缓冲(Depth Buffer)/z缓冲(Z-buffer):存储所有片段的深度信息/z值
深度测试:在渲染每个片段时,比较当前片段的深度值和深度缓冲中的值,如果当前的片段在其它片段之后,它将会被丢弃,否则将会覆盖
1 2 3 4
| glEnable(GL_DEPTH_TEST);
glClear(GL_DEPTH_BUFFER_BIT);
|

6. 视口变换