0%

OpenGL绘制太阳系-其二

上次已经基本把太阳系模型的雏形搞出来了,但是还是太简陋。拖沓了一个多星期以后,总算是把一些必要的东西给补全了。相较于之前的版本,此次把太阳系八大行星都画上了,而且为行星们创建了一个行星类方便使用。其次,解决了之前加载贴图资源导致运行卡顿的问题。然后为场景添加了两种不同的相机模式。以下细述。

Planet类

我们把所有行星的通用属性都定义到一个planet类中写在头文件里就可以随处调用并实例化了:

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
#pragma once
#ifndef PLANET_H
#define PLANET_H

class Planet
{
public:
float srot;
float arot;
float radius;
GLuint texture_name;
float distance;

float counter_a = 0.0f;
float counter_s = 0.0f;
public:
Planet(float sr, float ar, float r, GLuint tex, float dis)
{
this->srot = sr;
this->arot = ar;
this->radius = r;
this->texture_name = tex;
this->distance = dis;
}

~Planet()
{

}
};

#endif

对于planet构造函数的实现,其实和我们之前画地球的步骤差不多,这里直接上代码。

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
void draw_planet(Planet &p,GLuint tex)
{
//std::cout << all_texture[1] << std::endl;


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

glRotatef(p.counter_a, 0.0f, 1.0f, 0.0f);
glTranslatef(p.distance, 0.0f, 0.0f);

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

glRotatef(p.counter_s, 0.0f, 0.0f, 1.0f);

gluQuadricTexture(e_tex, GLU_TRUE);
glPushAttrib(GL_ENABLE_BIT | GL_TEXTURE_BIT);
glEnable(GL_TEXTURE_2D);
//glTexEnvf(GL_TEXTURE_2D, GL_TEXTURE_EVN_MODE, GL_REPLACE);
glBindTexture(GL_TEXTURE_2D, tex);
//std::cout << all_texture[4] << std::endl;

//glColor3ub(0, 0, 255);

gluSphere(e_tex, p.radius, 15.0f, 15.0f);
glBindTexture(GL_TEXTURE_2D, 0);
//glDisable(GL_TEXTURE_2D);

glPopAttrib();
gluQuadricTexture(e_tex, GLU_FALSE);


p.counter_a += 360.0/(p.arot*24.0);
if (p.counter_a >= 360.0f)
p.counter_a = int(p.counter_a)%360;
p.counter_s += (360.0/p.srot) * 0.2;
//std::cout << p.srot << std::endl;
if (p.counter_s >= 360.0f)
p.counter_s = int(p.counter_s) % 360;

glPopMatrix();
}

接着我们只要在主函数中定义好八大行星的各个参数并绘制他们就可以了(PS: 这里的参数是以地球的参数作为基本单位的,并且为了方便观察做出了一些比例上的调整)

1
2
3
4
5
6
7
Planet mars =  Planet(25.19f, 686.0f, 15 * 0.532, all_texture[4], 300.0*1.52);
Planet mercury = Planet(58.64f, 87.0f, 15 * 0.382, all_texture[5], 300.0*0.38);
Planet venus = Planet(-243.0f, 224.7f, 15 * 0.95, all_texture[6], 300.0*0.72);
Planet jupiter = Planet(12.13f, 433.6f, 15 * 11, all_texture[7], 300.0*5.2);
Planet saturn = Planet(26.73f, 1075.2f, 15 * 9.14, all_texture[8], 300.0*9.5);
Planet uranus = Planet(97.73f, 3068.0f, 15 * 4.0, all_texture[9], 300.0*19.2);
Planet neptune = Planet(28.32f, 6018.0f, 15 * 3.8, all_texture[10], 300.0* 25);

BTW,我们知道土星最出名的就是漂亮的土星环,那么就可以使用gluDisk()这个内置函数来实现,只要在画球体的地方顺便画一个环就可以了。

优化内存

上次留下来的一个很大的问题就是每次我们加载纹理贴图的时候都是动态从图片中加载数据的,这样就导致当我们使用的贴图数量比较多或者图片比较大的时候,运行效率非常低下,画面一顿一顿的,不能忍啊。解决方法就是在绘制之前先把纹理数据存起来,然后释放图片资源。因为在OpenGL里每一份贴图数据都是用一个GLuint类型的数来标识的,而这些数存在一个数组里。对于贴图数据,我们自定义一个texture_data数据结构,只需要保存图片的宽和高,以及其中的字节数据即可,然后依次为每一份数据绑定到唯一的id上,每次根据不同的贴图id找到对应的数据就行了。具体实现如下:

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
typedef struct texture_data {
unsigned char* data;
int width, height;
} texture_data;

