# First experiment in computer graphics **Repository Path**: cyb_c/first-experiment-in-computer-graphics ## Basic Information - **Project Name**: First experiment in computer graphics - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2024-04-09 - **Last Updated**: 2024-04-16 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## 1 实验环境 ### 1.1 环境版本 - Windows 10 家庭中文版 - GLFW: 3.4 下载地址:[下载 |GLFW系列](https://www.glfw.org/download.html) - CMake:3.29.2 下载地址:[Download CMake](https://cmake.org/download/) - GLAD:3.3 下载地址:[glad.dav1d.de](https://glad.dav1d.de/) - GLM 下载地址:[OpenGL Mathematics (g-truc.net)](https://glm.g-truc.net/0.9.8/index.html) ### 1.2 环境配置 详细过程见实验文档 #### 1.2.1 可能差错 image-20240409215827108 ##### 1.2.1.1 **一开始选错generator, 导致无法进行Configure** 解决方案 ``` 1.点击File > DeleteCache; 2.点击Configure按钮 3.重新进行配置 ``` ##### 1.2.1.2 **visual studio 运行程序出现拒绝访问** 解决方案 ``` 禁用360安全卫士,关闭即可 ``` image-20240410144545213 ## 2 创建窗口 ### 2.1 GLFW 创建窗口 ```c++ int main() { glfwInit();//初始化GLFW glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);//主版本号 glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);//次版本号 glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);//设置为核心模式,明确只使用OpenGL功能的一个子集。 //glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);//for Mac return 0; } ``` ​ 这些函数用于配置GLFW环境,先调用glfwInit()函数来初始化GLSW,然后调用。 ​ `**glfwWindowHint()`**函数的第一个参数代表选项的名称,我们可以从很多以**GLFW_**开头的枚举值中选择;第二个参数接受一个整型,用来设置这个选项的值,该函数的所有的选项以及对应的值都可以在 [GLFW’s window handling](http://www.glfw.org/docs/latest/window.html#window_hints) 这篇文档中找到。 ​ 接下来创建一个**窗口**对象`window`,这个对象存放了所有和窗口相关的数据,并且会被GLFW的其他函数频繁地用到。 ```c++ int main() { ...... GLFWwindow* window = glfwCreateWindow(800, 600, "OpenGLExperiment", NULL, NULL);//(宽,高,窗口名)返回一个GLFWwindow类的实例:window if (window == NULL) { // 生成错误则输出错误信息 std::cout << "Failed to create GLFW window" << std::endl; glfwTerminate(); return -1; } glfwMakeContextCurrent(window); } ``` ​ `glfwCreateWindow()函数`需要窗口的宽和高作为它的前两个参数,第三个参数表示这个窗口的名称。 ​ 创建完窗口我们就通过`glfwMakeContextCurrent(window)`通知GLFW将我们窗口的上下文设置为当前线程的主上下文。 ### 2.2 GLAD 初始化 ​ GLAD是用来管理OpenGL的函数指针的,所以在调用任何OpenGL的函数之前我们需要初始化GLAD。 ```c++ if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { std::cout << "Failed to initialize GLAD" << std::endl; return -1; } ``` ​ 我们给GLAD传入了用来加载系统相关的OpenGL函数指针地址的函数,GLFW给我们的是`glfwGetProcAddress`,它根据我们编译的系统定义了正确的函数。 ### 2.3 glViewport ​ 通过调用glViewport函数来设置窗口的**维度**(Dimension): ```c++ glViewport(0,0,800,600) ``` ​ `glViewport()`函数前两个参数控制 **窗口左下角的位置**,第三个和第四个参数控制渲染窗口的**宽度和高度**(像素)。 ​ 当用户改变窗口的大小的时候,视口也应该被调整,我们可以对窗口注册一个 **回调函数(Callback Function)**,它会在每次窗口大小被调整的时候被调用,这个回调函数的原型如下: ```c++ void framebuffer_size_callback(GLFWwindow* window, int width, int height); ``` ​ 这个帧缓冲大小函数需要一个`GLFWwindow`作为它的第一个参数,以及两个整数表示窗口的新维度,每当窗口改变大小,GLFW会调用这个函数并填充相应的参数 ```c++ void framebuffer_size_callback(GLFWwindow* window, int width, int height) { glViewport(0, 0, width, height); } ``` ​ 注册这个函数,告诉GLFW我们希望每当窗口调整大小的时候调用这个函数。 ```c++ glfwSetFramebufferSizeCallback(window, framebuffer_size_callback); ``` ### 2.4 渲染循环 ​ 为了在我们主动关闭窗口之前不断绘制图形并能够接收用户输入,因此,需要添加一个while循环。 ```c++ while(!glfwWindowShouldClose(window)) { glfwSwapBuffers(window); glfwPollEvents(); } ``` - `glfwWindowShouldClose()` 函数在我们每次循环的开始前检查一次GLFW是否被要求退出,如果是的话该函数返回 true 然后渲染循环便结束了,之后为我们就可以关闭应用程序了。 - `glfwPollEvents()` 函数检查有没有触发什么事件(比如键盘输入、鼠标移动等)、更新窗口状态,并调用对应的回调函数(可以通过回调方法手动设置)。 - `glfwSwapBuffers()` 函数会交换颜色缓冲(它是一个储存着GLFW窗口每一个像素颜色值的大缓冲),它在这一迭代中被用来绘制,并且将会作为输出显示在屏幕上。 ### 2.5 释放资源 在渲染循环结束之后,正确释放/删除之前分配的所有资源,在main函数中调用`glwfwTerminate()`来实现。 ```c++ glfwTerminate(); return 0; ``` ### 2.6 输入 ​ 为了在GLFW中实现一些输入控制,这可以通过使用GLFW的几个输入函数来完成。该项目中使用GLFW的`glfwGetKey`函数,它需要一个窗口以及一个按键作为输入,这个函数将会返回这个按键是否被按下。 ```c++ void processInput(GLFWwindow *window) { if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)//是否按下了返回键 glfwSetWindowShouldClose(window, true); } ``` ​ 实现效果为用户按下Esc,关闭GLFW,下一次while循环的条件检测将会失败,程序就会进入`return 0`关闭。 ​ 在渲染循环的每一个迭代中加入processInput的调用: ```c++ while (!glfwWindowShouldClose(window)) { //输入 processInput(window); //渲染指令 ...... // 检查并调用事件,交换缓冲 glfwSwapBuffers(window); // 检查触发什么事件,更新窗口状态 glfwPollEvents(); } ``` ### 2.7 背景渲染 ```c++ //渲染指令 glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); ``` ​ 通过调用`glClear函数`来清空屏幕的颜色缓冲,它接受一个缓冲位 (Buffer Bit) 来指定要清空的缓冲。 ​ 调用了 `glClearColor()` 来设置清空屏幕所用的颜色,当调用 `glClear()` 函数,清除颜色缓冲之后,整个颜色缓冲都会被填充为 `glClearColor()` 里所设置的颜色。。 ​ 最后,我们可以得到窗口如下。 image-20240413180315632 ## 3 实验代码解释 ### 3.1 着色器配置 ​ 为了方便着色器的使用,在shader_s.h文件中创建了一个类用于存储着色器,具体实现代码及解释如下。 ```c++ #ifndef SHADER_H #define SHADER_H #include ; // 包含glad来获取所有的必须OpenGL头文件 #include #include #include #include class Shader { public: // 程序ID unsigned int ID; // 构造器读取并构建着色器 Shader(const char* vertexPath, const char* fragmentPath) { // 1. 从文件路径中获取顶点/片段着色器 std::string vertexCode; std::string fragmentCode; std::ifstream vShaderFile; std::ifstream fShaderFile; // 保证ifstream对象可以抛出异常: vShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit); fShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit); try { // 打开文件 vShaderFile.open(vertexPath); fShaderFile.open(fragmentPath); std::stringstream vShaderStream, fShaderStream; // 读取文件的缓冲内容到数据流中 vShaderStream << vShaderFile.rdbuf(); fShaderStream << fShaderFile.rdbuf(); // 关闭文件处理器 vShaderFile.close(); fShaderFile.close(); // 转换数据流到string vertexCode = vShaderStream.str(); fragmentCode = fShaderStream.str(); } catch (std::ifstream::failure e) { std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ" << std::endl; } const char* vShaderCode = vertexCode.c_str(); const char* fShaderCode = fragmentCode.c_str(); // 2. 编译着色器 unsigned int vertex, fragment; // 顶点着色器 vertex = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertex, 1, &vShaderCode, NULL); glCompileShader(vertex); checkCompileErrors(vertex, "VERTEX"); // 片段着色器也类似 fragment = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragment, 1, &fShaderCode, NULL); glCompileShader(fragment); checkCompileErrors(fragment, "FRAGMENT"); // 着色器程序 ID = glCreateProgram(); glAttachShader(ID, vertex); glAttachShader(ID, fragment); glLinkProgram(ID); checkCompileErrors(ID, "PROGRAM"); // 删除着色器,它们已经链接到我们的程序中了,已经不再需要了 glDeleteShader(vertex); glDeleteShader(fragment); } // 激活着色器 void use() { glUseProgram(ID); } // uniform的setter函数 void setBool(const std::string& name, bool value) const { glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value); } void setInt(const std::string& name, int value) const { glUniform1i(glGetUniformLocation(ID, name.c_str()), value); } void setFloat(const std::string& name, float value) const { glUniform1f(glGetUniformLocation(ID, name.c_str()), value); } void setFloat4(const std::string& name, float value1, float value2, float value3, float value4) { glUniform4f(glGetUniformLocation(ID, name.c_str()), value1, value2, value3, value4); } void setMat4(const std::string& name, const glm::mat4& mat) const { glUniformMatrix4fv(glGetUniformLocation(ID, name.c_str()), 1, GL_FALSE, &mat[0][0]); } void setVec3(const std::string& name, const glm::vec3& value) const { glUniform3fv(glGetUniformLocation(ID, name.c_str()), 1, &value[0]); } void setVec3(const std::string& name, float x, float y, float z) const { glUniform3f(glGetUniformLocation(ID, name.c_str()), x, y, z); } private: // 检查编译或链接是否出错 void checkCompileErrors(unsigned int shader, std::string type) { int success; char infoLog[1024]; if (type != "PROGRAM") { glGetShaderiv(shader, GL_COMPILE_STATUS, &success); if (!success) //打印连接错误(如果有的话) { glGetShaderInfoLog(shader, 1024, NULL, infoLog); std::cout << "ERROR::SHADER_COMPILATION_ERROR of type: " << type << "\n" << infoLog << "\n -- --------------------------------------------------- -- " << std::endl; } } else { glGetProgramiv(shader, GL_LINK_STATUS, &success); if (!success) //打印连接错误(如果有的话) { glGetProgramInfoLog(shader, 1024, NULL, infoLog); std::cout << "ERROR::PROGRAM_LINKING_ERROR of type: " << type << "\n" << infoLog << "\n -- --------------------------------------------------- -- " << std::endl; } } } }; #endif ``` 类的使用方法为 ```c++ //直接将顶点着色器和片段着色器的代码文本置入 Shader shader2D("vertexSourceExperiment2D.txt","fragmentSourceExperiment2D.txt"); ``` ### 3.2 二维图形绘制 #### 3.2.1 绘制三角形 ```c++ int main{ ... //三角形顶点数据 float vertices[] = { -0.5f, -0.5f, 0.0f, 0.5f, -0.5f, 0.0f, 0.0f, 0.5f, 0.0f }; unsigned int VBO; glGenBuffers(1, &VBO); unsigned int VAO; glGenVertexArrays(1, &VAO); //初始化代码 // 1. 绑定VAO glBindVertexArray(VAO); // 2. 把顶点数组复制到缓冲中供OpenGL使用 glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 3. 设置顶点属性指针 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); // 渲染循环 while (!glfwWindowShouldClose(window)) { ..... //进行图形的渲染 shader2D.use(); glBindVertexArray(VAO); glDrawArrays(GL_TRIANGLES, 0, 3); //删去绑定 glBindVertexArray(0); // 检查并调用事件,交换缓冲 glfwSwapBuffers(window); // 检查触发什么事件,更新窗口状态 glfwPollEvents(); } // 释放之前的分配的所有资源 glfwTerminate(); glDeleteVertexArrays(1, &VAO); glDeleteBuffers(1, &VBO); } ``` 绘制结果如下 image #### 3.2.2 绘制正方形 ```c++ int main(){ ...... //正方形顶点数据 float vertices[] = { 0.5f, 0.5f, 0.0f, // 0号点 0.5f, -0.5f, 0.0f, // 1号点 -0.5f, -0.5f, 0.0f, // 2号点 -0.5f, 0.5f, 0.0f // 3号点 }; //正方形索引数据 unsigned int indices[] = { // 注意索引从0开始! 0, 1, 3, // 第一个三角形 1, 2, 3 // 第二个三角形 }; unsigned int VBO; glGenBuffers(1, &VBO); unsigned int VAO; glGenVertexArrays(1, &VAO); unsigned int EBO; glGenBuffers(1, &EBO); // 初始化代码 // 1. 绑定VAO glBindVertexArray(VAO); // 2. 把顶点数组复制到缓冲中供OpenGL使用 glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 3. 复制索引数组到索引缓冲中供OpenGL使用 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); // 4. 设置顶点属性指针 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); // 渲染循环 while (!glfwWindowShouldClose(window)) { // 输入 processInput(window); // 渲染指令 glClearColor(0.78f, 0.88f, 0.89f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); //进行图形的渲染 shader2D.use(); glBindVertexArray(VAO); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); glBindVertexArray(0); // 检查并调用事件,交换缓冲 glfwSwapBuffers(window); // 检查触发什么事件,更新窗口状态 glfwPollEvents(); } // 释放之前的分配的所有资源 glfwTerminate(); glDeleteVertexArrays(1, &VAO); glDeleteBuffers(1, &VBO); glDeleteBuffers(1, &EBO); } ``` 绘制结果如下 image ### 3.3 二维变换 #### 3.3.1 缩放 $$ \begin{vmatrix} S_1&0&0&0\\ 0&S_2&0&0\\ 0&0&1&0\\ 0&0&0&1 \end{vmatrix}· \left(\begin{matrix} x\\y\\0\\1 \end{matrix}\right) = \left(\begin{matrix} S_1·x\\ S_2·y\\ 0\\ 1 \end{matrix}\right) $$ ```c++ //将物体沿时间变换从1-2进行缩放 float elapsed = glfwGetTime(); float scaleFactor = 1.5f + 0.5f * (sin(elapsed)); model = glm::scale(model, glm::vec3(scaleFactor, scaleFactor, scaleFactor)); ``` #### 3.3.2 位移 $$ \begin{vmatrix} 1&0&0&T_x\\ 0&1&0&T_y\\ 0&0&1&0\\ 0&0&0&1 \end{vmatrix}· \left(\begin{matrix} x\\y\\0\\1 \end{matrix}\right) = \left(\begin{matrix} x+T_x\\ y+T_y\\ 0\\ 1 \end{matrix}\right) $$ ```c++ //将物体分别沿x轴和y轴移动一个标准单位 glm::translate(model, glm::Vec3(1.0f,1.0f,0.0f)); ``` #### 3.3.3 旋转 $$ \begin{vmatrix} cos\theta&-sin\theta&0&0\\ sin\theta&cos\theta&0&0\\ 0&0&1&0\\ 0&0&0&1 \end{vmatrix}· \left(\begin{matrix} x\\y\\z\\1 \end{matrix}\right) = \left(\begin{matrix} cos\theta·x-sin\theta·y\\ sin\theta·x+cos\theta·y\\ z\\ 1 \end{matrix}\right) $$ ```c++ //将物体沿着z轴进行旋转 glm::rotate(model, (float)glfwGetTime(), glm::vec3(0.0f, 0.0f, 1.0f)); ``` #### 3.3.4 反射 关于x轴反射 $$ \begin{vmatrix} 1&0&0&0\\ 0&-1&0&0\\ 0&0&1&0\\ 0&0&0&0 \end{vmatrix}· \left(\begin{matrix} x\\y\\z\\1 \end{matrix}\right) =\left(\begin{matrix} cos\theta·x-sin\theta·y\\ x\\-y\\z\\1 \end{matrix}\right) $$ #### 3.3.5 切变 $$ \begin{vmatrix} 1&b&0&0\\ c&1&0&0\\ 0&0&1&0\\ 0&0&0&0 \end{vmatrix}· \left(\begin{matrix} x\\y\\z\\1 \end{matrix}\right) =\left(\begin{matrix} cos\theta·x-sin\theta·y\\ x+by\\ cx+y\\ z\\ 1 \end{matrix}\right) $$ ### 3.4 二维组合变换 #### 3.4.1 缩放 $$ \begin{vmatrix} S_1&0&0&0\\ 0&S_2&0&0\\ 0&0&S_3&0\\ 0&0&0&1 \end{vmatrix}· \left(\begin{matrix} x\\y\\z\\1 \end{matrix}\right) = \left(\begin{matrix} S_1·x\\ S_2·y\\ S_3·z\\ 1 \end{matrix}\right) $$ #### 3.4.2 位移 $$ \begin{vmatrix} 1&0&0&T_x\\ 0&1&0&T_y\\ 0&0&1&T_z\\ 0&0&0&1 \end{vmatrix}· \left(\begin{matrix} x\\y\\z\\1 \end{matrix}\right) = \left(\begin{matrix} x+T_x\\ y+T_y\\ z+T_z\\ 1 \end{matrix}\right) $$ #### 3.4.3 旋转 ### 3.5 三维图形绘制 ### 3.6 三维变换 ### 3.7 三维组合变换 ### 3.8 键盘控制 #### 3.8.1 摄像机 ##### 3.8.1.1 Look At矩阵 $$ LookAt= \begin{vmatrix} R_x&R_y&R_z&0\\ U_x&U_y&U_z&0\\ D_x&D_y&D_z&0\\ 0&0&0&1 \end{vmatrix}· \begin{vmatrix} 1&0&0&-P_x\\ 0&1&0&-P_y\\ 0&0&1&-P_z\\ 0&0&0&1 \end{vmatrix} $$ ##### 3.8.1.1 摄像机类 将摄像机类单独写进一个头文件中 ```c++ #ifndef CAMERA_H #define CAMERA_H #include #include #include #include // 为摄像机的移动定义了几种的选项 enum Camera_Movement { FORWARD, BACKWARD, LEFT, RIGHT }; // 初始化摄像机变量 const float YAW = -90.0f; const float PITCH = 0.0f; const float SPEED = 2.5f; const float SENSITIVITY = 0.1f; const float ZOOM = 45.0f; // 摄像机类,处理输入并计算相应的欧拉角,矢量和矩阵 class Camera { public: // 摄像机变量 glm::vec3 Position; glm::vec3 Front; glm::vec3 Up; glm::vec3 Right; glm::vec3 WorldUp; // 欧拉角 float Yaw; float Pitch; // 可调选项 float MovementSpeed; float MouseSensitivity; float Zoom; // 向量构造器 Camera(glm::vec3 position = glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f), float yaw = YAW, float pitch = PITCH) : Front(glm::vec3(0.0f, 0.0f, -1.0f)), MovementSpeed(SPEED), MouseSensitivity(SENSITIVITY), Zoom(ZOOM) { Position = position; WorldUp = up; Yaw = yaw; Pitch = pitch; updateCameraVectors(); } // 含标量的构造器 Camera(float posX, float posY, float posZ, float upX, float upY, float upZ, float yaw, float pitch) : Front(glm::vec3(0.0f, 0.0f, -1.0f)), MovementSpeed(SPEED), MouseSensitivity(SENSITIVITY), Zoom(ZOOM) { Position = glm::vec3(posX, posY, posZ); WorldUp = glm::vec3(upX, upY, upZ); Yaw = yaw; Pitch = pitch; updateCameraVectors(); } // 返回使用欧拉角和LookAt矩阵计算的view矩阵 glm::mat4 GetViewMatrix() { return glm::lookAt(Position, Position + Front, Up); } // 处理从任何类似键盘的输入系统接收的输入,以摄像机定义的ENUM形式接受输入参数(从窗口系统中抽象出来) void ProcessKeyboard(Camera_Movement direction, float deltaTime) { float velocity = MovementSpeed * deltaTime; if (direction == FORWARD) Position += Front * velocity; if (direction == BACKWARD) Position -= Front * velocity; if (direction == LEFT) Position -= Right * velocity; if (direction == RIGHT) Position += Right * velocity; } // 处理从鼠标输入系统接收的输入,预测x和y方向的偏移值 void ProcessMouseMovement(float xoffset, float yoffset, GLboolean constrainPitch = true) { xoffset *= MouseSensitivity; yoffset *= MouseSensitivity; Yaw += xoffset; Pitch += yoffset; // 确保当pitch超出范围时,屏幕不会翻转 if (constrainPitch) { if (Pitch > 89.0f) Pitch = 89.0f; if (Pitch < -89.0f) Pitch = -89.0f; } // 使用更新的欧拉角更新3个向量 updateCameraVectors(); } // 处理从鼠标滚轮事件接收的输入 void ProcessMouseScroll(float yoffset) { if (Zoom >= 1.0f && Zoom <= 45.0f) Zoom -= yoffset; if (Zoom <= 1.0f) Zoom = 1.0f; if (Zoom >= 45.0f) Zoom = 45.0f; } private: // 从更新的CameraEuler的欧拉角计算前向量 void updateCameraVectors() { // 计算新的前向量 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)); Front = glm::normalize(front); // 再计算右向量和上向量 Right = glm::normalize(glm::cross(Front, WorldUp)); // 标准化 Up = glm::normalize(glm::cross(Right, Front)); } }; #endif ``` 在主函数中设置鼠标事件和滚轮事件 ```c++ glfwSetCursorPosCallback(window, mouse_callback); glfwSetScrollCallback(window, scroll_callback); ```