Das Sonnensystem in OpenGL erstellen

C++Beginner
Jetzt üben

Einführung

In diesem Projekt werden wir eine Solarsystem-Simulation mit OpenGL erstellen. Die Simulation wird die Sonne, Planeten und deren Bewegungen und Rotationen umfassen. Wir werden GLUT (OpenGL Utility Toolkit) verwenden, um Fenster- und Eingabefunktionen zu behandeln, und OpenGL für die Darstellung.

Durch die Fertigstellung dieses Projekts lernen Sie:

  • Grundlagen des Grafikprogrammierens mit OpenGL
  • Wie man 3D-Modelle erstellt und in einer simulierten Umgebung darstellt
  • Wie man Benutzer-Eingaben behandelt und die Simulation entsprechend aktualisiert
  • Wie man ein grundlegendes Lichtsytem implementiert, um die visuelle Qualität der Simulation zu verbessern
  • Wie man den Code mit objektorientierten Programmierungsprinzipien organisiert

Dieses Projekt setzt ein grundlegendes Verständnis von C++-Programmierung und eine gewisse Vertrautheit mit Grafikprogrammierungskonzepten voraus. Es bietet eine praktische Erfahrung beim Aufbau einer einfachen Grafikanwendung mit OpenGL.

👀 Vorschau

Vorschau der Solarsystem-Simulation

🎯 Aufgaben

In diesem Projekt lernen Sie:

  • Wie man die erforderlichen Bibliotheken installiert und die Entwicklungsumgebung einrichtet.
  • Wie man die erforderlichen Klassen erstellt und die grundlegende Funktionalität für die Planetenrotation und -umrundung implementiert.
  • Wie man die Perspektive und Projektion für die 3D-Szene einrichtet.
  • Wie man das Lichtsytem implementiert, um die visuelle Qualität der Simulation zu verbessern.
  • Wie man Benutzer-Eingaben behandelt, um es dem Benutzer zu ermöglichen, die Perspektive der Simulation zu steuern.
  • Wie man die Simulation testet und optimiert, um sicherzustellen, dass sie wie erwartet funktioniert.

🏆 Errungenschaften

Nach Abschluss dieses Projekts werden Sie in der Lage sein:

  • Grundlagen des Grafikprogrammierens mit OpenGL anzuwenden.
  • 3D-Modelle zu erstellen und in einer simulierten Umgebung darzustellen.
  • Ein grundlegendes Lichtsytem zu implementieren, um die visuelle Qualität der Simulation zu verbessern.
  • Den Code mit objektorientierten Programmierungsprinzipien zu organisieren.
  • Problemlösungs- und Debugging-Fähigkeiten zu demonstrieren.

OpenGL und GLUT verstehen

OpenGL enthält eine Vielzahl von Rendering-Funktionen, aber deren Designzweck ist unabhängig von jedem Fenster-System oder Betriebssystem. Daher enthält es keine Funktionen zum Erstellen von offenen Fenstern, das Lesen von Ereignissen von Tastatur oder Maus oder sogar die am einfachsten zu implementierende Funktionalität von Fensterdarstellungen. Es ist daher völlig unmöglich, ein vollständiges Grafikprogramm nur mit OpenGL zu erstellen. Darüber hinaus müssen die meisten Programme mit dem Benutzer interagieren (auf Tastatur- und Mausoperationen reagieren). GLUT bietet diese Bequemlichkeit.

GLUT steht für OpenGL Utility Toolkit. Es ist eine Toolbibliothek zur Handhabung von OpenGL-Programmen und ist hauptsächlich dafür zuständig, Aufrufe an das zugrunde liegende Betriebssystem und I/O-Operationen zu verarbeiten. Die Verwendung von GLUT kann einige Details der GUI-Implementierung des zugrunde liegenden Betriebssystems verdecken, und es wird nur die GLUT-API benötigt, um Anwendungsfenster zu erstellen, Maus- und Tastaturevents zu behandeln usw., wodurch eine plattformübergreifende Kompatibilität erreicht wird.

Lassen Sie uns zunächst GLUT in der Experiment-Umgebung installieren:

sudo apt-get update && sudo apt-get install freeglut3 freeglut3-dev

Die Struktur eines standardmäßigen GLUT-Programms ist im folgenden Code dargestellt:

// Grundheaderdatei für die Verwendung von GLUT
#include <GL/glut.h>

// Grundmakros für das Erstellen eines grafischen Fensters
#define WINDOW_X_POS 50
#define WINDOW_Y_POS 50
#define WIDTH 700
#define HEIGHT 700

// Callback-Funktionen, die mit GLUT registriert werden
void onDisplay(void);
void onUpdate(void);
void onKeyboard(unsigned char key, int x, int y);

int main(int argc, char* argv[]) {

    // Initialisiere GLUT und verarbeite alle Befehlszeilenargumente
    glutInit(&argc, argv);
    // Diese Funktion bestimmt, ob das RGBA-Modus oder der Farbindexmodus für die Darstellung verwendet werden soll. Sie kann auch angeben, ob ein einzeln gepuffertes oder doppelt gepuffertes Fenster verwendet werden soll. Hier verwenden wir das RGBA- und doppelt gepufferte Fenster.
    glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE);
    // Setze die Position der oberen linken Ecke des Fensters, wenn es auf dem Bildschirm erstellt wird
    glutInitWindowPosition(WINDOW_X_POS, WINDOW_Y_POS);
    // Setze die Breite und Höhe des Fensters, wenn es erstellt wird, für die Einfachheit
    glutInitWindowSize(WIDTH, HEIGHT);
    // Erstelle ein Fenster, und der Eingabestring ist der Titel des Fensters
    glutCreateWindow("SolarSystem at LabEx");

    // Das Prototype von glutDisplayFunc ist glutDisplayFunc(void (*func)(void))
    // Dies ist eine Callback-Funktion, und sie wird ausgeführt, wenn GLUT bestimmt, dass der Inhalt eines Fensters aktualisiert und dargestellt werden muss.
    //
    // glutIdleFunc(void (*func)(void)) gibt eine Funktion an, die ausgeführt werden soll, wenn die Ereignisschleife inaktiv ist. Diese Callback-Funktion nimmt einen Funktionszeiger als ihren einzigen Parameter.
    //
    // glutKeyboardFunc(void (*func)(unsigned char key, int x, int y)) assoziiert eine Taste auf der Tastatur mit einer Funktion. Diese Funktion wird aufgerufen, wenn die Taste gedrückt oder losgelassen wird.
    //
    // Daher registrieren die drei folgenden Zeilen tatsächlich die drei Schlüssel-Callback-Funktionen mit GLUT
    glutDisplayFunc(onDisplay);
    glutIdleFunc(onUpdate);
    glutKeyboardFunc(onKeyboard);

    glutMainLoop();
    return 0;

}

