0%

OpenGL绘制太阳系

给别人讲了大半学期的OpenGL和图形学了,忍不住手痒自己也用OpenGL来写点什么。绘制一个太阳系作为用来练手的OpenGL项目,再合适不过了,可以做到形状绘制,相机投影,光照贴图五脏俱全。
起手写的时候尝试着从底层的那些东西来入手,手撕各种shader、图元和矩阵变换,然而创业未半而中道偷懒,shader还是太硬核了一时半会玩不来,还是觉得用现成的函数来的舒服,反复造轮子是对生产力的极大浪费不是嘛XD
好了不废话,开始主题

Requirement & Environment

  • OpenGL
  • freeglut
  • GLEW
  • SOIL(用于读取图片信息的一个库)
  • Visual Studio 2015
  • Win10 C++

Camera & Perspective

相机和投影视角是图形学的重要内容,我们这里先简单的使用透视投影以及可以由鼠标操控的相机,主要的两个函数就是gluPerspective()和gluLookAt(),对应到图形学里的投影矩阵和模视变换矩阵。对于相机的视点参数,也就是相机的位置,我们使用三角函数来构建球形环绕轨迹,使得通过拖动鼠标可以围绕着某一点转动相机视角。于是有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void RenderScene(void)
{
float ex = radius * cos(upAngle * M_PI / 180.0) * sin(rotateAngle * M_PI / 180.0);
float ey = radius * sin(upAngle * M_PI / 180.0);
float ez = radius * cos(upAngle * M_PI / 180.0) * cos(rotateAngle * M_PI / 180.0);
glLoadIdentity();
gluLookAt(ex, ey, ez, 0, 0, 0, 0, 1, 0);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

//光源设置在太阳中心
GLfloat sun_light_position[] = { 0.0f, 0.0f, 0.0f, 1.0f };
glLightfv(GL_LIGHT0, GL_POSITION, sun_light_position);

//这里是我们后面画各种物体的地方
//draw_milky_way(-500+ex, -500 + ey, -500 + ez, 1000.0f, 1000.0f, 1000.0f);
//sun();
//earth();


glutPostWindowRedisplay(mainWindow);

glutSwapBuffers();
}

其中glLoadIdentity();是对当前矩阵恢复初始化。

关于鼠标操作,用OpenGL回调函数glutMouseFunc(Mouse)和glutMotionFunc(onMouseMove)来调用如下函数:

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
void Mouse(int button, int state, int x, int y) //处理鼠标点击
{
if (button == 3 || button == 4) {
//currentTransform = TRANSFORM_SCALE;
if (button == 3)
radius -= 10;

if (button == 4)
radius += 10;

}

//std::cout << radius << std::endl;

if (state == GLUT_DOWN) {
//currentTransform = TRANSFORM_ROTATE;
oldmx = x, oldmy = y;
}

}

GLfloat deltax = 0.0f;
GLfloat deltay = 0.0f;
void onMouseMove(int x, int y) //处理鼠标拖动
{

deltax = x - oldmx; //鼠标在窗口x轴方向上的增量加到视点绕y轴的角度上,这样就左右转了

deltay = y - oldmy; //鼠标在窗口y轴方向上的改变加到视点的y坐标上,就上下转了

rotateAngle += deltax;
upAngle += deltay;

oldmx = x, oldmy = y; //把此时的鼠标坐标作为旧值,为下一次计算增量做准备

}

Sun & Earth & Moon

对于各种天体的绘制使用gluSphere()函数来实现非常简单,利用glTranslatef()来调整位置,利用glRotatef()来改变不同的公转自转以及倾斜角,由于这些矩阵操作都是对当前的矩阵进行操作的,为了不使每个星球的位移与旋转操作相互影响,我们使用glPushMatrix()来将矩阵入栈之后再修改,当前星球的操作结束后,将先前保存的矩阵glPopMatrix()出栈,来恢复位置状态。

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
void sun()
{
glPushMatrix();
material_sun();
gluQuadricTexture(e_tex, GLU_TRUE);
glPushAttrib(GL_ENABLE_BIT | GL_TEXTURE_BIT);
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, load_texture("sun.jpg"));
gluSphere(e_tex, 30.0f, 30.0f, 30.0f);
glBindTexture(GL_TEXTURE_2D, 0);
glPopAttrib();
gluQuadricTexture(e_tex, GLU_FALSE);
glPopMatrix();
}

void earth()
{

glPushMatrix();
material_planet();
glEnable(GL_LIGHTING);

static float aMoonRot = 0.0f;
static float aEarthRot = 0.0f;
static float sMoonRot = 0.0f;
static float sEarthRot = 0.0f;

glRotatef(aEarthRot, 0.0f, 1.0f, 0.0f);
glTranslatef(105.0f, 0.0f, 0.0f);

glRotatef(90, 1.0f, 0.0f, 0.0f);

glRotatef(sEarthRot, 0.0f, 0.0f, 1.0f);

gluQuadricTexture(e_tex, GLU_TRUE);
glPushAttrib(GL_ENABLE_BIT | GL_TEXTURE_BIT);
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, load_texture("earth.jpg"));

