此系列记录我跟随 LearnOpenGL 学习的历程。
LearnOpenGL相关代码
# 入门
# 安装 OpenGL
如果是 Windows 端,我使用的 msvc+xmake+vscode 开发环境。msvc 安装直接从微软官方下载 Visual Studio,xmake 用于项目构建和 c++ 库管理,vscode 轻巧方便。
xmake 的安装,这里推荐从 Scoop 安装 xmake。
# 可选:首次运行远程脚本时需要 | |
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser | |
# 安装 Scoop 的命令 | |
Invoke-RestMethod get.scoop.sh | Invoke-Expression | |
# 从 Scoop 安装 xmake | |
scoop install xmake | |
# xmake 生成项目 | |
xmake create opengl | |
cd opengl |
关于 xmake 在 vscode 环境的配置,xmake 无法用 vscode C/C++ 扩展默认的 InteltiSense 进行语法补全和语法检查,所以我们需要用 Clangd 代替,在 vscode 安装 Clangd 扩展,如果原先没有安装 Clangd 会自动安装,也可以用 scoop 安装 Clangd。
但是仅仅有 Clangd 是不够的,我们需要在 xmake.lua 中进行如下配置修改,修改后编译成功通过一次后则可以检测到新的库。
xmake.lua | |
... | |
+ add_rules("plugin.compile_commands.autoupdate", {outputdir = ".vscode"}) | |
... |
而 xmake 自身的调试无法在 vscode 上进行方便的断点检查,所以我们利用 vscode C/C++ 扩展所带的调试功能,在.vscode 文件夹下新建 launch.json,配置如下:
{ | |
// 使用 IntelliSense 了解相关属性。 | |
// 悬停以查看现有属性的描述。 | |
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 | |
"version": "0.2.0", | |
"configurations": [ | |
{ | |
"name": "(gdb) 启动", | |
"type": "cppvsdbg", | |
"request": "launch", | |
"program": "${workspaceFolder}/build/windows/x64/debug/a.exe", | |
"args": [], | |
"stopAtEntry": false, | |
"cwd": "${fileDirname}", | |
"environment": [], | |
"externalConsole": false, | |
} | |
] | |
} |
我们进行 OpenGL 开发所需相关的库有 glad、glfw(后续还有 assimp、glm 等),在这里我们额外用 imgui 作为界面库,在 xmake.lua 中进行修改
xmake.lua | |
... | |
+ add_requires("glad","glfw") | |
+ add_requires("imgui",{configs = {glfw = true, opengl3 = true}}) | |
target("main") | |
set_kind("binary") | |
add_files("src/main.cpp") | |
+ add_packages("glfw", "glad", "imgui") | |
... |
关于 imgui 的配置,我们在 main.cpp 的添加如下头文件:
main.cpp | |
... | |
+ #include "imgui.h" | |
+ #include "imgui_impl_glfw.h" | |
+ #include "imgui_impl_opengl3.h" | |
+ #include "imgui_impl_opengl3_loader.h" | |
... |
之后利用 xmake 进行 build,xmake 会自动下载库进行配置。
# 纹理(引用我们自己的头文件)
在纹理一节中,我们需要添加新的 stb_image.h 单头文件图像加载库,我们可以在项目下新建 include 文件夹,将 stb_image.h 头文件放入 include 文件夹中,将.cpp 文件放入 source 文件夹中(如果有的话)。之后在 xmake.lua 下将 include 文件夹中的头文件都加入链接:
xmake.lua | |
... | |
target("main") | |
set_kind("binary") | |
+ add_includedirs("include") | |
add_files("src/main.cpp") | |
add_packages("glfw", "glm", "imgui", "assimp", "glad") | |
... |
由于 OpenGL 要求 y 轴坐标在图片底部,但是图片的 y 轴坐标通常在顶部,所以我们可以借助 stb_image.h 在图像加载时帮助我们翻转 y 轴,只需要在加载任何图像前加入以下语句即可:
stbi_set_flip_vertically_on_load(true); |
值得注意的是,后续用 assimp 导入的模型纹理,大部分模型纹理 (比如米哈游官方提供的各类模型) 进行了处理,本身图像 y 轴便是反的,此时不用再翻转 y 轴。
# 变换
这一章主要涉及的是矩阵相关知识,比较重点的是关于旋转部分,欧拉角与四元数
:
欧拉角:
1、欧拉角的旋转顺序非常重要,不同的旋转顺序得到的最终旋转结果不一致。
2、使用欧拉角描述三维旋转时,当一个旋转轴与另一个旋转轴重合时,系统失去一个自由度,导致无法独立控制所有旋转方向,即万向节死锁现象。
3、物体角度状态与欧拉角坐标并非一一对应关系,某些位置状态并不唯一确定一组欧拉角坐标。
四元数:
1、包含了四个实参数以及三个虚部(一个实部三个虚部)。
2、不会产生万向节死锁现象。
3、可以插值,不同的四元数对应的旋转是唯一的,多次旋转可以进行计算上的优化。
同时引用了 glm 库,该库同样只需要通过 xmake 引入即可:
xmake.lua | |
... | |
+ add_requires("glm") | |
target("main") | |
set_kind("binary") | |
add_includedirs("include") | |
add_files("src/main.cpp") | |
- add_packages("glfw", "glad", "imgui") | |
+ add_packages("glfw", "glad", "imgui","glm") | |
... |
# 坐标系统
首先要记住,OpenGL 默认是 x 轴朝右,y 轴朝上,z 轴朝内 (朝向你自己)。
局部坐标,是对象相对于局部原点的坐标,也是物体起始的坐标。比如,最原始的处于远点的一个立方体。
世界空间,世界空间坐标是处于一个更大的空间范围的。这些坐标相对于世界的全局原点,它们会和其它物体一起相对于世界的原点进行摆放。比如,我们将处于原点的立方体摆放到我们需要摆放到的位置。
观察空间,每个坐标都是从摄像机或者说观察者的角度进行观察的。比如,我们将立方体相对于原点的坐标转换为相对于摄像机的坐标,如果我们要将摄像机向后移动,那么我们将整个场景向前移动,两者是一样的。
裁剪空间,坐标到达观察空间之后,我们需要将其投影到裁剪坐标。裁剪坐标会被处理至 - 1.0 到 1.0 的范围内,并判断哪些顶点将会出现在屏幕上。最常用的投影有正视投影、透视投影。这里要注意,经过透视投影后是非线性的,后续对法向量的处理需要注意。
屏幕空间,我们将使用一个叫做视口变换 (Viewport Transform) 的过程。视口变换将位于 - 1.0 到 1.0 范围的坐标变换到由 glViewport 函数所定义的坐标范围内。最后变换出来的坐标将会送到光栅器,将其转化为片段。
各个坐标的变换关系是这样的:
局部空间 --model--> 世界空间 --view--> 观察空间 --projection--> 裁剪空间 -- 视口变换 --> 屏幕空间
在这里的 model、view、projection 为变换矩阵,比如对坐标 x 进行变换时候,最后变换的结果是 projection*view*model*x,矩阵乘法的顺序不能错,其中 projection*view*model 得到的结果称为 MVP 矩阵。
# 摄像机
一个仿照 Unity 摄像机移动系统(WASD 控制前后左右,Q 上升,E 下降)
main.cpp | |
... | |
void processInput(GLFWwindow *window) | |
{ | |
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) | |
glfwSetWindowShouldClose(window, true); | |
if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS) | |
myCamera.ProcessKeyboard(FORWARD, deltaTime * MoveSpeed); | |
if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS) | |
myCamera.ProcessKeyboard(BACKWARD, deltaTime * MoveSpeed); | |
if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS) | |
myCamera.ProcessKeyboard(LEFT, deltaTime * MoveSpeed); | |
if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS) | |
myCamera.ProcessKeyboard(RIGHT, deltaTime * MoveSpeed); | |
if (glfwGetKey(window, GLFW_KEY_Q) == GLFW_PRESS) | |
myCamera.ProcessKeyboard(UP, deltaTime * MoveSpeed); | |
if (glfwGetKey(window, GLFW_KEY_E) == GLFW_PRESS) | |
myCamera.ProcessKeyboard(DOWN, deltaTime * MoveSpeed); | |
if (glfwGetKey(window, GLFW_KEY_F) == GLFW_PRESS) | |
cameraMouseControl = !cameraMouseControl; // 切换摄像机控制模式 | |
if (glfwGetKey(window, GLFW_KEY_R) == GLFW_PRESS) | |
myCamera.Reset(); // 重置摄像机位置和方向 | |
} | |
... | |
camera.h | |
... | |
void ProcessKeyboard(Camera_Movement direction, float deltaTime) | |
{ | |
float velocity = MovementSpeed * deltaTime; | |
if (direction == FORWARD) | |
Position += glm::vec3(Front.x, 0.0f, Front.z) * velocity; | |
if (direction == BACKWARD) | |
Position -= glm::vec3(Front.x, 0.0f, Front.z) * velocity; | |
if (direction == LEFT) | |
Position -= glm::vec3(Right.x, 0.0f, Right.z) * velocity; | |
if (direction == RIGHT) | |
Position += glm::vec3(Right.x, 0.0f, Right.z) * velocity; | |
if (direction == UP) | |
Position += glm::vec3(0.0f, 1.0f, 0.0f) * velocity; | |
if (direction == DOWN) | |
Position -= glm::vec3(0.0f, 1.0f, 0.0f) * velocity; | |
} | |
... |
如果你引用了 imgui 作为界面库,为了使鼠标作用不冲突,可以进行如下修改:
main.cpp | |
... | |
void mouse_callback(GLFWwindow *window, double xpos, double ypos) | |
{ | |
// 获取 ImGui 的 IO 状态 | |
ImGuiIO &io = ImGui::GetIO(); | |
// 如果 ImGui 正在使用鼠标,或左键未按下时,不处理视角移动 | |
if (io.WantCaptureMouse || glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_LEFT) != GLFW_PRESS || !cameraMouseControl) | |
{ | |
firstMouse = true; // 重置初始位置标记 | |
return; | |
} | |
if (firstMouse) // 这个 bool 变量初始时是设定为 true 的 | |
{ | |
lastX = xpos; | |
lastY = ypos; | |
firstMouse = false; | |
} | |
float xoffset = (xpos - lastX); | |
float yoffset = lastY - ypos; // 注意这里是相反的,因为 y 坐标是从底部往顶部依次增大的 | |
if (cameraMouseControl) | |
{ | |
xoffset *= cameraSpeed; | |
yoffset *= cameraSpeed; | |
} | |
lastX = xpos; | |
lastY = ypos; | |
myCamera.ProcessMouseMovement(xoffset, yoffset); | |
} | |
... |