Erstellen Sie in dem Verzeichnis ~/project/ eine Datei main.cpp und schreiben Sie den folgenden Code:

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

Tipps

  • Einfache Pufferung beinhaltet das direkte Ausführen aller Zeichnungsbefehle auf dem Fenster, was langsam ist. Wenn die Rechenleistung des Computers nicht ausreicht und einfache Pufferung verwendet wird, kann der Bildschirm flackern.
  • Doppelte Pufferung beinhaltet das Ausführen der Zeichnungsbefehle in einem Puffer im Arbeitsspeicher, was viel schneller ist. Nachdem die Zeichnungsbefehle abgeschlossen sind, werden die Ergebnisse durch einen Pufferswap auf den Bildschirm kopiert. Der Vorteil dieser Methode ist, dass wenn wir die Zeichnungsoperationen in Echtzeit mit der Grafikkarte ausführen lassen, bei komplexen Zeichnungsaufgaben die I/O-Operationen komplexer werden und die Leistung sinkt. Im Gegensatz dazu wird mit doppelter Pufferung die abgeschlossene Zeichnergebnisse direkt an die Grafikkarte zum Rendern geschickt, wenn der Puffer getauscht wird, was die I/O erheblich reduziert.

In OpenGL wird empfohlen, GLfloat zur Darstellung von Gleitkommazahlen zu verwenden.

Klassenentwurf

Beim objektorientierten Programmieren ist es wichtig, zunächst zu klären, mit welchen Objekten wir es zu tun haben. Offensichtlich sind in unserem gesamten Himmelsystem alle Planeten (Star), und der Unterschied zwischen Planeten und Sternen hängt nur davon ab, ob sie einen Elternknoten haben. Zweitens haben verschiedene Planeten normalerweise ihre eigenen Materialien, und unterschiedliche Materialien zeigen an, ob sie Licht emittieren. Daher haben wir ein vorläufiges Objektmodell. Wir teilen die Planeten daher in: gewöhnliche Planeten, die sich um einen Punkt drehen und sich umkreisen können (Star), Planeten mit speziellen Materialien (Planet) und Planeten, die Licht emittieren können (LightPlanet).

Zusätzlich, um die Programmimplementierung zu erleichtern, müssen wir einige Annahmen über das tatsächliche Programmmodell in der realen Welt treffen:

  1. Die Bahn des Planeten ist kreisförmig;
  2. Die Drehspeed bleibt gleich;
  3. Es wird angenommen, dass sich ein Tag vergangen ist, wenn der Bildschirm aktualisiert wird.

Zunächst können wir die folgenden Schritte für die Implementierung der Logik betrachten:

  1. Initialisiere die Planetenobjekte;
  2. Initialisiere den OpenGL-Engine, implementiere onDraw und onUpdate;
  3. Jeder Planet sollte für die Verwaltung seiner eigenen Eigenschaften, seiner Umlaufbeziehung und seiner transformationsbezogenen Darstellung verantwortlich sein. Daher sollte bei der Planetenklasse eine Zeichnungsfunktion draw() bereitgestellt werden;
  4. Der Planet sollte auch seine eigenen Updates in Bezug auf die Rotation und den Umlauf für die Darstellung handhaben. Daher sollte bei der Planetenklasse auch eine Updatefunktion update() bereitgestellt werden;
  5. Rufe die draw()-Methode des Planeten in onDraw() auf;
  6. Rufe die update()-Methode des Planeten in onUpdate() auf;
  7. Verändere die Darstellung des gesamten Sonnensystems mit der Tastatur in onKeyboard().

Weiterhin haben die einzelnen Planeten die folgenden Attribute:

  1. Farbe color
  2. Umlaufradius radius
  3. Drehspeed selfSpeed
  4. Umlaufgeschwindigkeit speed
  5. Entfernung zum Sonnenmittelpunkt distance
  6. Umlaufender Elternplanet parentStar
  7. Aktueller Rotationswinkel alphaSelf
  8. Aktueller Umlaufwinkel alpha

Im Verzeichnis ~/project/ erstellen Sie eine Datei stars.hpp. Basierend auf der obigen Analyse können wir den folgenden Klassencode entwerfen:

class Star {
public:
    // Umlaufradius des Planeten
    GLfloat radius;
    // Umlauf- und Drehspeed des Planeten
    GLfloat speed, selfSpeed;
    // Entfernung vom Planetenmittelpunkt zum Mittelpunkt des Elternplaneten
    GLfloat distance;
    // Planetenfarbe
    GLfloat rgbaColor[4];

    // Elternplanet
    Star* parentStar;

