C++ 实现太阳系行星系统(OpenGL)
项目说明
这是一个基于 OpenGL 的太阳系模拟程序设计项目。目标是通过编写 C++ 程序实现一个简单的太阳系动态展示效果。项目包含以下主要内容:
基本框架:
- 初始化 OpenGL 窗口
- 设置窗口大小和分辨率
- 设置投影矩阵和观察矩阵
日心历法:- 绘制地球轨道
- 绘制行星轨迹
行星系统:- 绘制中心天体(地球)
- 绘制卫星(月球)
- 绘制其他行星及其卫星
光照效果:- 添加环境光
- 实现点光源效果
- 使用 Phong 光照模型
控制界面:- 使用键盘控制视角变换
- 实现缩放功能
关键技术点解析
OpenGL 基础知识
利用 OpenGL 进行三维图形渲染
设置投影矩阵(gluPerspective)
设置观察矩阵(gluLookAt)
调用 glDrawArrays 来绘制三维对象
使用 glMatrix 和 glLight 来管理矩阵变换和光照参数
星球绘制逻辑
每个星球有其自定义的颜色、半径和其他属性
使用 gluSphere 绘制行星表面
使用 glRotate 和 glTranslate 方法进行旋转和平移变换
日心历法实现
绘制地球轨道(椭圆)
计算并绘制各行星轨迹
调整轨道长度以匹配实际比例关系
光照效果实现
添加环境光以避免 DirectDraw 渲示问题
使用 glLight 模型设置点光源参数
应用 Phong 光照模型设置材质属性(反射度、亮度等)
控制界面设计
使用标准键盘事件处理 (W, S, A, D, X, R) 控制视角变换
实现缩放功能以调整整体比例建议与注意事项
图形渲染问题
- 如果图形渲染不正常,请检查 gluDrawArrays 是否被正确调用。
- 确保 window 宽度和高度设置正确。
光照效果问题- 如果光照效果不明显,请检查材质属性设置是否正确。
- 确保反射度参数合理设置。
键盘控制问题
基本框架设计
一、介绍
本次实验将使用 OpenGL GLUT 编写一个简单的太阳系运行系统。
实验涉及的知识点
- C++基础语法
- 核心Makefile
- OOP开发理念
- 掌握其功能架构
实验效果图
运行效果如图所示:

