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

实验2.1 OpenGL的控制与交互方式

一、 实验目的

  1. 掌握OpenGL子窗口的生成。
  2. 学会OpenGL的简单键盘交互。
  3. 掌握OpenGL的简单鼠标交互。
  4. 了解和使用空闲回调函数生成动画。
  5. 进一步巩固OpenGL的基本图元绘制基础。

二、 理论背景

键盘交互事件
当鼠标位于窗口内并且键盘有某个键被按下或释放,就会产生键盘事件。当发生键盘事件时,系统返回用来生成该事件的键盘按键的ASCII码和鼠标的位置。

鼠标交互事件
有两类事件与鼠标之类的定位设备相关联。如果鼠标在某个按键被按下时移动,那么就发生了移动事件。如果鼠标在移动时没有按键被按下,这个事件叫做被动移动事件。移动事件发生后,应用程序就可以获得鼠标的位置。当鼠标的一个按键被按下或者释放,就发生了鼠标事件。当鼠标的一个按键被按下,就会产生一个鼠标按键按下事件,而当鼠标的一个按键被释放,则产生一个鼠标按键释放事件。返回的信息包括产生这个事件的鼠标按键,在这个事件产生之后,按键的状态(释放或者按下),以及窗口坐标系中跟踪这个鼠标的光标位置。

空闲回调函数
当没有其他事件发生时,GLUT会调用空闲回调函数,它的默认返回值是空函数指针。空闲回调函数经常用来在没有任何其他的事件发生时利用某个显示函数持续地生成图元,也可以使用空闲回调函数生成动画。

三、 实验内容

1. 创建基本工程项目

参考实验1.2,新建一个Interaction工程项目,并在主窗口中绘制出多个黑白的正方形。

2. 在子窗口中绘制图形

子窗口与主窗口的主体相似,子窗口中也需要单独的subWindowInit()函数设置窗口内需要用到的数据,以及单独的subWindowDisplay()函数对窗口内进行渲染。为了生成并显示子窗口,需要对实验1.2中的main.cpp文件进行以下的修改。
(1) 修改main()主函数
子窗口首先需要通过glutCreateSubWindow()函数创建得到,并可在函数中设置相对于父窗口的偏移和自身的宽度与高度。
在main函数中:

	subWindow = glutCreateSubWindow(mainWindow, 0, 0, width / 4, height / 4);

int glutCreateSubWindow(int parentWindow, int x, int y, int width, int height);
在ID为parentWindow的父窗口中,创建一个相对于父窗口左上角偏移x和y,宽为width,高为height的子窗口。

随后需要编写一个与主窗口初始化相似的subWindowInit()函数进行子窗口的初始化

subWindowInit();

还需要编写一个subWindowDisplay()函数在子窗口中进行相关渲染工作,并由GLUT库提供的glutDisplayFunc设置子窗口的显示回调函数。

glutDisplayFunc(subWindowDisplay);

(2) 添加subWindowInit()函数
为子窗口进行初始化的subWindowInit()函数与主窗口的mainWindowInit()并没有太大的差别,差别仅在于绘制的内容和传入着色器的数据。在本例的子窗口中,将通过调用generateEllipsePoints()函数,绘制出一个红色的椭圆。

void subWindowInit()
{
	vec2 vertices[ELLIPSE_NUM_POINTS];
	vec3 colors[ELLIPSE_NUM_POINTS];

	// 创建子窗口中的椭圆
	generateEllipsePoints(vertices, colors, subWindowObjectColor, 0, ELLIPSE_NUM_POINTS,
		vec2(0.0, 0.0), 0.7, 0.5);
}

void generateEllipsePoints
从startVertexIndex的索引位置开始,在vertices数组存入圆的顶点的坐标信息,对colors数组中所有元素存入color颜色值。且中心为圆center,缩放比例为scale,y轴与x轴的比例为verticalScale,若verticalScale
= 1.0为圆形,在0到1.0之间则为椭圆。

void generateEllipsePoints(vec2 vertices[], vec3 colors[], vec3 color, int startVertexIndex, int numPoints,
	vec2 center, double scale, double verticalScale)
{
	double angleIncrement = (2 * M_PI) / numPoints;
	double currentAngle = M_PI / 2;

	for (int i = startVertexIndex; i < startVertexIndex + numPoints; i++) {
		vertices[i] = getEllipseVertex(center, scale, verticalScale, currentAngle);
		colors[i] = color;

		currentAngle += angleIncrement;
	}
}

vec2 getEllipseVertex() 获得椭圆上的点


vec2 getEllipseVertex(vec2 center, double scale, double verticleScale, double angle)
{
	vec2 vertex(sin(angle), cos(angle));
	vertex *= scale;
	vertex.y *= verticleScale;    // 修改垂直分量
	vertex += center;
	return vertex;
}

(3) 添加subWindowDisplay()函数
这里针对子窗口的渲染绘制函数与主窗口mainWindowDisplay()绘制函数的差别只在于用glDrawArrays()绘制了不同的内容。

void subWindowDisplay()
{
	subWindowInit();    // 重绘时写入新的颜色数据
	glClear(GL_COLOR_BUFFER_BIT);
	glDrawArrays(GL_TRIANGLE_FAN, 0, ELLIPSE_NUM_POINTS);
	glutSwapBuffers();
}

在完成上述步骤后,将得到一个具有子窗口的效果。

在这里插入图片描述

3. 在子窗口中通过键盘事件更换椭圆形状颜色

(1) 添加subWindowKeyboard()函数
在程序中为子窗口增加的键盘回调函数如下,实现了根据键盘按键对子窗口中物体颜色的赋值。

void subWindowKeyboard(unsigned char key, int x, int y)
{
	switch (key) {
	case 'r':
		subWindowObjectColor = RED;
		break;
	case 'g':
		subWindowObjectColor = GREEN;
		break;
	case 'b':
		subWindowObjectColor = BLUE;
		break;
	case 'y':
		subWindowObjectColor = YELLOW;
		break;
	case 'o':
		subWindowObjectColor = ORANGE;
		break;
	case 'p':
		subWindowObjectColor = PURPLE;
		break;
	case 'w':
		subWindowObjectColor = WHITE;
		break;
	}
	glutPostWindowRedisplay(subWindow);
}

事实上 这里还需要在全局函数中添加这几个vec颜色的

全局变量:

最后调用glutPostWindowRedisplay()函数标记subWindow子窗口进行重绘。在subWindowDisplay()中需要通过subWindowInit()对子窗口中形状颜色的重新设定。

void subWindowKeyboard(unsigned char key, int x, int y)
{
	switch (key) {
//按键内容
	}
	glutPostWindowRedisplay(subWindow);    // 标记subWindow子窗口进行重绘
	
}

void glutPostWindowRedisplay(void);
标记当前窗口需要重绘。在下一次执行过程中,将会向由glutDisplayFunc()注册的回调函数发出请求。

注意:操作的时候,必须先用鼠标点击该子窗口,激活当前子窗口后,才可以用键盘控制形状颜色的设定。

(2) 在main()函数中指定
通过glutKeyboardFunc()函数指定在键盘响应下调用subWindowKeyboard()函数。

int main(int argc, char **argv)
{
//此处省略内容
    在子窗口中指定函数subWindowKeyboard,当一个能够生成ASCII字符的键释放时会被调用
	glutKeyboardFunc(subWindowKeyboard);
	
	glutMainLoop();
	return 0;
}

void glutKeyboardFunc(void (*func)(unsigned char key, int x, int y));
指定了函数func,当一个能够生成ASCII字符的键按下时,这个函数便会被调用。key回调参数就是生成的ASCII字符的值。x和y回调参数表示当这个键按下时鼠标在窗口下的位置。

4.1 实现菜单栏的交互

其实别看手册看上去那么长,其实很简单:
首先创建一个setupmenu函数,新建一个菜单,然后为其菜单中添加一个显示的栏。
glutAddMenuEntry()其中这个函数,第一个参数为显示的字符串。

void mainWindowSetupMenu()
{
	mainWindowMenu = glutCreateMenu(mainWindowMenuEvents);
	glutAddMenuEntry("Red", MENU_CHOICE_RED);
}

当按下菜单键该栏时便会自动调用mainWindowMenuEvents(int menuChoice)这个函数,而不需要显式调用,传入的参数便是glutAddMenuEntry()函数的第二个参数。

void mainWindowMenuEvents(int menuChoice)
{
	switch (menuChoice) {
	case MENU_CHOICE_WHITE:
	}
}

最后在主函数这样即可:

int main(int argc, char **argv)
{
	mainWindow = glutCreateWindow("Interaction and Submenu");
	// 主窗口初始化
	mainWindowInit();
	mainWindowSetupMenu();
}
4.2 在主窗口中添加菜单设置形状颜色

使用菜单必须定义菜单中每个选项的功能,还必须把菜单和鼠标的某个特定按键关联。最后必须为每个菜单注册一个回调函数。
(1) 定义菜单回调函数
定义mainWindowMenuEvents()回调函数与键盘回调函数类似,代码如下:

void mainWindowMenuEvents(int menuChoice)
{
	switch (menuChoice) {
	case MENU_CHOICE_WHITE://其实这些都是数字
		mainWindowSquareColor = WHITE;
		break;
	case MENU_CHOICE_BLACK:
		mainWindowSquareColor = BLACK;
		break;
	case MENU_CHOICE_RED:
		mainWindowSquareColor = RED;
		break;
	case MENU_CHOICE_GREEN:
		mainWindowSquareColor = GREEN;
		break;
	case MENU_CHOICE_BLUE:
		mainWindowSquareColor = BLUE;
		break;
	case MENU_CHOICE_YELLOW:
		mainWindowSquareColor = YELLOW;
		break;
	case MENU_CHOICE_ORANGE:
		mainWindowSquareColor = ORANGE;
		break;
	case MENU_CHOICE_PURPLE:
		mainWindowSquareColor = PURPLE;
		break;
	}
	glutPostWindowRedisplay(mainWindow);
}

事实上这里还需要定义几个全局变量:

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;

通过菜单选中时的menuChoice标识符对主窗口中正方形形状颜色mainWindowSquareColor进行赋值,
并在最后通过glutPostWindowRedisplay()进行窗口重绘

void mainWindowMenuEvents(int menuChoice)
{
	switch (menuChoice) {//此处省略
	}

    // 标记mainWindow主窗口进行重绘
	glutPostWindowRedisplay(mainWindow);
}

同时,需要在mainWindowDisplay()中通过mainWindowInit()对主窗口中形状颜色重新设定。

(2) 创建菜单并关联
创建菜单的函数如下:通过glutCreateMenu()函数注册菜单回调函数mainWindowMenuEvents,