    // Konstruktor, wenn ein Planet konstruiert wird, sollten der Umlaufradius, die Rotationsgeschwindigkeit, die Rotationsgeschwindigkeit und der Umlauf (Elternplanet) angegeben werden
    Star(GLfloat radius, GLfloat distance,
         GLfloat speed,  GLfloat selfSpeed,
         Star* parentStar);
    // Zeichne die Bewegung, Rotation und andere Aktivitäten eines gewöhnlichen Planeten
    void drawStar();
    // Gib eine Standardimplementierung an, um drawStar() aufzurufen
    virtual void draw() { drawStar(); }
    // Der Parameter ist die Zeitspanne für jede Bildschirmaktualisierung
    virtual void update(long timeSpan);
protected:
    GLfloat alphaSelf, alpha;
};
class Planet : public Star {
public:
    // Konstruktor
    Planet(GLfloat radius, GLfloat distance,
           GLfloat speed,  GLfloat selfSpeed,
           Star* parentStar, GLfloat rgbColor[3]);
    // Füge Materialien zu Planeten mit eigenen Materialien hinzu
    void drawPlanet();
    // Öffne die Überschreibungsfunktion weiterhin für seine Unterklassen
    virtual void draw() { drawPlanet(); drawStar(); }
};
class LightPlanet : public Planet {
public:
    LightPlanet(GLfloat Radius, GLfloat Distance,
                GLfloat Speed,  GLfloat SelfSpeed,
                Star* ParentStar, GLfloat rgbColor[]);
    // Füge Beleuchtung zu Sternen hinzu, die Lichtquellen liefern
    void drawLight();
    virtual void draw() { drawLight(); drawPlanet(); drawStar(); }
};

Zusätzlich müssen wir auch das Design der SolarSystem-Klasse berücksichtigen. Im Sonnensystem besteht es offensichtlich aus verschiedenen Planeten. Für das Sonnensystem sollte die Bildschirmaktualisierung nach der Planetenbewegung vom Sonnensystem behandelt werden. Daher sollten die Membervariablen von SolarSystem Variablen enthalten, die die Planeten beinhalten, und die Memberfunktionen sollten die Bildschirmaktualisierung und die Tastaturantwortereignisse innerhalb des Sonnensystems behandeln. Daher erstellen Sie im Verzeichnis ~/project/ eine Datei solarsystem.hpp, und in ihr können wir die SolarSystem-Klasse entwerfen:

class SolarSystem {

public:

    SolarSystem();
    ~SolarSystem();

    void onDisplay();
    void onUpdate();
    void onKeyboard(unsigned char key, int x, int y);

private:
    Star *stars[STARS_NUM];

    // Definiere die Parameter des Blickwinkels
    GLdouble viewX, viewY, viewZ;
    GLdouble centerX, centerY, centerZ;
    GLdouble upX, upY, upZ;
};

Tipps

  • Hier verwenden wir die traditionelle Arrayform, um alle Planeten zu verwalten, anstatt den Vektor in C++ zu verwenden, da die traditionelle Arrayform ausreicht.
  • Das Definieren des Blickwinkels in OpenGL ist ein komplexes Konzept, das eine gewisse Länge benötigt, um erklärt zu werden. Wir werden kurz erwähnen, dass das Definieren des Blickwinkels hier mindestens neun Parameter erfordert. Wir werden deren Funktionen im nächsten Abschnitt im Detail erklären, wenn wir sie implementieren.

Schließlich müssen wir auch die grundlegenden Parameter- und Variableneinstellungen berücksichtigen.

Im SolarSystem, einschließlich der Sonne, gibt es insgesamt neun Planeten (excluding Pluto), aber in der von uns entworfenen Star-Klasse hat jedes Star-Objekt die Attribute eines Star, so dass wir zusätzlich die Satelliten dieser Planeten implementieren können, wie den Mond, der um die Erde kreist. Daher betrachten wir die Implementierung von insgesamt zehn Planeten. Daher können wir das folgende Enum setzen, um die Planeten in einem Array zu indizieren:

#define STARS_NUM 10
enum STARS {
    Sun,        // Sonne
    Mercury,    // Merkur
    Venus,      // Venus
    Earth,      // Erde
    Moon,       // Mond
    Mars,       // Mars
    Jupiter,    // Jupiter
    Saturn,     // Saturn
    Uranus,     // Uranus
    Neptune     // Neptun
};
Star * stars[STARS_NUM];

Wir haben auch angenommen, dass die Rotationsgeschwindigkeiten gleich sind, daher setzen wir ihre Geschwindigkeit mit einer Macro:

#define TIMEPAST 1
#define SELFROTATE 3

An diesem Punkt verschieben wir die nicht implementierten Memberfunktionen in die entsprechende .cpp-Datei, und wir haben das Experiment in diesem Abschnitt abgeschlossen.

Zusammenfassung des Codes

Lassen Sie uns den Code zusammenfassen, der in den obigen Experimenten abgeschlossen werden muss:

Zunächst erstellen wir in main.cpp ein SolarSystem-Objekt und übergeben dann die Anzeigeaktualisierung, die Leerlaufaktualisierung und die Behandlung von Tastaturevents an glut:

Klicken Sie, um den vollständigen Code anzuzeigen
//
//  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;
}

Zweitens erstellen wir in stars.hpp die Klassen Star, Planet und LightPlanet:

Klicken Sie, um den vollständigen Code anzuzeigen
//
//  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 */

Im Verzeichnis ~/project/ erstellen Sie eine Datei stars.cpp und füllen Sie die entsprechenden Memberfunktionsimplementierungen aus stars.hpp ein:

Klicken Sie, um den vollständigen Code anzuzeigen
//
//  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:
}

In solarsystem.hpp wird die SolarSystem-Klasse entworfen:

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

Im Verzeichnis ~/project/ erstellen Sie eine Datei solarsystem.cpp und implementieren die entsprechenden Memberfunktionen aus solarsystem.hpp:

Klicken Sie, um den vollständigen Code anzuzeigen
//
// 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:
}

Im Verzeichnis ~/project/ erstellen Sie eine Datei Makefile und fügen Sie den folgenden Code hinzu:

Bitte geben Sie es manuell ein, kopieren Sie es nicht direkt, und denken Sie daran, die <tab>-Taste zu verwenden, statt der Leertaste.

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

