Menu

OpenGL 实现简易的星球旋转效果

使用 OpenGL 实现简易的星球旋转效果,如图 1。程序执行效果见附带压缩包的 EXE 程序(EXE 文件为 Win 程序,使用 Mac OS 和 Linux 的同学请参照 Win 平台下的执行效果) 。

功能要求

  1. 使用不同尺寸的线框球体(Wire Sphere)表示大小两个星球
  2. 使用平移和旋转操作实现小星球自转和绕大星球旋转的功能,键盘事件响应如下: d 和 shift+d: 分别控制小星球正反两个方向的自转; y 和 shift+y: 分别控制小星球绕大星球的正方向和反方向旋转
  3. 语言不限,开发平台不限。具体效果展示允许略有差异。
  4. 要求使用 OpenGL 着色器编程方式实现程序。

实现提示

正方向旋转可以通过以下方式求得:d = (d + 10) % 360;反方向则为 d = (d - 10) % 360

Project3

一开始我的开发环境是搭在 Linux 环境下的,但是运行时发生了各种异常,在吴飚、王威等同学讨论之后(感谢他们),重新在 Windows 下使用 VS2019 开发。相关项目目录是/Project3,打包好的成品是project3.exe

运行效果

这里不太会画线框球体,于是用二十个三角形去模拟这个球体。

另外,遇到很迷的问题,恒星没有显示出来…

打开的效果如下。

2

按了若干下 d 和 y 之后如图,行星转到了上方,并且发生了一定的自转(极点位置变动了)。

3

原理

二维坐标下绕原点自转

记$\theta$是要旋转的角度,记$dayd$是幅角为$\theta$的单位向量。考虑复数的乘法意义:幅角相加、模长相乘,只需要将原来的点在复数域上与dayd做一次乘法即可。

$aPos’=(aPos.xdayd.x-aPos.ydayd.y, aPos.ydayd.x+aPos.xdayd.y)$

二维坐标下绕原点公转

公转相对容易,只要算出现在位置和原位置的向量差$yeard$即可。

$aPos’=aPos+yeard$

源代码main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
#include <cmath>

int year = 0, day = 0;

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

// settings
const unsigned int SCR_WIDTH = 1024;
const unsigned int SCR_HEIGHT = 768;

const char* vertexShaderSource =
"#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n"
"uniform vec2 yeard;\n"
"uniform vec2 dayd;\n"
"void main()\n"
"{\n"
"if(aPos.x*aPos.x+ aPos.y * aPos.y+ aPos.x * aPos.y<0.06)\n"
"   gl_Position = vec4(aPos.x*dayd.x-aPos.y*dayd.y+yeard.x, aPos.y*dayd.x+aPos.x*dayd.y+yeard.y, aPos.z, 1.0);\n"
"else\n"
"   gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
"}\0";
const char* fragmentShaderSource =
"#version 330 core\n"
"out vec4 FragColor;\n"
"void main()\n"
"{\n"
"   FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
"}\n\0";