texture_data load_texture2(const char *path)
{
texture_data t;
//unsigned char* image;
int width, height;
t.data = SOIL_load_image(path, &width, &height, 0, SOIL_LOAD_RGB);
t.width = width;
t.height = height;
//SOIL_free_image_data(image);
return t;
}

GLUquadricObj* e_tex = gluNewQuadric();//texture
const int img_num = 11;
GLuint all_texture[img_num];
texture_data TextureImage[img_num];// 创建纹理的存储空间
//memset(TextureImage,0,sizeof(void *) * 4);
void add_textures() // 载入位图(调用上面的代码)并转换成纹理
{

//int Status = FALSE; // 状态指示器

memset(TextureImage, 0, sizeof(void *) * img_num); // 将指针设为 NULL
TextureImage[0] = load_texture2("sun.jpg");
TextureImage[1] = load_texture2("earth.jpg");
TextureImage[2] = load_texture2("moon.jpg");
TextureImage[3] = load_texture2("sky.png");
TextureImage[4] = load_texture2("mars.jpg");
TextureImage[5] = load_texture2("mercury.jpg");
TextureImage[6] = load_texture2("venus.jpg");
TextureImage[7] = load_texture2("jupiter.jpg");
TextureImage[8] = load_texture2("saturn.jpg");
TextureImage[9] = load_texture2("uranus.jpg");
TextureImage[10] = load_texture2("neptune.jpg");


glGenTextures(img_num, &all_texture[0]); // 创建纹理
for (int i = 0; i < img_num; i++)
{
glBindTexture(GL_TEXTURE_2D, all_texture[i]);// 生成纹理
glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage[i].width, TextureImage[i].height, 0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[i].data);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); // 线形滤波
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // 线形滤波
}

for (int i = 0; i < img_num; i++)
{
SOIL_free_image_data(TextureImage[i].data);
}


}

相机漫游

上一次我们已经实现了一种相机,即视觉中心固定在太阳,使用鼠标拖动可以改变观察角度。但是在三维场景中更为普遍的是漫游相机,也就是所谓的第一人称视角,我们可以通过鼠标拖动观察四周,同时键盘控制移动。这一部分较多的涉及了数学中空间向量的概念。我们知道OpenGL大多数的关于相机的操作都是归于gluLookAt()这个函数来实现的。这个函数接受9个参数,也即三个三维向量eye、at、和up,分别代表相机在空间中的位置,相机注视的点,以及相机朝上的方向。我们要做的就是计算得到这些向量的值来更改相机的位置和角度。对于相机位置的移动,我们使用键盘进行控制,只需对eye向量的x、z分量做修改即可,不做垂直方向的变化,于是在camera类中我们实现两个方法moveCamera()和yawCamera():

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
void Camera::moveCamera(float speed)
{
/** 计算方向向量 */
vec3 vector = at - eye;
vector = normalize(vector); /**< 单位化 */

/** 更新摄像机 */
eye.x += vector.x * speed; /**< 根据速度更新位置 */
eye.z += vector.z * speed;
eye.y += vector.y * speed;
at.x += vector.x * speed; /**< 根据速度更新方向 */
at.z += vector.z * speed;
at.y += vector.y * speed;


}
void Camera::yawCamera(float speed)
{
vec3 yaw;
vec3 crossProduct = at - eye;
crossProduct = cross(crossProduct, up);

// Normalize the strafe vector
yaw = normalize(crossProduct);

eye.x += yaw.x * speed;
eye.z += yaw.z * speed;

// Add the strafe vector to our view
at.x += yaw.x * speed;
at.z += yaw.z * speed;

}

void keyboard(unsigned char key, int x, int y)
{


rad = float(PI*s_angle / 180.0f); //计算SIN和COS函数中需要的参数。
// 前进,后退请求
switch (key) {
case 'w':
cam.moveCamera(10);
break;
case 's':
cam.moveCamera(-10);
break;
case 'a':
cam.yawCamera(-10);
break;
case 'd':
cam.yawCamera(10);
break;
case 'm':
camera_mode = !camera_mode;
//std::cout << camera_mode << std::endl;
break;
case 033:
// Esc按键
exit(EXIT_SUCCESS);
break;
}
glutPostWindowRedisplay(mainWindow);
}