Wenn Sie die Kompilierungsbefehle schreiben, achten Sie auf die Platzierung von -lglut -lGLU -lGL. Dies ist, weil die Verwendung der -l-Option im g++-Compiler etwas speziell ist.

Zum Beispiel: foo1.cpp -lz foo2.cpp, wenn die Zieldatei foo2.cpp Funktionen aus der Bibliothek z verwendet, werden diese Funktionen nicht direkt geladen. Wenn jedoch foo1.o Funktionen aus der z-Bibliothek verwendet, tritt keine Kompilierungsfehler auf.

Mit anderen Worten, der gesamte Verknüpfungsprozess geht von links nach rechts. Wenn in foo1.cpp ein nicht aufgelöstes Funktionssymbol auftritt, sucht es in der rechten Linkbibliothek. Wenn es die Option z trifft, sucht es in z und findet die Funktion, wodurch die Verknüpfung erfolgreich abgeschlossen wird. Daher sollte die Bibliothek mit der -l-Option rechts von allen kompilierten Dateien platziert werden.

Schließlich führen Sie im Terminal aus:

make && ./solarsystem

Sie können sehen, dass das Fenster erstellt wurde, aber darin ist nichts (es zeigt das, was hinter dem Fenster ist). Dies liegt daran, dass wir noch den Aktualisierungsmechanismus für die Grafik im Fenster nicht implementiert haben. Wir werden im nächsten Experiment den verbleibenden Code vervollständigen, um die gesamte Solarsystem-Simulation laufen zu lassen.

Der Begriff von Matrizen in OpenGL

In der linearen Algebra sind wir mit dem Begriff der Matrizen vertraut, aber unser Verständnis ihrer spezifischen Zwecke und Funktionen kann begrenzt sein. Was ist eigentlich eine Matrix?

Lassen Sie uns mit der Betrachtung der folgenden Gleichung beginnen:

x = Ab

Wobei A eine Matrix ist und x, b Vektoren sind.

Perspektive Eins:

Wenn x und b beide Vektoren in unserem dreidimensionalen Raum sind, was macht A? Es transformiert b in (oder zu) x. Aus dieser Perspektive kann eine Matrix A als eine Transformation verstanden werden.

Betrachten wir nun eine weitere Gleichung:

Ax = By

Wobei A, B Matrizen sind und x, y Vektoren sind.

Perspektive Zwei:

Für zwei verschiedene Vektoren x und y sind sie im Grunde genommen gleich, weil sie durch Multiplikation mit den Matrizen A und B gleich gemacht werden können. Aus dieser Perspektive kann eine Matrix A als ein Koordinatensystem verstanden werden. Mit anderen Worten, die Vektoren selbst sind eindeutig, aber wir definieren ein Koordinatensystem, um sie zu beschreiben. Da verschiedene Koordinatensysteme verwendet werden, variieren die Koordinaten der Vektoren. In diesem Fall hat ein und derselbe Vektor in verschiedenen Koordinatensystemen unterschiedliche Koordinaten. Die Matrix A beschreibt genau ein Koordinatensystem, und die Matrix B beschreibt ein anderes Koordinatensystem. Wenn diese beiden Koordinatensysteme auf x und y angewendet werden, ergeben sie das gleiche Ergebnis, was bedeutet, dass x und y im Grunde genommen der gleiche Vektor ist, aber mit unterschiedlichen Koordinatensystemen.

Indem wir diese beiden Perspektiven kombinieren, können wir schließen, dass das Wesen einer Matrix darin besteht, Bewegung zu beschreiben.

Im Kontext von OpenGL gibt es eine Matrix, die für die Rendering-Transformationen verantwortlich ist, die Matrixmodus in OpenGL genannt wird.

Wie bereits erwähnt, kann eine Matrix sowohl die Transformation eines Objekts als auch das Koordinatensystem beschreiben, in dem es existiert. Daher müssen wir bei der Bearbeitung unterschiedlicher Operationen in OpenGL verschiedene Matrixmodi einstellen. Dies wird mit der Funktion glMatrixMode() erreicht.

Diese Funktion akzeptiert drei verschiedene Modi: GL_PROJECTION für Projektionsoperationen, GL_MODELVIEW für Modellansichtsoperationen und GL_TEXTURE für Texturoperationen.

GL_PROJECTION sagt OpenGL, dass Projektionsoperationen durchgeführt werden, um das Objekt auf eine Ebene zu projizieren. Wenn dieser Modus aktiviert ist, muss die Matrix mit glLoadIdentity() als Einheitsmatrix gesetzt werden, gefolgt von Operationen wie der Einstellung der Perspektive mit gluPerspective (wir werden diese Funktion später im Zusammenhang mit dem Begriff der Ansicht in OpenGL im Detail erklären).

GL_MODELVIEW sagt OpenGL, dass die folgenden Anweisungen zur Darstellung einer modellbasierten Operation verwendet werden sollen, wie die Einstellung der Kameraperspektive. Ebenso müssen wir nach Aktivierung dieses Modus die OpenGL-Matrixmodus als Einheitsmatrix setzen.

GL_TEXTURE wird für texturrelevante Operationen verwendet, auf die wir uns hier nicht nähern werden.

Wenn Sie nicht vertraut mit dem Begriff der Matrizen sind, können Sie glMatrixMode() einfach als Deklarationen für OpenGL über die kommenden Operationen verstehen. Bevor Sie ein Objekt rendern oder rotieren, müssen Sie mit glPushMatrix die aktuelle Matrixumgebung speichern; andernfalls können mysteriöse Zeichnungsfehler auftreten.

Übliche OpenGL -Bildzeichnungs-APIs

