Loading... [参考文章: 变换](https://learnopengl-cn.github.io/01%20Getting%20started/07%20Transformations/) [参考文章: 坐标系统](https://learnopengl-cn.github.io/01%20Getting%20started/08%20Coordinate%20Systems) [参考文章: 摄像机](https://learnopengl-cn.github.io/01%20Getting%20started/09%20Camera/) 矩阵与向量计算的事可以先放在一边, 我们可以先试着构建一个视口摄像头, 然后在去回顾然后计算(主要是这样比较有成就感😎) ## GLM 要在程序中实现矩阵运算, 可以用到 GLM 库 (Open**GL** **M**athematics) GLM [下载地址](https://glm.g-truc.net/0.9.8/index.html) 包含三个头文件即可使用: ```cpp #include <glm/glm.hpp> #include <glm/gtc/matrix_transform.hpp> #include <glm/gtc/type_ptr.hpp> ``` 常见的运算函数: ```cpp // 初始化一个单位矩阵 // GLM 0.9.9及以上版本, 采用如下方式初始化矩阵 // glm::mat4 matrix = glm::mat4(1.0f) // 移动矩阵: 单位矩阵 * 三向量 glm::translate(单位矩阵, 三向量); // 旋转矩阵 // rotate(单位矩阵, 旋转角度, 三向量) // 哪个向量为 1, 则绕哪个轴旋转 glm::rotate(trans, glm::radians(90.0f), glm::vec3(0.0f, 0.0f, 1.0f)) // 缩放矩阵 // scale(单位矩阵, 三向量) glm::scale(trans, glm::vec3(0.5, 0.5, 0.5)); // 矩阵叉乘 // cross(向量 1, 向量 2) glm::cross(up, cameraDirection); // 向量长度 // length(向量) glm::length(vec) ``` --- ## 坐标系统 比较重要的, 总共有5个不同的坐标系统: * 局部空间(Local Space,或者称为物体空间(Object Space)) * 世界空间(World Space) * 观察空间(View Space,或者称为视觉空间(Eye Space)) * 裁剪空间(Clip Space) * 屏幕空间(Screen Space) > 最重要的矩阵, 分别是模型(Model)、观察(View)、投影(Projection) 顶点坐标起始于局部空间, 这里的坐标称为局部坐标, 然后通过变换, 变为世界坐标, 观察坐标, 裁剪坐标 最后以屏幕坐标的形式结束  可以这么理解, 局部空间是构成模型本身的空间, 世界空间表示的是模型摆放的空间, 观察空间就是摄像机能看到的空间, 裁剪空间就是将屏幕无法显示的空间去掉所剩余的空间 > OpenGL 采用的是右手坐标系  --- ### 投影矩阵 将顶点坐标从观察变换到裁剪空间需要用到**投影矩阵**, 它指定了一个范围的坐标 (观察箱), 超出其指定的范围坐标, 就会被裁剪, 剩余未超出的就会转化成标准坐标(-1.0, 1.0) 由投影矩阵创建的**观察箱** (Viewing Box) ***被称为**平截头体** (Frustum) > 将特定范围内的坐标转化到标准化设备坐标系的过程被称之为投影 (Projection) 一旦所有顶点被变换到裁剪空间,最终的操作——**透视除法** (Perspective Division) 将会执行,在这个过程中我们将位置向量的x,y,z分量分别除以向量的齐次w分量;透视除法是将4D裁剪空间坐标变换为3D标准化设备坐标的过程。这一步会在每一个顶点着色器运行的最后被自动执行。 --- 有两种投影矩阵: - 正射投影矩阵 (Orthographic Projection Matrix) - 透视投影矩阵 (Perspective Projection Matrix) --- #### 正射投影 创建正射投影矩阵 正射投影不存在透视效果 ```cpp // 最前两个参数指定了平截头体的左右坐标 // 第三和第四参数指定了平截头体的底部和顶部 // 第五和第六个参数则定义了近平面和远平面的距离 glm::ortho(0.0f, 800.0f, 0.0f, 600.0f, 0.1f, 100.0f); ```  --- #### 透视投影 要想让显示的效果更贴近人眼, 就要用到透视矩阵 与正射透视不同的是, 透视矩阵会修改每个顶点坐标的w值,从而使得离观察者越远的顶点坐标w分量越大。被变换到裁剪空间的坐标都会在-w到w的范围之间 > 可以了解一下透视投影矩阵是如何计算的 创建一个透视投影矩阵: ```cpp // 第一个参数定义了fov的值, 表示的是视野(Field of View), 值通常为 45.0 // 第二个参数设置了宽高比 // 第三和第四个参数设置了平截头体的近和远平面, 通常设置近距离为0.1f,而远距离设为100.0f glm::mat4 proj = glm::perspective(glm::radians(45.0f), (float)width/(float)height, 0.1f, 100.0f); ```  --- ### 3D 正方形 创建三个变换矩阵: ```cpp glm::mat4 model = glm::mat4(1.0f); // 模型矩阵 glm::mat4 view = glm::mat4(1.0f); // 观察矩阵 glm::mat4 projection = glm::mat4(1.0f); // 投影矩阵 model = glm::rotate(model, glm::radians(-55.0f), glm::vec3(1.0f, 0.0f, 0.0f)); view = glm::translate(view, glm::vec3(0.0f, 0.0f, -3.0f)); // 注意,我们将矩阵向我们要进行移动场景的反方向移动 projection = glm::perspective(glm::radians(45.0f), screenWidth / screenHeight, 0.1f, 100.0f); ``` 在顶点着色器中创建对应的 3 个 `uniform`: ```c layout (location = 0) in vec3 aPos; uniform mat4 model; uniform mat4 view; uniform mat4 projection; void main(){ // 位置坐标 gl_Position = projection * view * model * vec4(aPos, 1.0f); } ``` --- ### 摄像机 OpenGL本身没有**摄像机**, 但我们可以通过把场景中的所有物体往相反方向移动的方式来模拟出摄像机  要模拟出相机, 首先需要将摄像机的轴向给确定好, 然后再通过轴向创建 **LookAt 矩阵** 确定轴向的步骤: 1. 假定摄像机的坐标为 `(0, 0, 2)` 2. `摄像机坐标 - 坐标原点` 来构建**第一个方向轴** (摄像机方向) 3. 假定一个上轴 `(0, 1, 1)`, 通过将假定的上轴和摄像机方向轴进行叉乘算出其**右轴** 4. 再次通过叉乘算出**上轴** 有了**摄像机方向轴, 右轴, 上轴**和**摄像机坐标**就可以构建 **LookAt 矩阵** 了 通过 **LookAt 矩阵**, 就可以直接变换所有坐标了 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} $$ $\color{red} R$ 为右向量, $\color{green} U$ 为上向量, $\color{blue} D$ 为摄像机方向, $\color{purple}P$ 为摄像机位置向量 构建 LookAt 矩阵的代码 (GLM 提供了 LookAt 的构建函数): > LookAt 构建函数只需摄像头位置向量, 上轴向量, 目标向量 > > `glm::lookAt` 函数可以通过上述参数完成确定轴向的所有步骤 ```cpp // LookAt 矩阵推导过程: // 摄像机位置 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); // 获取摄像机右轴 glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f); // 假定的上轴 glm::vec3 cameraRight = glm::normalize(glm::cross(up, cameraDirection)); // 继续获取上轴 glm::vec3 cameraUp = glm::normalize(glm::cross(cameraDirection, cameraRight)); /********************************************************************************************/ // 一步到位: // 构建 LookAt 矩阵 // lookAt(位置向量, 目标向量, 上轴向量) // 这里的目标是坐标原点 glm::mat4 view = glm::mat4(1.0f); view = glm::lookAt(cameraPos, cameraTarget, glm::vec3(0.0f, 1.0f, 0.0f)); ``` 现在观察矩阵承担着模拟摄像机的作用, 但还不够, 我们还需监听鼠标事件, 让用户控制摄像头 通过 `摄像头位置向量 + 移动量(自定义一个数) * 摄像头轴向` 来算出摄像头移动后的位置: ```cpp // 摄像头速度 // delta 为前后帧时间差值, 用于平衡速度 float cameraSpeed = delta * 0.5f; ``` 但一般情况下, 我们移动鼠标都是绕其摄像头的轴继续旋转, 而不是移动摄像头位置 因此需要引入一个概念, 欧拉角: 如图, 第一个是**俯仰角**, 第二个是**偏航角**, 第三个是**翻转角**  - [ ] TODO 把分量的计算过程记录下来 假装这里有计算摄像头方向分量的过程....💦 完整代码: ```cpp // 摄像机初始化向量 glm:vec3 Position = glm:vec3(0.0f, 0.0f, 3.0f); glm:vec3 Front = glm:vec3(0.0f, 0.0f, -1.0f); glm:vec3 Up = glm:vec3(0.0f, 1.0f, 0.0f); 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)); Front = glm::normalize(front); glm::lookAt(Position, Position + Front, Up); ``` --- #### IMGUI-GLFW 鼠标事件 GLFW 官方文档: [鼠标输入](https://www.glfw.org/docs/latest/input_guide.html#input_mouse) 我的代码中使用了 IMGUI, 故可以不使用 GLFW 的鼠标回调函数, 使用 IMGUI 提供的封装来处理鼠标输入会更方便 IMGUI 处理鼠标的输入在 `imgui_demo.cpp` 中的 Inputs & Focus 片段中有示例: ```cpp ImGuiIO& io = ImGui::GetIO() // 判断鼠标是否悬停在 GUI 上 if (io.WantCaptureMouse) { // 鼠标悬停在 GUI 上的操作 } // 获取鼠标位置 io.MousePos.x io.MousePos.y // 获取鼠标滚轮 // > 0 滚轮向上; < 0 滚轮向下 // 注意: 要想获取到 io.MouseWheel, 下面注释的代码必须放在前面 /* ImGui_ImplOpenGL3_NewFrame(); ImGui_ImplGlfw_NewFrame(); ImGui::NewFrame(); */ io.MouseWheel // 获取鼠标按键 // 0 左键; 1 右键; 2 中键 if (ImGui::IsMouseDown(0)) { // 按下鼠标左键的操作... } ``` 最后修改:2024 年 09 月 22 日 © 允许规范转载 赞 1 如果觉得我的文章对你有用,请随意赞赏