介绍
在本项目中,我们将使用 OpenGL 创建一个太阳系模拟。该模拟将包括太阳、行星及其运动和旋转。我们将使用 GLUT(OpenGL 实用工具包)来处理窗口和输入函数,并使用 OpenGL 进行渲染。
通过完成本项目,你将学习到:
- 使用 OpenGL 进行图形编程的基本概念
- 如何在模拟环境中创建 3D 模型并进行渲染
- 如何处理用户输入并相应地更新模拟
- 如何实现基本照明系统以提高模拟的视觉质量
- 如何使用面向对象编程原则组织代码
本项目假定你对 C++ 编程有基本的了解,并对图形编程概念有一定的熟悉程度。它将提供一个使用 OpenGL 构建简单图形应用程序的实践经验。
👀 预览

🎯 任务
在本项目中,你将学习:
- 如何安装必要的库并设置开发环境。
- 如何创建必要的类并实现行星旋转和公转的基本功能。
- 如何为 3D 场景设置透视和投影。
- 如何实现照明系统以提高模拟的视觉质量。
- 如何处理用户输入以允许用户控制模拟的视角。
- 如何测试和优化模拟以确保其按预期运行。
🏆 成果
完成本项目后,你将能够:
- 应用使用 OpenGL 进行图形编程的基本概念。
- 在模拟环境中创建 3D 模型并进行渲染。
- 实现基本照明系统以提高模拟的视觉质量。
- 使用面向对象编程原则组织代码。
- 展示解决问题和调试的技能。
了解 OpenGL 和 GLUT
OpenGL 包含许多渲染函数,但其设计目的独立于任何窗口系统或操作系统。因此,它不包括创建打开窗口、从键盘或鼠标读取事件,甚至显示窗口的最基本功能。所以,仅使用 OpenGL 完全不可能创建一个完整的图形程序。此外,大多数程序需要与用户交互(响应键盘和鼠标操作)。GLUT 提供了这种便利。
GLUT 代表 OpenGL 实用工具包。它是一个用于处理 OpenGL 程序的工具库,主要负责处理对底层操作系统的调用和 I/O 操作。使用 GLUT 可以屏蔽底层操作系统 GUI 实现的一些细节,只需要 GLUT 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 LabEx");
// glutDisplayFunc 的原型是 glutDisplayFunc(void (*func)(void))
// 这是一个回调函数,每当 GLUT 确定需要更新和显示窗口内容时就会执行。
//
// glutIdleFunc(void (*func)(void)) 指定在事件循环空闲时要执行的函数。此回调函数以函数指针作为其唯一参数。
//
// glutKeyboardFunc(void (*func)(unsigned char key, int x, int y)) 将键盘上的一个键与一个函数关联。当按下或释放该键时会调用此函数。
//
// 因此,下面三行实际上是在向 GLUT 注册三个按键回调函数
glutDisplayFunc(onDisplay);
glutIdleFunc(onUpdate);
glutKeyboardFunc(onKeyboard);
glutMainLoop();
return 0;
}
在~/project/目录中创建一个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 LabEx");
glutDisplayFunc(onDisplay);
glutIdleFunc(onUpdate);
glutKeyboardFunc(onKeyboard);
glutMainLoop();
return 0;
}
提示
- 单缓冲涉及直接在窗口上执行所有绘图命令,这很慢。如果计算机处理能力不足且使用单缓冲,屏幕可能会闪烁。
- 双缓冲涉及在内存中的一个缓冲区中执行绘图命令,这要快得多。绘图命令完成后,通过缓冲区交换将结果复制到屏幕上。这种方法的优点是,如果我们让绘图操作与图形卡实时执行,当绘图任务复杂时,I/O 操作会变得复杂,导致性能降低。相比之下,使用双缓冲时,完成的绘图结果在缓冲区交换时直接发送到图形卡进行渲染,这大大减少了 I/O。
在 OpenGL 中,建议使用GLfloat来表示浮点数。
类设计
在面向对象编程中,首先明确我们要处理的对象至关重要。显然,在整个天体系统中,它们都是行星(Star),行星和恒星的区别仅在于是否有父节点。其次,对于不同的行星,它们通常有各自的材质,不同的材质会决定它们是否发光。因此,我们有了一个初步的对象模型。所以,我们将行星分为:能围绕一个点旋转和公转的普通行星(Star)、具有特殊材质的行星(Planet)以及能发光的行星(LightPlanet)。
此外,为了便于编程实现,我们需要对现实世界中的实际编程模型做一些假设:
- 行星的轨道是圆形的;
- 旋转速度保持不变;
- 每次屏幕刷新,假设过去了一天。
首先,我们可以考虑按以下步骤实现逻辑:
- 初始化行星对象;
- 初始化 OpenGL 引擎,实现 onDraw 和 onUpdate;
- 每个行星应负责处理自身的属性、公转关系以及与变换相关的绘图。因此,在设计行星类时,应提供一个绘图方法 draw();
- 行星还应处理自身与旋转和公转相关的更新以进行显示。所以,在设计行星类时,还应提供一个更新方法 update();
- 在 onDraw() 中调用行星的 draw() 方法;
- 在 onUpdate() 中调用行星的 update() 方法;
- 在 onKeyboard() 中用键盘调整整个太阳系的显示。
此外,对于每个行星,它们有以下属性:
- 颜色 color
- 公转半径 radius
- 自转速度 selfSpeed
- 公转速度 speed
- 到太阳中心的距离 distance
- 公转的父行星 parentStar
- 当前自转角度 alphaSelf
- 当前公转角度 alpha
在~/project/目录下,创建一个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(); }
};
此外,我们还需要考虑 SolarSystem 类的设计。在太阳系中,显然太阳系由各种行星组成。对于太阳系来说,行星运动后的视图刷新应由太阳系来处理。因此,SolarSystem 的成员变量应包括包含行星的变量,成员函数应处理太阳系内部的视图刷新和键盘响应事件。所以,在~/project/目录下,创建一个solarsystem.hpp文件,在其中我们可以设计SolarSystem类:
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 类中,每个 Star 对象都有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 LabEx");
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 */
在~/project/目录下,创建一个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];
// Define the parameters of the viewing angle
GLdouble viewX, viewY, viewZ;
GLdouble centerX, centerY, centerZ;
GLdouble upX, upY, upZ;
};
在~/project/目录下,创建一个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:
}
在~/project/目录下,创建一个Makefile文件,并向其中添加以下代码:
请手动输入,不要直接复制粘贴,记得使用<tab>键而不是空格键。
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选项的库应该放在所有编译文件的右侧。
最后,在终端中运行:
make && ./solarsystem
你会看到窗口已经创建,但里面什么都没有(显示的是窗口后面的内容)。这是因为我们还没有实现窗口中图形的刷新机制。我们将在下一个实验中继续完成剩余的代码,以使整个太阳系模拟运行起来。
OpenGL 中的矩阵概念
在线性代数中,我们对矩阵的概念并不陌生,但对其具体用途和功能的理解可能有限。那么矩阵到底是什么呢?
让我们先来看下面这个等式:
x = Ab
其中,A 是一个矩阵,x 和 b 是向量。
视角一:
如果 x 和 b 都是我们三维空间中的向量,那么 A 起到什么作用呢?它将 b 变换成(或转换为)x。从这个角度来看,矩阵 A 可以被理解为一种变换。
现在让我们再考虑另一个等式:
Ax = By
其中,A 和 B 是矩阵,x 和 y 是向量。
视角二:
对于两个不同的向量 x 和 y,它们本质上是相同的,因为通过分别与矩阵 A 和 B 相乘可以使它们相等。从这个角度来看,矩阵 A 可以被理解为一个坐标系。换句话说,向量本身是唯一的,但我们定义了一个坐标系来描述它们。由于使用了不同的坐标系,向量的坐标会有所不同。在这种情况下,对于同一个向量,它在不同的坐标系中有不同的坐标。
矩阵 A 精确地描述了一个坐标系,矩阵 B 描述了另一个坐标系。当这两个坐标系应用于 x 和 y 时,会得到相同的结果,这意味着 x 和 y 本质上是同一个向量,但处于不同的坐标系中。
综合这两个视角,我们可以得出结论:矩阵的本质是描述运动。
在 OpenGL 的环境中,有一个负责渲染变换的矩阵,即 OpenGL 中的矩阵模式。
如前所述,矩阵既可以描述物体的变换,也可以描述它所处的坐标系。因此,在处理不同的操作时,我们需要在 OpenGL 中设置不同的矩阵模式。这是通过函数 glMatrixMode() 来实现的。
这个函数接受三种不同的模式:GL_PROJECTION 用于投影操作,GL_MODELVIEW 用于模型视图操作,GL_TEXTURE 用于纹理操作。
GL_PROJECTION 告诉 OpenGL 将执行投影操作,把物体投影到一个平面上。启用此模式后,需要使用 glLoadIdentity() 将矩阵设置为单位矩阵,然后使用 gluPerspective 等操作来设置透视(稍后在讨论 OpenGL 中的视图概念时,我们将更详细地解释这个函数)。
GL_MODELVIEW 告诉 OpenGL 后续的语句将用于描述基于模型的操作,比如设置相机的视角。同样,启用此模式后,我们需要将 OpenGL 矩阵模式设置为单位矩阵。
GL_TEXTURE 用于与纹理相关的操作,我们目前暂不深入探讨。
如果你对矩阵的概念不熟悉,可以简单地将
glMatrixMode()理解为向 OpenGL 声明即将进行的操作。在渲染或旋转物体之前,我们必须使用glPushMatrix来保存当前的矩阵环境;否则可能会出现莫名其妙的绘图错误。
OpenGL 常见图像绘制 API
OpenGL 提供了许多与图形绘制相关的常用 API。在这里,我们将简要介绍其中的几个,并在以下代码中使用它们:
- **glEnable(GLenum cap)**:此函数用于激活 OpenGL 提供的各种功能。参数 cap 是 OpenGL 内部的一个宏,它提供诸如光照、雾化和抖动等效果。
- glPushMatrix() 和 **glPopMatrix()**:这些函数将当前矩阵保存到栈顶(保存当前矩阵)。
- **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()**:当我们想要绘制一个形状时,需要在绘制之前和之后调用这两个函数。
glBegin()指定要绘制的形状类型。例如,GL_POINTS表示绘制点,GL_LINES表示绘制由线连接的点,GL_TRIANGLES用每三个点完成一个三角形,GL_POLYGON从第一个点到第 n 个点绘制一个多边形,等等。例如,当我们需要绘制一个圆时,可以使用一个有许多边的多边形来模拟它:
// 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();
OpenGL 中的透视坐标
在上一节中,我们在 OpenGL 的 SolarSystem 类中定义了九个成员变量:
GLdouble viewX, viewY, viewZ;
GLdouble centerX, centerY, centerZ;
GLdouble upX, upY, upZ;
为了理解这九个变量,我们首先需要在使用 OpenGL 进行 3D 编程时建立相机透视的概念。
想象一下,我们通常在电影中看到的场景实际上是从相机的视角拍摄的。因此,OpenGL 也有类似的概念。如果我们把相机想象成我们自己的头部,那么:
viewX、viewY、viewZ对应于头部(相机)在 OpenGL 世界坐标系中的坐标;centerX、centerY、centerZ对应于被观察物体的坐标(相机所看到的);upX、upY、upZ对应于从头部顶部(相机顶部)向上指向的方向向量(因为我们可以倾斜头部来观察物体)。
有了这个理解,你现在对 OpenGL 中的坐标系有了一个概念。
对于这个实验,我们假设初始透视位于坐标 (x, -x, x)。因此,我们有:
#define REST 700
#define REST_Y (-REST)
#define REST_Z (REST)
被观察物体(太阳)的位置在 (0,0,0),所以在 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);
接下来,让我们看看 gluPerspective(GLdouble fovy,GLdouble aspect,GLdouble zNear,GLdouble zFar)。
这个函数将创建一个对称的透视观察体积,在使用这个函数之前,OpenGL 的矩阵模式需要设置为 GL_PROJECTION。
如下图所示:

