【深圳大学计算机图形学】实验二 三维模型读取与控制

目录

实验目的与要求

实验过程及内容

实验2.1 OpenGL鼠标键盘的交互

实验2.2 OFF格式的模型显示

实验2.3三维模型的平移、缩放和旋转

1. Task-1:在TriMesh.cpp中完善storeFacesPoints函数。

2. Task-2:在TriMesh.cpp中完善generateCube函数。

3. Task-3:打开main.cpp的bindObjectAndData()里的注释。

4.Task-4:在main.cpp中完善display函数。

实验二 三维模型读取与控制

实验结论

实验代码

实验2.1——main.cpp

实验2.2——main.cpp

实验2.3——main.cpp

实验2.3——TriMesh.cpp

实验二——main.cpp

实验二——TriMesh.cpp


实验目的与要求

  1. 熟悉OpenGL 三维模型的读取与处理;理解三维模型的基本变换操作;掌握鼠标键盘交互控制逻辑;掌握着色器中uniform关键字的使用以及数据传输方法。
  2. OFF格式三维模型文件的读取:完成对OFF格式三维模型文件的读取与显示,可改变物体的显示颜色。
  3. 三维模型的旋转动画:结合模型进行旋转变换的过程,为模型添加自动的旋转动画。
  4. 键盘鼠标的交互:通过键盘设定选择绕x、y、z轴进行旋转,鼠标左右键控制动画的开始与暂停。

实验过程及内容

实验2.1 OpenGL鼠标键盘的交互

(1)查看key_callback函数,即键盘按键响应回调函数。它实现了根据按键的不同而进行不同的操作,即更改正方形的颜色以及关闭窗口。

(2)查看mouse_button_callback函数,即鼠标点击响应回调函数。它实现了根据鼠标的不同点击而进行不同的操作

(3)在main函数中设置键盘回调函数和鼠标点击回调函数,使得在点击鼠标或者敲击键盘时会跳转到响应函数进行判断和进行相关的操作。

(4)添加鼠标回调函数sroll_callback,根据鼠标滚轮的y偏移量来旋转图形,向上滚动时顺时针旋转,向下滚动时逆时针旋转。

(5)在main函数中绑定鼠标回调函数,使得滚动鼠标时会跳转到回调函数并执行相应操作。

(6)运行代码,滚动鼠标滚轮,发现成功旋转了正方形,如图所示。

实验2.2 OFF格式的模型显示

1. 从文件中读取顶点和面片信息,然后将它们分别保存到 vertices 和 faces 这两个数据结构中,以便后续的图形渲染或处理。这是加载和解析三维模型数据时的常见操作。

2. 将每个三角面片的顶点坐标和颜色信息从相应的数据结构中提取出来,并将它们按照一定的顺序添加到 points 和 colors 这两个数据结构中。通常用于准备渲染的数据,以便后续渲染时可以正确绘制三维模型。

3. 这两行代码的目的是将顶点坐标和颜色数据存储在GPU的缓冲区对象中,以便在渲染时可以高效地访问和使用这些数据。通常,这些数据将在着色器程序中用于绘制三维模型。

4. 这段代码的作用是清除颜色缓冲和深度缓冲,然后使用 glDrawArrays 函数渲染场景中的三维模型。在渲染循环中,该函数通常会被多次调用,以连续更新屏幕上的图像。清除颜色和深度缓冲是为了准备绘制新的场景,确保之前的图像不会留在屏幕上。

5. 以下代码的作用也是将顶点数据和颜色数据存储在GPU的缓冲区对象中,以便在渲染时可以高效地访问和使用这些数据。

6. 这段代码根据用户的按键操作来启用或禁用深度测试。深度测试是在三维渲染中非常重要的功能,它决定了物体的渲染顺序以及遮挡关系,对于实现真实感渲染非常关键。这里设置通过按下"1"和“Shift+1” 来切换深度测试的状态。

7. 此处允许用户通过按下不同的数字键来控制剔除的状态,包括启用或关闭反面剔除和正面剔除,是一种用于提高渲染性能和减少不必要绘制的常见渲染优化技术。

8. 这段代码允许用户通过按下“4”和“Shift+4”来切换渲染模式,从填充模式切换到线绘制模式,或从线绘制模式切换回填充模式。线绘制模式通常用于可视化边界或轮廓,对于调试和观察模型非常有用。

9. 此处更改窗口标题,改为“学号_姓名_实验2.2”的格式,如下图所示。

10. 按1 启用深度测试

11. 按Shift+1 关闭深度测试

12. 按2 启用反面剔除

13. 按Shift+2 关闭反面剔除

14. 按3 启用正面剔除

15. 按Shift+3 关闭正面剔除

16. 按4 启用线绘制模式

17. 按Shift+4 关闭线绘制模式

实验2.3三维模型的平移、缩放和旋转

1. Task-1:在TriMesh.cpp中完善storeFacesPoints函数。

首先,清空之前存储在points和colors容器中的任何数据,以确保这两个容器是空的,然后准备存储新的数据。

接着通过循环遍历存储三角面片顶点索引的faces容器。每个vec3i结构包含了三个无符号整数,表示一个三角面片上的三个顶点的索引。

对于每个三角面片,从vertex_positions和vertex_colors容器中提取顶点的坐标和颜色。这是通过使用三个无符号整数索引idx,idy,和idz来实现的,这些整数分别代表了当前三角面片的三个顶点。

将提取的顶点坐标按照适当的顺序(通常是逆时针)添加到points容器,以便稍后渲染这些点。同时,将相应的颜色值按照相同的顺序添加到colors容器中,以确保颜色正确地与点关联。

2. Task-2:在TriMesh.cpp中完善generateCube函数。

cleanData() 函数用于清除之前的数据,确保数据容器是空的。

使用一个 for 循环遍历八个顶点。在这个循环中,将每个顶点的位置存储在 vertex_positions 容器中,并将相应的颜色存储在 vertex_colors 容器中。这样,每个顶点都有一个位置和颜色。

紧接着,定义了每个三角面片的顶点索引。这些面片用于构建立方体的模型。每个 vec3i 结构包含了三个无符号整数,表示一个三角面片上的三个顶点的索引。注意,这里的顶点索引是按照适当的顺序组织,以确保面片正确渲染。

一旦所有的顶点位置、颜色和面片信息都准备好,接着调用 storeFacesPoints() 函数,这个函数在内部会将每个面片的点和颜色信息存储在 points 和 colors 容器中。这是为了将这些数据传递给 GPU 以供渲染使用。

3. Task-3:打开main.cpp的bindObjectAndData()里的注释。

glBufferSubData函数用于更新缓冲区对象(Buffer Object)的数据。此处用于更新顶点数据和颜色数据。

第一行代码的作用是将mesh对象的顶点数据复制到当前绑定的缓冲区对象中。

第二行代码的作用是将mesh对象的颜色数据复制到当前绑定的缓冲区对象中,紧挨着顶点数据。

4.Task-4:在main.cpp中完善display函数。

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);:用于清除颜色缓冲和深度缓冲,准备开始渲染新的帧。

glUseProgram(cube_object.program);:这里指定了要使用的着色器程序,它将用于处理顶点和片段着色。cube_object.program表示立方体着色器程序。

glBindVertexArray(cube_object.vao);:这一行绑定了VAO(Vertex Array Object),它包含了渲染所需的所有顶点数据、顶点属性配置等信息。通过绑定VAO,告诉OpenGL要使用这个VAO中的数据来渲染。

创建一个4x4的变换矩阵glm::mat4 m,并初始化为单位矩阵。这个矩阵将用于对立方体进行平移、旋转和缩放操作。

glm::mat4 rotationMatrix:这个部分构建了一个旋转矩阵,根据rotateTheta变量的值绕不同的轴进行旋转。旋转顺序是X轴、Y轴、Z轴。这个矩阵表示了希望对模型进行旋转变换。

glm::mat4 scaleMatrix:这里构建了一个缩放矩阵,根据scaleTheta的值在三个轴上缩放模型。这个矩阵表示了希望对模型进行缩放变换。

glm::mat4 translateMatrix:这一部分构建了一个平移矩阵,根据translateTheta的值在X、Y和Z轴上进行平移。这个矩阵表示了希望对模型进行平移变换。

m = translateMatrix * rotationMatrix * scaleMatrix;:这一行将平移、旋转和缩放矩阵按照指定的顺序相乘,得到最终的变换矩阵m。这个变换矩阵表示了所有变换操作的组合,用于将模型从局部坐标系变换到世界坐标系。

glUniformMatrix4fv(cube_object.matrixLocation, 1, GL_FALSE, glm::value_ptr(m));:这一行将最终的变换矩阵传递给着色器,以便在顶点着色器中对顶点进行变换。cube_object.matrixLocation表示变换矩阵在着色器中的位置。

最后,glDrawArrays(GL_TRIANGLES, 0, cube->getPoints().size());:用于绘制立方体。它指定了要绘制的图元类型(这里是三角形),起始顶点索引(0),以及顶点的数量(由cube->getPoints().size()得到)。这将触发着色器程序对顶点进行渲染,实现最终的渲染效果。