接下来使用鼠标修改相机看向的方向,首先是获得鼠标在窗口中的点击位置,然后固定这一位置计算每一时刻鼠标与这一初始位置的偏移量,然后根据偏移的角度和方向去更新at向量,由于这里我段位还不够高,于是借鉴了网上一些其他文章的做法
https://blog.csdn.net/u010223072/article/details/53379231 来实现这个功能,虽然看不太懂但是意外地巧妙且可行,做了一些修改之后代码如下:

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
void Camera::rotateView(float angle, float x, float y, float z)
{
vec3 newView;

/** 计算方向向量 */
vec3 view = at - eye;

//std::cout << "eye:" << eye << std::endl;
/** 计算 sin 和cos值 */
float cosTheta = (float)cos(angle);
float sinTheta = (float)sin(angle);

/** 计算旋转向量的x值 */
newView.x = (cosTheta + (1 - cosTheta) * x * x) * view.x;
newView.x += ((1 - cosTheta) * x * y - z * sinTheta) * view.y;
newView.x += ((1 - cosTheta) * x * z + y * sinTheta) * view.z;

/** 计算旋转向量的y值 */
newView.y = ((1 - cosTheta) * x * y + z * sinTheta) * view.x;
newView.y += (cosTheta + (1 - cosTheta) * y * y) * view.y;
newView.y += ((1 - cosTheta) * y * z - x * sinTheta) * view.z;

/** 计算旋转向量的z值 */
newView.z = ((1 - cosTheta) * x * z - y * sinTheta) * view.x;
newView.z += ((1 - cosTheta) * y * z + x * sinTheta) * view.y;
newView.z += (cosTheta + (1 - cosTheta) * z * z) * view.z;

/** 更新摄像机的方向 */
at = eye + newView;


}

void Camera::setViewByMouse(int x, int y)
{
POINT mousePos; /**< 保存当前鼠标位置 */

//int middleX = GetSystemMetrics(SM_CXSCREEN) >> 1; /**< 得到屏幕宽度的一半 */
//int middleY = GetSystemMetrics(SM_CYSCREEN) >> 1; /**< 得到屏幕高度的一半 */
float angleY = 0.0f; /**< 摄像机左右旋转角度 */
float angleZ = 0.0f; /**< 摄像机上下旋转角度 */
static float currentRotX = 0.0f;

int middleX = x;
int middleY = y;

/** 得到当前鼠标位置 */
GetCursorPos(&mousePos);
ShowCursor(TRUE);

/** 如果鼠标没有移动,则不用更新 */
if ((mousePos.x == middleX) && (mousePos.y == middleY))
return;
//std::cout << "x:" << x << " y"<<y<<std::endl;
/** 设置鼠标位置在屏幕中心 */
SetCursorPos(x, y);

/**< 得到鼠标移动方向 */
angleY = (float)((middleX - mousePos.x)) / 1000.0f;
angleZ = (float)((middleY - mousePos.y)) / 1000.0f;

static float lastRotX = 0.0f; /**< 用于保存旋转角度 */
lastRotX = currentRotX;

/** 跟踪摄像机上下旋转角度 */
currentRotX += angleZ;

/** 如果上下旋转弧度大于1.0,我们截取到1.0并旋转 */
if (currentRotX > 1.0f)
{
currentRotX = 1.0f;

/** 根据保存的角度旋转方向 */
if (lastRotX != 1.0f)
{
/** 找到与旋转方向垂直向量 */
vec3 vAxis = at - eye;
vec3 vAxis2 = cross(vAxis, up);
vec3 vAxis3 = normalize(vAxis2);

///旋转
rotateView(angleZ, vAxis3.x, vAxis3.y, vAxis3.z);
}
}
/** 如果旋转弧度小于-1.0,则也截取到-1.0并旋转 */
else if (currentRotX < -1.0f)
{
currentRotX = -1.0f;

if (lastRotX != -1.0f)
{

/** 找到与旋转方向垂直向量 */
vec3 vAxis = at - eye;
vec3 vAxis2 = cross(vAxis, up);
vec3 vAxis3 = normalize(vAxis2);

///旋转
rotateView(angleZ, vAxis3.x, vAxis3.y, vAxis3.z);
}
}
/** 否则就旋转angleZ度 */
else
{
/** 找到与旋转方向垂直向量 */
vec3 vAxis = at - eye;
vec3 vAxis2 = cross(vAxis, up);
vec3 vAxis3 = normalize(vAxis2);

///旋转
rotateView(angleZ, vAxis3.x, vAxis3.y, vAxis3.z);
}

/** 总是左右旋转摄像机 */
rotateView(angleY, 0, 1, 0);

}

最后把所有变化后的值给到gluLookAt():

1
2
3
4
5
void Camera::view2()
{
glLoadIdentity();
gluLookAt(eye.x, eye.y, eye.z, at.x, at.y, at.z, up.x, up.y, up.z);
}

这样就可以像游戏中一样在场景中漫游了。

下一次打算实现的内容是物体的选取和UI制作,可能吧:P
solar system