void mainWindowSetupMenu()
{
	mainWindowSubmenu = glutCreateMenu(mainWindowMenuEvents);//注意这里是子菜单
	//此处还有
	mainWindowMenu = glutCreateMenu(mainWindowMenuEvents);
}

这里通过glutAddMenuEntry()添加菜单选项,

void mainWindowSetupMenu()
{
	mainWindowSubmenu = glutCreateMenu(mainWindowMenuEvents);//注意这里是子菜单
	glutAddMenuEntry("Yellow", MENU_CHOICE_YELLOW);
	glutAddMenuEntry("Orange", MENU_CHOICE_ORANGE);
	glutAddMenuEntry("Purple", MENU_CHOICE_PURPLE);
	glutAddMenuEntry("Black", MENU_CHOICE_BLACK);

	mainWindowMenu = glutCreateMenu(mainWindowMenuEvents);
	glutAddMenuEntry("Red", MENU_CHOICE_RED);
	glutAddMenuEntry("Green", MENU_CHOICE_GREEN);
	glutAddMenuEntry("Blue", MENU_CHOICE_BLUE);
	glutAddMenuEntry("White", MENU_CHOICE_WHITE);
	glutAttachMenu(GLUT_RIGHT_BUTTON);
}

还可以通过 glutAddSubMenu()添加子菜单。

void mainWindowSetupMenu()
{
    // 在主菜单中添加子菜单
	glutAddSubMenu("Other Square Colors", mainWindowSubmenu);
}

完整代码:

void mainWindowSetupMenu()
{
	// 创建子菜单,并注册菜单回调函数mainWindowMenuEvents
    mainWindowSubmenu = glutCreateMenu(mainWindowMenuEvents);
	glutAddMenuEntry("Yellow", MENU_CHOICE_YELLOW);
	glutAddMenuEntry("Orange", MENU_CHOICE_ORANGE);
	glutAddMenuEntry("Purple", MENU_CHOICE_PURPLE);
	glutAddMenuEntry("Black", MENU_CHOICE_BLACK);

    // 创建主菜单
	mainWindowMenu = glutCreateMenu(mainWindowMenuEvents);
	glutAddMenuEntry("Red", MENU_CHOICE_RED);
	glutAddMenuEntry("Green", MENU_CHOICE_GREEN);
	glutAddMenuEntry("Blue", MENU_CHOICE_BLUE);
	glutAddMenuEntry("White", MENU_CHOICE_WHITE);
    
    // 在主菜单中添加子菜单
	glutAddSubMenu("Other Square Colors", mainWindowSubmenu);
    
    // 关联鼠标右键激活菜单
	glutAttachMenu(GLUT_RIGHT_BUTTON);
}

最后,需要将mainWindowSetupMenu()菜单创建函数放入main()函数中。

int main(int argc, char **argv)
{
//此处省略
	mainWindowSetupMenu();
}
5. 在主窗口中添加鼠标交互控制动画

在添加鼠标交互之前,需要先定义好一个动画。在这里通过一个角度偏移量offsetAngle,在getSquareAngle()中来改变每个正方形顶点所在的角度,然后可以在空闲回调函数idleFunction()中不断地修改这个角度偏移量offsetAngle,使角度不断产生变化,以此产生正方形旋转的效果。最终通过鼠标按钮的按下或释放,控制空闲回调函数是否被指定,这样便能让动画开始或停止。
另外,可在glutInitDisplayMode中设定启用双重缓冲技术,并在display渲染函数中调用glutSwapBuffers (),此函数的功能就是把后台缓存的内容交换到前台显示。

(1) 定义空闲回调函数
这个idle不禁让我想到了unity学动画的时候

void idleFunction()
{
	offsetAngle += delta;
	glutPostWindowRedisplay(mainWindow);
}

在函数中通过delta值改变角度偏移量offsetAngle,并标记重绘主窗口。

(2) 定义鼠标回调函数
主窗口的鼠标回调函数mainWindowMouse()的代码如下:

void mainWindowMouse(int button, int state, int x, int y)
{传入的x和y是鼠标点击在窗口坐标系下的值
	if (button == GLUT_MIDDLE_BUTTON && state == GLUT_DOWN) {
			glutIdleFunc(idleFunction);
	} else if (button == GLUT_MIDDLE_BUTTON && state == GLUT_UP) {
		glutIdleFunc(NULL);
	}
}

void glutIdleFunc(void (*func)(void));
当没有其他事件需要进行处理时,func函数将会执行。如果这个参数设置为NULL(0),func的执行就会被禁止。

在这里,鼠标回调函数发挥的作用是在点击鼠标中键时,利用glutIdleFun()来指定当没有其他事件处理的时候去调用idleFunction()函数。而当鼠标释放后,解除调用idleFunction(),以此来停止动画。

(3) 关联鼠标回调函数
最后,在主函数main()中通过glutMouseFunc()函数对鼠标回调函数mainWindowMouse()关联。

int main(int argc, char **argv)
{
    在主窗口中指定函数mainWindowMouse,在鼠标按下或释放时将会被调用
	glutMouseFunc(mainWindowMouse);
}

void glutMouseFunc (void (*func)(int button, int stat, int x, int y));
指定了函数func ,当鼠标被按下或释放时将会被调用。

参考函数
1.	int glutCreateSubWindow(int parentWindow, int x, int y, int width, int height);
在ID为parentWindow的父窗口中,创建一个相对于父窗口左上角偏移x和y,宽为width,高为height的子窗口。

2.	void generateEllipsePoints(vec2 vertices[], vec3 colors[], vec3 color, int startVertexIndex, int numPoints, vec2 center, double scale, double verticalScale);
从startVertexIndex的索引位置开始,在vertices数组存入圆的顶点的坐标信息,对colors数组中所有元素存入color颜色值。且中心为圆center,缩放比例为scale,y轴与x轴的比例为verticalScale,若verticalScale = 1.0为圆形,在01.0之间则为椭圆。

3.	void glutPostWindowRedisplay(void);
标记当前窗口需要重绘。在下一次执行过程中,将会向由glutDisplayFunc()注册的回调函数发出请求。

4.	void glutKeyboardFunc(void (*func)(unsigned char key, int x, int y));
指定了函数func,当一个能够生成ASCII字符的键按下时,这个函数便会被调用。key回调参数就是生成的ASCII字符的值。x和y回调参数表示当这个键按下时鼠标在窗口下的位置。

5.	void glutIdleFunc(void (*func)(void));
当没有其他事件需要进行处理时,func函数将会执行。如果这个参数设置为NULL0),func的执行就会被禁止。

6.	void glutMouseFunc (void (*func)(int button, int stat, int x, int y));
指定了函数func ,当鼠标被按下或释放时将会被调用。

四、 示例和练习

1. 实验结果

根据上述实验内容描述,将示例的代码成功编译程序后,最终将得到以下结果。一个具有键盘鼠标交互和带有子窗口的OpenGL应用程序。