4.运行代码,效果如下图所示。

实验二 三维模型读取与控制

1.选择实验2.3完成后的代码作为基础代码进行改动。

2.更改init函数以读入cow.off文件。

3.运行代码,成果读入文件并绘制出了牛的图像。

实验结论

通过此次实验,加深了对opengl相关知识的掌握和应用。

熟悉了OpenGL 三维模型的读取与处理;理解三维模型的基本变换操作;掌握鼠标键盘交互控制逻辑;掌握着色器中uniform关键字的使用以及数据传输方法。

OFF格式三维模型文件的读取:完成对OFF格式三维模型文件的读取与显示,可改变物体的显示颜色。

三维模型的旋转动画:结合模型进行旋转变换的过程,为模型添加自动的旋转动画。

键盘鼠标的交互:通过键盘设定选择绕x、y、z轴进行旋转,鼠标左右键控制动画的开始与暂停。

总的来说,基本学会了OpenGL 三维模型的读取与处理、三维模型的基本变换操作、鼠标键盘交互控制逻辑等知识。

实验代码

实验2.1——main.cpp

#include "Angel.h"	// Angel.h 包含了初始化OpenGL和GLFW的必要头文件
#include <string>

// 定义菜单选项的常量
const int MENU_CHOICE_WHITE = 0;
const int MENU_CHOICE_BLACK = 1;
const int MENU_CHOICE_RED = 2;
const int MENU_CHOICE_GREEN = 3;
const int MENU_CHOICE_BLUE = 4;
const int MENU_CHOICE_YELLOW = 5;
const int MENU_CHOICE_ORANGE = 6;
const int MENU_CHOICE_PURPLE = 7;

// 定义动画开始和停止的常量
const int MENU_START_ANIMATION = 8;
const int MENU_STOP_ANIMATION = 9;

// 设置颜色
const glm::vec3 WHITE(1.0, 1.0, 1.0);	// 白色
const glm::vec3 BLACK(0.0, 0.0, 0.0);	// 黑色
const glm::vec3 RED(1.0, 0.0, 0.0);		// 红色
const glm::vec3 GREEN(0.0, 1.0, 0.0);	// 绿色
const glm::vec3 BLUE(0.0, 0.0, 1.0);	// 蓝色
const glm::vec3 YELLOW(1.0, 1.0, 0.0);	// 黄色
const glm::vec3 ORANGE(1.0, 0.65, 0.0);	// 橙色
const glm::vec3 PURPLE(0.8, 0.0, 0.8);	// 紫色

// 主窗口变量
const int SQUARE_NUM = 6;						// 正方形的数量
const int SQUARE_NUM_POINTS = 4 * SQUARE_NUM;	// 每个正方形的顶点数量
int mainWindow;
int mainWindowMenu;
int mainWindowSubmenu;
int width = 600;	// 主窗口宽度
int height = 600;	// 主窗口高度
double offsetAngle = 0;		// 角度偏移量
double delta = 0.01;		// 每次改变角度偏移的变化量
glm::vec3 mainWindowSquareColor = WHITE;	// 主窗口中正方形的颜色

GLuint square_vao, ellipse_vao, program;	// OpenGL对象和程序的标识符

// 窗口大小变化回调函数
void framebuffer_size_callback(GLFWwindow* window, int width, int height){
	//确保视口与新窗口尺寸相匹配;
	//请注意,宽度和高度将明显大于在视网膜显示器上指定的宽度和高度。
	glViewport(0, 0, width, height);
}

// 获得正方形的每个角度
double getSquareAngle(int point){
	return (M_PI / 4 + (M_PI / 2 * point)) + offsetAngle;
}

// 生成正方形顶点的属性
void generateSquarePoints(glm::vec2 vertices[], glm::vec3 colors[], int squareNum, int startVertexIndex) {
	double scale = 0.90;					// 初始的正方形尺度(0.9是一个缩放因子)
	double scaleAdjust = scale / squareNum; // 每个正方形的尺度递减量
	glm::vec2 center(0.0, 0.0);				// 正方形中心坐标

	int vertexIndex = startVertexIndex;		// 顶点数组索引,用于确定从哪个索引开始存储顶点数据

	for (int i = 0; i < squareNum; i++) {
		// 交替选择当前正方形的颜色,奇数和偶数正方形颜色不同
		glm::vec3 currentColor = 0 == i % 2 ? mainWindowSquareColor : BLACK;

		for (int j = 0; j < 4; j++) {
			double currentAngle = getSquareAngle(j); // 获取正方形的每个角度

			// 使用三角函数计算角度位置,并通过缩放和中心平移获得每个顶点的位置
			vertices[vertexIndex] = glm::vec2(cos(currentAngle), sin(currentAngle)) * glm::vec2(scale, scale) + center;
			
			colors[vertexIndex] = currentColor;		// 将颜色赋给当前顶点
			vertexIndex++;							// 移动到下一个顶点
		}

		scale -= scaleAdjust; // 调整下一个正方形的尺度,使其逐渐缩小
	}
}

void mainWindowInit()
{
	// 定义位置数组和颜色数组
	glm::vec2 vertices[SQUARE_NUM * 4];
	glm::vec3 colors[SQUARE_NUM * 4];

	// 创建主窗口中多个正方形
	generateSquarePoints(vertices, colors, SQUARE_NUM, 0);

	// 创建顶点数组对象
	glGenVertexArrays(1, &square_vao);  	// 分配1个顶点数组对象
	glBindVertexArray(square_vao);  		// 绑定顶点数组对象

	// 创建并初始化顶点缓存对象。
	GLuint vbo;
	glGenBuffers(1, &vbo);	// 生成1个顶点缓存对象的名称,并将其分配给vbo
	glBindBuffer(GL_ARRAY_BUFFER, vbo);	// 绑定GL_ARRAY_BUFFER,指示vbo是一个顶点缓存对象

	// 分配空间并填充数据
	// 创建一个缓冲区,分配足够的空间来存储顶点位置和颜色数据
	//使用GL_STATIC_DRAW标志表示数据不会频繁变化。
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertices) + sizeof(colors), NULL, GL_STATIC_DRAW);

	// 分别读取数据
	// 将顶点位置数据拷贝到缓冲区的起始位置
	glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices);

	// 将顶点颜色数据拷贝到缓冲区中顶点位置数据之后的位置
	glBufferSubData(GL_ARRAY_BUFFER, sizeof(vertices), sizeof(colors), colors);

	// 读取着色器并使用
	std::string vshader, fshader;

	vshader = "shaders/vshader.glsl";	// 指定顶点着色器文件路径
	fshader = "shaders/fshader.glsl";	// 指定片段着色器文件路径

	// 创建着色器程序对象并使用。
	program = InitShader(vshader.c_str(), fshader.c_str());
	glUseProgram(program);

	// 从顶点着色器中初始化顶点的位置
	GLuint pLocation = glGetAttribLocation(program, "vPosition");
	glEnableVertexAttribArray(pLocation);
	glVertexAttribPointer(pLocation, 2, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(0));

	// 从顶点着色器中初始化顶点的颜色
	GLuint cLocation = glGetAttribLocation(program, "vColor");
	glEnableVertexAttribArray(cLocation);
	glVertexAttribPointer(cLocation, 3, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(sizeof(vertices)));

	// 黑色背景
	glClearColor(0.0, 0.0, 0.0, 1.0);
}

// 主窗口的渲染函数
void mainWindowDisplay(){
	mainWindowInit();				// 重绘时重新初始化颜色数据,确保颜色最新
	glClear(GL_COLOR_BUFFER_BIT);	// 清空颜色缓冲区,准备渲染
	glUseProgram(program);			// 使用指定的着色器程序

	// 创建顶点数组对象
	glBindVertexArray(square_vao);  	// 绑定顶点数组对象,告诉OpenGL使用哪个VAO

	for (int i = 0; i < SQUARE_NUM; i++) {
		// 使用三角扇形渲染正方形,每个正方形使用4个顶点
		// 第一个参数是渲染模式,这里使用三角扇形
		// 第二个参数是要渲染的顶点数组的起始索引
		// 第三个参数是渲染的顶点数量
		glDrawArrays(GL_TRIANGLE_FAN, (i * 4), 4);
	}	
}

// 主窗口键盘回调函数。
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode){
	if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS){
		glfwSetWindowShouldClose(window, GL_TRUE);			// 如果按下Esc键,关闭窗口
	}
	else if (key == GLFW_KEY_R && action == GLFW_PRESS){
		mainWindowSquareColor = RED;						// 如果按下R键,将正方形设置为红色
	}
	else if (key == GLFW_KEY_B && action == GLFW_PRESS){
		mainWindowSquareColor = BLUE;						// 如果按下B键,将正方形设置为蓝色
	}
	else if (key == GLFW_KEY_W && action == GLFW_PRESS){
		mainWindowSquareColor = WHITE;						// 如果按下W键,将正方形设置为白色
	}
}