gluSphere(e_tex, 15.0f, 15.0f, 15.0f);
glBindTexture(GL_TEXTURE_2D, 0);

glPopAttrib();
gluQuadricTexture(e_tex, GLU_FALSE);

glColor3ub(200, 200, 200);
glRotatef(aMoonRot, 0.0f, 0.0f, 1.0f);
glTranslatef(30.0f, 0.0f, 0.0f);
aMoonRot += 3.0f;
if (aMoonRot >= 360.0f)
aMoonRot = 0.0f;
gluQuadricTexture(e_tex, GLU_TRUE);
glPushAttrib(GL_ENABLE_BIT | GL_TEXTURE_BIT);
glEnable(GL_TEXTURE_2D);

glBindTexture(GL_TEXTURE_2D, load_texture("moon.jpg"));
gluSphere(e_tex, 4.0f, 15.0f, 15.0f);
glBindTexture(GL_TEXTURE_2D, 0);

aEarthRot += 1.0f;
if (aEarthRot >= 360.0f)
aEarthRot = 0.0f;
sEarthRot += 5.0f;

if (sEarthRot >= 360.0f)
sEarthRot = 0.0f;

glPopMatrix();
}

Texture & Material

贴图和材质直接决定了绘制图形的美观程度。上一步中构建太阳地球月亮的时候已经调用了材质和贴图函数,现在来具体看看内部实现。

材质

材质上,由于太阳在我们的系统里是充当着光源的角色,所以材质与其他天体是有区别的。太阳应该是一个发光体,所以关键要加上一句glMaterialfv(GL_FRONT, GL_EMISSION, sun_mat_emission);使其自发光

1
2
3
4
5
6
7
8
9
10
11
12
13
void material_sun()                               
{
GLfloat sun_mat_ambient[] = { 1.0f, 1.0f, 1.0f, 1.0f }; //定义材质的环境光颜色
GLfloat sun_mat_diffuse[] = { 1.0f, 1.0f, 1.0f, 1.0f }; //定义材质的漫反射光颜色
GLfloat sun_mat_specular[] = { 1.0f, 1.0f, 1.0f, 1.0f }; //定义材质的镜面反射光颜色
GLfloat sun_mat_emission[] = { 1.0f, 1.0f, 1.0f, 0.0f }; //定义材质的辐射广颜色
GLfloat sun_mat_shininess = 0.0f;
glMaterialfv(GL_FRONT, GL_AMBIENT, sun_mat_ambient);
glMaterialfv(GL_FRONT, GL_DIFFUSE, sun_mat_diffuse);
glMaterialfv(GL_FRONT, GL_SPECULAR, sun_mat_specular);
glMaterialfv(GL_FRONT, GL_EMISSION, sun_mat_emission);
glMaterialf(GL_FRONT, GL_SHININESS, sun_mat_shininess);
}

对于其他星球,我们则可以用统一的材料,辐射光设为0使其不自发光,其他的根据个人喜好调节参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
void material_planet()
{
GLfloat earth_mat_ambient[] = { 1.0f, 1.0f, 1.0f, 1.0f }; //定义材质的环境光颜色
GLfloat earth_mat_diffuse[] = { 1.0f, 1.0f, 1.0f, 1.0f }; //定义材质的漫反射光颜色
GLfloat earth_mat_specular[] = { 0.8f, 0.8f, 0.8f, 0.2f }; //定义材质的镜面反射光颜色
GLfloat earth_mat_emission[] = { 0.0f, 0.0f, 0.0f, 1.0f }; //定义材质的辐射光颜色
GLfloat earth_mat_shininess = 5.0f;
glMaterialfv(GL_FRONT, GL_AMBIENT, earth_mat_ambient);
glMaterialfv(GL_FRONT, GL_DIFFUSE, earth_mat_diffuse);
glMaterialfv(GL_FRONT, GL_SPECULAR, earth_mat_specular);
glMaterialfv(GL_FRONT, GL_EMISSION, earth_mat_emission);
glMaterialf(GL_FRONT, GL_SHININESS, earth_mat_shininess);
}

不过在这个环节我曾经被一个很蠢的问题卡了很久,就是一开始不知道为什么怎么调地球明暗面都非常不明显,总以为是哪个参数设错了,折腾了好久发现,原来是选的贴图本身就很暗,导致看不出阴影效果,换了张贴图就可以了,我无语。

贴图

