使用OpenGL创建太阳系

C++C++Beginner
立即练习

💡 本教程由 AI 辅助翻译自英文原版。如需查看原文,您可以 切换至英文原版

简介

在本项目中,我们将使用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)。

此外,为了便于编程实现,我们需要对现实世界中的实际编程模型做一些假设:

  1. 行星的轨道是圆形的;
  2. 旋转速度保持不变;
  3. 每次屏幕刷新,假设过去了一天。

首先,我们可以考虑按以下步骤实现逻辑:

  1. 初始化行星对象;
  2. 初始化OpenGL引擎,实现onDraw和onUpdate;
  3. 每个行星应负责处理自身的属性、公转关系以及与变换相关的绘图。因此,在设计行星类时,应提供一个绘图方法draw();
  4. 行星还应处理自身与旋转和公转相关的更新以进行显示。所以,在设计行星类时,还应提供一个更新方法update();
  5. 在onDraw()中调用行星的draw()方法;
  6. 在onUpdate()中调用行星的update()方法;
  7. 在onKeyboard()中用键盘调整整个太阳系的显示。

此外,对于每个行星,它们有以下属性:

  1. 颜色color
  2. 公转半径radius
  3. 自转速度selfSpeed
  4. 公转速度speed
  5. 到太阳中心的距离distance
  6. 公转的父行星parentStar
  7. 当前自转角度alphaSelf
  8. 当前公转角度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中,我们创建StarPlanetLightPlanet类:

点击查看完整代码
//
//  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 是一个矩阵,xb 是向量。

视角一

如果 xb 都是我们三维空间中的向量,那么 A 起到什么作用呢?它将 b 变换成(或转换为)x。从这个角度来看,矩阵 A 可以被理解为一种变换。

现在让我们再考虑另一个等式:

Ax = By

其中,AB 是矩阵,xy 是向量。

视角二

对于两个不同的向量 xy,它们本质上是相同的,因为通过分别与矩阵 AB 相乘可以使它们相等。从这个角度来看,矩阵 A 可以被理解为一个坐标系。换句话说,向量本身是唯一的,但我们定义了一个坐标系来描述它们。由于使用了不同的坐标系,向量的坐标会有所不同。在这种情况下,对于同一个向量,它在不同的坐标系中有不同的坐标。
矩阵 A 精确地描述了一个坐标系,矩阵 B 描述了另一个坐标系。当这两个坐标系应用于 xy 时,会得到相同的结果,这意味着 xy 本质上是同一个向量,但处于不同的坐标系中。

综合这两个视角,我们可以得出结论:矩阵的本质是描述运动。

在 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 也有类似的概念。如果我们把相机想象成我们自己的头部,那么:

  1. viewXviewYviewZ 对应于头部(相机)在 OpenGL 世界坐标系中的坐标;
  2. centerXcenterYcenterZ 对应于被观察物体的坐标(相机所看到的);
  3. upXupYupZ 对应于从头部顶部(相机顶部)向上指向的方向向量(因为我们可以倾斜头部来观察物体)。

有了这个理解,你现在对 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

如下图所示:

OpenGL 透视投影图

窗口中的图像是由相机捕捉的,实际捕捉的内容在远平面上,而显示的内容在近平面上。因此,这个函数需要四个参数:

  • 第一个参数是透视角度的大小。
  • 第二个参数是实际窗口的宽高比,如图中 aspect=w/h
  • 第三个参数是到近平面的距离。
  • 第四个参数是到远平面的距离。
✨ 查看解决方案并练习

OpenGL 中的光照效果

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)。

材质

要设置物体的材质,通常需要五个属性:

  1. 多次反射后留在环境中的光的强度。
  2. 漫反射后的光的强度。
  3. 镜面反射后的光的强度。
  4. OpenGL 中非发光物体发出的光的强度,很微弱且不影响其他物体。
  5. 镜面指数,它表示材质的粗糙度。值越小意味着材质越粗糙,当点光源发出的光照在上面时,会产生较大的亮点。相反,值越大意味着材质更像镜面,产生较小的亮点。

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); // 镜面反射后的光强度

}
✨ 查看解决方案并练习

绘制窗口

在上一节中,我们提到了用于处理图像显示的两个最重要的函数:glutDisplayFuncglutIdleFunc。当 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.cppsolarsystem.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中的光照系统,我们能够看到行星围绕太阳公转时的光照效果。此外,我们可以使用键盘调整视角,从不同角度观察太阳系。

您可能感兴趣的其他 C++ 教程