// 主窗口鼠标点击回调函数。
void mouse_button_callback(GLFWwindow* window, int button, int action, int mode){
	// 按下鼠标左键,图形变为绿色。
	if (button == GLFW_MOUSE_BUTTON_LEFT && action == GLFW_PRESS){
		mainWindowSquareColor = GREEN;
	}
	// 按下鼠标右键,图形变为黄色。
	else if (button == GLFW_MOUSE_BUTTON_RIGHT && action == GLFW_PRESS){
		mainWindowSquareColor = YELLOW;
	}
}

// 鼠标滚轮回调函数
void sroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
	// 定义旋转速度,可根据需要调整
	double rotationSpeed = 0.1;

	// 根据鼠标滚轮的y偏移量来旋转图形
	// 向上滚动时顺时针旋转,向下滚动时逆时针旋转
	offsetAngle -= yoffset * rotationSpeed;

	// 重新绘制图形
	mainWindowDisplay();
}

void printHelp() {
	printf("%s\n\n", "Interaction");
	printf("Keys to update the background color:\n");
	printf("'r' - red\n'b' - blue\n'w' - white\n");
	printf("Mouse click to update color:\n");
	printf("'left' - green\n'right' - yellow\n");
	printf("Mouse sroll to rotate:\n");
	printf("'up' - clockwise\n");
	printf("'down' - anticlockwise\n");
}

int main(int argc, char** argv)
{

	glfwInit();		// 初始化GLFW

	// 配置OpenGL版本和核心模式
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

	// 如果在Mac OS上编译,启用OpenGL前向兼容性
#ifdef __APPLE__
	glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif

	// 创建主窗口。
	GLFWwindow* mainwindow = glfwCreateWindow(width, height, "mainWindow hyf2021150047", NULL, NULL);
	if (mainwindow == NULL){
		std::cout << "Failed to create GLFW window!" << std::endl;
		glfwTerminate();
		return -1;
	}
	glfwMakeContextCurrent(mainwindow);		// 将主窗口的OpenGL上下文设置为当前上下文
	glfwSetFramebufferSizeCallback(mainwindow, framebuffer_size_callback);	// 设置窗口大小变化回调函数
	glfwSetKeyCallback(mainwindow, key_callback);							// 设置键盘回调函数
	glfwSetMouseButtonCallback(mainwindow, mouse_button_callback);			 // 设置鼠标点击回调函数

	// @TODO: 创建鼠标滚轮回调函数函数并绑定。
	glfwSetScrollCallback(mainwindow, sroll_callback);

	// 初始化glad,确保OpenGL函数指针可用
	if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)){
		std::cout << "Failed to initialize GLAD" << std::endl;
		return -1;
	}

	mainWindowInit();					// 初始化主窗口
	printHelp();						// 打印交互操作说明

	// 循环渲染
	while (!glfwWindowShouldClose(mainwindow)){
		mainWindowDisplay();			// 渲染主窗口内容
		glfwSwapBuffers(mainwindow);	// 交换前后缓冲区
		glfwPollEvents();				// 处理事件
	}

	glfwTerminate();					// 清理并关闭窗口
	return 0;
}

实验2.2——main.cpp

#include "Angel.h"

#include <vector>
#include <fstream>
#include <string>

//#pragma comment(lib, "glew32.lib")

using namespace std;

int window;

// 三角面片中的顶点序列
typedef struct vIndex {
	unsigned int a, b, c;
	vIndex(int ia, int ib, int ic) : a(ia), b(ib), c(ic) {}
} vec3i;

std::vector<glm::vec3> vertices;
std::vector<vec3i> faces;

int nVertices = 0;
int nFaces = 0;
int nEdges = 0;

std::vector<glm::vec3> points;   // 传入着色器的绘制点
std::vector<glm::vec3> colors;   // 传入着色器的颜色

const int NUM_VERTICES = 8;

const glm::vec3 vertex_colors[NUM_VERTICES] = {
	glm::vec3(1.0, 1.0, 1.0),  // White
	glm::vec3(1.0, 1.0, 0.0),  // Yellow
	glm::vec3(0.0, 1.0, 0.0),  // Green
	glm::vec3(0.0, 1.0, 1.0),  // Cyan
	glm::vec3(1.0, 0.0, 1.0),  // Magenta
	glm::vec3(1.0, 0.0, 0.0),  // Red
	glm::vec3(0.0, 0.0, 0.0),  // Black
	glm::vec3(0.0, 0.0, 1.0)   // Blue
};

void read_off(const std::string filename)
{
	// fin打开文件读取文件信息
	if (filename.empty()) {
		return;
	}
	std::ifstream fin;
	fin.open(filename);
	// @TODO: Task1:修改此函数读取OFF文件中三维模型的信息
	if (!fin)
	{
		printf("Failed to open the file.\n");
		return;
	}
	else
	{
		printf("The file opens succesfully.\n");
		vertices.clear();
		faces.clear();

		// 读取OFF字符串
		string str;
		fin >> str;
		// 读取文件中顶点数、面片数、边数
		fin >> nVertices >> nFaces >> nEdges;

		//根据顶点数,循环读取每个顶点坐标,将其保存到vertices
		for (int i = 0; i < nVertices; i++) {
			double a, b, c;
			fin >> a >> b >> c;
			glm::vec3 vertex(a, b, c);
			vertices.push_back(vertex);
		}

		// 根据面片数,循环读取每个面片信息,并用构建的vec3i结构体保存到faces
		for (int i = 0; i < nFaces; i++) {
			int num;
			fin >> num;
			int a, b, c;
			fin >> a >> b >> c;
			vec3i face(a, b, c);
			faces.push_back(face);
		}
	}
	fin.close();
}

void storeFacesPoints()
{
	points.clear();
	colors.clear();
	// @TODO: Task1:修改此函数在points和colors容器中存储每个三角面片的各个点和颜色信息
	// 在points容器中,依次添加每个面片的顶点,并在colors容器中,添加该点的颜色信息
	// 比如一个正方形由两个三角形构成,那么vertices会由4个顶点的数据构成,faces会记录两个三角形的顶点下标,
	// 而points就是记录这2个三角形的顶点,总共6个顶点的数据。
	// colors容器则是和points的顶点一一对应,保存这个顶点的颜色,这里我们可以使用顶点坐标或者自己设定的颜色赋值。
	for (const vec3i& face : faces) {
		int a = face.a, b = face.b, c = face.c;

		// 添加顶点信息到points容器
		points.push_back(vertices[a]);
		points.push_back(vertices[b]);
		points.push_back(vertices[c]);

		// 添加颜色信息到colors容器
		colors.push_back(vertex_colors[a]);
		colors.push_back(vertex_colors[b]);
		colors.push_back(vertex_colors[c]);
	}
}

void init()
{
	// 读取off模型文件
	read_off("./assets/cube.off");
	storeFacesPoints();

	// 创建顶点数组对象
	GLuint vao[1];
	glGenVertexArrays(1, vao);  	// 分配1个顶点数组对象
	glBindVertexArray(vao[0]);  	// 绑定顶点数组对象

// 创建并初始化顶点缓存对象
	GLuint buffer;
	glGenBuffers(1, &buffer);
	glBindBuffer(GL_ARRAY_BUFFER, buffer);
	glBufferData(GL_ARRAY_BUFFER, points.size() * sizeof(glm::vec3) + colors.size() * sizeof(glm::vec3), NULL, GL_DYNAMIC_DRAW);

	// @TODO: Task1:修改完成后再打开下面注释,否则程序会报错
	// 分别读取数据
	glBufferSubData(GL_ARRAY_BUFFER, 0, points.size() * sizeof(glm::vec3), &points[0]);
	glBufferSubData(GL_ARRAY_BUFFER, points.size() * sizeof(glm::vec3), colors.size() * sizeof(glm::vec3), &colors[0]);

	// 读取着色器并使用
	std::string vshader, fshader;
	vshader = "shaders/vshader.glsl";
	fshader = "shaders/fshader.glsl";
	GLuint program = InitShader(vshader.c_str(), fshader.c_str());
	glUseProgram(program);

	// 从顶点着色器中初始化顶点的位置
	GLuint pLocation = glGetAttribLocation(program, "vPosition");
	glEnableVertexAttribArray(pLocation);
	glVertexAttribPointer(pLocation, 3, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(0));
	// 从顶点着色器中初始化顶点的颜色
	GLuint cLocation = glGetAttribLocation(program, "vColor");
	glEnableVertexAttribArray(cLocation);
	glVertexAttribPointer(cLocation, 3, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(points.size() * sizeof(glm::vec3)));

	// 黑色背景
	glClearColor(0.0, 0.0, 0.0, 1.0);
}

void display(void)
{
	// @TODO: Task2:清理窗口,包括颜色缓存和深度缓存
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	glDrawArrays(GL_TRIANGLES, 0, points.size());
}

