# 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 可能差错
##### 1.2.1.1 **一开始选错generator, 导致无法进行Configure**
解决方案
```
1.点击File > DeleteCache;
2.点击Configure按钮
3.重新进行配置
```
##### 1.2.1.2 **visual studio 运行程序出现拒绝访问**
解决方案
```
禁用360安全卫士,关闭即可
```
## 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()` 里所设置的颜色。。
最后,我们可以得到窗口如下。
## 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);
}
```
绘制结果如下
#### 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);
}
```
绘制结果如下
### 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);
```