窗口中的图像是由相机捕捉的,实际捕捉的内容在远平面上,而显示的内容在近平面上。因此,这个函数需要四个参数:
- 第一个参数是透视角度的大小。
- 第二个参数是实际窗口的宽高比,如图中
aspect=w/h。 - 第三个参数是到近平面的距离。
- 第四个参数是到远平面的距离。
OpenGL 中的光照效果
OpenGL 将光照系统分为三个部分:光源、材质和光照环境。
顾名思义,光源是光的来源,比如太阳; 材质是指各种接收光的物体表面,比如太阳系中除太阳之外的行星和卫星; 光照环境包括决定最终光照效果的其他参数,比如光线的反射,可通过设置一个名为“环境光亮度”的参数来控制,以使最终图像更接近现实。
在物理学中,当平行光照射到光滑表面时,反射光保持平行。这种反射称为“镜面反射”。另一方面,由不平整表面引起的反射称为“漫反射”。

光源
要在 OpenGL 中实现光照系统,首先要做的是设置光源。值得一提的是,OpenGL 支持有限数量的光源(总共八个),由宏 GL_LIGHT0 到 GL_LIGHT7 表示。可以使用 glEnable 函数启用它们,使用 glDisable 函数禁用它们。例如:glEnable(GL_LIGHT0);
光源的位置使用 glLightfv 函数设置,例如:
GLfloat light_position[] = {0.0f, 0.0f, 0.0f, 1.0f};
glLightfv(GL_LIGHT0, GL_POSITION, light_position); // 指定光源 0 的位置
位置由四个值 (x, y, z, w) 表示。当 w 为 0 时,表示光源在无限远处。x、y 和 z 的值指定这个无限远光源的方向。 当 w 不为 0 时,表示一个位置光源,其位置为 (x/w, y/w, z/w)。
材质
要设置物体的材质,通常需要五个属性:
- 多次反射后留在环境中的光的强度。
- 漫反射后的光的强度。
- 镜面反射后的光的强度。
- OpenGL 中非发光物体发出的光的强度,很微弱且不影响其他物体。
- 镜面指数,它表示材质的粗糙度。值越小意味着材质越粗糙,当点光源发出的光照在上面时,会产生较大的亮点。相反,值越大意味着材质更像镜面,产生较小的亮点。
OpenGL 提供了两个版本的函数来设置材质:
void glMaterialf(GLenum face, GLenum pname, TYPE param);
void glMaterialfv(GLenum face, GLenum pname, TYPE *param);
它们之间的区别在于,对于镜面指数只需设置一个值,所以使用 glMaterialf。对于其他需要多个值的材质设置,使用数组,并使用带有指针向量参数的版本 glMaterialfv。例如:
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);
光照环境
默认情况下,OpenGL 不处理光照。要启用光照功能,需要使用宏 GL_LIGHTING,即 glEnable(GL_LIGHTING);
绘制行星
在绘制行星时,我们首先需要考虑它的公转角度和自转角度。因此,我们可以先在 ~/project/stars.cpp 文件中实现 Star 类的成员函数 Star::update(long timeSpan):
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>。
光照绘制
对于表示不发光天体的 Planet 类,我们需要绘制它的光照效果。在文件 ~/project/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); // 指定光源 0 的位置
glLightfv(GL_LIGHT0, GL_AMBIENT, light_ambient); // 表示从各种光源到达材质的光线强度,经过多次反射和追踪后
glLightfv(GL_LIGHT0, GL_DIFFUSE, light_diffuse); // 漫反射后的光强度
glLightfv(GL_LIGHT0, GL_SPECULAR, light_specular); // 镜面反射后的光强度
}
绘制窗口
在上一节中,我们提到了用于处理图像显示的两个最重要的函数:glutDisplayFunc 和 glutIdleFunc。当 GLUT 确定窗口内容需要更新时,glutDisplayFunc 会执行回调函数,而 glutIdleFunc 则在事件循环空闲时处理回调。
为了使整个太阳系移动,我们需要考虑何时更新行星的位置以及何时刷新视图。
显然,glutDisplayFunc 应该专注于刷新视图,当事件空闲时,我们可以开始更新行星的位置。在位置更新之后,我们可以调用视图刷新函数来刷新显示。
因此,我们可以首先实现 glutDisplayFunc 中调用的成员函数 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() {
// 清除视口缓冲区
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();
// 我们在主函数中初始化显示模式时使用了 GLUT_DOUBLE
// 绘制完成后,我们需要使用 glutSwapBuffers 来实现双缓冲的缓冲区交换
glutSwapBuffers();
}
类的构造函数和析构函数
在 stars.hpp 中定义的类的构造函数需要初始化类的成员变量。这部分相对简单,甚至可以使用默认析构函数,所以请你自己实现这些构造函数:
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];
}
使用键盘按键实现视角变化
为了控制视角变化,我们可以使用键盘上的五个按键 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 的代码如下:
点击查看完整代码
//
// stars.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
结果如图所示:

由于行星颜色单一,光照效果不是很明显,但仍可看见。例如,在黄色木星右侧有发白的外观。
总结
在这个项目中,我们实现了一个简单的太阳系模型。通过使用 OpenGL 中的光照系统,我们能够看到行星围绕太阳公转时的光照效果。此外,我们可以使用键盘调整视角,从不同角度观察太阳系。