// 窗口键盘回调函数。
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode)
{
	if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
	{
		glfwSetWindowShouldClose(window, GL_TRUE);
	}
	else if (key == GLFW_KEY_MINUS && action == GLFW_PRESS)
	{
		cout << "read cube.off" << endl;
		read_off("./assets/cube.off");
		storeFacesPoints();
		// @TODO: Task1:修改完成后再打开下面注释,否则程序会报错
		glBufferSubData(GL_ARRAY_BUFFER, 0, points.size() * sizeof(glm::vec3), &points[0]);
		glBufferSubData(GL_ARRAY_BUFFER, points.size() * sizeof(glm::vec3), colors.size() * sizeof(glm::vec3), &colors[0]);
		//glutPostWindowRedisplay(window);
	}
	else if (key == GLFW_KEY_EQUAL && action == GLFW_PRESS)
	{
		cout << "read cube2.off" << endl;
		read_off("./assets/cube2.off");
		storeFacesPoints();
		// @TODO: Task1:修改完成后再打开下面注释,否则程序会报错
		 glBufferSubData(GL_ARRAY_BUFFER, 0, points.size() * sizeof(glm::vec3), &points[0]);
		 glBufferSubData(GL_ARRAY_BUFFER, points.size() * sizeof(glm::vec3), colors.size() * sizeof(glm::vec3), &colors[0]);
	}
	else if (key == GLFW_KEY_0 && action == GLFW_PRESS)
	{
		cout << "reset" << endl;
		// 关闭深度测试
		glDisable(GL_DEPTH_TEST);
		// 关闭面剔除
		glDisable(GL_CULL_FACE);
		// 使用填充绘制模式
		glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
	}
	// @TODO: Task2:启用深度测试
	else if (key == GLFW_KEY_1 && action == GLFW_PRESS && mode == 0x0000){// 0x0000表示组合键为空 
		cout << "depth test: enable" << endl;
		glEnable(GL_DEPTH_TEST);
	}
	// @TODO: Task2:关闭深度测试
	else if (key == GLFW_KEY_1 && action == GLFW_PRESS && mode == GLFW_MOD_SHIFT){
		cout << "depth test: disable" << endl;
		glDisable(GL_DEPTH_TEST);
	}
	// @TODO: Task3:启用反面剔除
	else if (key == GLFW_KEY_2 && action == GLFW_PRESS && mode == 0x0000){
		cout << "cull back: enable" << endl;
		glEnable(GL_CULL_FACE);
		glCullFace(GL_BACK);
	}
	// @TODO: Task3:关闭反面剔除
	else if (key == GLFW_KEY_2 && action == GLFW_PRESS && mode == GLFW_MOD_SHIFT){
		cout << "cull back: disable" << endl;
		glDisable(GL_CULL_FACE);
	}
	// @TODO: Task4:启用正面剔除
	else if (key == GLFW_KEY_3 && action == GLFW_PRESS && mode == 0x0000){
		cout << "cull front: enable" << endl;
		glEnable(GL_CULL_FACE);
		glCullFace(GL_FRONT);
	}
	// @TODO: Task4:关闭正面剔除
	else if (key == GLFW_KEY_3 && action == GLFW_PRESS && mode == GLFW_MOD_SHIFT)
	{
		cout << "cull front: disable" << endl;
		glDisable(GL_CULL_FACE);
	}
	// @TODO: Task5:启用线绘制模式
	else if (key == GLFW_KEY_4 && action == GLFW_PRESS && mode == 0x0000){
		cout << "line mode: enable" << endl;
		glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
	}
	// @TODO: Task5:关闭线绘制模式
	else if (key == GLFW_KEY_4 && action == GLFW_PRESS && mode == GLFW_MOD_SHIFT){
		cout << "line mode: disable" << endl;
		glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
	}
}

void framebuffer_size_callback(GLFWwindow* window, int width, int height);

int main(int argc, char** argv)
{
	// 初始化GLFW库,必须是应用程序调用的第一个GLFW函数
	glfwInit();
	
	// 配置GLFW
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

#ifdef __APPLE__
	glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif

	// 配置窗口属性
	GLFWwindow* window = glfwCreateWindow(600, 600, "2021150047_hyf_实验2.2", NULL, NULL);
	if (window == NULL)
	{
		std::cout << "Failed to create GLFW window" << std::endl;
		glfwTerminate();
		return -1;
	}
	glfwMakeContextCurrent(window);
	glfwSetKeyCallback(window, key_callback);
	glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

	// 调用任何OpenGL的函数之前初始化GLAD
	// ---------------------------------------
	if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
	{
		std::cout << "Failed to initialize GLAD" << std::endl;
		return -1;
	}

	init();
	while (!glfwWindowShouldClose(window))
	{
		display();

		// 交换颜色缓冲 以及 检查有没有触发什么事件(比如键盘输入、鼠标移动等)
		// -------------------------------------------------------------------------------
		glfwSwapBuffers(window);
		glfwPollEvents();
	}
	return 0;
}

// 每当窗口改变大小,GLFW会调用这个函数并填充相应的参数供你处理。
// ---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
	// make sure the viewport matches the new window dimensions; note that width and 
	// height will be significantly larger than specified on retina displays.
	glViewport(0, 0, width, height);
}

实验2.3——main.cpp

#include "Angel.h"
#include "TriMesh.h"

#include <vector>
#include <string>

// 定义三个坐标轴的常量
const int X_AXIS = 0;
const int Y_AXIS = 1;
const int Z_AXIS = 2;

// 定义三种变换类型的常量
const int TRANSFORM_SCALE = 0;
const int TRANSFORM_ROTATE = 1;
const int TRANSFORM_TRANSLATE = 2;

// 定义Delta的变化率和默认值
const double DELTA_DELTA = 0.3;        // Delta的变化率
const double DEFAULT_DELTA = 0.5;      // 默认的Delta值

// 初始化Delta值
double scaleDelta = DEFAULT_DELTA;
double rotateDelta = DEFAULT_DELTA;
double translateDelta = DEFAULT_DELTA;

// 初始化三种变换的控制变量
glm::vec3 scaleTheta(1.0, 1.0, 1.0);     // 缩放控制变量
glm::vec3 rotateTheta(0.0, 0.0, 0.0);    // 旋转控制变量
glm::vec3 translateTheta(0.0, 0.0, 0.0); // 平移控制变量

// 设置当前变换类型为旋转
int currentTransform = TRANSFORM_ROTATE;
int mainWindow;

// 定义OpenGL对象结构体
struct openGLObject
{
	// 顶点数组对象
	GLuint vao;
	// 顶点缓存对象
	GLuint vbo;

	// 着色器程序
	GLuint program;
	// 着色器文件
	std::string vshader;
	std::string fshader;
	// 着色器变量
	GLuint pLocation;
	GLuint cLocation;
	GLuint matrixLocation;
	GLuint darkLocation;
};

openGLObject cube_object;

// 创建一个三角网格对象
TriMesh* cube = new TriMesh();

// 回调函数,用于设置OpenGL窗口的大小
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
	glViewport(0, 0, width, height);
}

// 绑定对象和数据
void bindObjectAndData(TriMesh* mesh, openGLObject& object, const std::string& vshader, const std::string& fshader) {
	// 创建顶点数组对象
	glGenVertexArrays(1, &object.vao);    // 分配1个顶点数组对象
	glBindVertexArray(object.vao);         // 绑定顶点数组对象

	// 创建并初始化顶点缓存对象
	glGenBuffers(1, &object.vbo);
	glBindBuffer(GL_ARRAY_BUFFER, object.vbo);
	glBufferData(GL_ARRAY_BUFFER,
		mesh->getPoints().size() * sizeof(glm::vec3)
			+ mesh->getColors().size() * sizeof(glm::vec3),
		NULL,
		GL_STATIC_DRAW);

	// @TODO: Task3-修改完TriMesh.cpp的代码成后再打开下面注释,否则程序会报错
	 glBufferSubData(GL_ARRAY_BUFFER, 0, mesh->getPoints().size() * sizeof(glm::vec3), &mesh->getPoints()[0]);
	 glBufferSubData(GL_ARRAY_BUFFER, mesh->getPoints().size() * sizeof(glm::vec3), mesh->getColors().size() * sizeof(glm::vec3), &mesh->getColors()[0]);

	object.vshader = vshader;
	object.fshader = fshader;
	object.program = InitShader(object.vshader.c_str(), object.fshader.c_str());

	// 从顶点着色器中初始化顶点的位置
	object.pLocation = glGetAttribLocation(object.program, "vPosition");
	glEnableVertexAttribArray(object.pLocation);
	glVertexAttribPointer(object.pLocation, 3, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(0));

	// 从顶点着色器中初始化顶点的颜色
	object.cLocation = glGetAttribLocation(object.program, "vColor");
	glEnableVertexAttribArray(object.cLocation);
	glVertexAttribPointer(object.cLocation, 3, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(mesh->getPoints().size() * sizeof(glm::vec3)));

	// 获得矩阵存储位置
	object.matrixLocation = glGetUniformLocation(object.program, "matrix");
}