在这里插入图片描述
按下中键可以旋转,主菜单中选中可以修改颜色,子菜单可以通过键盘来改变颜色。(做动图好麻烦懒得做了

2. 课堂练习

(1)在主窗体中添加一个退出程序的按键的键盘响应事件mainWindowKeyboard(unsigned char key, int x, int y),并在main()函数中利用glutKeyboardFunc()函数对其进行指定。其中Esc按键对应的ASCII字符为033,退出程序的命令为exit(EXIT_SUCCESS)。

void mainWindowKeyboard(unsigned char key, int x, int y)
{
	/*在此添加按下Esc按键退出的代码*/
	switch (key) {
	case 033:
		// 033对应八进制的ASCII码中的Esc按键
		exit(EXIT_SUCCESS);
		break;
	}
}

(2)在主窗体中添加一个Start Animation 和Stop Animation的两个菜单选项,分别控制正方形旋转动画的开始和停止。提示:需要修改的函数有:mainWindowSetupMenu,mainWindowMenuEvents(里面对应动画开始和结束的事件响应可参看函数mainWindowMouse里的调用)。

mainWindowSetupMenu:

void mainWindowSetupMenu()
{
	//省略部分代码
	mainWindowMenu = glutCreateMenu(mainWindowMenuEvents);
	glutAddMenuEntry("Stop Animation", MENU_CHOICE_STOP);
	glutAddMenuEntry("Start Animation", MENU_CHOICE_START);
	//省略部分代码
}

mainWindowMenuEvents:

void mainWindowMenuEvents(int menuChoice)
{
	switch (menuChoice) {
    /*在此处添加控制旋转动画开始和停止的菜单选项*/
	case MENU_CHOICE_START:
		glutIdleFunc(idleFunction);
		break;
	case MENU_CHOICE_STOP:
		glutIdleFunc(NULL);
		break;
	}
	glutPostWindowRedisplay(mainWindow);
}

全局变量新增:

const int MENU_CHOICE_START = 8;
const int MENU_CHOICE_STOP = 9;
完整代码:
#include "Angel.h"

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

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_CHOICE_START = 8;
const int MENU_CHOICE_STOP = 9;

const vec3 WHITE(1.0, 1.0, 1.0);
const vec3 BLACK(0.0, 0.0, 0.0);
const vec3 RED(1.0, 0.0, 0.0);
const vec3 GREEN(0.0, 1.0, 0.0);
const vec3 BLUE(0.0, 0.0, 1.0);
const vec3 YELLOW(1.0, 1.0, 0.0);
const vec3 ORANGE(1.0, 0.65, 0.0);
const 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;

// 子窗口
const int ELLIPSE_NUM_POINTS = 100;
int subWindow;
int subWindowMenu;

int width = 600;
int height = 600;

double time = 0;   // 时间变量
double delta = 0.05; // 时间增量

vec3 mainWindowSquareColor = WHITE;
vec3 subWindowObjectColor = RED;

// 获得圆上的点
vec2 getEllipseVertex(vec2 center, double scale, double verticleScale, double angle)
{
	vec2 vertex(sin(angle), cos(angle));
	vertex += center;
	vertex *= scale;
	vertex.y *= verticleScale;
	return vertex;
}

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

// 生成圆上顶点的属性
void generateEllipsePoints(vec2 vertices[], vec3 colors[], vec3 color, int startVertexIndex, int numPoints,
	vec2 center, double scale, double verticalScale)
{
	double angleIncrement = (2 * M_PI) / numPoints;
	double currentAngle = M_PI / 2;

	for (int i = startVertexIndex; i < startVertexIndex + numPoints; i++) {
		vertices[i] = getEllipseVertex(center, scale, verticalScale, currentAngle);
		colors[i] = color;

		currentAngle += angleIncrement;
	}
}

// 生成正方形上顶点的属性
void generateSquarePoints(vec2 vertices[], vec3 colors[], int squareNum, int startVertexIndex) {
	double scale = .90;
	double scaleAdjust = 0.15;//1.0 / squareNum;
	vec2 center(0.0, -0.25);

	int vertexIndex = startVertexIndex;

	for (int i = 0; i < squareNum; i++) {
		vec3 currentColor = 0 == i % 2 ? mainWindowSquareColor : BLACK;

		for (int j = 0; j < 4; j++) {
			double currentAngle = getSquareAngle(j);
			vertices[vertexIndex] = vec2(sin(currentAngle), cos(currentAngle)) * scale + center;
			colors[vertexIndex] = currentColor;

			vertexIndex++;
		}

		scale -= scaleAdjust;
	}
}

// 空闲回调函数
void idleFunction()
{
	time += delta;
	glutPostWindowRedisplay(mainWindow);
}

void mainWindowInit()
{
	vec2 vertices[SQUARE_NUM * 4];
	vec3 colors[SQUARE_NUM * 4] = { vec3(0.0, 0.0, 0.0) };

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

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

	// 创建并初始化顶点缓存对象
	GLuint buffer;
	glGenBuffers(1, &buffer);
	glBindBuffer(GL_ARRAY_BUFFER, buffer);
	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);

	// 读取着色器并使用
	GLuint program = InitShader("vshader.glsl", "fshader.glsl");
	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 mainWindowMenuEvents(int menuChoice)
{
	switch (menuChoice) {
	case MENU_CHOICE_WHITE:
		mainWindowSquareColor = WHITE;
		break;
	case MENU_CHOICE_BLACK:
		mainWindowSquareColor = BLACK;
		break;
	case MENU_CHOICE_RED:
		mainWindowSquareColor = RED;
		break;
	case MENU_CHOICE_GREEN:
		mainWindowSquareColor = GREEN;
		break;
	case MENU_CHOICE_BLUE:
		mainWindowSquareColor = BLUE;
		break;
	case MENU_CHOICE_YELLOW:
		mainWindowSquareColor = YELLOW;
		break;
	case MENU_CHOICE_ORANGE:
		mainWindowSquareColor = ORANGE;
		break;
	case MENU_CHOICE_PURPLE:
		mainWindowSquareColor = PURPLE;
		break;
    /*在此处添加控制旋转动画开始和停止的菜单选项*/
	case MENU_CHOICE_START:
		glutIdleFunc(idleFunction);
		break;
	case MENU_CHOICE_STOP:
		glutIdleFunc(NULL);
		break;
	}
	glutPostWindowRedisplay(mainWindow);
}

void mainWindowSetupMenu()
{
	mainWindowSubmenu = glutCreateMenu(mainWindowMenuEvents);
	glutAddMenuEntry("Yellow", MENU_CHOICE_YELLOW);
	glutAddMenuEntry("Orange", MENU_CHOICE_ORANGE);
	glutAddMenuEntry("Purple", MENU_CHOICE_PURPLE);
	glutAddMenuEntry("Black", MENU_CHOICE_BLACK);

	mainWindowMenu = glutCreateMenu(mainWindowMenuEvents);
	glutAddMenuEntry("Stop Animation", MENU_CHOICE_STOP);
	glutAddMenuEntry("Start Animation", MENU_CHOICE_START);
	glutAddMenuEntry("Red", MENU_CHOICE_RED);
	glutAddMenuEntry("Green", MENU_CHOICE_GREEN);
	glutAddMenuEntry("Blue", MENU_CHOICE_BLUE);
	glutAddMenuEntry("White", MENU_CHOICE_WHITE);
	glutAddSubMenu("Other Square Colors", mainWindowSubmenu);
	glutAttachMenu(GLUT_RIGHT_BUTTON);
}

void mainWindowDisplay()
{
	mainWindowInit();
	glClear(GL_COLOR_BUFFER_BIT);
	for (int i = 0; i < SQUARE_NUM; i++) {
		glDrawArrays(GL_TRIANGLE_FAN, (i * 4), 4);
	}

	glutSwapBuffers();
}

void mainWindowKeyboard(unsigned char key, int x, int y)
{
	/*在此添加按下Esc按键退出的代码*/
	switch (key) {
	case 033:
		// Esc按键
		exit(EXIT_SUCCESS);
		break;
	}
}

void mainWindowMouse(int button, int state, int x, int y)
{
	if (button == GLUT_MIDDLE_BUTTON && state == GLUT_DOWN) {
		// 按下鼠标中键
		glutIdleFunc(idleFunction);
	} else if (button == GLUT_MIDDLE_BUTTON && state == GLUT_UP) {
		// 松开鼠标中键
		glutIdleFunc(NULL);
	}
}

void subWindowInit()
{
	vec2 vertices[ELLIPSE_NUM_POINTS];
	vec3 colors[ELLIPSE_NUM_POINTS];

	// 创建子窗口中的椭圆
	generateEllipsePoints(vertices, colors, subWindowObjectColor, 0, ELLIPSE_NUM_POINTS,
		vec2(0.0, 0.0), 0.75, 0.5);

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

	// 创建并初始化顶点缓存对象
	GLuint buffer;
	glGenBuffers(1, &buffer);
	glBindBuffer(GL_ARRAY_BUFFER, buffer);
	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);

	// 读取着色器并使用
	GLuint program = InitShader("vshader.glsl", "fshader.glsl");
	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(1.0, 1.0, 1.0, 1.0);
}

void subWindowDisplay()
{
	subWindowInit();
	glClear(GL_COLOR_BUFFER_BIT);
	glDrawArrays(GL_TRIANGLE_FAN, 0, ELLIPSE_NUM_POINTS);
	glFlush();
	glutSwapBuffers();
}

void subWindowKeyboard(unsigned char key, int x, int y)
{
	switch (key) {
	case 'r':
		subWindowObjectColor = RED;
		break;
	case 'g':
		subWindowObjectColor = GREEN;
		break;
	case 'b':
		subWindowObjectColor = BLUE;
		break;
	case 'y':
		subWindowObjectColor = YELLOW;
		break;
	case 'o':
		subWindowObjectColor = ORANGE;
		break;
	case 'p':
		subWindowObjectColor = PURPLE;
		break;
	case 'w':
		subWindowObjectColor = WHITE;
		break;
	}
	glutPostWindowRedisplay(subWindow);
}

void printHelp() {
	printf("%s\n\n", "Interaction and Submenu");
	printf("Keys to update the background color in sub window:\n");
	printf("'r' - red\n'g' - green\n'b' - blue\n'y' - yellow\n'o' - orange\n'p' - purple\n'w' - white\n");
}

int main(int argc, char **argv)
{
	glutInit(&argc, argv);
	glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE);    // 启用双重缓冲
	glutInitWindowSize(width, height);
	mainWindow = glutCreateWindow("Interaction and Submenu");

	glewExperimental = GL_TRUE;
	glewInit();

	// 主窗口初始化
	mainWindowInit();
	mainWindowSetupMenu();
	glutDisplayFunc(mainWindowDisplay);
	glutKeyboardFunc(mainWindowKeyboard);
	glutMouseFunc(mainWindowMouse);

	// 创建子窗口和初始化
	subWindow = glutCreateSubWindow(mainWindow, 0, 0, width / 3, height / 3);
	subWindowInit();
	glutDisplayFunc(subWindowDisplay);
	glutKeyboardFunc(subWindowKeyboard);
	// 输出帮助信息
	printHelp();

	glutMainLoop();
	return 0;
}

在这里插入图片描述

运行后便可以看到菜单栏中新增了两个选项,选中时则会开始和停止动画

实验 2.2 OFF格式的模型显示

一、 实验目的

  1. 了解和熟悉OFF模型文件格式。
  2. 掌握读取OFF模型文件。
  3. 了解基本3D图元的绘制。
  4. 了解深度测试技术

二、 理论背景

OFF 格式文件

OFF,Object File Format,即物体文件格式,是一种三维模型文件格式。
物体文件格式(.off)文件通过描述物体表面的多边形来表示一个模型的几何结构,这里的多边形可以有任意数量的顶点。
普林斯顿形状 Banchmark(Princeton Shape Benchmark)中的 .off 文件遵循以下标准:

  • OFF文件全是以OFF关键字开始的ASCII文件。
  • 下一行说明顶点的数量、面片的数量、边的数量。边的数量可以安全地省略。
  • 顶点按每行一个列出x、y、z坐标。
  • 在顶点列表后,面片按照每行一个列表,对于每个面片,顶点的数量是指定的,接下来是顶点索引列表。
深度测试

在绘制时,如果屏幕上当前像素要绘制新的候选颜色只有对应物体比之前的物体更靠近观察者,我们才能绘制它

初始状态下,深度缓存的值是一个距视点尽可能远的最大值,而所有物体的深度值都要比这个值更靠近视点。每帧重绘场景时都要清除深度缓存数据。
下面两幅图对比了开启深度测试的效果,上面是没有开启深度测试的结果,下面开启了深度测试技术。可见深度测试技术能带来更准确的渲染结果。
在这里插入图片描述
在这里插入图片描述

三、 实验内容

实验内容主要是设计并实现读取OFF文件的接口函数:
void read_off(const std::string filename)
请修改本实验提供的main.cpp文件,按照下面的说明顺序完成实验。

  1. 创建基本工程项目
    参考实验1.2,新建一个LoadModel工程项目。并包含实验所提供的main.cpp文件。
  2. 读取OFF文件,并存储信息到外部变量中
void read_off(const std::string filename)
{
	if (filename.empty()) {
		return;
	}

	std::ifstream fin;
	fin.open(filename);

	// @TODO:修改此函数读取OFF文件中三维模型的信息
	fin.close();
}
  1. 存储用于着色器中顶点和颜色信息
void storeFacesPoints()
{
	points.clear();
	colors.clear();

	// @TODO: 修改此函数在points和colors容器中中存储每个三角面片的各个点和颜色信息
}

注意:代码中用到C++标准模板库中的vector容器。如“std::vector
vertices”。容器vector在这里被用来当数组用,但比数组更易随意改变大小,使用也简单,具体请自行百度简单学习一下。

答案

虽然一个立方体有8个顶点,但是其有6个面,需要用12个三角形来绘制,因此需要36个点。
off模型的解析:
cube文件:
OFF//需要读入一行字符串 开头OFF代表这个文件是OFF格式的
8 12 0//这三个数字依次是正方形的顶点数 绘制的三角形的点的数量(6个面由12个三角形绘制成因此是12) 边的数目(边数目用不上)
-0 0.1987 -0.8429//绘制的三角形片元的顶点的坐标
0 -0.7285 -0.4683
0.7071 -0.4636 0.1873
0.7071 0.4636 -0.1873
-0.7071 0.4636 -0.1873
-0.7071 -0.4636 0.1873
0 -0.1987 0.8429
-0 0.7285 0.4683
3 1 0 3//前面的3代表是三角形片元 这个实验都是3 听老师说后面有4(矩形片元) 然后这三个数字是索引(123就是代表选择上面的第一第二第三个点来形成三角形) 三个点形成一个三角形
3 1 3 2
3 2 3 7
3 2 7 6
3 3 0 4
3 3 4 7
3 6 5 1
3 6 1 2
3 4 5 6
3 4 6 7
3 5 4 0
3 5 0 1
以上是个人的解析,除了我之外有位大佬做了更详细的解析: (以下是部分摘抄):

