Создание солнечной системы в OpenGL

C++C++Beginner
Практиковаться сейчас

💡 Этот учебник переведен с английского с помощью ИИ. Чтобы просмотреть оригинал, вы можете перейти на английский оригинал

Введение

В этом проекте мы создадим симуляцию Солнечной системы с использованием OpenGL. Симуляция будет включать солнце, планеты и их движения и вращения. Мы будем использовать GLUT (OpenGL Utility Toolkit) для обработки функций окон и ввода, а OpenGL для рендеринга.

Завершив этот проект, вы научитесь:

  • Основным концепциям графического программирования с использованием OpenGL
  • Как создавать 3D-модели и рендерить их в симулированной среде
  • Как обрабатывать пользовательский ввод и обновлять симуляцию в соответствии с ним
  • Как реализовать базовую систему освещения для улучшения визуального качества симуляции
  • Как организовать код с использованием принципов объектно-ориентированного программирования

В этом проекте предполагается базовое понимание программирования на C++ и некоторое знакомство с концепциями графического программирования. Он даст практический опыт в создании простого графического приложения с использованием OpenGL.

👀 Предпросмотр

Предпросмотр симуляции Солнечной системы

🎯 Задачи

В этом проекте вы научитесь:

  • Как установить необходимые библиотеки и настроить开发环境.
  • Как создать необходимые классы и реализовать базовую функциональность вращения и公转ания планет.
  • Как настроить перспективу и проекцию для 3D-объекта.
  • Как реализовать систему освещения для улучшения визуального качества симуляции.
  • Как обрабатывать пользовательский ввод, чтобы пользователь мог контролировать перспективу симуляции.
  • Как протестировать и усовершенствовать симуляцию, чтобы убедиться, что она работает как ожидается.

🏆 Достижения

После завершения этого проекта вы сможете:

  • Применять основные концепции графического программирования с использованием OpenGL.
  • Создавать 3D-модели и рендерить их в симулированной среде.
  • Реализовать базовую систему освещения для улучшения визуального качества симуляции.
  • Организовать код с использованием принципов объектно-ориентированного программирования.
  • Демонстрировать навыки решения проблем и отладки.

Введение в OpenGL и GLUT

OpenGL содержит множество функций рендеринга,但其设计目的独立于任何窗口系统或操作系统。因此,它不包括创建打开窗口、从键盘或鼠标读取事件,甚至显示窗口的最基本功能。因此,仅使用OpenGL完全不可能创建完整的图形程序。此外,大多数程序需要与用户交互(响应键盘和鼠标操作)。GLUT提供了这种便利。

GLUT расшифровывается как OpenGL Utility Toolkit。这是一个用于处理OpenGL程序的工具库,主要负责处理对底层操作系统的调用和I/O操作。使用GLUT可以屏蔽底层操作系统GUI实现的一些细节,并且只需要GLUT API即可创建应用程序窗口、处理鼠标和键盘事件等,从而实现跨平台兼容性。

Давайте сначала установим GLUT в среде Experiment:

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;

}

Создайте файл main.cpp в директории ~/project/ и напишите следующий код:

//
//  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;
}

Советы

  • Одноканальный буферизация подразумевает выполнение всех команд рисования непосредственно на окне, что является медленным. Если вычислительная мощность компьютера недостаточна и используется одноканальный буферизация, экран может мигать.
  • Двойной буферизация подразумевает выполнение команд рисования в буфере в памяти, что гораздо быстрее. После завершения команд рисования результаты копируются на экран через обмен буферов. Преимущество этого подхода заключается в том, что если мы позволяем рисовать операции в режиме реального времени с графическим ускорителем, когда рисование задач сложно, операции ввода-вывода станут сложными, что приведет к понижению производительности. Вместо этого, с двойной буферизацией, завершенные результаты рисования отправляются непосредственно на графический ускоритель для рендеринга при обмене буферов, что значительно снижает ввод-вывод.