// 初始化函数
void init()
{
	std::string vshader, fshader;
	// 读取着色器文件路径
	vshader = "shaders/vshader.glsl";
	fshader = "shaders/fshader.glsl";

	cube->generateCube();
	bindObjectAndData(cube, cube_object, vshader, fshader);

	// 设置背景色为黑色
	glClearColor(0.0, 0.0, 0.0, 1.0);
}

// 渲染函数
void display()
{
	// 清空颜色缓冲和深度缓冲
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	glUseProgram(cube_object.program);

	glBindVertexArray(cube_object.vao);

	// 初始化变换矩阵  glm::mat4表示 4x4 矩阵
	glm::mat4 m(1.0, 0.0, 0.0, 0.0,
		0.0, 1.0, 0.0, 0.0,
		0.0, 0.0, 1.0, 0.0,
		0.0, 0.0, 0.0, 1.0);

	// @TODO: Task4-在此处修改函数,计算最终的变换矩阵
	// 调用函数传入三种变化的变化量,累加得到变化矩阵
	// 注意三种变化累加的顺序
	// 构建旋转矩阵
	glm::mat4 rotationMatrix = glm::rotate(glm::mat4(1.0), rotateTheta.x, glm::vec3(1.0, 0.0, 0.0))
		* glm::rotate(glm::mat4(1.0), rotateTheta.y, glm::vec3(0.0, 1.0, 0.0))
		* glm::rotate(glm::mat4(1.0), rotateTheta.z, glm::vec3(0.0, 0.0, 1.0));

	// 构建缩放矩阵
	glm::mat4 scaleMatrix = glm::scale(glm::mat4(1.0), glm::vec3(scaleTheta.x, scaleTheta.y, scaleTheta.z));

	// 构建平移矩阵
	glm::mat4 translateMatrix = glm::translate(glm::mat4(1.0), glm::vec3(translateTheta.x, translateTheta.y, translateTheta.z));

	// 按照平移、旋转、缩放的顺序相乘得到最终的变换矩阵
	m = translateMatrix * rotationMatrix * scaleMatrix;

	// 传递变换矩阵到着色器
	glUniformMatrix4fv(cube_object.matrixLocation, 1, GL_FALSE, glm::value_ptr(m));

	// 绘制立方体中的各个三角形
	glDrawArrays(GL_TRIANGLES, 0, cube->getPoints().size());
}

// 通过Delta值更新Theta
// axis 表示坐标轴,sign 表示增加或减少
// currentTransform 表示当前变换类型
void updateTheta(int axis, int sign) {
	switch (currentTransform) {
		// 根据变换类型,增加或减少某种变换的变化量
	case TRANSFORM_SCALE:
		//增加或减少缩放的 Theta 值
		scaleTheta[axis] += sign * scaleDelta;
		break;
	case TRANSFORM_ROTATE:
		//增加或减少旋转的 Theta 值
		rotateTheta[axis] += sign * rotateDelta;
		break;
	case TRANSFORM_TRANSLATE:
		//增加或减少平移的 Theta 值
		translateTheta[axis] += sign * translateDelta;
		break;
	}
}

// 复原Theta和Delta
void resetTheta()
{
	scaleTheta = glm::vec3(1.0, 1.0, 1.0);	//scaleTheta 表示缩放变换的角度
	rotateTheta = glm::vec3(0.0, 0.0, 0.0);
	translateTheta = glm::vec3(0.0, 0.0, 0.0);
	scaleDelta = DEFAULT_DELTA;				//缩放变换的单位变化量
	rotateDelta = DEFAULT_DELTA;
	translateDelta = DEFAULT_DELTA;
}

// 更新变化Delta值
void updateDelta(int sign)
{
	switch (currentTransform) {
		// 根据变化类型增加或减少每一次变化的单位变化量
	case TRANSFORM_SCALE:
		scaleDelta += sign * DELTA_DELTA;
		break;
	case TRANSFORM_ROTATE:
		rotateDelta += sign * DELTA_DELTA;
		break;
	case TRANSFORM_TRANSLATE:
		translateDelta += sign * DELTA_DELTA;
		break;
	}
}

// 处理键盘输入的回调函数
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode)
{
	switch (key)
	{
		// 退出。
	case GLFW_KEY_ESCAPE:
		if (action == GLFW_PRESS) glfwSetWindowShouldClose(window, GL_TRUE);
		break;
		// 1:缩放模式
	case GLFW_KEY_1:
		if (action == GLFW_PRESS) currentTransform = TRANSFORM_SCALE;
		break;
		// 2: 旋转模式
	case GLFW_KEY_2:
		if (action == GLFW_PRESS) currentTransform = TRANSFORM_ROTATE;
		break;
		// 3: 移动模式
	case GLFW_KEY_3:
		if (action == GLFW_PRESS) currentTransform = TRANSFORM_TRANSLATE;
		break;
		// 4: 绘制线。
	case GLFW_KEY_4:
		if (action == GLFW_PRESS) glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
		break;
		// 5: 绘制面。
	case GLFW_KEY_5:
		if (action == GLFW_PRESS) glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
		break;
		// Q: 增加 x。
	case GLFW_KEY_Q:
		if (action == GLFW_PRESS || action == GLFW_REPEAT) updateTheta(X_AXIS, 1);
		break;
		// A: 减少 x。
	case GLFW_KEY_A:
		if (action == GLFW_PRESS || action == GLFW_REPEAT) updateTheta(X_AXIS, -1);
		break;
		// W: 增加 y。
	case GLFW_KEY_W:
		if (action == GLFW_PRESS || action == GLFW_REPEAT) updateTheta(Y_AXIS, 1);
		break;
		// S: 减少 y。
	case GLFW_KEY_S:
		if (action == GLFW_PRESS || action == GLFW_REPEAT) updateTheta(Y_AXIS, -1);
		break;
		// E: 增加 z。
	case GLFW_KEY_E:
		if (action == GLFW_PRESS || action == GLFW_REPEAT) updateTheta(Z_AXIS, 1);
		break;
		// D: 减少 z。
	case GLFW_KEY_D:
		if (action == GLFW_PRESS || action == GLFW_REPEAT) updateTheta(Z_AXIS, -1);
		break;
		// R: 增加变化量。
	case GLFW_KEY_R:
		if (action == GLFW_PRESS) updateDelta(1);
		break;
		// F: 减少变化量。
	case GLFW_KEY_F:
		if (action == GLFW_PRESS) updateDelta(-1);
		break;
		// T: 所有值重置。
	case GLFW_KEY_T:
		if (action == GLFW_PRESS) resetTheta();
		break;
	}
}

// 输出帮助信息
void printHelp() {
	printf("%s\n\n", "3D Transfomations");
	printf("Keyboard options:\n");
	printf("1: Transform Scale\n");
	printf("2: Transform Rotate\n");
	printf("3: Transform Translate\n");
	printf("q: Increase x\n");
	printf("a: Decrease x\n");
	printf("w: Increase y\n");
	printf("s: Decrease y\n");
	printf("e: Increase z\n");
	printf("d: Decrease z\n");
	printf("r: Increase delta of currently selected transform\n");
	printf("f: Decrease delta of currently selected transform\n");
	printf("t: Reset all transformations and deltas\n");
}

// 清理数据
void cleanData() {
	cube->cleanData();

	// 释放内存
	delete cube;
	cube = NULL;

	// 删除绑定的对象
	glDeleteVertexArrays(1, &cube_object.vao);

	glDeleteBuffers(1, &cube_object.vbo);
	glDeleteProgram(cube_object.program);
}

int main(int argc, char** argv)
{
	// 初始化GLFW库,必须是应用程序调用的第一个GLFW函数
	glfwInit();

	// 配置GLFW
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

#ifdef __APPLE__
	glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif

	// 配置窗口属性
	GLFWwindow* window = glfwCreateWindow(600, 600, "3D Transfomations hyf_2021150047", NULL, NULL);
	if (window == NULL)
	{
		std::cout << "Failed to create GLFW window" << std::endl;
		glfwTerminate();
		return -1;
	}
	glfwMakeContextCurrent(window);
	glfwSetKeyCallback(window, key_callback);
	glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

	// 调用任何OpenGL的函数之前初始化GLAD
	if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
	{
		std::cout << "Failed to initialize GLAD" << std::endl;
		return -1;
	}

	init();
	// 输出帮助信息
	printHelp();
	// 启用深度测试
	glEnable(GL_DEPTH_TEST);
	while (!glfwWindowShouldClose(window))
	{
		display();

		// 交换颜色缓冲 以及 检查有没有触发什么事件(比如键盘输入、鼠标移动等)
		glfwSwapBuffers(window);
		glfwPollEvents();
	}
	cleanData();

	return 0;
}

实验2.3——TriMesh.cpp

#include "TriMesh.h"