OFF 格式的模型非常直球,你要啥它提供啥。回想上一篇博客中我们如何生成立方体?

首先我们确定立方体 8 个顶点的位置,其次我们确定立方体 6 个面共 12
个三角面片的顶点的下标,最后我们生成顶点。OFF格式的模型恰好提供了这些数据。

OFF 模型的第一行是 OFF 字符串。之后会有三个整数,代表模型顶点数 vertexNum,三角面片数 faceNum,边数
edgeNum,其中我们只关心前两个数字。

之后的 vertexNum 行,是模型三维顶点的位置坐标,分别是 x,y,z 坐标。

紧接着 faceNum 行给出了每个三角面片的顶点下标。其中第一个数字是 3,表示该面片有 3 个顶点(一般都是 3
所以我们忽略他),之后的三个整数表示了该面片的顶点位置坐标,等于上面给出的顶点集合的第 i 个元素。

下面的图片描述了 OFF 文件的格式:
在这里插入图片描述
如图:通过 faces 数组指定三角面片的顶点:
在这里插入图片描述

首先看开头的全局变量:

// 三角面片中的顶点序列
typedef struct vIndex {
	unsigned int a, b, c;
	vIndex(int ia, int ib, int ic) : a(ia), b(ib), c(ic) {}
} vec3i;//注意这里是3i而不是3,这是自己定义的一个类型,与系统自带的vec3是不同的

std::string filename;
std::vector<vec3> vertices;
std::vector<vec3i> faces;

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

std::vector<vec3> points;   //传入着色器的绘制点
std::vector<vec3> colors;   //传入着色器的颜色
void read_off(const std::string filename)
{
	if (filename.empty()) {
		return;
	}

	std::ifstream fin;
	std::string a;
	fin.open(filename);
	fin >> a;读入首行字符串"OFF"
	fin >> nVertices >> nFaces >> nEdges;读入点、面、边数目
	// @TODO:修改此函数读取OFF文件中三维模型的信息

	vec3 point;
	// 读取每个顶点的坐标
	for (int i = 0; i < nVertices; i++) {
		fin >> point.x >> point.y >> point.z;
		vertices.push_back(point);
	}
	vec3i point2;
	face代表面片,其中,faces所存储的是索引信息,并不是真正的点的坐标,
	下面会将点的坐标真正存到points中
	for (int i = 0; i < nFaces; i++) {
		int num;
		fin >> num;
		fin >> point2.a >> point2.b >> point2.c;
		faces.push_back(point2);
	}
	// 读取面的顶点序列

	fin.close();
}
void storeFacesPoints()
{
	points.clear();
	colors.clear();
	points和colors才是真正存储每个点的坐标和颜色
	// @TODO: 修改此函数在points和colors容器中存储每个三角面片的各个点和颜色信息
	for (int i = 0;i< nFaces;i++ ) {
		points.push_back(vertices[faces[i].a]);
		一个face[i]存储一个三角形也就是三个点的索引信息,也就是下标,
		通过vertices[i]来获得真正的坐标值
		colors.push_back(vertex_colors[faces[i].a]);直接用索引下标对应颜色数组的下标来生成颜色,正好有8个颜色,那么一个下标将对应一个颜色
		points.push_back(vertices[faces[i].b]);
		colors.push_back(vertex_colors[faces[i].b]);
		points.push_back(vertices[faces[i].c]);
		colors.push_back(vertex_colors[faces[i].c]);
	}

}
  1. 启用深度测试
    启用深度测试需要在main()中的glutInitDisplayMode()函数中启用对窗口显示的支持,并调用glEnable()启用深度测试。同时,在display()函数中的中清除深度缓存
void init()
{
	// @TODO:修改完成后再打开下面注释,否则程序会报错
	// 分别读取数据
	glBufferSubData(GL_ARRAY_BUFFER, 0, points.size() * sizeof(vec3), &points[0]);
	glBufferSubData(GL_ARRAY_BUFFER, points.size() * sizeof(vec3), colors.size() * sizeof(vec3), &colors[0]);
}

在main函数中启用深度测试:

int main(int argc, char **argv)
{
//省略部分代码
	// @TODO:窗口显示模式支持深度测试
	glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH);

//省略部分代码
	// @TODO:启用深度测试 
	// finish
	glEnable(GL_DEPTH_TEST);
	glutMainLoop();
	return 0;
}

四、 参考函数

void glutInitDisplayMode(unsigned int mode);
函数功能为设置初始显示模式。mode可以取多种组合,其中GLUT_DEPTH可让窗口使用深度缓存

void glEnable(GLenum capability);
glEnable()会开启一个模式,例如GL_DEPTH_TEST可以用来开启深度测试。

void glClear(GLbitfield mask);
可以使用 | 运算符组合不同的缓冲标志位,表明需要清除的缓冲。例如glClear(GL_COLOR_BUFFER_BIT |
GL_DEPTH_BUFFER_BIT)表示要清除颜色缓存以及深度缓存。

五、完整代码

为了让你们方便的复制粘贴还是放个完整代码(才不是水长度

#include "Angel.h"

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

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

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

std::string filename;
std::vector<vec3> vertices; //从文件中读取的顶点坐标
std::vector<vec3i> faces;   //从文件中读取的面顶点索引

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

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

const int NUM_VERTICES = 8;

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

void read_off(const std::string filename)
{
	if (filename.empty()) {
		return;
	}

	std::ifstream fin;
	std::string a;
	fin.open(filename);
	fin >> a;
	fin >> nVertices >> nFaces >> nEdges;
	// @TODO:修改此函数读取OFF文件中三维模型的信息

	//读入首行字符串"OFF"

	//读入点、面、边数目
	vec3 point;
	// 读取每个顶点的坐标
	for (int i = 0; i < nVertices; i++) {
		fin >> point.x >> point.y >> point.z;
		vertices.push_back(point);
	}
	vec3i point2;
	for (int i = 0; i < nFaces; i++) {
		int num;
		fin >> num;
		fin >> point2.a >> point2.b >> point2.c;
		faces.push_back(point2);
	}
	for (int i = 0; i < nFaces; i++) {
		std::cout << faces[i].a << " " << faces[i].b << " " << faces[i].c;
		std::cout << std::endl;
	}

	// 读取面的顶点序列

	fin.close();
}

void storeFacesPoints()
{
	points.clear();
	colors.clear();

	// @TODO: 修改此函数在points和colors容器中存储每个三角面片的各个点和颜色信息
	for (int i = 0;i< nFaces;i++ ) {
		points.push_back(vertices[faces[i].a]);
		colors.push_back(vertex_colors[faces[i].a]);
		points.push_back(vertices[faces[i].b]);
		colors.push_back(vertex_colors[faces[i].b]);
		points.push_back(vertices[faces[i].c]);
		colors.push_back(vertex_colors[faces[i].c]);
	}

}

void init()
{
	storeFacesPoints();

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

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

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

	// 读取着色器并使用
	GLuint program = InitShader("vshader.glsl", "fshader.glsl");
	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(vec3)));

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

void display(void)
{
	// 清理窗口,包括颜色缓存和深度缓存
	glClear(GL_COLOR_BUFFER_BIT);

	 //绘制边
	//glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
	 //消除背面光照
	glEnable(GL_CULL_FACE);
	glCullFace(GL_FRONT);

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

int main(int argc, char **argv)
{
	glutInit(&argc, argv);
	// @TODO:窗口显示模式支持深度测试
	glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE);
	glutInitWindowSize(600, 600);
	glutCreateWindow("3D OFF Model");

	glewExperimental = GL_TRUE;
	glewInit();

	// 读取off模型文件
	read_off("cube.off");

	init();
	glutDisplayFunc(display);

	// @TODO:启用深度测试

	glutMainLoop();
	return 0;
}

随后可以看到这样的运行结果:
在这里插入图片描述

为什么和答案所给的示例不同?因为这里只显示了一个面,事实上显示其他面的话就可以看到制作效果了(在2.3中会展示旋转的效果)
(指导手册答案示例如下)在这里插入答案实力述

还有一件让我迷惑的事,在本题实验中我开不开启深度测试并不能发现有什么区别,这是为什么也不是很清楚,但是在后面的例子会有需要开启深度测试的情况。

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

一、 实验目的

  1. 掌握三维模型顶点与三角面片之间关系。
  2. 了解和掌握三维模型的基本变换操作。
  3. 掌握在着色器中使用变换矩阵。

二、 理论背景

由于物体是在坐标系中存在的,我们移动物体的位置,虽然可以通过改变其每个顶点的坐标来实现,但是确实有点麻烦。
我们可以通过矩阵来存储物体的坐标信息,当对物体的位置进行改变时,比如改变(1,2,3)(即将物体沿xyz轴平移1、2、3个单位),我们只需要对其乘以一个变换矩阵即可实现。而这个变换矩阵可以通过调用Translate(1,2,3)就可以实现。(这个函数在mat.h中)。

同理,旋转和缩放也可以通过这样的方式来实现

齐次坐标

首先需要学习一下齐次坐标

首先我们知道
在这里插入图片描述
在这里插入图片描述

但是
在这里插入图片描述
在这里插入图片描述

我们可以采用标架的形式来实现:
在这里插入图片描述
在这里插入图片描述

但是就会出现相似的表示方法。
在这里插入图片描述

并且其实我们也知道:

“如果是坐标,那么我们平移这个向量,对应的坐标需要发生改变。如果是方向向量,那么我们平移这个向量,对应的坐标不能发生改变。”

于是乎,我们引入一个新的维度,在第四个维度中用w来表示,w的0和1就代表了是向量还是点。在这里插入图片描述

由于用齐次坐标表示,三维几何变换的矩阵是一个4阶方阵,其形式如下:
在这里插入图片描述
在这里插入图片描述

(1)平移变换

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

平移变换把点沿着给定的方向移动固定的距离。参照二维的平移变换,可得到三维平移变换矩阵:
在这里插入图片描述

(2)缩放变换