int main()
{
	//glfw初始化
	glfwInit();
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
	//glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);//MacOS

	//glfw window creation
	GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "17341163 WuK", NULL, NULL);
	if (window == NULL)
	{
		std::cout << "Failed to create GLFW window" << std::endl;
		glfwTerminate();
		return -1;
	}
	glfwMakeContextCurrent(window);
	glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

	//glad: load all OpenGL function pointers
	if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
	{
		std::cout << "Failed to initialize GLAD" << std::endl;
		return -1;
	}

	//build and compile 着色器程序

		//顶点着色器
	unsigned int vertexShader;
	vertexShader = glCreateShader(GL_VERTEX_SHADER);
	glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
	glCompileShader(vertexShader);
	//检查顶点着色器是否编译错误
	int  success;
	char infoLog[512];
	glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
	if (!success)
	{
		glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
		std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
	}
	else {
		std::cout << "vertexShader complie SUCCESS" << std::endl;
	}
	//片段着色器
	unsigned int fragmentShader;
	fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
	glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
	glCompileShader(fragmentShader);
	//检查片段着色器是否编译错误
	glGetShaderiv(fragmentShader, GL_LINK_STATUS, &success);
	if (!success) {
		glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
		std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
	}
	else {
		std::cout << "fragmentShader complie SUCCESS" << std::endl;
	}

	//连接到着色器程序
	unsigned int shaderProgram;
	shaderProgram = glCreateProgram();
	glAttachShader(shaderProgram, vertexShader);
	glAttachShader(shaderProgram, fragmentShader);
	glLinkProgram(shaderProgram);
	//检查片段着色器是否编译错误
	glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
	if (!success) {
		glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
		std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
	}
	else {
		std::cout << "shaderProgram complie SUCCESS" << std::endl;
	}
	//连接后删除
	glDeleteShader(vertexShader);
	glDeleteShader(fragmentShader);

	const double r1 = 0.2,r2=0.3,r3=0.7,pi=acos(-1);
	const int num = 9;

	float vertices[(2+num)*3*2] = {
		0.0f, -0.1f, sqrt(r1*r1-0.01),
		0.0f, 0.1f, -sqrt(r1*r1-0.01)
	};
	unsigned int indices[num * 2 * 3 * 2];
	for (int i = 0; i < num; ++i)
	{
		vertices[i * 3 + 6] = r1*cos(2*pi*i/num);
		vertices[i * 3 + 1 + 6] = r1*sin(2 * pi * i / num);
		vertices[i * 3 + 2 + 6] = 0;
	}
	for (int i = 0; i < (2 + num) * 3; ++i)
		vertices[i + (2 + num) * 3] = r2 / r1 * vertices[i];
	for (int i = 0; i < num; ++i)
	{
		int j = (i + 1) % num;
		indices[2 * i * 3]= indices[2 * i * 3 + 3] = i+2 ;
		indices[2 * i * 3 + 1] = indices[2 * i * 3+ 4]= j+2;
		indices[2 * i * 3 + 2] = 0 ;
		indices[2 * i * 3 + 5] = 1 ;

		indices[2 * i * 3+ num * 2 * 3] = indices[2 * i * 3 + 3+ num * 2 * 3] = i + 2+ (2 + num) * 3;
		indices[2 * i * 3 + 1+ num * 2 * 3] = indices[2 * i * 3 + 4+ num * 2 * 3] = j + 2+ (2 + num) * 3;
		indices[2 * i * 3 + 2+ num * 2 * 3] = 0+ (2 + num) * 3;
		indices[2 * i * 3 + 5+ num * 2 * 3] = 1+ (2 + num) * 3;
	}

	unsigned int VBO;
	glGenBuffers(1, &VBO);
	unsigned int VAO;
	glGenVertexArrays(1, &VAO);
	unsigned int EBO;
	glGenBuffers(1, &EBO);

	//初始化代码(只运行一次 (除非你的物体频繁改变))
		// 1. 绑定VAO
	glBindVertexArray(VAO);
	// 2. 把我们的顶点数组复制到一个顶点缓冲中,供OpenGL使用
	glBindBuffer(GL_ARRAY_BUFFER, VBO);
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
	// 3. 复制我们的索引数组到一个索引缓冲中,供OpenGL使用
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
	// 4. 设定顶点属性指针
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
	glEnableVertexAttribArray(0);

	glBindBuffer(GL_ARRAY_BUFFER, 0);
	glBindVertexArray(0);

	//线框模式wireframe
	glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);

	// 渲染循环
	while (!glfwWindowShouldClose(window))
	{
		// 输入
		processInput(window);

		// 渲染指令
		glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
		glClear(GL_COLOR_BUFFER_BIT);

		glUseProgram(shaderProgram);
		glBindVertexArray(VAO);
		glDrawElements(GL_TRIANGLES, sizeof(indices)/sizeof(indices[0]), GL_UNSIGNED_INT, 0);

		glUseProgram(shaderProgram);//通过glUniform4f函数设置uniform值
		glUniform2f(glGetUniformLocation(shaderProgram, "yeard"),r3*cos(2*pi*year/360),r3*sin(2*pi*year/360));
		glUniform2f(glGetUniformLocation(shaderProgram, "dayd"), cos(2 * pi * day / 360),sin(2 * pi * day / 360));

		// 检查并调用事件,交换缓冲
		glfwSwapBuffers(window);
		glfwPollEvents();
	}
	glDeleteVertexArrays(1, &VAO);
	glDeleteBuffers(1, &VBO);
	glDeleteBuffers(1, &EBO);

	glfwTerminate();
	return 0;
}