// 一些基础颜色
const glm::vec3 basic_colors[8] = {
	glm::vec3(1.0, 1.0, 1.0),	// White
	glm::vec3(1.0, 1.0, 0.0),	// Yellow
	glm::vec3(0.0, 1.0, 0.0),	// Green
	glm::vec3(0.0, 1.0, 1.0),	// Cyan
	glm::vec3(1.0, 0.0, 1.0),	// Magenta
	glm::vec3(1.0, 0.0, 0.0),	// Red
	glm::vec3(0.0, 0.0, 0.0),	// Black
	glm::vec3(0.0, 0.0, 1.0)	// Blue
};

// 立方体的各个点
const glm::vec3 cube_vertices[8] = {
	glm::vec3(-0.5, -0.5, -0.5),
	glm::vec3(0.5, -0.5, -0.5),
	glm::vec3(-0.5,  0.5, -0.5),
	glm::vec3(0.5,  0.5, -0.5),
	glm::vec3(-0.5, -0.5,  0.5),
	glm::vec3(0.5, -0.5,  0.5),
	glm::vec3(-0.5,  0.5,  0.5),
	glm::vec3(0.5,  0.5,  0.5)
};

TriMesh::TriMesh()
{
}

TriMesh::~TriMesh()
{
}

std::vector<glm::vec3> TriMesh::getVertexPositions()
{
	return vertex_positions;
}

std::vector<glm::vec3> TriMesh::getVertexColors()
{
	return vertex_colors;
}

std::vector<vec3i> TriMesh::getFaces()
{
	return faces;
}

std::vector<glm::vec3> TriMesh::getPoints()
{
	return points;
}

std::vector<glm::vec3> TriMesh::getColors()
{
	return colors;
}

void TriMesh::cleanData() {
	// 清空顶点位置和颜色
	vertex_positions.clear();
	vertex_colors.clear();

	// 清空面片信息
	faces.clear();

	// 清空用于传递给 GPU 的顶点和颜色数据
	points.clear();
	colors.clear();
}

void TriMesh::storeFacesPoints() {
	// @TODO: Task-2 修改此函数以将每个三角面片的点和颜色信息存储在 points 和 colors 容器中
	// 根据每个三角面片的顶点索引存储数据,以便将其传递给 GPU
	
	// 清空之前的数据
	points.clear();
	colors.clear();

	// 清空之前的数据
	for (const vec3i& face : faces) {
		// 获取当前面片的顶点索引
		//使用无符号整数可以确保索引始终为非负数,避免了负数索引可能引发的错误或不必要的检查
		unsigned int idx = face.x;
		unsigned int idy = face.y;
		unsigned int idz = face.z;

		// 使用索引获取顶点坐标和颜色
		glm::vec3 vertex_x = vertex_positions[idx];
		glm::vec3 vertex_y = vertex_positions[idy];
		glm::vec3 vertex_z = vertex_positions[idz];

		glm::vec3 colorx = vertex_colors[idx];
		glm::vec3 colory = vertex_colors[idy];
		glm::vec3 colorz = vertex_colors[idz];

		// 将顶点坐标和颜色添加到 points 和 colors 中
		points.push_back(vertex_x);
		points.push_back(vertex_y);
		points.push_back(vertex_z);

		colors.push_back(colorx);
		colors.push_back(colory);
		colors.push_back(colorz);
	}
}

// 生成立方体的 12 个三角形的顶点索引
void TriMesh::generateCube() {
	// 首先清理数据
	cleanData();

	// @TODO: Task1 修改此函数以存储立方体的各个面信息
	// 首先将每个顶点的位置和颜色存储在 vertex_positions 和 vertex_colors 中
	for (int i = 0; i < 8; i++) {
		vertex_positions.push_back(cube_vertices[i]);
		vertex_colors.push_back(basic_colors[i]);
	}

	// 然后记录每个面片的信息,以便构建立方体模型
	faces.push_back(vec3i(0, 1, 2));
	faces.push_back(vec3i(2, 1, 3));
	faces.push_back(vec3i(4, 5, 6));
	faces.push_back(vec3i(6, 5, 7));
	faces.push_back(vec3i(0, 4, 1));
	faces.push_back(vec3i(1, 4, 5));
	faces.push_back(vec3i(2, 3, 6));
	faces.push_back(vec3i(6, 3, 7));
	faces.push_back(vec3i(0, 2, 4));
	faces.push_back(vec3i(4, 2, 6));
	faces.push_back(vec3i(1, 5, 3));
	faces.push_back(vec3i(3, 5, 7));

	// 调用 storeFacesPoints 函数存储数据
	storeFacesPoints();
}

void TriMesh::readOff(const std::string& filename)
{
	// 打开文件并读取数据
	if (filename.empty())
	{
		return;
	}
	std::ifstream fin;
	fin.open(filename);
	if (!fin)
	{
		printf("File on error\n");
		return;
	}
	else
	{
		printf("File open success\n");
		cleanData();
		int nVertices, nFaces, nEdges;

		// 读取 OFF 文件头
		std::string str;
		fin >> str;

		// 读取文件中的顶点数、面片数和边数
		fin >> nVertices >> nFaces >> nEdges;

		// 根据顶点数,循环读取每个顶点的坐标
		for (int i = 0; i < nVertices; i++)
		{
			glm::vec3 tmp_node;
			fin >> tmp_node.x >> tmp_node.y >> tmp_node.z;
			vertex_positions.push_back(tmp_node);
			vertex_colors.push_back(tmp_node);
		}

		// 根据面片数,循环读取每个面片信息并使用 vec3i 结构保存
		for (int i = 0; i < nFaces; i++)
		{
			int num, a, b, c;
			// num 记录此面片由几个顶点构成,a、b、c 为构成该面片的顶点索号
			fin >> num >> a >> b >> c;
			faces.push_back(vec3i(a, b, c));
		}
	}
	fin.close();
	// 存储数据以供 GPU 使用
	storeFacesPoints();
};

实验二——main.cpp

#include "Angel.h"
#include "TriMesh.h"

#include <vector>
#include <string>

// 定义三个坐标轴的常量
const int X_AXIS = 0;
const int Y_AXIS = 1;
const int Z_AXIS = 2;

// 定义三种变换类型的常量
const int TRANSFORM_SCALE = 0;
const int TRANSFORM_ROTATE = 1;
const int TRANSFORM_TRANSLATE = 2;

// 定义Delta的变化率和默认值
const double DELTA_DELTA = 0.3;        // Delta的变化率
const double DEFAULT_DELTA = 0.5;      // 默认的Delta值

// 初始化Delta值
double scaleDelta = DEFAULT_DELTA;
double rotateDelta = DEFAULT_DELTA;
double translateDelta = DEFAULT_DELTA;

// 初始化三种变换的控制变量
glm::vec3 scaleTheta(1.0, 1.0, 1.0);     // 缩放控制变量
glm::vec3 rotateTheta(0.0, 0.0, 0.0);    // 旋转控制变量
glm::vec3 translateTheta(0.0, 0.0, 0.0); // 平移控制变量

// 设置当前变换类型为旋转
int currentTransform = TRANSFORM_ROTATE;
int mainWindow;

// 定义OpenGL对象结构体
struct openGLObject
{
	// 顶点数组对象
	GLuint vao;
	// 顶点缓存对象
	GLuint vbo;

	// 着色器程序
	GLuint program;
	// 着色器文件
	std::string vshader;
	std::string fshader;
	// 着色器变量
	GLuint pLocation;
	GLuint cLocation;
	GLuint matrixLocation;
	GLuint darkLocation;
};

openGLObject cube_object;

// 创建一个三角网格对象
TriMesh* cube = new TriMesh();

// 回调函数,用于设置OpenGL窗口的大小
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
	glViewport(0, 0, width, height);
}

// 绑定对象和数据
void bindObjectAndData(TriMesh* mesh, openGLObject& object, const std::string& vshader, const std::string& fshader) {
	// 创建顶点数组对象
	glGenVertexArrays(1, &object.vao);    // 分配1个顶点数组对象
	glBindVertexArray(object.vao);         // 绑定顶点数组对象

	// 创建并初始化顶点缓存对象
	glGenBuffers(1, &object.vbo);
	glBindBuffer(GL_ARRAY_BUFFER, object.vbo);
	glBufferData(GL_ARRAY_BUFFER,
		mesh->getPoints().size() * sizeof(glm::vec3)
			+ mesh->getColors().size() * sizeof(glm::vec3),
		NULL,
		GL_STATIC_DRAW);

	// @TODO: Task3-修改完TriMesh.cpp的代码成后再打开下面注释,否则程序会报错
	 glBufferSubData(GL_ARRAY_BUFFER, 0, mesh->getPoints().size() * sizeof(glm::vec3), &mesh->getPoints()[0]);
	 glBufferSubData(GL_ARRAY_BUFFER, mesh->getPoints().size() * sizeof(glm::vec3), mesh->getColors().size() * sizeof(glm::vec3), &mesh->getColors()[0]);

	object.vshader = vshader;
	object.fshader = fshader;
	object.program = InitShader(object.vshader.c_str(), object.fshader.c_str());

	// 从顶点着色器中初始化顶点的位置
	object.pLocation = glGetAttribLocation(object.program, "vPosition");
	glEnableVertexAttribArray(object.pLocation);
	glVertexAttribPointer(object.pLocation, 3, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(0));

	// 从顶点着色器中初始化顶点的颜色
	object.cLocation = glGetAttribLocation(object.program, "vColor");
	glEnableVertexAttribArray(object.cLocation);
	glVertexAttribPointer(object.cLocation, 3, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(mesh->getPoints().size() * sizeof(glm::vec3)));

	// 获得矩阵存储位置
	object.matrixLocation = glGetUniformLocation(object.program, "matrix");
}