OpenGL bietet viele häufig verwendete APIs, die mit der Grafikzeichnung zusammenhängen. Hier werden wir einige von ihnen kurz vorstellen und in dem folgenden Code verwenden:

  • glEnable(GLenum cap): Diese Funktion wird verwendet, um verschiedene Funktionalitäten von OpenGL zu aktivieren. Der Parameter cap ist ein Makro innerhalb von OpenGL, das Effekte wie Beleuchtung, Nebel und Dithering bereitstellt.
  • glPushMatrix() und glPopMatrix(): Diese Funktionen speichern die aktuelle Matrix auf den Stapel (speichern die aktuelle Matrix).
  • glRotatef(alpha, x, y, z): Repräsentiert die Drehung der aktuellen Form im Uhrzeigersinn um alpha Grad entlang der Achse (x, y, z).
  • glTranslatef(distance, x, y): Repräsentiert die Verschiebung der aktuellen Form um distance entlang der Richtung (x, y).
  • glutSolidSphere(GLdouble radius, GLint slices, GLint stacks): Zeichnet eine Kugel, wobei radius der Radius ist, slices die Anzahl der Längslinien und stacks die Anzahl der Breitenlinien.
  • glBegin() und glEnd(): Wenn wir eine Form zeichnen möchten, müssen wir diese beiden Funktionen vor und nach der Zeichnung aufrufen. glBegin() gibt den Typ der zu zeichnenden Form an. Beispielsweise stellt GL_POINTS die Zeichnung von Punkten dar, GL_LINES die Zeichnung von Punkten, die durch Linien verbunden sind, GL_TRIANGLES ein Dreieck mit jeweils drei Punkten und GL_POLYGON zeichnet ein Polygon vom ersten Punkt bis zum n-ten Punkt usw. Beispielsweise können wir, wenn wir einen Kreis zeichnen möchten, ein Polygon mit vielen Seiten verwenden, um ihn zu simulieren:
// r ist der Radius, n ist die Anzahl der Seiten
glBegin(GL_POLYGON);
    for(i=0; i<n; ++i)
        glVertex2f(r*cos(2*PI/n*i), r*sin(2*PI/n*i));
glEnd();

Perspektivische Koordinaten in OpenGL

Im vorherigen Abschnitt haben wir in der SolarSystem-Klasse in OpenGL neun Membervariablen definiert:

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

Um diese neun Variablen zu verstehen, müssen wir zuerst das Konzept der Kameraperspektive in der 3D-Programmierung mit OpenGL aufbauen.

Stellen Sie sich vor, dass die Szenen, die wir normalerweise im Film betrachten, tatsächlich aus der Perspektive der Kamera aufgenommen werden. Daher hat OpenGL auch ein ähnliches Konzept. Wenn wir uns die Kamera als unser eigenes Kopf vorstellen, dann:

  1. viewX, viewY, viewZ entsprechen den Koordinaten des Kopfes (Kameras) im OpenGL-Weltkoordinatensystem;
  2. centerX, centerY, centerZ entsprechen den Koordinaten des betrachteten Objekts (wie es von der Kamera gesehen wird);
  3. upX, upY, upZ entsprechen dem Richtungsvektor, der von der Spitze des Kopfes (Kamerakopf) nach oben zeigt (da wir unseren Kopf neigen können, um ein Objekt zu beobachten).

Mit diesem Verständnis haben Sie jetzt ein Konzept des Koordinatensystems in OpenGL.

Für dieses Experiment nehmen wir an, dass die Anfangsperspektive bei den Koordinaten (x, -x, x) liegt. Daher haben wir:

#define REST 700
#define REST_Y (-REST)
#define REST_Z (REST)

Die Position des beobachteten Objekts (Sonne) liegt bei (0,0,0), daher initialisieren wir in dem Konstruktor der SolarSystem-Klasse die Perspektive wie folgt:

viewX = 0;
viewY = REST_Y;
viewZ = REST_Z;
centerX = centerY = centerZ = 0;
upX = upY = 0;
upZ = 1;

Dann können wir die neun Parameter der Perspektive mit der gluLookAt-Funktion setzen:

gluLookAt(viewX, viewY, viewZ, centerX, centerY, centerZ, upX, upY, upZ);

Nun schauen wir uns gluPerspective(GLdouble fovy,GLdouble aspect,GLdouble zNear,GLdouble zFar) an.

Diese Funktion wird ein symmetrisches perspektivisches Sichtvolumen erstellen, und bevor diese Funktion verwendet wird, muss der Matrixmodus von OpenGL auf GL_PROJECTION gesetzt werden.

Wie in der folgenden Abbildung gezeigt:

OpenGL perspective projection diagram

Das Bild im Fenster wird von der Kamera aufgenommen, und der tatsächliche Inhalt, der aufgenommen wird, befindet sich auf der Fernebene, während der angezeigte Inhalt auf der Nahbene liegt. Daher erfordert diese Funktion vier Parameter:

  • Der erste Parameter ist die Größe des Perspektivwinkels.
  • Der zweite Parameter ist das Seitenverhältnis des tatsächlichen Fensters, wie in der Abbildung aspect=w/h gezeigt.
  • Der dritte Parameter ist der Abstand zur Nahbene.
  • Der vierte Parameter ist der Abstand zur Fernebene.

Lichteffekte in OpenGL

OpenGL teilt das Beleuchtungssystem in drei Teile auf: Lichtquellen, Materialien und Beleuchtungsumgebung.

Wie der Name schon sagt, ist eine Lichtquelle die Quelle des Lichts, wie die Sonne; Materialien beziehen sich auf die Oberflächen verschiedener Objekte, die Licht empfangen, wie Planeten und Satelliten im Sonnensystem außer der Sonne; Die Beleuchtungsumgebung umfasst zusätzliche Parameter, die das endgültige Beleuchtungsergebnis bestimmen, wie die Reflexion von Lichtstrahlen, die durch die Einstellung eines Parameters namens "Ambienthelligkeit" gesteuert werden kann, um das endgültige Bild realistischer zu machen.

In der Physik bleibt das reflektierte Licht parallel, wenn paralleles Licht auf eine glatte Oberfläche trifft. Dieser Art von Reflexion wird "Spiegelreflexion" genannt. Andererseits wird die Reflexion, die durch eine unebene Oberfläche verursacht wird, "Diffusreflexion" genannt.