void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
	glViewport(0, 0, width, height);
}

void processInput(GLFWwindow* window)
{
	int dday=0, dyear=0;
	if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
		dday = 1;
	if (glfwGetKey(window, GLFW_KEY_Y) == GLFW_PRESS)
		dyear = 1;
	if (glfwGetKey(window, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS||glfwGetKey(window,GLFW_KEY_RIGHT_SHIFT) == GLFW_PRESS)//是否按下了返回键
		dday = 360 - dday, dyear = 360 - dyear;
	day = (day + dday) % 360;
	year = (year + dyear) % 360;
}

star.c

一开始没有仔细阅读老师要求(「使用着色器编程」),因此先用 OpenGL 的固定管线实现了要求(简单了好多啊)。这一版是在 Linux 下开发的,得到的生成文件是star.out。后来我在 Windows 下重新编译了这段代码得到了star.exe

运行star.exe结果如下。

运行结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/*
gcc star.c -o star.exe -lGL -lGLU -lglut
./star.exe
*/
#include <GL/glut.h>
int year = 0, day = 0;
void display()
{
	glClear(GL_COLOR_BUFFER_BIT); //清空颜色缓冲区
	glPushMatrix();				  //压栈
	glColor3f(1, 0.5, 0);		  //恒星是橙黄色的
	glutWireSphere(3, 20, 16);	//绘制恒星
	glRotatef(year, 0, 1, 0);	 //沿y轴旋转
	glTranslatef(6, 0, 0);		  //移动画笔,画行星
	glRotatef(day, 0, 1, 0);	  //沿y轴旋转
	glColor3f(0, 0.5, 1);		  //行星是蔚蓝色的
	glutWireSphere(1, 15, 12);	//绘制行星
	glPopMatrix();				  //弹栈,恢复绘制坐标
	glutSwapBuffers();
	glFlush(); //刷新窗口以显示当前绘制图形
}
void reshape(int w, int h)
{
	glViewport(0, 0, (GLsizei)w, (GLsizei)h); //设置机口
	glMatrixMode(GL_PROJECTION);			  //指定哪一个矩阵是当前矩阵
	glLoadIdentity();
	gluPerspective(60, (GLfloat)w / (GLfloat)h, 1.0, 20); //透视投影矩阵(fovy,aspect,zNear,zFar);
	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();
	gluLookAt(9, 9, 9, 0, 0, 0, 0, 1, 0); //观察者位置,观察者朝向的位置,观察者头顶位置
}
void keyboard(unsigned char key, int x, int y)
{
	if (key == 'd')
		day = (day + 10) % 360;
	else if (key == 'D') // 大写情况下是逆向的
		day = (day + 350) % 360;
	else if (key == 'y')
		year = (year + 10) % 360;
	else if (key == 'Y') // 大写情况下是逆向的
		year = (year + 350) % 360;
	else
		return;
	glutPostRedisplay();
}
int main(int argc, char **argv)
{
	glutInit(&argc, argv);
	glutInitDisplayMode(GLUT_RGB | GLUT_SINGLE); //缓存模式
	glutInitWindowSize(600, 600);				 //显示框的大小
	glutInitWindowPosition(100, 100);			 //确定显示框左上角的位置
	glutCreateWindow("17341163_吴坎_CG_HW3");
	glClearColor(0, 0, 0, 0); // 初始化
	glShadeModel(GL_FLAT);	//选择平面明暗模式或光滑明暗模式
	glutDisplayFunc(display);
	glutReshapeFunc(reshape);
	glutKeyboardFunc(keyboard);
	glutMainLoop();
}