缩放是一种仿射变换,但不是刚体变换,通过缩放变换可以放大或者缩小对象。
考虑缩放变换,先考虑最简单的沿坐标轴进行伸展或收缩。
在这里插入图片描述
在这里插入图片描述
看到这张图让我想到了unity里的缩放变换
在这里插入图片描述
在这里插入图片描述
接下来我们考虑对一个物体进行多个操作的时候:
在这里插入图片描述

那么接下来来看看最一般的形式,也就是绕固定点旋转:

缩放不一定是均匀的,目前直接考虑相对于参考点(xf,yf,zf)的缩放变换,其步骤为:
A. 平移到坐标原点处;
B. 进行缩放变换;
C. 将参考点(xf,yf,zf)移回原来位置
则变换矩阵为:
在这里插入图片描述

(3)绕坐标轴的旋转变换

首先我们可以看二维坐标的旋转:
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
此处考虑最简单的绕一个轴旋转:
在这里插入图片描述
因此:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

其他的旋转变换

(实验要求并不需要掌握)
考虑完绕坐标轴的旋转,我们来考虑下面这种情况在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

需要重点注意的地方

在对一个物体进行矩阵的复合运算时,我们通查会先改变其规模、再对其旋转,然后再改变其位置。
(如果分成三行代码则不需要考虑这个东西)
相乘时要按照M=TRS的顺序
在这里插入图片描述

在OpenGL中使用变换矩阵

在这里插入图片描述
投影矩阵是下一节内容,之后再讲在这里插入图片描述
我们旋转立方体,可以通过两种方式来实现:
在这里插入图片描述

mat4 ctm = RotateX(theta[0])*RotateY(theta[1])*RotateZ(theta[2]);

point4 new_points[36]; //======更新顶点位置
for(int i=0; i<36; i++)
{
new_points[i] = ctm*points[i];
}
……
//======将顶点坐标送入GPU重绘
loc = glGetAttribLocation(program, "vPosition");  ……
glutSwapBuffers();

但是这种方法有个不好的地方在于其实这无疑是加重了cpu的负担。我们可以尝试另外一种方法,这种方法将变换交给GPU来计算,也就是传入着色器。

在这里插入图片描述
第二种方式其实并没有修改那些点的位置在坐标中的信息,而是在传入着色器时,为其着色的时候在更改位置后的地方着色。

void main() // 顶点着色器中的代码
{
	gl_Position = rotation*vPosition;
	color = vColor;
}

但是我们要在main函数中把这个矩阵传给着色器才能让着色器进行操作,想传给着色器必须在着色器中声明一个矩阵变量,声明的方法是:
uniform mat4 matrix;
注意,此处是我们第一次自己添加uniform类型的变量,因此我这里来介绍一下uniform:

Uniform是一种从CPU中的应用向GPU中的着色器发送数据的方式,但uniform和顶点属性有些不同。首先,uniform是全局的(Global)。全局意味着uniform变量必须在每个着色器程序对象中都是独一无二的,而且它可以被着色器程序的任意着色器在任意阶段访问。第二,无论你把uniform值设置成什么,uniform会一直保存它们的数据,直到它们被重置或更新。

这里我们就可以看出来uniform与in的变量有个不同的地方在于,in是每一个都是不一样的,而uniform是统一的:举个例子:

在这里插入图片描述
这里每个model都是一样的,但是in类型的顶点变量都是不一样的。

此外,还需要在main函数中声明一个变量,并且通过glGetUniformLocation()函数,以及传入一个绑定了shader的program参数来获得着色器中该变量的存储位置。
首先声明绑定着色器的program:


void init()
{
	// 读取着色器并使用
	GLuint program = InitShader("vshader.glsl", "fshader.glsl");
	glUseProgram(program);
}

然后

matrixLocation = glGetUniformLocation(program, "matrix");
//一般也是放在init函数

然后在display函数中传入矩阵

	// 从指定位置中传入变换矩阵
	glUniformMatrix4fv(matrixLocation, 1, GL_TRUE, m);

着色器中的代码修改成如下:qis c

#version 330 core

in vec4 vPosition;
in vec4 vColor;
out vec4 color;
uniform mat4 matrix;

void main()
{    
	gl_Position = matrix * vPosition;    
	color = vColor;
}

其实此处只是将顶点乘以了一个矩阵变换而已。
参考函数:

mat4 Translate( const GLfloat x, const GLfloat y, const GLfloat z );
通过x、y、z分量构建平移变换矩阵。
mat4 RotateX( const GLfloat theta );
获得绕X轴旋转的变化矩阵。
mat4 RotateY( const GLfloat theta );
获得绕Y轴旋转的变化矩阵。
mat4 RotateZ( const GLfloat theta );
获得绕Z轴旋转的变化矩阵。
mat4 Scale( const GLfloat x, const GLfloat y, const GLfloat z );
通过x、y、z分量构建缩放变换矩阵。
void glUniformMatrix4fv(Glint location, Glsizei count, GLboolean transpose, const GL float * values);
设置与location索引位置对应的uniform变量的值。

三、 实验内容

  1. 创建基本工程项目
    参考实验1.2和实验2.2的实验内容,新建一个Transformation工程项目。
  2. 在程序内直接定义立方体
    参考下图中立方体的顶点索引,根据索引生成立方体的12个三角面片,并将三角形上顶点信息存储。
    在这里插入图片描述
// 立方体生成8个顶点和12个三角形的顶点索引
void generateCube()
{
		storeTrianglePoints();
//@TODO: 存储每个三角形的顶点索引信息到faces里面
}
  1. 添加菜单,让用户自定义变换(缩放、旋转、平移)
    参考实验2.1的内容,提供菜单选项选择当前应用的变换。

在setupMenu中通过glutCreateMenu()函数注册菜单回调函数menuEvents(),menuEvents()函数将保存当前变换的方式。然后通过glutAddMenuEntry()添加菜单选项,再通过 glutAttachMenu()绑定到鼠标中键上。

void setupMenu()
{
	glutCreateMenu(menuEvents);
	glutAddMenuEntry("Scale", TRANSFORM_SCALE);
	glutAddMenuEntry("Rotate", TRANSFORM_ROTATE);
	glutAddMenuEntry("Translate", TRANSFORM_TRANSLATE);
	glutAttachMenu(GLUT_RIGHT_BUTTON);
}
  1. 设置键盘的交互
    参考实验2.1中的内容,在这里设置六个按键分别控制X、Y、Z轴的变换。
void keyboard(unsigned char key, int x, int y)
{
	switch (key) {
	case 'q':
		updateTheta(X_AXIS, 1);
		break;
	case 'a':
		updateTheta(X_AXIS, -1); 
		break;
	case 'w':
		updateTheta(Y_AXIS, 1);
		break;
	case 's':
		updateTheta(Y_AXIS, -1); 
		break;
	case 'e':
		updateTheta(Z_AXIS, 1); 
		break;
	case 'd':
		updateTheta(Z_AXIS, -1); 
		break;
	case 't':
		resetTheta();
		break;
	case 033:
		// Esc按键
		exit(EXIT_SUCCESS);
		break;
	}
	glutPostWindowRedisplay(mainWindow);
}

  1. 将变换矩阵应用于三维立方体
    (1)构造和计算变换矩阵
    下面的代码是在构造和计算变换矩阵的过程。
void display()
{
	// 清理窗口
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	// 生成变换矩阵
	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);

	m = m * Translate(translateTheta);
	m = m * RotateX(rotateTheta.x);
	m = m * RotateY(rotateTheta.y);
	m = m * RotateZ(rotateTheta.z);
	m=m*Scale(scaleTheta);
	// @TODO: 在此处修改函数,计算变换矩阵

	从指定位置中传入变换矩阵
	glUniformMatrix4fv(matrixLocation, 1, GL_TRUE, m);
	//glDrawArrays(GL_TRIANGLES, 0, points.size());
	//注意这里是开始使用glDrawElements函数了
	glDrawElements(GL_TRIANGLES, int(faces.size() * 3), GL_UNSIGNED_INT, BUFFER_OFFSET(0));

	glutSwapBuffers();
}

在矩阵的计算过程中,可以调用定义在mat.h头文件中所提供的Translate()、Scale()、RotateX()、RotateY()、RotateZ()函数计算的到最终的变换矩阵。

(2)传入着色器
注意,本次代码中的fshader并未发生改变,但是vshader发生了变化。
vshader代码

#version 330 core

in vec4 vPosition;
in vec4 vColor;
out vec4 color;
uniform mat4 matrix;
uniform变量的传入需要一些函数来实现,详细见下面

void main()
{
    gl_Position = matrix * vPosition;
    color = vColor;
}

在init()中需要通过glGetUniformLocation()获得uniform变量matrix的存储位置,将其保存到matrixLocation当中

void init()
{
	// 读取着色器并使用
	GLuint program = InitShader("vshader.glsl", "fshader.glsl");
	glUseProgram(program);

	// 从顶点着色器中初始化顶点的位置
	GLuint pLocation = glGetAttribLocation(program, "vPosition");
	glEnableVertexAttribArray(pLocation);
	glVertexAttribPointer(pLocation, 4, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(0));
	// 从片元着色器中初始化顶点的颜色
	GLuint cLocation = glGetAttribLocation(program, "vColor");
	glEnableVertexAttribArray(cLocation);
	glVertexAttribPointer(cLocation, 4, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(points.size() * sizeof(vec4)));
	// 获得矩阵存储位置

	注意这里
	matrixLocation = glGetUniformLocation(program, "matrix");

	// 白色背景
	glClearColor(1.0, 1.0, 1.0, 1.0);
}

然后再在绘制的函数中通过glUniformMatrix4fv()函数设置uniform变量matrix的值。

void display()
{
	// 从指定位置中传入变换矩阵
	glUniformMatrix4fv(matrixLocation, 1, GL_TRUE, m);
}

通常uniform修饰符可以制定一个在应用程序中设置好的变量,不会在图元处理的过程中发生变化。uniform变量在所有可用的着色阶段之间都是共享的,它必须定义为全局变量。从特性上,变换矩阵适合被定义为uniform变量。

(3)应用于顶点着色器
在顶点着色器中,需要先声明uniform mat4 matrix;
然后在main()函数中用gl_Position = matrix * vPosition;这一行语句将变换矩阵应用于三维立方体的顶点位置上。

#version 330 core

in vec4 vPosition;
in vec4 vColor;
out vec4 color;
uniform mat4 matrix;

void main()
{
    gl_Position = matrix * vPosition;
    color = vColor;
}

