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

#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结果如下。

运行结果

/*
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();
}