// 初始化函数
void init()
{
	std::string vshader, fshader;
	// 读取着色器文件路径
	vshader = "shaders/vshader.glsl";
	fshader = "shaders/fshader.glsl";

	//cube->generateCube();
	cube->readOff("./Models/cow.off");
	bindObjectAndData(cube, cube_object, vshader, fshader);

	// 设置背景色为黑色
	glClearColor(0.0, 0.0, 0.0, 1.0);
}

// 渲染函数
void display()
{
	// 清空颜色缓冲和深度缓冲
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	glUseProgram(cube_object.program);

	glBindVertexArray(cube_object.vao);

	// 初始化变换矩阵  glm::mat4表示 4x4 矩阵
	glm::mat4 m(1.0, 0.0, 0.0, 0.0,
		0.0, 1.0, 0.0, 0.0,
		0.0, 0.0, 1.0, 0.0,
		0.0, 0.0, 0.0, 1.0);

	// @TODO: Task4-在此处修改函数,计算最终的变换矩阵
	// 调用函数传入三种变化的变化量,累加得到变化矩阵
	// 注意三种变化累加的顺序
	// 构建旋转矩阵
	glm::mat4 rotationMatrix = glm::rotate(glm::mat4(1.0), rotateTheta.x, glm::vec3(1.0, 0.0, 0.0))
		* glm::rotate(glm::mat4(1.0), rotateTheta.y, glm::vec3(0.0, 1.0, 0.0))
		* glm::rotate(glm::mat4(1.0), rotateTheta.z, glm::vec3(0.0, 0.0, 1.0));

	// 构建缩放矩阵
	glm::mat4 scaleMatrix = glm::scale(glm::mat4(1.0), glm::vec3(scaleTheta.x, scaleTheta.y, scaleTheta.z));

	// 构建平移矩阵
	glm::mat4 translateMatrix = glm::translate(glm::mat4(1.0), glm::vec3(translateTheta.x, translateTheta.y, translateTheta.z));

	// 按照平移、旋转、缩放的顺序相乘得到最终的变换矩阵
	m = translateMatrix * rotationMatrix * scaleMatrix;

	// 传递变换矩阵到着色器
	glUniformMatrix4fv(cube_object.matrixLocation, 1, GL_FALSE, glm::value_ptr(m));

	// 绘制立方体中的各个三角形
	glDrawArrays(GL_TRIANGLES, 0, cube->getPoints().size());
}

// 通过Delta值更新Theta
// axis 表示坐标轴,sign 表示增加或减少
// currentTransform 表示当前变换类型
void updateTheta(int axis, int sign) {
	switch (currentTransform) {
		// 根据变换类型,增加或减少某种变换的变化量
	case TRANSFORM_SCALE:
		//增加或减少缩放的 Theta 值
		scaleTheta[axis] += sign * scaleDelta;
		break;
	case TRANSFORM_ROTATE:
		//增加或减少旋转的 Theta 值
		rotateTheta[axis] += sign * rotateDelta;
		break;
	case TRANSFORM_TRANSLATE:
		//增加或减少平移的 Theta 值
		translateTheta[axis] += sign * translateDelta;
		break;
	}
}

// 复原Theta和Delta
void resetTheta()
{
	scaleTheta = glm::vec3(1.0, 1.0, 1.0);	//scaleTheta 表示缩放变换的角度
	rotateTheta = glm::vec3(0.0, 0.0, 0.0);
	translateTheta = glm::vec3(0.0, 0.0, 0.0);
	scaleDelta = DEFAULT_DELTA;				//缩放变换的单位变化量
	rotateDelta = DEFAULT_DELTA;
	translateDelta = DEFAULT_DELTA;
}

// 更新变化Delta值
void updateDelta(int sign)
{
	switch (currentTransform) {
		// 根据变化类型增加或减少每一次变化的单位变化量
	case TRANSFORM_SCALE:
		scaleDelta += sign * DELTA_DELTA;
		break;
	case TRANSFORM_ROTATE:
		rotateDelta += sign * DELTA_DELTA;
		break;
	case TRANSFORM_TRANSLATE:
		translateDelta += sign * DELTA_DELTA;
		break;
	}
}

// 处理键盘输入的回调函数
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode)
{
	switch (key)
	{
		// 退出。
	case GLFW_KEY_ESCAPE:
		if (action == GLFW_PRESS) glfwSetWindowShouldClose(window, GL_TRUE);
		break;
		// 1:缩放模式
	case GLFW_KEY_1:
		if (action == GLFW_PRESS) currentTransform = TRANSFORM_SCALE;
		break;
		// 2: 旋转模式
	case GLFW_KEY_2:
		if (action == GLFW_PRESS) currentTransform = TRANSFORM_ROTATE;
		break;
		// 3: 移动模式
	case GLFW_KEY_3:
		if (action == GLFW_PRESS) currentTransform = TRANSFORM_TRANSLATE;
		break;
		// 4: 绘制线。
	case GLFW_KEY_4:
		if (action == GLFW_PRESS) glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
		break;
		// 5: 绘制面。
	case GLFW_KEY_5:
		if (action == GLFW_PRESS) glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
		break;
		// Q: 增加 x。
	case GLFW_KEY_Q:
		if (action == GLFW_PRESS || action == GLFW_REPEAT) updateTheta(X_AXIS, 1);
		break;
		// A: 减少 x。
	case GLFW_KEY_A:
		if (action == GLFW_PRESS || action == GLFW_REPEAT) updateTheta(X_AXIS, -1);
		break;
		// W: 增加 y。
	case GLFW_KEY_W:
		if (action == GLFW_PRESS || action == GLFW_REPEAT) updateTheta(Y_AXIS, 1);
		break;
		// S: 减少 y。
	case GLFW_KEY_S:
		if (action == GLFW_PRESS || action == GLFW_REPEAT) updateTheta(Y_AXIS, -1);
		break;
		// E: 增加 z。
	case GLFW_KEY_E:
		if (action == GLFW_PRESS || action == GLFW_REPEAT) updateTheta(Z_AXIS, 1);
		break;
		// D: 减少 z。
	case GLFW_KEY_D:
		if (action == GLFW_PRESS || action == GLFW_REPEAT) updateTheta(Z_AXIS, -1);
		break;
		// R: 增加变化量。
	case GLFW_KEY_R:
		if (action == GLFW_PRESS) updateDelta(1);
		break;
		// F: 减少变化量。
	case GLFW_KEY_F:
		if (action == GLFW_PRESS) updateDelta(-1);
		break;
		// T: 所有值重置。
	case GLFW_KEY_T:
		if (action == GLFW_PRESS) resetTheta();
		break;
	}
}

// 输出帮助信息
void printHelp() {
	printf("%s\n\n", "3D Transfomations");
	printf("Keyboard options:\n");
	printf("1: Transform Scale\n");
	printf("2: Transform Rotate\n");
	printf("3: Transform Translate\n");
	printf("q: Increase x\n");
	printf("a: Decrease x\n");
	printf("w: Increase y\n");
	printf("s: Decrease y\n");
	printf("e: Increase z\n");
	printf("d: Decrease z\n");
	printf("r: Increase delta of currently selected transform\n");
	printf("f: Decrease delta of currently selected transform\n");
	printf("t: Reset all transformations and deltas\n");
}

// 清理数据
void cleanData() {
	cube->cleanData();

	// 释放内存
	delete cube;
	cube = NULL;

	// 删除绑定的对象
	glDeleteVertexArrays(1, &cube_object.vao);

	glDeleteBuffers(1, &cube_object.vbo);
	glDeleteProgram(cube_object.program);
}

int main(int argc, char** argv)
{
	// 初始化GLFW库,必须是应用程序调用的第一个GLFW函数
	glfwInit();

	// 配置GLFW
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

#ifdef __APPLE__
	glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif
	//设置字符格式
#pragma execution_character_set("utf-8");
	GLFWwindow* window = glfwCreateWindow(600, 600, "2021150047_hyf_实验二", NULL, NULL);
	if (window == NULL)
	{
		std::cout << "Failed to create GLFW window" << std::endl;
		glfwTerminate();
		return -1;
	}
	glfwMakeContextCurrent(window);
	glfwSetKeyCallback(window, key_callback);
	glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

	// 调用任何OpenGL的函数之前初始化GLAD
	if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
	{
		std::cout << "Failed to initialize GLAD" << std::endl;
		return -1;
	}

	init();
	// 输出帮助信息
	printHelp();
	// 启用深度测试
	glEnable(GL_DEPTH_TEST);
	while (!glfwWindowShouldClose(window))
	{
		display();

		// 交换颜色缓冲 以及 检查有没有触发什么事件(比如键盘输入、鼠标移动等)
		glfwSwapBuffers(window);
		glfwPollEvents();
	}
	cleanData();

	return 0;
}

 实验二——TriMesh.cpp