В 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++, потому что традиционный массив вполне достаточно.
  • Определение угла обзора в OpenGL - это сложный концепт, требующий некоторой длины для объяснения. Мы кратко укажем, что определение угла обзора требует здесь как минимум девяти параметров. Мы подробно объясним их функции позже, когда будем их реализовывать в следующем разделе.

Наконец, мы также должны рассмотреть настройку основных параметров и переменных.

В SolarSystem, включая Солнце, всего девять планет (не считая Плутона), но в классе Star, который мы спроектировали, каждый объект Star имеет атрибуты Star, поэтому мы можем дополнительно реализовать спутники этих планет, например, Луну, вращающуюся вокруг Земли. Поэтому мы предполагаем реализацию десяти планет. Поэтому мы можем задать следующий enum для индексирования планет в массиве:

#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. Это связано с тем, что использование опции -l в компиляторе g++ немного особенное.

Например: 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 для сохранения текущей матричной среды; в противном случае могут возникать странные ошибки при рисовании.

✨ Проверить решение и практиковаться

Часто используемые API для рисования изображений в OpenGL

OpenGL предоставляет множество часто используемых API, связанных с рисованием графики. Здесь мы кратко рассмотрим несколько из них и применим их в следующем коде:

  • glEnable(GLenum cap): Эта функция используется для активации различных функций, предоставляемых OpenGL. Параметр cap - это макрос внутри OpenGL, который обеспечивает эффекты, такие как освещение, туман и抖动.
  • glPushMatrix() и glPopMatrix(): Эти функции сохраняют текущую матрицу на вершину стека (сохраняют текущую матрицу).
  • glRotatef(alpha, x, y, z): Представляет вращение текущей формы против часовой стрелки на alpha градусов вокруг оси (x, y, z).
  • glTranslatef(distance, x, y): Представляет перемещение текущей формы на distance по направлению (x, y).
  • 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

В предыдущем разделе мы определили девять переменных-членов в классе SolarSystem в OpenGL:

GLdouble viewX, viewY, viewZ;
GLdouble centerX, centerY, centerZ;
GLdouble upX, upY, upZ;

Для понимания этих девяти переменных мы сначала должны сформулировать концепцию перспективы камеры в трехмерном программировании с использованием OpenGL.

Представьте, что сцены, которые мы обычно смотрим в фильмах, на самом деле снимались с точки зрения камеры. Поэтому и в OpenGL есть аналогичная концепция. Если представить камеру как нашу собственную голову, то:

  1. viewX, viewY, viewZ соответствуют координатам головы (камеры) в мировых координатах OpenGL;
  2. centerX, centerY, centerZ соответствуют координатам объекта, который мы наблюдаем (как видит камера);
  3. 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.

Как показано на следующем рисунке:

Диаграмма перспективного проектирования в 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);

✨ Проверить решение и практиковаться

Рисование планет

При рисовании планеты первым делом нужно рассмотреть ее угол公转 и угол вращения. Поэтому мы можем сначала реализовать метод-член Star::update(long timeSpan) класса Star в файле ~/project/stars.cpp:

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 на расстояние, при этом оси 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 на расстояние, при этом оси 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. glutDisplayFunc выполняет функцию обратного вызова, когда GLUT определяет, что содержимое окна нужно обновить, в то время как glutIdleFunc обрабатывает обратный вызов, когда цикл событий находится в состоянии неактивности.

Для того, чтобы вся солнечная система двигалась, нам нужно определить, когда обновлять позиции планет и когда обновлять представление.

Очевидно, что glutDisplayFunc должен сосредоточиться на обновлении представления, а когда событие находится в состоянии неактивности, мы можем начать обновлять позиции планет. После обновления позиций мы можем вызвать функцию обновления представления, чтобы обновить отображение.

Поэтому мы можем сначала реализовать метод-член SolarSystem::onUpdate(), который вызывается в glutDisplayFunc:

#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, мы можем наблюдать за эффектами освещения планет при их вращении вокруг солнца. Кроме того, мы можем调整 перспективу с помощью клавиатуры, чтобы наблюдать за солнечной системой с разных углов.