OpenGL lighting effects example

Lichtquellen

Um das Beleuchtungssystem in OpenGL zu implementieren, müssen wir zuerst die Lichtquellen einrichten. Es ist erwähnenswert, dass OpenGL nur eine begrenzte Anzahl von Lichtquellen unterstützt (insgesamt acht), die durch die Makros GL_LIGHT0 bis GL_LIGHT7 repräsentiert werden. Sie können mit der glEnable-Funktion aktiviert und mit der glDisable-Funktion deaktiviert werden. Beispiel: glEnable(GL_LIGHT0);

Die Position der Lichtquelle wird mit der glLightfv-Funktion gesetzt, Beispiel:

GLfloat light_position[] = {0.0f, 0.0f, 0.0f, 1.0f};
glLightfv(GL_LIGHT0, GL_POSITION, light_position); // Specify the position of light source 0

Die Position wird durch vier Werte dargestellt, (x, y, z, w). Wenn w 0 ist, bedeutet dies, dass die Lichtquelle unendlich weit entfernt ist. Die Werte von x, y und z bestimmen die Richtung dieser unendlich entfernten Lichtquelle. Wenn w nicht 0 ist, repräsentiert es eine positionale Lichtquelle, und ihre Position ist (x/w, y/w, z/w).

Materialien

Um das Material eines Objekts zu setzen, werden im Allgemeinen fünf Attribute benötigt:

  1. Die Intensität des Lichts, das nach mehreren Reflexionen im Umfeld verbleibt.
  2. Die Intensität des Lichts nach Diffusreflexion.
  3. Die Intensität des Lichts nach Spiegelreflexion.
  4. Die Intensität des Lichts, das von nicht leuchtenden Objekten in OpenGL emittiert wird, was schwach ist und andere Objekte nicht beeinflusst.
  5. Der Spiegelungsindex, der die Rauheit des Materials repräsentiert. Ein kleinerer Wert bedeutet rauheres Material, und wenn das von einer Punktlichtquelle emittierte Licht darauf scheint, werden größere Leuchtpunkte erzeugt. Umgekehrt bedeutet ein größerer Wert, dass das Material eher eine Spiegelfläche ist und kleinere Leuchtpunkte produziert.

OpenGL bietet zwei Versionen von Funktionen zum Setzen von Materialien:

void glMaterialf(GLenum face, GLenum pname, TYPE param);
void glMaterialfv(GLenum face, GLenum pname, TYPE *param);

Der Unterschied zwischen ihnen besteht darin, dass nur ein Wert für den Spiegelungsindex gesetzt werden muss, daher wird glMaterialf verwendet. Für andere Materialeinstellungen, die mehrere Werte erfordern, wird ein Array verwendet, und die Version mit Zeigervektorparametern, glMaterialfv, wird verwendet. Beispiel:

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

Beleuchtungsumgebung

Standardmäßig behandelt OpenGL keine Beleuchtung. Um die Beleuchtungsfunktionalität zu aktivieren, müssen Sie den Makro GL_LIGHTING verwenden, d.h. glEnable(GL_LIGHTING);

Planeten zeichnen

Wenn wir einen Planeten zeichnen, müssen wir zuerst seinen Umlaufswinkel und Drehzahl berücksichtigen. Daher können wir zunächst die Memberfunktion Star::update(long timeSpan) der Star-Klasse in der Datei ~/project/stars.cpp implementieren:

void Star::update(long timeSpan) {
    alpha += timeSpan * speed;  // Aktualisiere Umlaufswinkel
    alphaSelf += selfSpeed;     // Aktualisiere Drehzahl
}

Nachdem wir die Umlauf- und Drehzahl aktualisiert haben, können wir den spezifischen Planeten basierend auf den Parametern zeichnen:

void Star::drawStar() {

    glEnable(GL_LINE_SMOOTH);
    glEnable(GL_BLEND);

    int n = 1440;

    // Speichere die aktuelle OpenGL-Matrixumgebung
    glPushMatrix();
    {
        // Umlauf

        // Wenn es sich um einen Planeten handelt und der Abstand nicht 0 ist, dann verschiebe ihn um einen Radius in den Ursprung
        // Dieser Teil wird für Satelliten verwendet
        if (parentStar!= 0 && parentStar->distance > 0) {
            // Drehe das zeichnende Grafikobjekt um die z-Achse um alpha
            glRotatef(parentStar->alpha, 0, 0, 1);
            // Verschiebe in x-Richtung um den Abstand, während die y- und z-Richtungen unverändert bleiben
            glTranslatef(parentStar->distance, 0.0, 0.0);
        }
        // Zeichne die Umlaufbahn
        glBegin(GL_LINES);
        for(int i=0; i<n; ++i)
            glVertex2f(distance * cos(2 * PI * i / n),
                       distance * sin(2 * PI * i / n));
        glEnd();
        // Drehe um die z-Achse um alpha
        glRotatef(alpha, 0, 0, 1);
        // Verschiebe in x-Richtung um den Abstand, während die y- und z-Richtungen unverändert bleiben
        glTranslatef(distance, 0.0, 0.0);

        // Drehung
        glRotatef(alphaSelf, 0, 0, 1);

        // Zeichne die Planetenfarbe
        glColor3f(rgbaColor[0], rgbaColor[1], rgbaColor[2]);
        glutSolidSphere(radius, 40, 32);
    }
    // Wiederherstelle die Matrixumgebung vor dem Zeichnen
    glPopMatrix();

}

Dieser Code verwendet die sin()- und cos()-Funktionen, was #include<cmath> erforderlich macht.

Zeichnen der Beleuchtung

Für die Klasse Planet, die einen nicht leuchtenden Himmelskörper darstellt, müssen wir seinen Beleuchtungseffekt zeichnen. Fügen Sie den folgenden Code zur Datei ~/project/stars.cpp hinzu:

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