四、 参考函数

  1. mat4 Translate( const GLfloat x, const GLfloat y, const GLfloat z );
    通过x、y、z分量构建平移变换矩阵。
  2. mat4 RotateX( const GLfloat theta );
    获得绕X轴旋转的变化矩阵。
  3. mat4 RotateY( const GLfloat theta );
    获得绕Y轴旋转的变化矩阵。
  4. mat4 RotateZ( const GLfloat theta );
    获得绕Z轴旋转的变化矩阵。
  5. mat4 Scale( const GLfloat x, const GLfloat y, const GLfloat z );
    通过x、y、z分量构建缩放变换矩阵。
  6. void glUniformMatrix4fv(Glint location, Glsizei count, GLboolean transpose, const GL float * values);
    设置与location索引位置对应的uniform变量的值。

五、 实验结果

首先读入正方形,这里上节内容都会了,现在详细讲讲如何通过菜单栏选择旋转轴并让其针对某个轴旋转(其实即使这里不掌握,实验也可以做出来,因为留空代码以及写好了,但为了后续的发展,还是早点掌握好,不然就会像我一样前面偷懒后面就得回来重新预习 )
快进到我前面内容没学懂结果期末疯狂预习前面内容到凌晨三点

平移、缩放、和旋转的具体实现

我接下来讲讲书写代码的逻辑:
其实思路很简单,在display函数中先让物体乘以三个矩阵即可实现。但是为了通过鼠标的菜单和键盘实现交互,需要写一个updateTheta函数,参数是下标和正负,然后在keyboard函数中调用,并传入参数,只不过参数用符号表示。当按下keyboard键的时候就执行函数,最重要的一点不要忘了在keyboard函数结尾中执行重绘!

首先在display函数中这样即可

	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);

	m = m * Translate(translateTheta);
	m = m * RotateX(rotateTheta.x);
	m = m * RotateY(rotateTheta.y);
	m = m * RotateZ(rotateTheta.z);
	m=m*Scale(scaleTheta);

看起来是不是很简单?但是需要做一些准备工作

但是为了实现交互,需要写一个变化的函数
updatetheta函数,由于有平移旋转缩放三种,因此需要用一个状态来表示,用变量currentTransform,根据不同的状态看是修改哪一个矩阵。
但是状态不能作为传入的参数,因为我们是通过鼠标点击产生的菜单栏来改变当前是对哪个执行,此时并没有修改theta,因此不能调用函数,因此currentTransform不能作为函数的参数传入。
但是我们可以用全局变量来表示。

int currentTransform = TRANSFORM_TRANSLATE;

初始化时我们为其设定了一个状态。
其中TRANSFORM_TRANSLATE是一个定义好的值,用符号表示状态。

const int TRANSFORM_SCALE = 0;
const int TRANSFORM_ROTATE = 1;
const int TRANSFORM_TRANSLATE = 2;

我们修改状态时,为其赋值也是赋值这三个变量的名字(为了增强可读性)。
(用变量来表示数字会更容易让人理解,在以后大型程序写代码的时候这样做会比用012好很多)

接下来实现在菜单中来修改状态:

void menuEvents(int menuChoice)
{
	currentTransform = menuChoice;
}

void setupMenu()
{
	glutCreateMenu(menuEvents);
	glutAddMenuEntry("Scale", TRANSFORM_SCALE);
	glutAddMenuEntry("Rotate", TRANSFORM_ROTATE);
	glutAddMenuEntry("Translate", TRANSFORM_TRANSLATE);
	glutAttachMenu(GLUT_RIGHT_BUTTON);
}

于是修改状态的方法实现了,接下来书写改变角度的函数:
其中可以看到,通过全局变量的不同来实现不同情况的不同操作。

对于三种情况,分别需要三个vec3向量来表示。

vec3 scaleTheta(1.0, 1.0, 1.0);
vec3 rotateTheta(0.0, 0.0, 0.0);
vec3 translateTheta(0.0, 0.0, 0.0);

vec可以用下标的形式来访问其单个元素,因此下面是用下标来访问

void updateTheta(int axis, int sign) {
	switch (currentTransform) {
	case TRANSFORM_SCALE:
		scaleTheta[axis] += sign * scaleDelta;
		break;
	case TRANSFORM_ROTATE:
		rotateTheta[axis] += sign * rotateDelta;
		break;
	case TRANSFORM_TRANSLATE:
		translateTheta[axis] += sign * translateDelta;
		break;
	}
}

那么接下来,有了修改的函数,什么时候调用呢?
很明显,在用户输入对应按键的时候调用对应的函数。

void keyboard(unsigned char key, int x, int y)
{
	switch (key) {
	case 'q':
		updateTheta(X_AXIS, 1);
		break;
	case 'a':
		updateTheta(X_AXIS, -1);
		break;
	case 'w':
		updateTheta(Y_AXIS, 1);
		break;
	case 's':
		updateTheta(Y_AXIS, -1);
		break;
	case 'e':
		updateTheta(Z_AXIS, 1);
		break;
	case 'd':
		updateTheta(Z_AXIS, -1);
		break;
	}
	glutPostWindowRedisplay(mainWindow);
	zheli biewangle chonghui !
}

我们回过头来看updateTheta函数,有三个变量scaleDelta、rotateDelta、translateDelta
上面有一个变量scaleDelta,代表的是每一次变化具体是多少,这个也可以通过键盘调用来改变一次变化量的多少:

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;
	}
}

以及重置函数:

// 复原Theta和Delta
void resetTheta()
{
	scaleTheta = vec3(1.0, 1.0, 1.0);
	rotateTheta = vec3(0.0, 0.0, 0.0);
	translateTheta = vec3(0.0, 0.0, 0.0);
	scaleDelta = DEFAULT_DELTA;
	rotateDelta = DEFAULT_DELTA;
	translateDelta = DEFAULT_DELTA;
}

同样也是在keyboard函数中调用:

void keyboard(unsigned char key, int x, int y)
{
	switch (key) {
	case 'r':
		updateDelta(1);
		break;
	case 'f':
		updateDelta(-1);
		break;
	case 't':
		resetTheta();
		break;
	case 033:
		// Esc按键
		exit(EXIT_SUCCESS);
		break;
	}
	glutPostWindowRedisplay(mainWindow);
}

为什么我在这里要说这么多呢?明明老师给的代码是有的,但是我觉得很重要的是要学会怎么自己完整的写出来,因此看懂是一方面,另外一方面还要有会写的思路。

换言之得思考,你每学完一个小节,看懂是一方面,你得思考自己要怎么完整的写出来才是很重要的。

	glutPostWindowRedisplay(mainWindow);
完整代码
#include "Angel.h"
#include "mat.h"
#include "vec.h"

#include<vector>

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

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

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;

const double DELTA_DELTA = 0.1;
const double DEFAULT_DELTA = 0.3;

double scaleDelta = DEFAULT_DELTA;
double rotateDelta = DEFAULT_DELTA;
double translateDelta = DEFAULT_DELTA;

vec3 scaleTheta(1.0, 1.0, 1.0);
vec3 rotateTheta(0.0, 0.0, 0.0);
vec3 translateTheta(0.0, 0.0, 0.0);

GLint matrixLocation;
int currentTransform = TRANSFORM_TRANSLATE;
int mainWindow;
const int NUM_VERTICES = 8;

// 单位立方体的各个点
const vec4 vertices[NUM_VERTICES] = {
	vec4(-0.5, -0.5,  0.5, 1.0),
	vec4(-0.5,  0.5,  0.5, 1.0),
	vec4(0.5,  0.5,  0.5, 1.0),
	vec4(0.5, -0.5,  0.5, 1.0),
	vec4(-0.5, -0.5, -0.5, 1.0),
	vec4(-0.5,  0.5, -0.5, 1.0),
	vec4(0.5,  0.5, -0.5, 1.0),
	vec4(0.5, -0.5, -0.5, 1.0)
};

// 每个顶点的对应颜色
const vec4 vertexColors[NUM_VERTICES] = {
	vec4(0.0, 0.0, 0.0, 1.0),  // Black
	vec4(1.0, 0.0, 0.0, 1.0),  // Red
	vec4(1.0, 1.0, 0.0, 1.0),  // Yellow
	vec4(0.0, 1.0, 0.0, 1.0),  // Green
	vec4(0.0, 0.0, 1.0, 1.0),  // Blue
	vec4(1.0, 0.0, 1.0, 1.0),  // Magenta
	vec4(1.0, 1.0, 1.0, 1.0),  // White
	vec4(0.0, 1.0, 1.0, 1.0)   // Cyan
};

std::vector<vec4> points;  //顶点集合
std::vector<vec4> colors;   //顶点颜色
std::vector<vec3i> faces;  //三角面片集合

// 存储立方体的8个顶点并为点赋色
void storeTrianglePoints()
{
	for (int i = 0; i < 8; i++)
	{
		points.push_back(vertices[i]);
		colors.push_back(vertexColors[i]);
	}
}

// 立方体生成8个顶点和12个三角形的顶点索引
void generateCube()
{
	storeTrianglePoints();
	faces.push_back(vec3i(1, 0, 3));
	faces.push_back(vec3i(1, 3, 2));
	faces.push_back(vec3i(2, 3, 7));
	faces.push_back(vec3i(2, 7, 6));
	faces.push_back(vec3i(3, 0, 4));
	faces.push_back(vec3i(3, 4, 7));
	faces.push_back(vec3i(6, 5, 1));
	faces.push_back(vec3i(6, 1, 2));
	faces.push_back(vec3i(4, 5, 6));
	faces.push_back(vec3i(4, 6, 7));
	faces.push_back(vec3i(5, 4, 0));
	faces.push_back(vec3i(5, 0, 1));
	// @TODO: 存储每个三角形的顶点索引信息到faces里面
}

void init()
{
	generateCube();

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

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

	// 分别读取数据
	glBufferSubData(GL_ARRAY_BUFFER, 0, points.size() * sizeof(vec4), &points[0]);
	glBufferSubData(GL_ARRAY_BUFFER, points.size() * sizeof(vec4), colors.size() * sizeof(vec4), &colors[0]);

	// 创建并初始化顶点索引缓存对象
	// @TODO: 修改完成generateCube函数后再打开下面注释,否则程序会报错
	GLuint vertexIndexBuffer;
	glGenBuffers(1, &vertexIndexBuffer);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vertexIndexBuffer);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, faces.size() * sizeof(vec3i), faces.data(), GL_STATIC_DRAW);

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

	// 从顶点着色器中初始化顶点的位置
	GLuint pLocation = glGetAttribLocation(program, "vPosition");
	glEnableVertexAttribArray(pLocation);
	glVertexAttribPointer(pLocation, 4, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(0));
	// 从片元着色器中初始化顶点的颜色
	GLuint cLocation = glGetAttribLocation(program, "vColor");
	glEnableVertexAttribArray(cLocation);
	glVertexAttribPointer(cLocation, 4, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(points.size() * sizeof(vec4)));
	// 获得矩阵存储位置
	matrixLocation = glGetUniformLocation(program, "matrix");

	// 白色背景
	glClearColor(1.0, 1.0, 1.0, 1.0);
}