#include "TriMesh.h"


// 一些基础颜色
const glm::vec3 basic_colors[8] = {
	glm::vec3(1.0, 1.0, 1.0),	// White
	glm::vec3(1.0, 1.0, 0.0),	// Yellow
	glm::vec3(0.0, 1.0, 0.0),	// Green
	glm::vec3(0.0, 1.0, 1.0),	// Cyan
	glm::vec3(1.0, 0.0, 1.0),	// Magenta
	glm::vec3(1.0, 0.0, 0.0),	// Red
	glm::vec3(0.0, 0.0, 0.0),	// Black
	glm::vec3(0.0, 0.0, 1.0)	// Blue
};

// 立方体的各个点
const glm::vec3 cube_vertices[8] = {
	glm::vec3(-0.5, -0.5, -0.5),
	glm::vec3(0.5, -0.5, -0.5),
	glm::vec3(-0.5,  0.5, -0.5),
	glm::vec3(0.5,  0.5, -0.5),
	glm::vec3(-0.5, -0.5,  0.5),
	glm::vec3(0.5, -0.5,  0.5),
	glm::vec3(-0.5,  0.5,  0.5),
	glm::vec3(0.5,  0.5,  0.5)
};

TriMesh::TriMesh()
{
}

TriMesh::~TriMesh()
{
}

std::vector<glm::vec3> TriMesh::getVertexPositions()
{
	return vertex_positions;
}

std::vector<glm::vec3> TriMesh::getVertexColors()
{
	return vertex_colors;
}

std::vector<vec3i> TriMesh::getFaces()
{
	return faces;
}

std::vector<glm::vec3> TriMesh::getPoints()
{
	return points;
}

std::vector<glm::vec3> TriMesh::getColors()
{
	return colors;
}

void TriMesh::cleanData() {
	// 清空顶点位置和颜色
	vertex_positions.clear();
	vertex_colors.clear();

	// 清空面片信息
	faces.clear();

	// 清空用于传递给 GPU 的顶点和颜色数据
	points.clear();
	colors.clear();
}

void TriMesh::storeFacesPoints() {
	// @TODO: Task-2 修改此函数以将每个三角面片的点和颜色信息存储在 points 和 colors 容器中
	// 根据每个三角面片的顶点索引存储数据,以便将其传递给 GPU
	
	// 清空之前的数据
	points.clear();
	colors.clear();

	// 清空之前的数据
	for (const vec3i& face : faces) {
		// 获取当前面片的顶点索引
		//使用无符号整数可以确保索引始终为非负数,避免了负数索引可能引发的错误或不必要的检查
		unsigned int idx = face.x;
		unsigned int idy = face.y;
		unsigned int idz = face.z;

		// 使用索引获取顶点坐标和颜色
		glm::vec3 vertex_x = vertex_positions[idx];
		glm::vec3 vertex_y = vertex_positions[idy];
		glm::vec3 vertex_z = vertex_positions[idz];

		glm::vec3 colorx = vertex_colors[idx];
		glm::vec3 colory = vertex_colors[idy];
		glm::vec3 colorz = vertex_colors[idz];

		// 将顶点坐标和颜色添加到 points 和 colors 中
		points.push_back(vertex_x);
		points.push_back(vertex_y);
		points.push_back(vertex_z);

		colors.push_back(colorx);
		colors.push_back(colory);
		colors.push_back(colorz);
	}
}

// 生成立方体的 12 个三角形的顶点索引
void TriMesh::generateCube() {
	// 首先清理数据
	cleanData();

	// @TODO: Task1 修改此函数以存储立方体的各个面信息
	// 首先将每个顶点的位置和颜色存储在 vertex_positions 和 vertex_colors 中
	for (int i = 0; i < 8; i++) {
		vertex_positions.push_back(cube_vertices[i]);
		vertex_colors.push_back(basic_colors[i]);
	}

	// 然后记录每个面片的信息,以便构建立方体模型
	faces.push_back(vec3i(0, 1, 2));
	faces.push_back(vec3i(2, 1, 3));
	faces.push_back(vec3i(4, 5, 6));
	faces.push_back(vec3i(6, 5, 7));
	faces.push_back(vec3i(0, 4, 1));
	faces.push_back(vec3i(1, 4, 5));
	faces.push_back(vec3i(2, 3, 6));
	faces.push_back(vec3i(6, 3, 7));
	faces.push_back(vec3i(0, 2, 4));
	faces.push_back(vec3i(4, 2, 6));
	faces.push_back(vec3i(1, 5, 3));
	faces.push_back(vec3i(3, 5, 7));

	// 调用 storeFacesPoints 函数存储数据
	storeFacesPoints();
}

void TriMesh::readOff(const std::string& filename)
{
	// 打开文件并读取数据
	if (filename.empty())
	{
		return;
	}
	std::ifstream fin;
	fin.open(filename);
	if (!fin)
	{
		printf("File on error\n");
		return;
	}
	else
	{
		printf("File open success\n");
		cleanData();
		int nVertices, nFaces, nEdges;

		// 读取 OFF 文件头
		std::string str;
		fin >> str;

		// 读取文件中的顶点数、面片数和边数
		fin >> nVertices >> nFaces >> nEdges;

		// 根据顶点数,循环读取每个顶点的坐标
		for (int i = 0; i < nVertices; i++)
		{
			glm::vec3 tmp_node;
			fin >> tmp_node.x >> tmp_node.y >> tmp_node.z;
			vertex_positions.push_back(tmp_node);
			vertex_colors.push_back(tmp_node);
		}

		// 根据面片数,循环读取每个面片信息并使用 vec3i 结构保存
		for (int i = 0; i < nFaces; i++)
		{
			int num, a, b, c;
			// num 记录此面片由几个顶点构成,a、b、c 为构成该面片的顶点索号
			fin >> num >> a >> b >> c;
			faces.push_back(vec3i(a, b, c));
		}
	}
	fin.close();
	// 存储数据以供 GPU 使用
	storeFacesPoints();
};

(by 归忆)

  • 26
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: CSDN是一个知名的技术社区平台,而山东大学计算机图形实验则是该计算机专业的一门实践性课程。 在计算机图形实验中,生将有机会习和应用计算机图形的基本原理、算法和技术,以及图像处理和 三维图形的渲染等相关知识。通过这门实验课程,生可以深入了解计算机图形的概念、理论和应用,提升自己的实践能力和解决问题的能力。 CSDN作为一个技术社区平台,与山东大学计算机图形实验也存在一定的关联。通过在CSDN上发表和分享个人在计算机图形实验中的经验、习心得和技术总结,生们可以扩展自己的影响力,与其他专业人士进行交流和合作,获取更多实践经验和习资源。 同时,CSDN还提供了丰富的技术文档、资讯、教程等资源,通过在CSDN上搜索相关的计算机图形实验资料,生们能够快速获取到最新的行业动态和术发展,帮助他们更好地理解和应用计算机图形的知识。 总之,CSDN和山东大学计算机图形实验相互促进,共同为生的习和成长提供支持和帮助。通过积极利用CSDN的资源和平台,生们可以在实践中不断提升自己的技能,为今后的习和工作打下坚实的基础。 ### 回答2: csdn是一个知识分享平台,而山东大学计算机图形实验是该计算机系开设的一门实践课程。这门实验课程旨在教授生有关计算机图形的基本理论和实际操作的知识。 在山东大学计算机图形实验中,生将习如何使用计算机图形的技术和工具来创建和处理图像、动画和虚拟现实等内容。课程涵盖了计算机图形的基本概念、算法和应用。生将通过编程和实践项目来加深对这些概念和技术的理解。 在实验课程中,生将习使用常见的图形库和软件工具,如OpenGL、CUDA等。通过使用这些工具,他们将能够实现各种图形技术,如三维渲染、光影处理、动画制作等。此外,他们还将习如何应用图形知识来解决实际问题,如计算机辅助设计、医图像处理等。 在教过程中,教师将为生提供必要的理论知识,并指导他们完成实践项目。生将通过小组合作和个人努力来完成各种编程任务和项目。课程结束时,生将具备良好的计算机图形技能,并能够独立进行图形相关的项目和研究。 通过参加山东大学计算机图形实验生将能够深入了解图形的基本原理和应用,掌握常用的图形工具和技术,培养解决实际问题的能力。这门实验课程将为生的术和职业发展提供强有力的支持。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

归忆_AC

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值