Was die Klasse LightPlanet betrifft, die einen leuchtenden Himmelskörper darstellt, müssen wir nicht nur sein Beleuchtungsmaterial einstellen, sondern auch seine Lichtquelleposition setzen:

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); // Specify the position of light source 0
    glLightfv(GL_LIGHT0, GL_AMBIENT,  light_ambient);  // Represents the intensity of light rays from various sources that reach the material, after multiple reflections and tracing
    glLightfv(GL_LIGHT0, GL_DIFFUSE,  light_diffuse);  // Intensity of light after diffuse reflection
    glLightfv(GL_LIGHT0, GL_SPECULAR, light_specular); // Intensity of light after specular reflection

}

Fenster zeichnen

Im vorherigen Abschnitt haben wir die zwei wichtigsten Funktionen für die Behandlung der Bildanzeige erwähnt: glutDisplayFunc und glutIdleFunc. glutDisplayFunc führt die Callback-Funktion aus, wenn GLUT bestimmt, dass der Fensterinhalt aktualisiert werden muss, während glutIdleFunc die Callback-Funktion bei einem inaktiven Ereignisschleife behandelt.

Um das gesamte Sonnensystem zu bewegen, müssen wir bedenken, wann die Positionen der Planeten aktualisiert werden sollen und wann die Ansicht aktualisiert werden soll.

Offensichtlich sollte glutDisplayFunc sich auf das Aktualisieren der Ansicht konzentrieren, und wenn das Ereignis inaktiv ist, können wir beginnen, die Positionen der Planeten zu aktualisieren. Nachdem die Positionen aktualisiert wurden, können wir dann die Ansichtsaktualisierungsfunktion aufrufen, um die Anzeige zu aktualisieren.

Wir können daher zunächst die Memberfunktion SolarSystem::onUpdate() implementieren, die in glutDisplayFunc aufgerufen wird:

#define TIMEPAST 1 // Nehmen wir an, dass jede Aktualisierung einen Tag dauert
void SolarSystem::onUpdate() {

    for (int i=0; i<STARS_NUM; i++)
        stars[i]->update(TIMEPAST); // Aktualisiere die Positionen der Sterne

    this->onDisplay(); // Aktualisiere die Anzeige
}

Als Nächstes wird die Aktualisierung der Anzeigeansicht in SolarSystem::onDisplay() implementiert:

void SolarSystem::onDisplay() {

    // Leere den Viewport-Puffer
    glClear(GL_COLOR_BUFFER_BIT  |  GL_DEPTH_BUFFER_BIT);
    // Leere und setze den Farbpuffer
    glClearColor(.7f,.7f,.7f,.1f);
    // Wähle die aktuelle Matrix als Projektionsmatrix
    glMatrixMode(GL_PROJECTION);
    // Setze die angegebene Matrix als Identitätsmatrix
    glLoadIdentity();
    // Definiere das aktuelle Sichtvolumen
    gluPerspective(75.0f, 1.0f, 1.0f, 40000000);
    // Wähle die aktuelle Matrix als Modellansichtsmatrix
    glMatrixMode(GL_MODELVIEW);
    // Setze die aktuelle Matrix als Identitätsmatrix
    glLoadIdentity();
    // Definiere die Sichtmatrix und multipliziere sie mit der aktuellen Matrix
    gluLookAt(viewX, viewY, viewZ, centerX, centerY, centerZ, upX, upY, upZ);

    // Setze die erste Lichtquelle (Lichtquelle 0)
    glEnable(GL_LIGHT0);
    // Aktiviere die Beleuchtung
    glEnable(GL_LIGHTING);
    // Aktiviere die Tiefentests, um automatisch verdeckte Grafiken basierend auf den Koordinaten zu verstecken
    glEnable(GL_DEPTH_TEST);

    // Zeichne die Planeten
    for (int i=0; i<STARS_NUM; i++)
        stars[i]->draw();

    // Wir haben GLUT_DOUBLE beim Initialisieren des Anzeigemodus in der Hauptfunktion verwendet
    // Wir müssen glutSwapBuffers verwenden, um das Pufferswapping für das Zweipuffersystem nach dem Zeichnen durchzuführen
    glutSwapBuffers();
}

Klassenkontstruktoren und -destruktoren

Die Konstruktoren der in stars.hpp definierten Klassen müssen die Membervariablen der Klassen initialisieren. Dieser Teil ist relativ einfach und sogar Standarddestruktoren können verwendet werden, daher sollten Sie diese Konstruktoren selbst implementieren:

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]);

Hinweis: Beachten Sie, dass Sie die Geschwindigkeitsvariable in die Winkelgeschwindigkeit umwandeln, wenn Sie sie initialisieren. Die Umrechnungsformel lautet: alpha_speed = 360/speed.

Für den Konstruktor von solarsystem.cpp müssen wir alle Planeten initialisieren. Hier geben wir die Parameter zwischen den Planeten für die Bequemlichkeit an:

// Umlaufradius
#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

// Entfernung zur Sonne
#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

// Bewegungsgeschwindigkeit
#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

// Eigenrotation
#define SELFROTATE 3

// Definiere eine Makro, um das Setzen eines mehrdimensionalen Arrays zu erleichtern
#define SET_VALUE_3(name, value0, value1, value2) \
                   ((name)[0])=(value0), ((name)[1])=(value1), ((name)[2])=(value2)

// Im vorherigen Experiment haben wir die Enumeration der Planeten definiert
enum STARS {Sun, Mercury, Venus, Earth, Moon,
    Mars, Jupiter, Saturn, Uranus, Neptune};

Hinweis:

Wir definieren hier ein Makro SET_VALUE_3. Sie könnten denken, dass wir eine Funktion schreiben können, um das gleiche Ziel zu erreichen.

Tatsächlich erledigen Makros die gesamte Substitutionsarbeit im Kompilierungsprozess, während die Definition von Funktionen

Dies erfordert Funktionsstackoperationen beim Aufruf, was weit weniger effizient ist als die Makroverarbeitung im Kompilierungsprozess.