void display()
{
	// 清理窗口
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	// 生成变换矩阵
	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);

	m = m * Translate(translateTheta);
	m = m * RotateX(rotateTheta.x);
	m = m * RotateY(rotateTheta.y);
	m = m * RotateZ(rotateTheta.z);
	m=m*Scale(scaleTheta);
	// @TODO: 在此处修改函数,计算变换矩阵

	// 从指定位置中传入变换矩阵
	glUniformMatrix4fv(matrixLocation, 1, GL_TRUE, m);
	//glDrawArrays(GL_TRIANGLES, 0, points.size());
	//注意这里是开始使用glDrawElements函数了
	glDrawElements(GL_TRIANGLES, int(faces.size() * 3), GL_UNSIGNED_INT, BUFFER_OFFSET(0));

	glutSwapBuffers();
}

// 通过Delta值更新Theta
void updateTheta(int axis, int sign) {
	switch (currentTransform) {
	case TRANSFORM_SCALE:
		scaleTheta[axis] += sign * scaleDelta;
		break;
	case TRANSFORM_ROTATE:
		rotateTheta[axis] += sign * rotateDelta;
		break;
	case TRANSFORM_TRANSLATE:
		translateTheta[axis] += sign * translateDelta;
		break;
	}
}

// 复原Theta和Delta
void resetTheta()
{
	scaleTheta = vec3(1.0, 1.0, 1.0);
	rotateTheta = vec3(0.0, 0.0, 0.0);
	translateTheta = 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 keyboard(unsigned char key, int x, int y)
{
	switch (key) {
	case 'q':
		updateTheta(X_AXIS, 1);
		break;
	case 'a':
		updateTheta(X_AXIS, -1);
		break;
	case 'w':
		updateTheta(Y_AXIS, 1);
		break;
	case 's':
		updateTheta(Y_AXIS, -1);
		break;
	case 'e':
		updateTheta(Z_AXIS, 1);
		break;
	case 'd':
		updateTheta(Z_AXIS, -1);
		break;
	case 'r':
		updateDelta(1);
		break;
	case 'f':
		updateDelta(-1);
		break;
	case 't':
		resetTheta();
		break;
	case 033:
		// Esc按键
		exit(EXIT_SUCCESS);
		break;
	}
	glutPostWindowRedisplay(mainWindow);
}

void menuEvents(int menuChoice)
{
	currentTransform = menuChoice;
}

void setupMenu()
{
	glutCreateMenu(menuEvents);
	glutAddMenuEntry("Scale", TRANSFORM_SCALE);
	glutAddMenuEntry("Rotate", TRANSFORM_ROTATE);
	glutAddMenuEntry("Translate", TRANSFORM_TRANSLATE);
	glutAttachMenu(GLUT_RIGHT_BUTTON);
}

void printHelp() {
	printf("%s\n\n", "3D Transfomations");
	printf("Keyboard options:\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");
}

int main(int argc, char **argv)
{
	glutInit(&argc, argv);
	glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH);    // 窗口支持双重缓冲和深度测试
	glutInitWindowPosition(100, 100);
	glutInitWindowSize(800, 800);
	mainWindow = glutCreateWindow("3D Transfomations");

	glewExperimental = GL_TRUE;
	glewInit();

	init();
	setupMenu();
	glutDisplayFunc(display);
	glutKeyboardFunc(keyboard);
	// 输出帮助信息
	printHelp();
	// 启用深度测试
	glEnable(GL_DEPTH_TEST);
	glutMainLoop();
	return 0;
}

实验结果

右键可以选中菜单,设置是旋转平移还是缩放,懒得放结果了自己运行吧
在这里插入图片描述

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

讲了那么多 终于来到了实验二

一、 实验内容

读取off格式三维模型,对其赋色。并利用鼠标和键盘的交互生成动画。

在这里插入图片描述
(把之前的东西拼起来就好了
具体内容包括:

  1. OFF格式三维模型文件的读取
    参考上机实验2.2的内容,完成对OFF格式三维模型文件的读取与显示,并附上自己的颜色,尽量特别,使得你的作业和其他同学的不一样。

  2. 三维模型的旋转动画
    参考实验2.1中动画的生成方式,并结合实验2.3中对模型进行旋转变换的过程,生成旋转动画。

  3. 键盘鼠标交互
    参考实验2.1、2.3中鼠标与键盘的交互,通过“键盘”设定选择绕x、y、z轴中的哪个轴旋转,通过“鼠标”左键和右键进行顺时针和逆时针方向的旋转。

二.实验步骤

  1. OFF格式三维模型文件的读取
    参考上机实验2.2的内容,完成对OFF格式三维模型文件的读取与显示,并附上自己的颜色,尽量特别,使得你的作业和其他同学的不一样。
  2. 三维模型的旋转动画
    参考实验2.1中动画的生成方式,并结合实验2.3中对模型进行旋转变换的过程,生成旋转动画
  3. 键盘鼠标交互
    参考实验2.1、2.3中鼠标与键盘的交互,通过“键盘”设定选择绕x、y、z轴中的哪个轴旋转,通过“鼠标”左键和右键进行顺时针和逆时针方向的旋转。

三、实验结果

代码解析

首先是文件的读取
此处参照实验2.2的进行读取函数
在这里插入图片描述
在这里插入图片描述
只是读取之前我们可以先选择读取哪个文件
在这里插入图片描述

在main函数中读取filename
在这里插入图片描述
选中菜单栏的translate或者scale或者是rorate时,我们通过改变矩阵来改变实际物体的这三个参数
改变矩阵大小的方法:(参照实验2.3)
在键盘中输入修改的按键(具体什么按键修改什么参数见printhelp函数)
我们会在主函数中调用keyboard函数,keyboard函数如下

在这里插入图片描述
当输入相应按键的时候,矩阵大小会变化,从而影响物体
在这里插入图片描述
到这里其实都是前三个实验的内容,我们需要新加绕xyz轴的旋转:

绕xyz轴的旋转的实现

动画函数:
这里与上同理,状态不能作为参数传入,而应该作为全局变量:

const int TRANSFORM_X = 3;
const int TRANSFORM_Y = 4;
const int TRANSFORM_Z = 5;
void idleFunction()
{
	switch (currentTransform) {
	case TRANSFORM_X:
		rotateTheta.x -= 0.5 * rotateDelta;
		break;
	case TRANSFORM_Y:
		rotateTheta.y -= 0.5 * rotateDelta;
		break;
	case TRANSFORM_Z:
		rotateTheta.z -= 0.5 * rotateDelta;
		break;
	}
	glutPostWindowRedisplay(mainWindow);
}

void idleFunction2()
{
	switch (currentTransform) {
	case TRANSFORM_X:
		rotateTheta.x += 0.5 * rotateDelta;
		break;
	case TRANSFORM_Y:
		rotateTheta.y += 0.5 * rotateDelta;
		break;
	case TRANSFORM_Z:
		rotateTheta.z += 0.5 * rotateDelta;
		break;
	}
	glutPostWindowRedisplay(mainWindow);
}

鼠标回调函数:

// 鼠标回调函数
void windowMouse(int button, int state, int x, int y) {
	if (button == GLUT_LEFT_BUTTON && state == GLUT_DOWN) {
		// 鼠标左键,动画开始
		glutIdleFunc(idleFunction);
	}
	else if (button == GLUT_LEFT_BUTTON && state == GLUT_UP) {
		// 鼠标左键松开,动画停止
		glutIdleFunc(NULL);
	}//将这个elseif和最下面这个elseif注释掉,就可以产生按下了一次就可以一直旋转的效果
	//想要按下一次后一直旋转,再按一次一直停止,只需要if(状态==运行){状态=不运行} else if(状态==不运行){状态=运行}
	else if (button == GLUT_RIGHT_BUTTON && state == GLUT_DOWN) {
		// 鼠标右键,动画开始
		glutIdleFunc(idleFunction2);
	}
	else if (button == GLUT_RIGHT_BUTTON && state == GLUT_UP) {
		// 鼠标右键松开,动画停止
		glutIdleFunc(NULL);
	}
}
运行结果:

按下鼠标中键即可出现菜单栏
在这里插入图片描述

当输入相应按键的时候,矩阵大小会变化,从而影响物体

例如:改变位置:
在这里插入图片描述

改变角度:

改变大小:
改变大小:
在这里插入图片描述
接下来进行绕特定旋转轴的操作:
注意给菜单栏添加新的栏
在这里插入图片描述
选中菜单栏中的xyz时,我们如果按下鼠标左键或鼠标右键则需要调用旋转函数

绕x轴旋转:
在这里插入图片描述
剩下的就不展示了 老懒狗了

完整代码
#include "Angel.h"
#include "mat.h"
#include "vec.h"
#include <vector>
#include <string>
#include <fstream>
#include <iostream>

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

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;
const int TRANSFORM_X = 3;
const int TRANSFORM_Y = 4;
const int TRANSFORM_Z = 5;

const double DELTA_DELTA = 0.1;
const double DEFAULT_DELTA = 0.3;

double scaleDelta = DEFAULT_DELTA;
double rotateDelta = DEFAULT_DELTA;
double translateDelta = DEFAULT_DELTA;

vec3 scaleTheta(1.0, 1.0, 1.0);
vec3 rotateTheta(0.0, 0.0, 0.0);
vec3 translateTheta(0.0, 0.0, 0.0);

GLint matrixLocation;
int currentTransform = TRANSFORM_TRANSLATE;
int mainWindow;

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

std::string fileName = "Models/cow.off";
std::vector<vec3> vertices; //从文件中读取的顶点坐标
std::vector<vec3i> faces;   //从文件中读取的面顶点索引

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

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



void read_off(const std::string filename)
{
	if (filename.empty()) {
		return;
	}

	std::ifstream fin;
	std::string a;
	fin.open(filename);
	fin >> a;
	fin >> nVertices >> nFaces >> nEdges;
	// @TODO:修改此函数读取OFF文件中三维模型的信息

	//读入首行字符串"OFF"

	//读入点、面、边数目
	vec3 point;
	// 读取每个顶点的坐标
	for (int i = 0; i < nVertices; i++) {
		fin >> point.x >> point.y >> point.z;
		vertices.push_back(point);
	}
	vec3i point2;
	for (int i = 0; i < nFaces; i++) {
		int num;
		fin >> num;
		fin >> point2.a >> point2.b >> point2.c;
		faces.push_back(point2);
	}

	// 读取面的顶点序列

	fin.close();
}

void storeFacesPoints()
{
	points.clear();
	colors.clear();

	// @TODO: 修改此函数在points和colors容器中存储每个三角面片的各个点和颜色信息
	for (int i = 0; i < nFaces; i++) {
		points.push_back(vertices[faces[i].a]);
		points.push_back(vertices[faces[i].b]);
		points.push_back(vertices[faces[i].c]);
	}

}

void init()
{
	storeFacesPoints();

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

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

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

	// 创建并初始化顶点索引缓存对象
	// @TODO: 修改完成generateCube函数后再打开下面注释,否则程序会报错
	GLuint vertexIndexBuffer;
	glGenBuffers(1, &vertexIndexBuffer);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vertexIndexBuffer);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, faces.size() * sizeof(vec3i), faces.data(), GL_STATIC_DRAW);

	// 读取着色器并使用
	GLuint program = InitShader("vshader.glsl", "fshader.glsl");
	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(vec3)));

	// 获得矩阵存储位置
	matrixLocation = glGetUniformLocation(program, "matrix");
	// 黑色背景
	glClearColor(0.0, 0.0, 0.0, 1.0);
}