加载贴图数据有很多种方法,那么这里我选择使用SOIL.h这个库来读取一张图片,然后用glGenTextures(1, &name)和glBindTexture(GL_TEXTURE_2D, name)来为这张贴图绑定一个名字,glTexImage2D()生成贴图数据。通过我们绑定的名字就可以为图形进行贴图。当然这里的名字是自动生成的,可以保证唯一对应。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
GLuint load_texture(const char *path) {
unsigned char* image;
int width, height;
GLuint name;
glGenTextures(1, &name);
glBindTexture(GL_TEXTURE_2D, name);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
image = SOIL_load_image(path, &width, &height, 0, SOIL_LOAD_RGB);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
SOIL_free_image_data(image);
glBindTexture(GL_TEXTURE_2D, 0);
return name;
}

Skybox

多数游戏和3D场景都有一个天空盒来作为背景,简单来讲天空盒就是一个把你绘制的所有物体包裹起来的一个带贴图的立方体,在这个例子里就是星空背景。这个立方体要一个一个面画,因为实际上每个面的贴图应该是连续且不重复的全景,但是这里我就偷懒全部用一张图片了,反正背景都是星星也看不出特别违和。天空盒的中心应该是我们的相机的位置,这样无论相机怎么移动,背景都不会有相对的大小变化,来形成一种背景无限远的感觉。画天空盒时先使用glDepthMask(GL_FALSE)来取消深度,可以使我们绘制的物体永远处于前景。

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
void draw_milky_way(float x, float y, float z, float width, float height, float len) 
{
glPushAttrib(GL_ENABLE_BIT | GL_TEXTURE_BIT);
glDepthMask(GL_FALSE);
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, load_texture("sky.png"));

//back face
glBegin(GL_QUADS);
glNormal3f(0.0, 0.0, 1.0);
glTexCoord2f(1.0f, 0.0f);
glVertex3f(x + width, y, z);

glTexCoord2f(1.0f, 1.0f);
glVertex3f(x + width, y + height, z);

glTexCoord2f(0.0f, 1.0f);
glVertex3f(x, y + height, z);

glTexCoord2f(0.0f, 0.0f);
glVertex3f(x, y, z);
glEnd();
//front face
glBegin(GL_QUADS);
glNormal3f(0.0, 0.0, -1.0);
glTexCoord2f(1.0f, 0.0f);
glVertex3f(x, y, z + len);

glTexCoord2f(1.0f, 1.0f);
glVertex3f(x, y + height, z + len);

glTexCoord2f(0.0f, 1.0f);
glVertex3f(x + width, y + height, z + len);

glTexCoord2f(0.0f, 0.0f);
glVertex3f(x + width, y, z + len);
glEnd();
//bottom face
glBegin(GL_QUADS);
glNormal3f(0.0, 1.0, 0.0);
glTexCoord2f(1.0f, 0.0f);
glVertex3f(x, y, z);

glTexCoord2f(1.0f, 1.0f);
glVertex3f(x, y, z + len);

glTexCoord2f(0.0f, 1.0f);
glVertex3f(x + width, y, z + len);

glTexCoord2f(0.0f, 0.0f);
glVertex3f(x + width, y, z);
glEnd();
//top face
glBegin(GL_QUADS);
glNormal3f(0.0, -1.0, 0.0);
glTexCoord2f(1.0f, 0.0f);
glVertex3f(x + width, y + height, z);


glTexCoord2f(1.0f, 1.0f);
glVertex3f(x + width, y + height, z + len);


glTexCoord2f(0.0f, 1.0f);
glVertex3f(x, y + height, z + len);


glTexCoord2f(0.0f, 0.0f);
glVertex3f(x, y + height, z);
glEnd();
//left face
glBegin(GL_QUADS);
glNormal3f(1.0, 0.0, 0.0);
glTexCoord2f(1.0f, 0.0f);
glVertex3f(x, y + height, z);

glTexCoord2f(1.0f, 1.0f);
glVertex3f(x, y + height, z + len);

glTexCoord2f(0.0f, 1.0f);
glVertex3f(x, y, z + len);

glTexCoord2f(0.0f, 0.0f);
glVertex3f(x, y, z);
glEnd();

//right face
glBegin(GL_QUADS);
glNormal3f(0.0, 0.0, -1.0);
glTexCoord2f(1.0f, 0.0f);
glVertex3f(x + width, y, z);

glTexCoord2f(1.0f, 1.0f);
glVertex3f(x + width, y, z + len);

glTexCoord2f(0.0f, 1.0f);
glVertex3f(x + width, y + height, z + len);

glTexCoord2f(0.0f, 0.0f);
glVertex3f(x + width, y + height, z);
glEnd();

glBindTexture(GL_TEXTURE_2D, 0);
glDepthMask(GL_TRUE);
glPopAttrib();

}

To Be Continue

搞定上述这些模块,我们就已经可以画出像模像样的场景了,完整代码在这里,但是毕竟代码写得非常简陋(甚至丑陋),还存在着一些比较难以忽视的问题,比如如果使用的贴图分辨率过高会使运行卡顿等等,还有多其他几个行星还没画,这些留着下次解决。
话说图形学的代码也真是冗长啊,实现这一点点小场景就已经四百行代码了,下次把代码结构也一并优化一下。
solar system