Deshalb können Makros effizienter sein.

Es ist jedoch zu beachten, dass obwohl Makros effizienter sein können, das übermäßige Verwenden zu hässlichem und weniger lesbarem Code führen kann. Die angemessene Verwendung von Makros wird dagegen empfohlen.

Wir können daher den Konstruktor der SolarSystem-Klasse implementieren, wobei die Farben der Planeten zufällig ausgewählt werden. Die Leser können die Farben der Planeten selbst ändern:

SolarSystem::SolarSystem() {

    // Definiere die Perspektivansicht, wie wir es zuvor bei der Initialisierung der Perspektivansicht diskutiert haben
    viewX = 0;
    viewY = REST_Y;
    viewZ = REST_Z;
    centerX = centerY = centerZ = 0;
    upX = upY = 0;
    upZ = 1;

    // Sonne
    GLfloat rgbColor[3] = {1, 0, 0};
    stars[Sun]     = new LightPlanet(SUN_RADIUS, 0, 0, SELFROTATE, 0, rgbColor);
    // Merkur
    SET_VALUE_3(rgbColor,.2,.2,.5);
    stars[Mercury] = new Planet(MER_RADIUS, MER_DIS, MER_SPEED, SELFROTATE, stars[Sun], rgbColor);
    // Venus
    SET_VALUE_3(rgbColor, 1,.7, 0);
    stars[Venus]   = new Planet(VEN_RADIUS, VEN_DIS, VEN_SPEED, SELFROTATE, stars[Sun], rgbColor);
    // Erde
    SET_VALUE_3(rgbColor, 0, 1, 0);
    stars[Earth]   = new Planet(EAR_RADIUS, EAR_DIS, EAR_SPEED, SELFROTATE, stars[Sun], rgbColor);
    // Mond
    SET_VALUE_3(rgbColor, 1, 1, 0);
    stars[Moon]    = new Planet(MOO_RADIUS, MOO_DIS, MOO_SPEED, SELFROTATE, stars[Earth], rgbColor);
    // Mars
    SET_VALUE_3(rgbColor, 1,.5,.5);
    stars[Mars]    = new Planet(MAR_RADIUS, MAR_DIS, MAR_SPEED, SELFROTATE, stars[Sun], rgbColor);
    // Jupiter
    SET_VALUE_3(rgbColor, 1, 1,.5);
    stars[Jupiter] = new Planet(JUP_RADIUS, JUP_DIS, JUP_SPEED, SELFROTATE, stars[Sun], rgbColor);
    // Saturn
    SET_VALUE_3(rgbColor,.5, 1,.5);
    stars[Saturn]  = new Planet(SAT_RADIUS, SAT_DIS, SAT_SPEED, SELFROTATE, stars[Sun], rgbColor);
    // Uranus
    SET_VALUE_3(rgbColor,.4,.4,.4);
    stars[Uranus]  = new Planet(URA_RADIUS, URA_DIS, URA_SPEED, SELFROTATE, stars[Sun], rgbColor);
    // Neptun
    SET_VALUE_3(rgbColor,.5,.5, 1);
    stars[Neptune] = new Planet(NEP_RADIUS, NEP_DIS, NEP_SPEED, SELFROTATE, stars[Sun], rgbColor);

}

Zusätzlich vergessen Sie nicht, den zugewiesenen Speicher im Destruktor freizugeben:

SolarSystem::~SolarSystem() {
    for(int i = 0; i<STARS_NUM; i++)
        delete stars[i];
}

Implementierung der Perspektivänderung mit Tastaturtasten

Um die Perspektivänderung zu steuern, können wir die fünf Tasten w, a, s, d, x auf der Tastatur verwenden und die Taste r verwenden, um die Perspektive zurückzusetzen. Zunächst müssen wir die Größe der Perspektivänderung nach jedem Tastendruck bestimmen. Hier definieren wir ein Makro OFFSET. Anschließend können wir das Tastendruckverhalten des Benutzers durch Überprüfung des übergebenen Parameters key bestimmen.

#define OFFSET 20
void SolarSystem::onKeyboard(unsigned char key, int x, int y) {

    switch (key)    {
        case 'w': viewY += OFFSET; break; // Erhöhe die Y-Achsenposition der Kamera um 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;
    }

}

Ausführen und Testen

Wir haben hauptsächlich den Code in den Dateien stars.cpp und solarsystem.cpp implementiert.

Der Code für stars.cpp lautet wie folgt:

Klicken Sie, um den vollständigen Code anzuzeigen
//
//  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);

}

Der Code für solarsystem.cpp lautet wie folgt:

Klicken Sie, um den vollständigen Code anzuzeigen
//
// 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];
}

Führen Sie im Terminal aus:

make && ./solarsystem

Das Ergebnis ist in der Abbildung dargestellt:

Solar system simulation preview

Da die Farben der Planeten einfarbig sind, ist der Beleuchtungseffekt nicht sehr offensichtlich, ist aber dennoch sichtbar. Beispielsweise hat Jupiter auf der rechten Seite einen weißlichen Anstrich.

Zusammenfassung

In diesem Projekt haben wir ein einfaches Modell des Sonnensystems erreicht. Mit Hilfe des Beleuchtungssystems in OpenGL können wir die Beleuchtungseffekte der Planeten beobachten, während sie sich um die Sonne drehen. Darüber hinaus können wir die Perspektive mit der Tastatur einstellen, um das Sonnensystem aus verschiedenen Winkeln zu beobachten.

✨ Lösung prüfen und üben✨ Lösung prüfen und üben✨ Lösung prüfen und üben✨ Lösung prüfen und üben✨ Lösung prüfen und üben✨ Lösung prüfen und üben✨ Lösung prüfen und üben✨ Lösung prüfen und üben✨ Lösung prüfen und üben✨ Lösung prüfen und üben✨ Lösung prüfen und üben✨ Lösung prüfen und üben✨ Lösung prüfen und üben