二、基础知识
认识 OpenGL 和 GLUT
OpenGL 融入了大量渲染相关功能[
OpenGL Utility Toolkit(GLUT)作为一个处理OpenGL程序的库,主要负责与底层操作系统进行交互以及执行输入输出操作。该库通过屏蔽底层GUI实现细节,使得开发者只需调用其API即可轻松实现跨平台的应用程序开发,涵盖从窗口创建到事件处理等多方面的功能
我们先在实验楼环境中安装 GLUT:
sudo apt-get update && sudo apt-get install freeglut3 freeglut3-dev
一个标准的 GLUT 程序结构如下代码所示:
// 使用 GLUT 的基本头文件
#include <GL/glut.h>
// 创建图形窗口的基本宏
#define WINDOW_X_POS 50
#define WINDOW_Y_POS 50
#define WIDTH 700
#define HEIGHT 700
// 用于注册 GLUT 的回调
void onDisplay(void);
void onUpdate(void);
void onKeyboard(unsigned char key, int x, int y);
int main(int argc, char* argv[]) {
// 对 GLUT 进行初始化,并处理所有的命令行参数
glutInit(&argc, argv);
// 这个函数指定了使用 RGBA 模式韩式颜色索引模式。另外还可以
// 指定是使用单缓冲还是双缓冲窗口。这里我们使用 RGBA 和 双缓冲窗口
glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE);
// 设置窗口被创建时左上角位于屏幕上的位置
glutInitWindowPosition(WINDOW_X_POS, WINDOW_Y_POS);
// 设置窗口被创建时的宽高, 为了简便起见
glutInitWindowSize(WIDTH, HEIGHT);
// 创建一个窗口,输入的字符串为窗口的标题
glutCreateWindow("SolarSystem at Shiyanlou");
// glutDisplayFunc 的函数原型为 glutDisplayFunc(void (*func)(void))
// 这是一个回调函数,每当 GLUT 确定一个窗口的内容需要更新显示的时候,
// glutDisplayFunc 注册的回调函数就会被执行.
//
// glutIdleFunc(void (*func)(void)) 将指定一个函数,用于处理当事件循环
// 处于空闲的时候,就执行这个函数。这个回调函数接受一个函数指针作为它的唯一参数
//
// glutKeyboardFunc(void (*func)(unsigned char key, int x, int y))
// 会将键盘上的键与一个函数关联,当这个键被按下或者释放时,函数就会调用
//
// 因此下面的三行实际上是在向 GLUT 注册关键的三个回调函数
glutDisplayFunc(onDisplay);
glutIdleFunc(onUpdate);
glutKeyboardFunc(onKeyboard);
glutMainLoop();
return 0;
}
在 /home/shiyanlou/ 目录下新建 main.cpp 文件,并向其中写入如下代码:
//
// main.cpp
// solarsystem
//
#include <GL/glut.h>
#include "solarsystem.hpp"
#define WINDOW_X_POS 50
#define WINDOW_Y_POS 50
#define WIDTH 700
#define HEIGHT 700
SolarSystem solarsystem;
void onDisplay(void) {
solarsystem.onDisplay();
}
void onUpdate(void) {
solarsystem.onUpdate();
}
void onKeyboard(unsigned char key, int x, int y) {
solarsystem.onKeyboard(key, x, y);
}
int main(int argc, char* argv[]) {
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE);
glutInitWindowPosition(WINDOW_X_POS, WINDOW_Y_POS);
glutCreateWindow("SolarSystem at Shiyanlou");
glutDisplayFunc(onDisplay);
glutIdleFunc(onUpdate);
glutKeyboardFunc(onKeyboard);
glutMainLoop();
return 0;
}
提示
单缓冲是指所有绘图指令将在窗口中执行。这种方式导致绘图速度较慢。当使用单缓冲时,电脑处理性能显得不足。
如果继续采用这种技术路径,在当前硬件条件下无法实现更好的效果。
通过分析发现,在这种情况下图形渲染效果会受到显著影响。
在这种模式下系统资源利用率较低。
双缓存机制通过两个独立的缓存区交替执行绘图操作,在这一特性下实现快速绘制效果。当绘制指令完成时,在另一个缓存区交换完成后即可立即呈现图形。这种机制不仅能够有效规避部分绘制不完整的情况,并显著提升了整体效率。
图形渲染中的双缓存技术主要包含两种类型:显示区和内存存储区。显示区即我们常说的显示屏幕界面,在这一区域的所有操作均会直接影响到用户的视觉体验;而内存存储区则用于临时存储图像数据以减少显存占用。在执行绘图操作时,在后台完成所有的图像绘制工作;完成后才会将生成的结果复制并展示到显示区上。
图形渲染中的双缓存技术主要包含两种类型:显示区和内存存储区。显示区即我们常说的显示屏幕界面,在这一区域的所有操作均会直接影响到用户的视觉体验;而内存存储区则用于临时存储图像数据以减少显存占用。在执行绘图操作时,在后台完成所有的图像绘制工作;完成后才会将生成的结果复制并展示到显示区上。
通过使绘制操作与显卡实现实时交互,在处理复杂的绘制任务时会不可避免地导致 IO 操作更加复杂;而双缓冲机制则仅在交换缓冲区时将渲染完成后生成的图像直接发送给显卡进行显示。
在 OpenGL 中,推荐使用 GLfloat 来表示浮点数。
三、类设计
在OOP编程中,在设计类的时候首先要明确需要处理的对象是什么?在设计类的时候,在分析问题时我们需要明确所涉及的对象是什么?在分析不同对象之间的关系时我们需要明确他们之间的联系是什么?这些对象通常具备哪些属性?这些属性如何影响系统的整体行为?这些问题都是我们在构建系统模型时需要重点考虑的因素。
在这个过程中,在分析不同对象之间的关系时我们需要关注的是他们的属性差异性。在这个过程中,在区分恒星与行星的方法在于它们是否具有中心天体围绕运行的关系?
其次,在分析不同对象之间的关系时我们需要关注的是他们的属性差异性。在这个过程中,在区分恒星与行星的方法在于它们是否具有中心天体围绕运行的关系?
然后,在分析不同对象之间的关系时我们需要关注的是他们的属性差异性。在这个过程中,在区分恒星与行星的方法在于它们是否具有中心天体围绕运行的关系?
然后,在分析不同对象之间的关系时我们需要关注的是他们的属性差异性。在这个过程中,在区分恒星与行星的方法在于它们是否具有中心天体围绕运行的关系?
因此,在这一体系下,我们将所有被研究的对象划分为三类:普通自转且绕某中心天体公转的(Star)、具有特殊材质的(Planet),以及能够发光的(LightPlanet)。
此外,在追求编程实现的可行性与效率方面考虑时,在处理现实世界的实际编程模型时, 我们需要建立合理的前提条件
- 星球遵循圆形轨道运行;
- 天体的自转速率保持恒定;
- 每次刷新时假定一天已经过去。
首先,我们可以考虑按下面的思路整个实现逻辑:
- 初始化星体对象;
- 初始化 OpenGL 引擎, 并实现 onDraw 和 onUpdate 功能;
- 星球应自主管理其属性参数、星体间的环绕关系以及绘制变换;
- 为星球类提供一个绘图 draw() 方法;
- 在 onDraw 中调用星球的 draw() 方法;
- 在 onUpdate 中调用星球的 update() 方法;
- 在 onKeyboard 处理整个太阳系的显示设置;
进一步,对于每个星球而言,都具有如下的属性:
- 颜色:色调属性
- 公转半径:轨道长度
- 自转速度:自转频率
- 公转速度:轨道线速度
- 距离太阳中心的距离:中心距离参数
- 绕行的星球:环绕天体
- 当前的自转的角度:当前自转方位角
- 当前的公转角度:当前轨道位角
建议在 /home/shiyanlou/ 目录中创建 stars.hpp 文件,并将其命名为 stars.hpp ,以便参考先前的内容为基础设计相应的类代码框架。
class Star {
public:
// 星球的运行半径
GLfloat radius;
// 星球的公转、自传速度
GLfloat speed, selfSpeed;
// 星球的中心与父节点星球中心的距离
GLfloat distance;
// 星球的颜色
GLfloat rgbaColor[4];
// 父节点星球
Star* parentStar;
// 构造函数,构造一颗星球时必须提供
// 旋转半径、旋转速度、自转速度、绕行(父节点)星球
Star(GLfloat radius, GLfloat distance,
GLfloat speed, GLfloat selfSpeed,
Star* parentStar);
// 对一般的星球的移动、旋转等活动进行绘制
void drawStar();
// 提供默认实现,负责调用 drawStar()
virtual void draw() { drawStar(); }
// 参数为每次刷新画面时的时间跨度
virtual void update(long timeSpan);
protected:
GLfloat alphaSelf, alpha;
};
class Planet : public Star {
public:
// 构造函数
Planet(GLfloat radius, GLfloat distance,
GLfloat speed, GLfloat selfSpeed,
Star* parentStar, GLfloat rgbColor[3]);
// 增加对具备自身材质的行星绘制材质
void drawPlanet();
// 继续向其子类开放重写功能
virtual void draw() { drawPlanet(); drawStar(); }
};
class LightPlanet : public Planet {
public:
LightPlanet(GLfloat Radius, GLfloat Distance,
GLfloat Speed, GLfloat SelfSpeed,
Star* ParentStar, GLfloat rgbColor[]);
// 增加对提供光源的恒星绘制光照
void drawLight();
virtual void draw() { drawLight(); drawPlanet(); drawStar(); }
};
`
此外还需要考虑设计一个SunSystem类在该系统中各行星显然是由其各自所处的位置所决定的;另外对于SunSystem而言其内部各行星运行后的图形更新应当由SunSystem本身负责处理因此SunSystem成员变量应当包含所有行星的信息成员函数则应负责处理图形更新以及键盘事件等操作因此建议将代码构建集中在src目录下的/solar_system.hpp文件中并在此基础上定义SunSystem类
class SolarSystem {
public:
SolarSystem();
~SolarSystem();
void onDisplay();
void onUpdate();
void onKeyboard(unsigned char key, int x, int y);
private:
Star *stars[STARS_NUM];
// 定义观察视角的参数
GLdouble viewX, viewY, viewZ;
GLdouble centerX, centerY, centerZ;
GLdouble upX, upY, upZ;
};
提示
采用传统的数组结构而非C++中的vector类型来管理所有星球的原因在于这种选择能够满足需求
在 OpenGL 中被定义为观察视角这一概念相对较为复杂,在实际阐述时需要较多篇幅进行说明。在此记录下观察视角至少需要九个参数的具体内容,并将在后续章节中详细解析其作用机制。
最后我们还需要考虑一下基本的参数和变量设置。
在 SolarSystem 系统中包含太阳在内的共有九颗行星(不计冥王星),然而,在我们的星体类(Star)中每个星体对象均具备星体属性的基础上,则可以进一步实现其卫星功能。例如围绕地球运行的月球等天体即可作为此类功能的具体体现,在这种前提下我们总计将实现十个不同的天体类型。进而可以构建如下的星体索引枚举表以供后续引用与管理使用
#define STARS_NUM 10
enum STARS {
Sun, // 太阳
Mercury, // 水星
Venus, // 金星
Earth, // 地球
Moon, // 月球
Mars, // 火星
Jupiter, // 木星
Saturn, // 土星
Uranus, // 天王星
Neptune // 海王星
};
Star * stars[STARS_NUM];
我们还假设了自转速度相同,使用一个宏来设置其速度,:
#define TIMEPAST 1
#define SELFROTATE 3
至此实现未实现的成员函数导入对应的.cpp文件中,从而完成了本节实验。
四、总结本节中的代码
我们来总结一下本节实验中需要完成的代码:
在程序中我们首先在 main.cpp 中实现了 SolarSystem 的开发,并负责显示更新、空闲刷新以及键盘事件的处理部分移交给 glut 进行管理。
//
// main.cpp
// solarsystem
//
#include <GL/glut.h>
#include "solarsystem.hpp"
#define WINDOW_X_POS 50
#define WINDOW_Y_POS 50
#define WIDTH 700
#define HEIGHT 700
SolarSystem solarsystem;
void onDisplay(void) {
solarsystem.onDisplay();
}
void onUpdate(void) {
solarsystem.onUpdate();
}
void onKeyboard(unsigned char key, int x, int y) {
solarsystem.onKeyboard(key, x, y);
}
int main(int argc, char* argv[]) {
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE);
glutInitWindowPosition(WINDOW_X_POS, WINDOW_Y_POS);
glutCreateWindow("SolarSystem at Shiyanlou");
glutDisplayFunc(onDisplay);
glutIdleFunc(onUpdate);
glutKeyboardFunc(onKeyboard);
glutMainLoop();
return 0;
}
其次,我们在 stars.hpp 中分别创建了 Star/Planet/LightPlanet 类。
//
// stars.hpp
// solarsystem
//
//
#ifndef stars_hpp
#define stars_hpp
#include <GL/glut.h>
class Star {
public:
GLfloat radius;
GLfloat speed, selfSpeed;
GLfloat distance;
GLfloat rgbaColor[4];
Star* parentStar;
Star(GLfloat radius, GLfloat distance,
GLfloat speed, GLfloat selfSpeed,
Star* parentStar);
void drawStar();
virtual void draw() { drawStar(); }
virtual void update(long timeSpan);
protected:
GLfloat alphaSelf, alpha;
};
class Planet : public Star {
public:
Planet(GLfloat radius, GLfloat distance,
GLfloat speed, GLfloat selfSpeed,
Star* parentStar, GLfloat rgbColor[3]);
void drawPlanet();
virtual void draw() { drawPlanet(); drawStar(); }
};
class LightPlanet : public Planet {
public:
LightPlanet(GLfloat Radius, GLfloat Distance,
GLfloat Speed, GLfloat SelfSpeed,
Star* parentStar, GLfloat rgbColor[]);
void drawLight();
virtual void draw() { drawLight(); drawPlanet(); drawStar(); }
};
#endif /* star_hpp */
为了在 /home/shiyanlou/ 目录下创建 stars.cpp 文件,并在其内实现 stars.hpp 类中的成员函数。
//
// stars.cpp
// solarsystem
//
#include "stars.hpp"
#define PI 3.1415926535
Star::Star(GLfloat radius, GLfloat distance,
GLfloat speed, GLfloat selfSpeed,
Star* parentStar) {
// TODO:
}
void Star::drawStar() {
// TODO:
}
void Star::update(long timeSpan) {
// TODO:
}
Planet::Planet(GLfloat radius, GLfloat distance,
GLfloat speed, GLfloat selfSpeed,
Star* parentStar, GLfloat rgbColor[3]) :
Star(radius, distance, speed, selfSpeed, parentStar) {
// TODO:
}
void Planet::drawPlanet() {
// TODO:
}
LightPlanet::LightPlanet(GLfloat radius, GLfloat distance, GLfloat speed,
GLfloat selfSpeed, Star* parentStar, GLfloat rgbColor[3]) :
Planet(radius, distance, speed, selfSpeed, parentStar, rgbColor) {
// TODO:
}
void LightPlanet::drawLight() {
// TODO:
}
在 solarsystem.hpp 中设计了 SolarSystem 类:
//
// solarsystem.hpp
// solarsystem
//
#include <GL/glut.h>
#include "stars.hpp"
#define STARS_NUM 10
class SolarSystem {
public:
SolarSystem();
~SolarSystem();
void onDisplay();
void onUpdate();
void onKeyboard(unsigned char key, int x, int y);
private:
Star *stars[STARS_NUM];
// 定义观察视角的参数
GLdouble viewX, viewY, viewZ;
GLdouble centerX, centerY, centerZ;
GLdouble upX, upY, upZ;
};
请在 /home/shiyanlou/ 目录下创建 solarsystem.cpp 文件,并详细编写对应的 solarsystem.hpp 中的成员函数:
//
//
// solarsystem
//
#include "solarsystem.hpp"
#define TIMEPAST 1
#define SELFROTATE 3
enum STARS {Sun, Mercury, Venus, Earth, Moon,
Mars, Jupiter, Saturn, Uranus, Neptune};
void SolarSystem::onDisplay() {
// TODO:
}
void SolarSystem::onUpdate() {
// TODO:
}
void SolarSystem::onKeyboard(unsigned char key, int x, int y) {
// TODO:
}
SolarSystem::SolarSystem() {
// TODO:
}
SolarSystem::~SolarSystem() {
// TODO:
}
在 /home/shiyanlou/ 目录下新建Makefile 文件,并向其中添加如下代码:
CXX = g++
EXEC = solarsystem
SOURCES = main.cpp stars.cpp solarsystem.cpp
OBJECTS = main.o stars.o solarsystem.o
LDFLAGS = -lglut -lGL -lGLU
all :
$(CXX) $(SOURCES) $(LDFLAGS) -o $(EXEC)
clean:
rm -f $(EXEC) *.gdb *.o
书写编译命令时请特别注意 -lglut -lGLU -lGL 的顺序
这是因为 g++ 编译器中 -l 选项的使用具有特殊性:例如,在编译命令中使用 foo1.cpp -lz foo2.cpp 时,
若目标文件 foo2.cpp 使用了库 z 中的函数,
这些函数将不会被直接加载。
而如果在 foo1.o 中引用了库 z 中的函数,
则会引发编译错误。
用另一种方式来说就是整个连接过程是从左到右依次进行的。每当foo1.cpp中遇到一个无法解析的功能符号时, 系统会转向右边的功能库中进行查找. 一旦识别出选项z, 并在z中找到对应的函数后, 就可以顺利完成连接过程.
所以 -l 选项的库应该存在于所有编译文件的右边。
更多编译器细节请学习:g++/gdb 使用技巧
最后,在终端中运行:
make && ./solarsystem
运行结果如图所示:

观察到窗口已正式建立。然而,在目前状态下看不到内容(因为显示的是窗口背面),这是因为尚未开发该功能。在后续实验中将补充未完成的功能,并最终将使整个太阳系系统正常运转。
五、拓展阅读
- D Shreiner. OpenGL 编程指南. 人民邮电出版社, 2005.
这本书详细介绍了 OpenGL 编程相关的方方面面,被誉为 『OpenGL 红宝书』。
编码实现
一、实验介绍
本节实验我们将对上一节实验中所设计的基本框架进行详细的实现。
涉及的知识点
- OpenGL 的矩阵结构
- OpenGL 常见的图形渲染接口
- OpenGL 中的视图变换
- OpenGL 中的光照模拟
二、基础知识
这一小节主要讲解与本节实验相关的基础知识。
2.1 OpenGL 中的矩阵概念
在线性代数课程中,我们系统地学习了矩阵的基本概念.然而,在理论学习与实际应用之间存在较大的鸿沟.那么问题来了:矩阵的本质属性到底是什么呢?
首先我们看这样一个式子:
x = Ab
其中 A 为矩阵,x,b 为向量。
观点一:
我们从三维空间中选取了两个向量 x 和 b。那么矩阵 A 发生了怎样的变化?矩阵 A 将向量 b 转换为了向量 x(或经历了从 b 到 x 的变换)。从这个角度来看, 矩阵 A 代表了一种线性变换的操作方式。
再来看另一个式子:
Ax = By
其中 A,B 为矩阵, x,y为向量。
观点二:
对于两个相异的向量x与y而言,在这种情况下它们本质上是一致的因为通过线性变换矩阵A与B的作用后它们呈现出相同的结果这表明从这个角度来看矩阵A可以被视为一种选定的标准基底从而使得每个向量都是独特的尽管如此我们依然必须定义一个坐标系来描述这些向量这是因为基于不同的坐标系基底计算得出的结果会有所不同因此矩阵A```和矩阵B```分别代表了两个独立的空间基准在此基准下对应的向量虽然具有相同的外在表现但由于所采用的基础标准不同其内部的具体数值表现也会发生改变
综合上述两个观点,所以矩阵的本质是:描述运动 。
OpenGL 内部存在用于绘制变换的矩阵,即构成该系统的矩阵模式。
如前所述,在计算机图形学中(尤其是OpenGL渲染 pipeline中),矩阵不仅能够表示运动变换的信息,并且还能够表示物体所处坐标系的关键信息。因此,在处理不同操作时(尤其是变换和光照计算),我们需要根据具体需求设定相应的矩阵类型和模式参数。这需要我们先明确所需的矩阵类型以及相应的参数设置逻辑才能顺利运行程序。
glMatrixMode()
该函数支持三种不同的功能模块:包括投影变换(GL_PROJECTION)、模型-视图变换(GL_MODELVIEW)以及纹理映射(GL_TEXTURE)。
改写说明
该函数将向OpenGL发布执行投影操作的指令,并将物体顶点投影到指定平面。在启用该功能后必须在应用此功能之前调用glLoadIdentity()以使变换矩阵归一化。之后可以通过调用gluPerspective函数来设定观察体裁剪(在后续章节中我们将详细探讨这一概念)。
GL_MODELVIEW负责告知OpenGL接下来将执行一系列与模型相关的指令,例如设置摄像机视点位置,并建议在完成这些操作后,默认需将OpenGL的矩阵变换模式恢复至单位矩阵状态。
GL_TEXTURE 则是进行纹理相关的操作,我们暂时用不到。
如果对矩阵的概念不够了解,在使用OpenGL进行图形操作时可以直接将glMatrixMode视为一种声明后续操作的方式。为了在执行绘制和旋转操作之前保护好当前的变换状态必须使用glPushMatrix来保存当前的矩阵环境否则将会导致无法预期的绘图异常。
2.2 常用的 OpenGL 图像绘制 API
OpenGL 包含了一系列常见的图形绘制相关功能库,在这里我们选择几个常用的功能库进行简要介绍,并在后续代码中实际应用
glExists(GLenum cap):该函数用于启用 OpenGL 提供的各种功能。
其中参数 cap 是OpenGL内部定义的宏变量。
例如光源渲染、雾化效果以及运动模糊等常见功能;
glPushMatrix() 和 glPopMatrix() 是用于管理模型视图矩阵堆栈的关键函数;glPushMatrix() 会将当前的模型视图矩阵压入(push)到堆栈顶端;而 glPopMatrix() 则会从堆栈中弹出(pop)并丢弃顶部的那个矩阵。
glRotatef(alpha, x, y, z) : 表示当前图形沿 (x,y,z) 逆时针旋转 alpha 度;
glTranslatef(distance, x, y) : 表示当前图形沿 (x,y) 方向平移 distance 距离;
glutSolidSphere(GLdouble radius, GLint slices, GLint stacks): 使用给定的参数生成一个完整且光滑的球体图形;其中radius指定球体的半径大小,slices确定经向线段的数量,stacks确定纬向线段的数量。
在图形处理过程中,在启动绘图操作之前以及完成绘图之后调用 glBegin() 和 glEnd() 函数。其中 gl_begin 指令用于指定绘图类型的控制功能:比如 GL_POINTS 控制的是单个点的显示;GL_LINES 则负责连接相邻顶点生成线条;而 GL_TRIANGLES 则是每三个连续顶点形成一个三角形;此外 GL_POLYGON 用于生成闭合多边形由这些顶点构成等。举个例子来说,在我们需要呈现圆形的时候,则会使用具有众多顶点的大致圆形形状来近似表现其曲线特征:
// r 是半径,n 是边数
glBegin(GL_POLYGON);
for(i=0; i<n; ++i)
glVertex2f(r*cos(2*PI/n*i), r*sin(2*PI/n*i));
glEnd();
2.3 OpenGL 里的视角坐标
上一节中,我们在 SolarSystem类中定义了九个成员变量
GLdouble viewX, viewY, viewZ;
GLdouble centerX, centerY, centerZ;
GLdouble upX, upY, upZ;
为深入掌握OpenGL三维编程的核心知识体系,在学习过程中我们需要逐步建立其摄像机视角这一概念模型。
改写说明
- 在OpenGL世界坐标系中,viewX、viewY、viewZ参数等价于相机的位置坐标;
- centerX、centerY、centerZ参数等价于相机所能看到物体的坐标位置;
- upX、upY、upZ参数等价于表示朝向上方的方向向量(因为我们可以倾斜头部观察物体)。
至此,你便有了 OpenGL 中坐标系的概念。
我们约定本次实验的初始视角在 (x, -x, x) 处,则即有:
#define REST 700
#define REST_Y (-REST)
#define REST_Z (REST)
当被观察到的目标天体位于坐标系原点时,则会使得SolarSystem类中的构造函数将视角设置为...]
viewX = 0;
viewY = REST_Y;
viewZ = REST_Z;
centerX = centerY = centerZ = 0;
upX = upY = 0;
upZ = 1;
则可以通过 gluLookAt 函数来设置视角的九个参数:
gluLookAt(viewX, viewY, viewZ, centerX, centerY, centerZ, upX, upY, upZ);
接着深入探讨该函数的参数设置及其在项目中的应用
该函数将生成一个对称且具有透视效果的视景体,在调用此函数之前,请确保将 OpenGL 的矩阵设置配置为 GL_PROJECTION。
如下图所示:

在窗口中呈现的画面来源于摄像机的捕获作用。实际捕捉到的画面反映了远处平面的信息,在显示界面中则对应于近处平面的具体细节。由此可见,在此函数中需要指定四个参数:
- 第一个参数代表观察视角的大小
- 第二个参数是图像显示区域的宽高比例
- 第三参数表示物体到相机近端面的距离
- 第四参数则是相机到物体远端面的距离
2.4 OpenGL 里的光照效果
1. 基本概念
OpenGL 在处理光照时将光照系统分为了三个部分:光源、材质、光照环境。
光源即为产生光线的来源,例如太阳。
物质表面是指接受光线照射的对象,例如太阳系内所有行星及卫星均属于此类物质表面。
光照条件则是一些辅助设定,它们用于定义最终获得的光照面积,例如光线在传播过程中可能经历多次反射,此时我们建议设置一个『环境明度』参数,这有助于使生成的画面更加贴近真实效果。
物理学中指出:平行光照射到光滑平面上所形成的反射光依然保持平行状态的现象被称为『镜面反射』;而来自不光滑表面的反射则被称为『漫反射』。

2. 光源
为了在 OpenGL 中实现光照系统, 首要任务是配置光源. OpenGL仅限于支持八种预先定义好的光源类型, 并通过GL_LIGHT0至GL_LIGHT7这八个全局变量来标识这些光源. 使用glEnable和glDisable函数分别进行开启与关闭操作. 例如, 如需启用某光源, 请调用相应的函数并传递正确的参数.
设置光源位置则需要使用 glMaterialfv进行设置,例如:
GLfloat light_position[] = {0.0f, 0.0f, 0.0f, 1.0f};
glLightfv(GL_LIGHT0, GL_POSITION, light_position); // 指定零号光源的位置
这里的坐标由四个参数 (x,y,z,w) 表示,在 w 等于 0 的情况下,则表示该坐标点位于无限远位置;而当 w 不等于 0 时,则该坐标点具有位置性,在此情况下其具体坐标由 (x/w, y/w, z/w) 给出。
3. 材质
设置一个物体的材质一般有五个属性需要设置:
多次反射后测量环境中的残留光照强度;
漫反射后仅限于表面区域的光分布情况;
镜面反射后光线路径严格遵循几何规律;
在OpenGL中非发光体发出的微弱且不影响其他物体的光照强度;
镜面系数(也称为布兰德系数),其值越小表示材料表面越粗糙,在点光源照射下会产生较大的亮斑;反之,则表示表面越光滑(类似于镜面),产生的亮斑会较小。
设置材质 OpenGL 提供了两个版本的函数:
void glMaterialf(GLenum face, GLenum pname, TYPE param);
void glMaterialfv(GLenum face, GLenum pname, TYPE *param);
在材料处理方式上存在显著差异。镜面材质仅需配置单一数值即可完成处理;而其他材质的配置则涉及多个参数。调用带指针向量的版本函数更为合适。例如,在渲染真实材质时会频繁调用该函数以确保图像质量。
GLfloat mat_ambient[] = {0.0f, 0.0f, 0.5f, 1.0f};
GLfloat mat_diffuse[] = {0.0f, 0.0f, 0.5f, 1.0f};
GLfloat mat_specular[] = {0.0f, 0.0f, 1.0f, 1.0f};
GLfloat mat_emission[] = {0.5f, 0.5f, 0.5f, 0.5f};
GLfloat mat_shininess = 90.0f;
glMaterialfv(GL_FRONT, GL_AMBIENT, mat_ambient);
glMaterialfv(GL_FRONT, GL_DIFFUSE, mat_diffuse);
glMaterialfv(GL_FRONT, GL_SPECULAR, mat_specular);
glMaterialfv(GL_FRONT, GL_EMISSION, mat_emission);
glMaterialf (GL_FRONT, GL_SHININESS, mat_shininess);
4. 光照环境
OpenGl 通常不会启用光线照射效果,默认情况下是关闭的;如果需要开启光线照射功能,则可以使用 GL_LIGHTING 这个宏来激活该功能,请参考以下代码:glEnable(GL_LIGHTING);
三、实现
本节将通过实践操作,带领大家实现太阳系行星系统。
3.1 行星的绘制
绘制过程中需要注意轨道倾角与自转轴倾角这两个关键参数。该成员函数的设计应在代码开发阶段早期完成。
void Star::update(long timeSpan) {
alpha += timeSpan * speed; // 更新角度
alphaSelf += selfSpeed; // 更新自转角度
}
改写说明
void Star::drawStar() {
glEnable(GL_LINE_SMOOTH);
glEnable(GL_BLEND);
int n = 1440;
// 保存 OpenGL 当前的矩阵环境
glPushMatrix();
{
// 公转
// 如果是行星,且距离不为0,那么 且向原点平移一个半径
// 这部分用于处理卫星
if (parentStar != 0 && parentStar->distance > 0) {
//将绘制的图形沿 z 轴旋转 alpha
glRotatef(parentStar->alpha, 0, 0, 1);
// x 轴方向上平移 distance , y,z 方向不变
glTranslatef(parentStar->distance, 0.0, 0.0);
}
// 绘制运行轨道
glBegin(GL_LINES);
for(int i=0; i<n; ++i)
glVertex2f(distance * cos(2 * PI * i / n),
distance * sin(2 * PI * i / n));
glEnd();
// 绕 z 轴旋转 alpha
glRotatef(alpha, 0, 0, 1);
// x 轴方向平移 distance, y,z 方向不变
glTranslatef(distance, 0.0, 0.0);
// 自转
glRotatef(alphaSelf, 0, 0, 1);
// 绘制行星颜色
glColor3f(rgbaColor[0], rgbaColor[1], rgbaColor[2]);
glutSolidSphere(radius, 40, 32);
}
// 恢复绘制前的矩阵环境
glPopMatrix();
}
这里用到了 sin() 和 cos() 函数,需要引入
#include<cmath>
3.2 光照的绘制
在 Planet 类中,在生成该天体的光照模型时,请确保将其代码实现包含在 /home/shiyanlou/stars.cpp 文件内。
void Planet::drawPlanet() {
GLfloat mat_ambient[] = {0.0f, 0.0f, 0.5f, 1.0f};
GLfloat mat_diffuse[] = {0.0f, 0.0f, 0.5f, 1.0f};
GLfloat mat_specular[] = {0.0f, 0.0f, 1.0f, 1.0f};
GLfloat mat_emission[] = {rgbaColor[0], rgbaColor[1], rgbaColor[2], rgbaColor[3]};
GLfloat mat_shininess = 90.0f;
glMaterialfv(GL_FRONT, GL_AMBIENT, mat_ambient);
glMaterialfv(GL_FRONT, GL_DIFFUSE, mat_diffuse);
glMaterialfv(GL_FRONT, GL_SPECULAR, mat_specular);
glMaterialfv(GL_FRONT, GL_EMISSION, mat_emission);
glMaterialf (GL_FRONT, GL_SHININESS, mat_shininess);
}
而对于 LightPlanet 类型的对象而言,在视觉效果渲染过程中不仅需要考虑材质属性的设定 还需要对光源参数进行详细的规划和精确计算
void LightPlanet::drawLight() {
GLfloat light_position[] = {0.0f, 0.0f, 0.0f, 1.0f};
GLfloat light_ambient[] = {0.0f, 0.0f, 0.0f, 1.0f};
GLfloat light_diffuse[] = {1.0f, 1.0f, 1.0f, 1.0f};
GLfloat light_specular[] = {1.0f, 1.0f, 1.0f, 1.0f};
glLightfv(GL_LIGHT0, GL_POSITION, light_position); // 指定零号光源的位置
glLightfv(GL_LIGHT0, GL_AMBIENT, light_ambient); // 表示各种光线照射到该材质上,经过很多次反射后追踪遗留在环境中的光线强度
glLightfv(GL_LIGHT0, GL_DIFFUSE, light_diffuse); // 漫反射后的光照强度
glLightfv(GL_LIGHT0, GL_SPECULAR, light_specular); // 镜面反射后的光照强度
}
3.3 实现窗口的绘制
在之前的实验中我们曾提及了 `glutDisplayFunc和 gluthIdleFunc这两个核心的图像显示函数。其中,
glutDisplayFunc 会根据 glut 的反馈,在检测到窗口内容需要更新时自动调用相应的回调函数;
而 gluthIdleFunc 则负责在事件循环空闲期间调用必要的回调机制。
为了使整个太阳系运动起来, 我们应该分别考虑如何更新行星的位置和如何刷新视图, 以确保系统的高效运行
该函数应当专注于在视图刷新时段处理显示逻辑,在事件空闲时段我们可以启动相应的计算过程,在计算完成后随后触发视图刷新机制完成显示更新。
为此,在 SolarSystem 类及其成员函数模块中被调用的地方,我们需要首先实现 SolarSystem::onUpdate() 这个成员函数,并确保其正确运行。
#define TIMEPAST 1 // 假设每次更新都经过了一天
void SolarSystem::onUpdate() {
for (int i=0; i<STARS_NUM; i++)
stars[i]->update(TIMEPAST); // 更新星球的位置
this->onDisplay(); // 刷新显示
}
其次,对于显示视图的刷新则是实现SolarSystem::onDisplay():
void SolarSystem::onDisplay() {
// 清除 viewport 缓冲区
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 清空并设置颜色缓存
glClearColor(.7f, .7f, .7f, .1f);
// 指定当前矩阵为投影矩阵
glMatrixMode(GL_PROJECTION);
// 将指定的矩阵指定为单位矩阵
glLoadIdentity();
// 指定当前的观察视景体
gluPerspective(75.0f, 1.0f, 1.0f, 40000000);
// 指定当前矩阵为视景矩阵堆栈应用术后的矩阵操作
glMatrixMode(GL_MODELVIEW);
// 指定当前的矩阵为单位矩阵
glLoadIdentity();
// 定义视图矩阵,并与当前矩阵相乘
gluLookAt(viewX, viewY, viewZ, centerX, centerY, centerZ, upX, upY, upZ);
// 设置第一个光源(0号光源)
glEnable(GL_LIGHT0);
// 启用光源
glEnable(GL_LIGHTING);
// 启用深度测试,根据坐标的远近自动隐藏被遮住的图形
glEnable(GL_DEPTH_TEST);
// 绘制星球
for (int i=0; i<STARS_NUM; i++)
stars[i]->draw();
// 我们在 main 函数中初始化显示模式时使用了 GLUT_DOUBLE
// 需要使用 glutSwapBuffers 在绘制结束后实现双缓冲的缓冲区交换
glutSwapBuffers();
}
3.4 类的构造函数和析构函数
在头文件stars.hpp$中定义的某个类^{\ast}的构造函数负责初始化其成员变量^{\ast}。相对来说并不复杂^{\ast}, 其中部分类型可以借助默认析构完成初始化^{\ast}。这部分建议读者自行完成相关构造函数的设计与实现^{\ast}。
Star::Star(GLfloat radius, GLfloat distance,
GLfloat speed, GLfloat selfSpeed,
Star* parent);
Planet::Planet(GLfloat radius, GLfloat distance,
GLfloat speed, GLfloat selfSpeed,
Star* parent, GLfloat rgbColor[3]);
LightPlanet::LightPlanet(GLfloat radius, GLfloat distance,
GLfloat speed, GLfloat selfSpeed,
Star* parent, GLfloat rgbColor[3]);
提示 :注意在初始化速度变量时将其转化为角速度
其转换公式为:alpha_speed = 360/speed
在solarsystem.cpp的构造函数中,我们需要为每一个行星进行初始化,并且为了更好地处理后续操作,在初始化时应提供适当的行星之间的相互关系参数。
// 公转半径
#define SUN_RADIUS 48.74
#define MER_RADIUS 7.32
#define VEN_RADIUS 18.15
#define EAR_RADIUS 19.13
#define MOO_RADIUS 6.15
#define MAR_RADIUS 10.19
#define JUP_RADIUS 42.90
#define SAT_RADIUS 36.16
#define URA_RADIUS 25.56
#define NEP_RADIUS 24.78
// 距太阳的距离
#define MER_DIS 62.06
#define VEN_DIS 115.56
#define EAR_DIS 168.00
#define MOO_DIS 26.01
#define MAR_DIS 228.00
#define JUP_DIS 333.40
#define SAT_DIS 428.10
#define URA_DIS 848.00
#define NEP_DIS 949.10
// 运动速度
#define MER_SPEED 87.0
#define VEN_SPEED 225.0
#define EAR_SPEED 365.0
#define MOO_SPEED 30.0
#define MAR_SPEED 687.0
#define JUP_SPEED 1298.4
#define SAT_SPEED 3225.6
#define URA_SPEED 3066.4
#define NEP_SPEED 6014.8
// 自转速度
#define SELFROTATE 3
// 为了方便操作数组,定义一个设置多为数组的宏
#define SET_VALUE_3(name, value0, value1, value2) \
((name)[0])=(value0), ((name)[1])=(value1), ((name)[2])=(value2)
// 在上一节实验中我们定义了星球的枚举
enum STARS {Sun, Mercury, Venus, Earth, Moon,
Mars, Jupiter, Saturn, Uranus, Neptune};
提示
我们声明了一个`SET_VALUE_3$的宏变量,并且可能会疑惑是否能编写一个函数以实现快速设置
事实上,宏会在编译过程就完成整体的替换工作,而定义函数
该方法在运行阶段需执行相应的堆栈操作。其性能相比预先编译完成的宏指令仍有显著差距。
因此,使用宏会变得更加高效
需要注意的是,在应用过程中应尽量避免过度依赖宏以确保代码质量与易读性
尽管如此,在合理范围内适当运用宏能够显著提升效率
通过SolarSystem类的构造函数实现功能模块时,请注意其中星球的颜色由系统随机选定;该功能模块设计具有较高的灵活性,在运行过程中允许读者自行修改行星颜色:
SolarSystem::SolarSystem() {
// 定义视角,在前面我们已经讨论过视角的初始化了
viewX = 0;
viewY = REST_Y;
viewZ = REST_Z;
centerX = centerY = centerZ = 0;
upX = upY = 0;
upZ = 1;
// 太阳
GLfloat rgbColor[3] = {1, 0, 0};
stars[Sun] = new LightPlanet(SUN_RADIUS, 0, 0, SELFROTATE, 0, rgbColor);
// 水星
SET_VALUE_3(rgbColor, .2, .2, .5);
stars[Mercury] = new Planet(MER_RADIUS, MER_DIS, MER_SPEED, SELFROTATE, stars[Sun], rgbColor);
// 金星
SET_VALUE_3(rgbColor, 1, .7, 0);
stars[Venus] = new Planet(VEN_RADIUS, VEN_DIS, VEN_SPEED, SELFROTATE, stars[Sun], rgbColor);
// 地球
SET_VALUE_3(rgbColor, 0, 1, 0);
stars[Earth] = new Planet(EAR_RADIUS, EAR_DIS, EAR_SPEED, SELFROTATE, stars[Sun], rgbColor);
// 月亮
SET_VALUE_3(rgbColor, 1, 1, 0);
stars[Moon] = new Planet(MOO_RADIUS, MOO_DIS, MOO_SPEED, SELFROTATE, stars[Earth], rgbColor);
// 火星
SET_VALUE_3(rgbColor, 1, .5, .5);
stars[Mars] = new Planet(MAR_RADIUS, MAR_DIS, MAR_SPEED, SELFROTATE, stars[Sun], rgbColor);
// 木星
SET_VALUE_3(rgbColor, 1, 1, .5);
stars[Jupiter] = new Planet(JUP_RADIUS, JUP_DIS, JUP_SPEED, SELFROTATE, stars[Sun], rgbColor);
// 土星
SET_VALUE_3(rgbColor, .5, 1, .5);
stars[Saturn] = new Planet(SAT_RADIUS, SAT_DIS, SAT_SPEED, SELFROTATE, stars[Sun], rgbColor);
// 天王星
SET_VALUE_3(rgbColor, .4, .4, .4);
stars[Uranus] = new Planet(URA_RADIUS, URA_DIS, URA_SPEED, SELFROTATE, stars[Sun], rgbColor);
// 海王星
SET_VALUE_3(rgbColor, .5, .5, 1);
stars[Neptune] = new Planet(NEP_RADIUS, NEP_DIS, NEP_SPEED, SELFROTATE, stars[Sun], rgbColor);
}
此外,不要忘了在析构函数中释放申请的内存:
SolarSystem::~SolarSystem() {
for(int i = 0; i<STARS_NUM; i++)
delete stars[i];
}
3.5 键盘按键变换视角的实现
为了提高操作效率与直观性建议采用键盘上的 w,a,s,d,x 五个方向键配合使用 r 键进行视图管理此外我们还需要设定单次按键对应视图变化幅度为此我们可以定义一个名为 OFFSET 的宏变量。为了实现这一功能接下来我们将详细讲解如何根据输入参数 key 对相应的按键行为进行解析
#define OFFSET 20
void SolarSystem::onKeyboard(unsigned char key, int x, int y) {
switch (key) {
case 'w': viewY += OFFSET; break; // 摄像机Y 轴位置增加 OFFSET
case 's': viewZ += OFFSET; break;
case 'S': viewZ -= OFFSET; break;
case 'a': viewX -= OFFSET; break;
case 'd': viewX += OFFSET; break;
case 'x': viewY -= OFFSET; break;
case 'r':
viewX = 0; viewY = REST_Y; viewZ = REST_Z;
centerX = centerY = centerZ = 0;
upX = upY = 0; upZ = 1;
break;
case 27: exit(0); break;
default: break;
}
}
四、总结本节中的代码(供参考)
在本节实验中,我们重点开发了 stars.cpp 和 solarsystem.cpp 这两个源代码文件中的核心部分.
stars.cpp 的代码如下:
//
// star.cpp
// solarsystem
//
#include "stars.hpp"
#include <cmath>
#define PI 3.1415926535
Star::Star(GLfloat radius, GLfloat distance,
GLfloat speed, GLfloat selfSpeed,
Star* parent) {
this->radius = radius;
this->selfSpeed = selfSpeed;
this->alphaSelf = this->alpha = 0;
this->distance = distance;
for (int i = 0; i < 4; i++)
this->rgbaColor[i] = 1.0f;
this->parentStar = parent;
if (speed > 0)
this->speed = 360.0f / speed;
else
this->speed = 0.0f;
}
void Star::drawStar() {
glEnable(GL_LINE_SMOOTH);
glEnable(GL_BLEND);
int n = 1440;
glPushMatrix();
{
if (parentStar != 0 && parentStar->distance > 0) {
glRotatef(parentStar->alpha, 0, 0, 1);
glTranslatef(parentStar->distance, 0.0, 0.0);
}
glBegin(GL_LINES);
for(int i=0; i<n; ++i)
glVertex2f(distance * cos(2 * PI * i / n),
distance * sin(2 * PI * i / n));
glEnd();
glRotatef(alpha, 0, 0, 1);
glTranslatef(distance, 0.0, 0.0);
glRotatef(alphaSelf, 0, 0, 1);
glColor3f(rgbaColor[0], rgbaColor[1], rgbaColor[2]);
glutSolidSphere(radius, 40, 32);
}
glPopMatrix();
}
void Star::update(long timeSpan) {
alpha += timeSpan * speed;
alphaSelf += selfSpeed;
}
Planet::Planet(GLfloat radius, GLfloat distance,
GLfloat speed, GLfloat selfSpeed,
Star* parent, GLfloat rgbColor[3]) :
Star(radius, distance, speed, selfSpeed, parent) {
rgbaColor[0] = rgbColor[0];
rgbaColor[1] = rgbColor[1];
rgbaColor[2] = rgbColor[2];
rgbaColor[3] = 1.0f;
}
void Planet::drawPlanet() {
GLfloat mat_ambient[] = {0.0f, 0.0f, 0.5f, 1.0f};
GLfloat mat_diffuse[] = {0.0f, 0.0f, 0.5f, 1.0f};
GLfloat mat_specular[] = {0.0f, 0.0f, 1.0f, 1.0f};
GLfloat mat_emission[] = {rgbaColor[0], rgbaColor[1], rgbaColor[2], rgbaColor[3]};
GLfloat mat_shininess = 90.0f;
glMaterialfv(GL_FRONT, GL_AMBIENT, mat_ambient);
glMaterialfv(GL_FRONT, GL_DIFFUSE, mat_diffuse);
glMaterialfv(GL_FRONT, GL_SPECULAR, mat_specular);
glMaterialfv(GL_FRONT, GL_EMISSION, mat_emission);
glMaterialf (GL_FRONT, GL_SHININESS, mat_shininess);
}
LightPlanet::LightPlanet(GLfloat radius, GLfloat distance, GLfloat speed,
GLfloat selfSpeed, Star* parent, GLfloat rgbColor[3]) :
Planet(radius, distance, speed, selfSpeed, parent, rgbColor) {
;
}
void LightPlanet::drawLight() {
GLfloat light_position[] = {0.0f, 0.0f, 0.0f, 1.0f};
GLfloat light_ambient[] = {0.0f, 0.0f, 0.0f, 1.0f};
GLfloat light_diffuse[] = {1.0f, 1.0f, 1.0f, 1.0f};
GLfloat light_specular[] = {1.0f, 1.0f, 1.0f, 1.0f};
glLightfv(GL_LIGHT0, GL_POSITION, light_position);
glLightfv(GL_LIGHT0, GL_AMBIENT, light_ambient);
glLightfv(GL_LIGHT0, GL_DIFFUSE, light_diffuse);
glLightfv(GL_LIGHT0, GL_SPECULAR, light_specular);
}
solarsystem.cpp 的代码如下:
//
// solarsystem.cpp
// solarsystem
//
#include "solarsystem.hpp"
#define REST 700
#define REST_Z (REST)
#define REST_Y (-REST)
void SolarSystem::onDisplay() {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glClearColor(.7f, .7f, .7f, .1f);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(75.0f, 1.0f, 1.0f, 40000000);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
gluLookAt(viewX, viewY, viewZ, centerX, centerY, centerZ, upX, upY, upZ);
glEnable(GL_LIGHT0);
glEnable(GL_LIGHTING);
glEnable(GL_DEPTH_TEST);
for (int i=0; i<STARS_NUM; i++)
stars[i]->draw();
glutSwapBuffers();
}
#define TIMEPAST 1
void SolarSystem::onUpdate() {
for (int i=0; i<STARS_NUM; i++)
stars[i]->update(TIMEPAST);
this->onDisplay();
}
#define OFFSET 20
void SolarSystem::onKeyboard(unsigned char key, int x, int y) {
switch (key) {
case 'w': viewY += OFFSET; break;
case 's': viewZ += OFFSET; break;
case 'S': viewZ -= OFFSET; break;
case 'a': viewX -= OFFSET; break;
case 'd': viewX += OFFSET; break;
case 'x': viewY -= OFFSET; break;
case 'r':
viewX = 0; viewY = REST_Y; viewZ = REST_Z;
centerX = centerY = centerZ = 0;
upX = upY = 0; upZ = 1;
break;
case 27: exit(0); break;
default: break;
}
}
#define SUN_RADIUS 48.74
#define MER_RADIUS 7.32
#define VEN_RADIUS 18.15
#define EAR_RADIUS 19.13
#define MOO_RADIUS 6.15
#define MAR_RADIUS 10.19
#define JUP_RADIUS 42.90
#define SAT_RADIUS 36.16
#define URA_RADIUS 25.56
#define NEP_RADIUS 24.78
#define MER_DIS 62.06
#define VEN_DIS 115.56
#define EAR_DIS 168.00
#define MOO_DIS 26.01
#define MAR_DIS 228.00
#define JUP_DIS 333.40
#define SAT_DIS 428.10
#define URA_DIS 848.00
#define NEP_DIS 949.10
#define MER_SPEED 87.0
#define VEN_SPEED 225.0
#define EAR_SPEED 365.0
#define MOO_SPEED 30.0
#define MAR_SPEED 687.0
#define JUP_SPEED 1298.4
#define SAT_SPEED 3225.6
#define URA_SPEED 3066.4
#define NEP_SPEED 6014.8
#define SELFROTATE 3
enum STARS {Sun, Mercury, Venus, Earth, Moon,
Mars, Jupiter, Saturn, Uranus, Neptune};
#define SET_VALUE_3(name, value0, value1, value2) \
((name)[0])=(value0), ((name)[1])=(value1), ((name)[2])=(value2)
SolarSystem::SolarSystem() {
viewX = 0;
viewY = REST_Y;
viewZ = REST_Z;
centerX = centerY = centerZ = 0;
upX = upY = 0;
upZ = 1;
GLfloat rgbColor[3] = {1, 0, 0};
stars[Sun] = new LightPlanet(SUN_RADIUS, 0, 0, SELFROTATE, 0, rgbColor);
SET_VALUE_3(rgbColor, .2, .2, .5);
stars[Mercury] = new Planet(MER_RADIUS, MER_DIS, MER_SPEED, SELFROTATE, stars[Sun], rgbColor);
SET_VALUE_3(rgbColor, 1, .7, 0);
stars[Venus] = new Planet(VEN_RADIUS, VEN_DIS, VEN_SPEED, SELFROTATE, stars[Sun], rgbColor);
SET_VALUE_3(rgbColor, 0, 1, 0);
stars[Earth] = new Planet(EAR_RADIUS, EAR_DIS, EAR_SPEED, SELFROTATE, stars[Sun], rgbColor);
SET_VALUE_3(rgbColor, 1, 1, 0);
stars[Moon] = new Planet(MOO_RADIUS, MOO_DIS, MOO_SPEED, SELFROTATE, stars[Earth], rgbColor);
SET_VALUE_3(rgbColor, 1, .5, .5);
stars[Mars] = new Planet(MAR_RADIUS, MAR_DIS, MAR_SPEED, SELFROTATE, stars[Sun], rgbColor);
SET_VALUE_3(rgbColor, 1, 1, .5);
stars[Jupiter] = new Planet(JUP_RADIUS, JUP_DIS, JUP_SPEED, SELFROTATE, stars[Sun], rgbColor);
SET_VALUE_3(rgbColor, .5, 1, .5);
stars[Saturn] = new Planet(SAT_RADIUS, SAT_DIS, SAT_SPEED, SELFROTATE, stars[Sun], rgbColor);
SET_VALUE_3(rgbColor, .4, .4, .4);
stars[Uranus] = new Planet(URA_RADIUS, URA_DIS, URA_SPEED, SELFROTATE, stars[Sun], rgbColor);
SET_VALUE_3(rgbColor, .5, .5, 1);
stars[Neptune] = new Planet(NEP_RADIUS, NEP_DIS, NEP_SPEED, SELFROTATE, stars[Sun], rgbColor);
}
SolarSystem::~SolarSystem() {
for(int i = 0; i<STARS_NUM; i++)
delete stars[i];
}
在终端中运行:
make && ./solarsystem
运行结果如图所示:

由于各行星呈现单一色调,整体视觉效果略显平淡但仍有细节可见;此外,在木星右侧区域可见区域呈现柔和色调,并可通过键盘操作灵活调节展示视角。

本课程相关代码:
wget http://labfile.oss.aliyuncs.com/courses/558/solarsystem.zip
五、拓展阅读
- D Shreiner. OpenGL 编程指南. 人民邮电出版社, 2005. 2.
该书籍全面涵盖了 OpenGL 编程的各个方面,并被公认为 『OpenGL 经典著作』。