void display()
{
	// 清理窗口
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	// 生成变换矩阵
	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);

	mat4 T = Translate(translateTheta);
	mat4 X = RotateX(rotateTheta.x);
	mat4 Y = RotateY(rotateTheta.y);
	mat4 Z = RotateZ(rotateTheta.z);
	mat4 S = Scale(scaleTheta);

	m = T * X*Y*Z*S*m;
	// @TODO: 在此处修改函数,计算变换矩阵

	// 从指定位置中传入变换矩阵
	glUniformMatrix4fv(matrixLocation, 1, GL_TRUE, m);
	//glDrawArrays(GL_TRIANGLES, 0, points.size());
	//注意这里是开始使用glDrawElements函数了
	glDrawArrays(GL_TRIANGLES, 0, points.size());

	glutSwapBuffers();
}

// 通过Delta值更新Theta
void updateTheta(int axis, int sign) {
	switch (currentTransform) {
	case TRANSFORM_SCALE:
		scaleTheta[axis] += sign * scaleDelta;
		break;
	case TRANSFORM_ROTATE:
		rotateTheta[axis] += sign * rotateDelta;
		break;
	case TRANSFORM_TRANSLATE:
		translateTheta[axis] += sign * translateDelta;
		break;
	case TRANSFORM_X:

		break;
	case TRANSFORM_Y:

		break;
	case TRANSFORM_Z:

		break;
	}
}

// 复原Theta和Delta
void resetTheta()
{
	scaleTheta = vec3(1.0, 1.0, 1.0);
	rotateTheta = vec3(0.0, 0.0, 0.0);
	translateTheta = 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 keyboard(unsigned char key, int x, int y)
{
	switch (key) {
	case 'q':
		updateTheta(X_AXIS, 1);
		break;
	case 'a':
		updateTheta(X_AXIS, -1);
		break;
	case 'w':
		updateTheta(Y_AXIS, 1);
		break;
	case 's':
		updateTheta(Y_AXIS, -1);
		break;
	case 'e':
		updateTheta(Z_AXIS, 1);
		break;
	case 'd':
		updateTheta(Z_AXIS, -1);
		break;
	case 'r':
		updateDelta(1);
		break;
	case 'f':
		updateDelta(-1);
		break;
	case 't':
		resetTheta();
		break;
	case 033:
		// Esc按键
		exit(EXIT_SUCCESS);
		break;
	}
	glutPostWindowRedisplay(mainWindow);
}

void menuEvents(int menuChoice)
{
	currentTransform = menuChoice;
}

void setupMenu()
{
	glutCreateMenu(menuEvents);
	glutAddMenuEntry("Scale", TRANSFORM_SCALE);
	glutAddMenuEntry("Rotate", TRANSFORM_ROTATE);
	glutAddMenuEntry("Translate", TRANSFORM_TRANSLATE);
	glutAddMenuEntry("x", TRANSFORM_X);//加入新的菜单栏xyz
	glutAddMenuEntry("y", TRANSFORM_Y);
	glutAddMenuEntry("z", TRANSFORM_Z);
	glutAttachMenu(GLUT_MIDDLE_BUTTON);//左键和右键分别操控旋转开始,因此中键是调出菜单栏
}

void idleFunction()
{
	switch (currentTransform) {
	case TRANSFORM_X:
		rotateTheta.x -= 0.5 * rotateDelta;
		break;
	case TRANSFORM_Y:
		rotateTheta.y -= 0.5 * rotateDelta;
		break;
	case TRANSFORM_Z:
		rotateTheta.z -= 0.5 * rotateDelta;
		break;
	}
	glutPostWindowRedisplay(mainWindow);
}

void idleFunction2()
{
	switch (currentTransform) {
	case TRANSFORM_X:
		rotateTheta.x += 0.5 * rotateDelta;
		break;
	case TRANSFORM_Y:
		rotateTheta.y += 0.5 * rotateDelta;
		break;
	case TRANSFORM_Z:
		rotateTheta.z += 0.5 * rotateDelta;
		break;
	}
	glutPostWindowRedisplay(mainWindow);
}

// 鼠标回调函数
void windowMouse(int button, int state, int x, int y) {
	if (button == GLUT_LEFT_BUTTON && state == GLUT_DOWN) {
		// 鼠标左键,动画开始
		glutIdleFunc(idleFunction);
	}
	else if (button == GLUT_LEFT_BUTTON && state == GLUT_UP) {
		// 鼠标左键松开,动画停止
		glutIdleFunc(NULL);
	}//将这个elseif和最下面这个elseif注释掉,就可以产生按下了一次就可以一直旋转的效果
	else if (button == GLUT_RIGHT_BUTTON && state == GLUT_DOWN) {
		// 鼠标右键,动画开始
		glutIdleFunc(idleFunction2);
	}
	else if (button == GLUT_RIGHT_BUTTON && state == GLUT_UP) {
		// 鼠标右键松开,动画停止
		glutIdleFunc(NULL);
	}
}


void printHelp() {
	printf("%s\n\n", "3D Transfomations");
	printf("Keyboard options:\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");
	printf("\n \n \nif you select x and push left-button of the mouse,it will rotate arount x axis clockwise.\n");
	printf("if you push right-button of the mouse,it will rotate arount x axis  counterclockwise.\n");
	printf("And if you release the button,it will stop rotating.");
}

int main(int argc, char **argv)
{
	glutInit(&argc, argv);
	glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH);    // 窗口支持双重缓冲和深度测试
	glutInitWindowPosition(100, 100);
	glutInitWindowSize(800, 800);
	mainWindow = glutCreateWindow("3D Transfomations");

	glewExperimental = GL_TRUE;
	glewInit();

	read_off(fileName);

	init();
	setupMenu();
	glutDisplayFunc(display);
	glutKeyboardFunc(keyboard);
	glutMouseFunc(windowMouse);
	// 输出帮助信息
	printHelp();
	// 启用深度测试
	glEnable(GL_DEPTH_TEST);
	glutMainLoop();
	return 0;
}

整体还算比较简单,认真去思考去做应该还是没问题的

其他特别的交互方式

这里介绍几种额外在游戏中更常见的交互方式:
首先需要一些资料:
freeglut官网提供了一些资料

另外还有两位大佬的博客写了中文版的资料:
Opengl中的GLUT下的回调函数
glut 主要函数介绍

这里介绍几个这次会用到的:

void glutMouseFunc(void (*func)(int button, int state, int x, int y));

注册当前窗口的鼠标回调函数

参数: func:形如void func(int button, int state, int x, int y);
button:鼠标的按键,为以下定义的常量 GLUT_LEFT_BUTTON:鼠标左键 GLUT_MIDDLE_BUTTON:鼠标中键
GLUT_RIGHT_BUTTON:鼠标右键 state:鼠标按键的动作,为以下定义的常量 GLUT_UP:鼠标释放
GLUT_DOWN:鼠标按下 x,y:鼠标按下式,光标相对于窗口左上角的位置

当点击鼠标时调用.

void glutMotionFunc(void (*func)(int x, int y));

当鼠标在窗口中按下并移动时调用glutMotionFunc注册的回调函数

void glutPassiveMotionFunc(void (*func)(int x, int y));

当鼠标在窗口中移动时调用glutPassiveMotionFunc注册的回调函数

参数: func:形如void func(int x, int y); x,y:鼠标按下式,光标相对于窗口左上角的位置,以像素为单位

鼠标拖动方块

这里借用了大佬博客的写法
在实验2.3的基础上添加这几个全局变量

vec3 scaleTheta(1.0, 1.0, 1.0);
vec3 rotateTheta(0.0, 0.0, 0.0);
vec3 translateTheta(0.0, 0.0, 0.0);

int windowWidth = 512;  // 窗口宽
int windowHeight = 512;

在display函数里

void display()
{
	// 清理窗口
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	// 生成变换矩阵
	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);

	m = m * Translate(translateTheta);
	m = m * RotateX(rotateTheta.x);
	m = m * RotateY(rotateTheta.y);
	m = m * RotateZ(rotateTheta.z);
	m=m*Scale(scaleTheta);

只需要写一个mouse函数:

void mouse(int x, int y)
{
	rotateTheta.y = -100 * (x - float(windowWidth) / 2.0) / windowWidth;
	rotateTheta.x = -100 * (y - float(windowHeight) / 2.0) / windowHeight;

	glutPostRedisplay();
	return;
}

然后在主函数中回调即可

	glutMotionFunc(mouse);

注意这里要用glutMotionFunc,之前用到的函数是glutMouseFunc,这俩个区别在于后者是按下调用,前者是要一直按住,并且传入的参数函数的参数也不同。

在这里插入图片描述
(但是这里有个问题在于释放后重新按下鼠标,正方体是不是在原来的基础上再变换了,而是重新计算。这个后续修改)

把glutMotionFunc换成glutPassiveMotionFunc的话,不按下鼠标也能移动了,我们可以通过这个原理来实现fps相机。

鼠标滚轮实现物体缩放

原理其实很简单,通过书写一个滚轮函数来改变scale的值即可 :

首先来看鼠标滚轮的注册回调函数

void glutMouseWheelFunc ( void( *callback )( int wheel, int direction, int x, int y ));

wheel是滚轮id,鼠标默认为零,direction代表前向后向
书写滚轮函数

void mouseWheel(int wheel, int direction, int x, int y)
{
	scaleTheta += 1 * direction * 0.1;
	glutPostRedisplay();    // 重绘
}

然后在主函数中注册回调函数即可:

glutMouseWheelFunc(mouseWheel);

在这里插入图片描述
又借大佬图大佬不要打我嘤嘤嘤

键盘实现方块移动

这个很简单,通过keyboard函数来改变translate矩阵就行了,不再赘述。

  • 23
    点赞
  • 100
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值