Création du système solaire avec OpenGL

C++C++Beginner
Pratiquer maintenant

💡 Ce tutoriel est traduit par l'IA à partir de la version anglaise. Pour voir la version originale, vous pouvez cliquer ici

Introduction

Dans ce projet, nous allons créer une simulation du système solaire à l'aide d'OpenGL. La simulation comprendra le soleil, les planètes et leurs mouvements et rotations. Nous utiliserons GLUT (OpenGL Utility Toolkit) pour gérer les fonctions de fenêtre et d'entrée, et OpenGL pour le rendu.

En terminant ce projet, vous apprendrez :

  • Les concepts de base de la programmation graphique utilisant OpenGL
  • Comment créer des modèles 3D et les rendre dans un environnement simulé
  • Comment gérer l'entrée de l'utilisateur et mettre à jour la simulation en conséquence
  • Comment implémenter un système d'éclairage de base pour améliorer la qualité visuelle de la simulation
  • Comment organiser le code en utilisant les principes de la programmation orientée objet

Ce projet suppose une compréhension de base de la programmation C++ et une certaine familiarité avec les concepts de programmation graphique. Il vous procurera une expérience pratique dans la construction d'une application graphique simple utilisant OpenGL.

👀 Aperçu

Aperçu de la simulation du système solaire

🎯 Tâches

Dans ce projet, vous apprendrez :

  • Comment installer les bibliothèques nécessaires et configurer l'environnement de développement.
  • Comment créer les classes nécessaires et implémenter la fonctionnalité de base de la rotation et de la révolution des planètes.
  • Comment configurer la perspective et la projection pour la scène 3D.
  • Comment implémenter le système d'éclairage pour améliorer la qualité visuelle de la simulation.
  • Comment gérer l'entrée de l'utilisateur pour permettre à l'utilisateur de contrôler la perspective de la simulation.
  • Comment tester et affiner la simulation pour vous assurer qu'elle fonctionne comme prévu.

🏆 Réalisations

Après avoir terminé ce projet, vous serez capable de :

  • Appliquer les concepts de base de la programmation graphique utilisant OpenGL.
  • Créer des modèles 3D et les rendre dans un environnement simulé.
  • Implémenter un système d'éclairage de base pour améliorer la qualité visuelle de la simulation.
  • Organiser le code en utilisant les principes de la programmation orientée objet.
  • Montrer des compétences en résolution de problèmes et en débogage.

Comprendre OpenGL et GLUT

OpenGL contient de nombreuses fonctions de rendu, mais leur but de conception est indépendant de tout système de fenêtre ou de système d'exploitation. Par conséquent, il ne comprend pas de fonctions pour créer des fenêtres ouvertes, lire des événements du clavier ou de la souris, ou même la fonctionnalité la plus basique d'affichage de fenêtres. Ainsi, il est complètement impossible de créer un programme graphique complet en utilisant uniquement OpenGL. De plus, la plupart des programmes doivent interagir avec l'utilisateur (répondre aux opérations du clavier et de la souris). GLUT offre cette commodité.

GLUT signifie OpenGL Utility Toolkit. C'est une bibliothèque d'outils pour gérer les programmes OpenGL, principalement responsable de la gestion des appels au système d'exploitation sous-jacent et des opérations d'entrée/sortie. L'utilisation de GLUT peut masquer certains détails de la mise en œuvre de l'interface graphique du système d'exploitation sous-jacent, et il suffit de l'API GLUT pour créer des fenêtres d'application, gérer les événements de souris et de clavier, etc., permettant ainsi d'obtenir une compatibilité multiplateforme.

Installons d'abord GLUT dans l'environnement d'expérience :

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

La structure d'un programme GLUT standard est montrée dans le code ci-dessous :

// Fichier d'en-tête de base pour utiliser GLUT
#include <GL/glut.h>

// Macros de base pour créer une fenêtre graphique
#define WINDOW_X_POS 50
#define WINDOW_Y_POS 50
#define WIDTH 700
#define HEIGHT 700

// Fonctions de rappel enregistrées avec GLUT
void onDisplay(void);
void onUpdate(void);
void onKeyboard(unsigned char key, int x, int y);

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

    // Initialise GLUT et traite tous les arguments de ligne de commande
    glutInit(&argc, argv);
    // Cette fonction spécifie si l'on utilise le mode RGBA ou le mode d'index de couleur pour l'affichage. Elle peut également spécifier si l'on utilise une fenêtre mono-bufférée ou double-bufférée. Ici, nous utilisons le RGBA et une fenêtre double-bufférée.
    glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE);
    // Définit la position du coin supérieur gauche de la fenêtre lorsqu'elle est créée sur l'écran
    glutInitWindowPosition(WINDOW_X_POS, WINDOW_Y_POS);
    // Définit la largeur et la hauteur de la fenêtre lorsqu'elle est créée, pour simplifier
    glutInitWindowSize(WIDTH, HEIGHT);
    // Crée une fenêtre, et la chaîne d'entrée est le titre de la fenêtre
    glutCreateWindow("SolarSystem at LabEx");

    // Le prototype de glutDisplayFunc est glutDisplayFunc(void (*func)(void))
    // Il s'agit d'une fonction de rappel, et elle sera exécutée chaque fois que GLUT détermine que le contenu d'une fenêtre doit être mis à jour et affiché.
    //
    // glutIdleFunc(void (*func)(void)) spécifie une fonction à exécuter lorsque la boucle d'événements est inactive. Cette fonction de rappel prend un pointeur de fonction comme seul paramètre.
    //
    // glutKeyboardFunc(void (*func)(unsigned char key, int x, int y)) associe une touche du clavier à une fonction. Cette fonction est appelée lorsque la touche est pressée ou relâchée.
    //
    // Par conséquent, les trois lignes suivantes enregistrent en fait les trois fonctions de rappel de touches avec GLUT
    glutDisplayFunc(onDisplay);
    glutIdleFunc(onUpdate);
    glutKeyboardFunc(onKeyboard);

    glutMainLoop();
    return 0;

}

Créez un fichier main.cpp dans le répertoire ~/project/ et écrivez le code suivant :

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

Astuces

  • Le mono-buffering consiste à exécuter toutes les commandes de dessin directement sur la fenêtre, ce qui est lent. Si la puissance de traitement de l'ordinateur est insuffisante et que le mono-buffering est utilisé, l'écran peut clignoter.
  • Le double-buffering consiste à exécuter les commandes de dessin dans un tampon en mémoire, ce qui est beaucoup plus rapide. Une fois les commandes de dessin terminées, les résultats sont copiés sur l'écran par un échange de tampons. L'avantage de cette approche est que si nous laissons les opérations de dessin s'exécuter en temps réel avec la carte graphique, lorsque les tâches de dessin sont complexes, les opérations d'entrée/sortie deviennent complexes, entraînant une performance plus faible. En revanche, avec le double-buffering, les résultats de dessin terminés sont envoyés directement à la carte graphique pour le rendu lors de l'échange de tampons, ce qui réduit considérablement l'entrée/sortie.

En OpenGL, il est recommandé d'utiliser GLfloat pour représenter les nombres à virgule flottante.

✨ Vérifier la solution et pratiquer

Conception de classes

En programmation orientée objet (POO), il est essentiel de préciser d'abord quels objets nous manipulons. Évidemment, dans l'ensemble du système céleste, ce sont toutes des planètes (Star), et la distinction entre les planètes et les étoiles ne dépend que du fait qu'elles ont ou non un nœud parent. Ensuite, pour les différentes planètes, elles ont généralement leurs propres matériaux, et différents matériaux déterminent s'ils émettent de la lumière ou non. Par conséquent, nous avons un modèle d'objet préliminaire. Nous divisons donc les planètes en : des planètes ordinaires qui peuvent tourner et orbiter autour d'un point (Star), des planètes avec des matériaux spéciaux (Planet), et des planètes qui peuvent émettre de la lumière (LightPlanet).

De plus, pour faciliter la mise en œuvre du programme, nous devons faire quelques hypothèses sur le modèle de programmation dans le monde réel :

  1. L'orbite de la planète est circulaire ;
  2. La vitesse de rotation reste constante ;
  3. Chaque fois que l'écran est rafraîchi, on suppose qu'un jour s'est écoulé.

Tout d'abord, nous pouvons considérer les étapes suivantes pour implémenter la logique :

  1. Initialiser les objets planètes ;
  2. Initialiser le moteur OpenGL, implémenter onDraw et onUpdate ;
  3. Chaque planète devrait être responsable de gérer ses propres propriétés, ses relations d'orbite et les dessins liés à la transformation. Par conséquent, lors de la conception de la classe planète, une méthode de dessin draw() devrait être fournie ;
  4. La planète devrait également gérer ses propres mises à jour liées à la rotation et à l'orbite pour l'affichage. Par conséquent, lors de la conception de la classe planète, une méthode de mise à jour update() devrait également être fournie ;
  5. Appeler la méthode draw() de la planète dans onDraw() ;
  6. Appeler la méthode update() de la planète dans onUpdate() ;
  7. Adapter l'affichage de l'ensemble du système solaire avec le clavier dans onKeyboard().

En outre, pour chaque planète, elles ont les attributs suivants :

  1. Couleur color
  2. Rayon d'orbite radius
  3. Vitesse de rotation selfSpeed
  4. Vitesse d'orbite speed
  5. Distance au centre du soleil distance
  6. Planète parent d'orbite parentStar
  7. Angle de rotation actuel alphaSelf
  8. Angle d'orbite actuel alpha

Dans le répertoire ~/project/, créez un fichier stars.hpp. Sur la base de l'analyse ci-dessus, nous pouvons concevoir le code de classe suivant :

class Star {
public:
    // Rayon d'orbite de la planète
    GLfloat radius;
    // Vitesse d'orbite et de rotation de la planète
    GLfloat speed, selfSpeed;
    // Distance du centre de la planète au centre de la planète parent
    GLfloat distance;
    // Couleur de la planète
    GLfloat rgbaColor[4];

    // Planète parent
    Star* parentStar;

    // Constructeur, lors de la construction d'une planète, le rayon de rotation, la vitesse de rotation, la vitesse de rotation et l'orbite (planète parent) doivent être fournis
    Star(GLfloat radius, GLfloat distance,
         GLfloat speed,  GLfloat selfSpeed,
         Star* parentStar);
    // Dessiner les mouvements, la rotation et autres activités de la planète commune
    void drawStar();
    // Fournir une implémentation par défaut pour appeler drawStar()
    virtual void draw() { drawStar(); }
    // Le paramètre est la durée de rafraîchissement de chaque écran
    virtual void update(long timeSpan);
protected:
    GLfloat alphaSelf, alpha;
};
class Planet : public Star {
public:
    // Constructeur
    Planet(GLfloat radius, GLfloat distance,
           GLfloat speed,  GLfloat selfSpeed,
           Star* parentStar, GLfloat rgbColor[3]);
    // Ajouter des matériaux aux planètes avec leurs propres matériaux
    void drawPlanet();
    // Continuer à ouvrir la fonction de redéfinition à ses sous-classes
    virtual void draw() { drawPlanet(); drawStar(); }
};
class LightPlanet : public Planet {
public:
    LightPlanet(GLfloat Radius, GLfloat Distance,
                GLfloat Speed,  GLfloat SelfSpeed,
                Star* ParentStar, GLfloat rgbColor[]);
    // Ajouter l'éclairage aux étoiles qui fournissent des sources de lumière
    void drawLight();
    virtual void draw() { drawLight(); drawPlanet(); drawStar(); }
};

En outre, nous devons également considérer la conception de la classe SolarSystem. Dans le système solaire, il est évident que le système solaire est composé de diverses planètes. Pour le système solaire, le rafraîchissement de l'affichage après le mouvement des planètes devrait être géré par le système solaire. Par conséquent, les variables membres de SolarSystem devraient inclure des variables contenant les planètes, et les fonctions membres devraient gérer le rafraîchissement de l'affichage et les événements de réponse au clavier à l'intérieur du système solaire. Par conséquent, dans le répertoire ~/project/, créez un fichier solarsystem.hpp, et dans celui-ci, nous pouvons concevoir la classe SolarSystem :

class SolarSystem {

public:

    SolarSystem();
    ~SolarSystem();

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

private:
    Star *stars[STARS_NUM];

    // Définir les paramètres de l'angle de vue
    GLdouble viewX, viewY, viewZ;
    GLdouble centerX, centerY, centerZ;
    GLdouble upX, upY, upZ;
};

Astuces

  • Ici, nous utilisons la forme traditionnelle d'un tableau pour gérer toutes les planètes au lieu d'utiliser le vecteur en C++, car la forme traditionnelle d'un tableau est suffisante.
  • La définition de l'angle de vue en OpenGL est un concept complexe qui nécessite une certaine longueur pour être expliqué. Nous mentionnerons brièvement que la définition de l'angle de vue nécessite au moins neuf paramètres ici. Nous expliquerons en détail leurs fonctions plus tard lors de leur implémentation dans la section suivante.

Enfin, nous devons également considérer les paramètres et les variables de base.

Dans SolarSystem, y compris le soleil, il y a au total neuf planètes (excluant Pluton), mais dans la classe Star que nous avons conçue, chaque objet Star a les attributs d'une Star, donc nous pouvons également implémenter les satellites de ces planètes, comme la Lune orbitant autour de la Terre. Par conséquent, nous considérons implémenter au total dix planètes. Nous pouvons donc définir l'énumération suivante pour indexer les planètes dans un tableau :

#define STARS_NUM 10
enum STARS {
    Sun,        // Soleil
    Mercury,    // Mercure
    Venus,      // Vénus
    Earth,      // Terre
    Moon,       // Lune
    Mars,       // Mars
    Jupiter,    // Jupiter
    Saturn,     // Saturne
    Uranus,     // Uranus
    Neptune     // Neptune
};
Star * stars[STARS_NUM];

Nous avons également supposé que les vitesses de rotation étaient les mêmes, donc nous les définissons à l'aide d'une macro :

#define TIMEPAST 1
#define SELFROTATE 3

À ce stade, déplacez les fonctions membres non implémentées vers le fichier .cpp correspondant, et nous avons terminé l'expérience de cette section.

✨ Vérifier la solution et pratiquer

Sommaire du code

Résumons le code à compléter dans les expériences précédentes :

Tout d'abord, dans main.cpp, nous créons un objet SolarSystem puis nous déléguons le rafraîchissement de l'affichage, le rafraîchissement au repos et la gestion des événements clavier à glut :

Cliquez pour voir le code complet
//
//  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;
}

Deuxièmement, dans stars.hpp, nous créons les classes Star, Planet et LightPlanet :

Cliquez pour voir le code complet
//
//  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 */

Dans le répertoire ~/project/, créez un fichier stars.cpp et complétez les implémentations des fonctions membres correspondantes issues de stars.hpp :

Cliquez pour voir le code complet
//
//  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:
}

Dans solarsystem.hpp, la classe SolarSystem est conçue :

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

Dans le répertoire ~/project/, créez un fichier solarsystem.cpp et implémentez les fonctions membres correspondantes issues de solarsystem.hpp :

Cliquez pour voir le code complet
//
// 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:
}

Dans le répertoire ~/project/, créez un fichier Makefile et ajoutez le code suivant à l'intérieur :

Veuillez le saisir manuellement, ne pas copier et coller directement, et n'oubliez pas d'utiliser la touche <tab> au lieu de l'espace.

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

Lorsque vous écrivez la commande de compilation, attention au placement de -lglut -lGLU -lGL. Cela est car l'utilisation de l'option -l dans le compilateur g++ est un peu particulière.

Par exemple : foo1.cpp -lz foo2.cpp, si le fichier cible foo2.cpp utilise des fonctions de la bibliothèque z, ces fonctions ne seront pas directement chargées. Cependant, si foo1.o utilise des fonctions de la bibliothèque z, aucune erreur de compilation n'aura lieu.

En d'autres termes, tout le processus de liaison se déroule de gauche à droite. Lorsqu'un symbole de fonction non résolu est rencontré dans foo1.cpp, il cherche la bibliothèque de liaison de droite. Lorsqu'il rencontre l'option z, il cherche dans z et trouve la fonction, permettant ainsi de terminer la liaison sans problème. Par conséquent, la bibliothèque avec l'option -l devrait être placée à droite de tous les fichiers compilés.

Enfin, exécutez dans le terminal :

make && ./solarsystem

Vous pouvez voir que la fenêtre a été créée, mais qu'il n'y a rien dedans (elle affiche ce qui se trouve derrière la fenêtre). C'est parce que nous n'avons pas encore implémenté le mécanisme de rafraîchissement des graphiques dans la fenêtre. Nous continuerons à compléter le code restant dans l'expérience suivante pour faire fonctionner la simulation complète du système solaire.

✨ Vérifier la solution et pratiquer

Le concept de matrices en OpenGL

En algèbre linéaire, nous sommes familiers avec le concept de matrices, mais notre compréhension de leur but et de leur fonction spécifique peut être limitée. Alors, qu'est-ce exactement qu'une matrice?

Commençons par examiner l'équation suivante :

x = Ab

A est une matrice, et x, b sont des vecteurs.

Première perspective :

Si x et b sont tous deux des vecteurs dans notre espace tridimensionnel, que fait A? Il transforme b en (ou vers) x. Sous cet angle, une matrice A peut être comprise comme une transformation.

Maintenant, considérons une autre équation :

Ax = By

A, B sont des matrices, et x, y sont des vecteurs.

Deuxième perspective :

Pour deux vecteurs différents x et y, ils sont essentiellement les mêmes car on peut les rendre égaux en les multipliant par les matrices A et B. Sous cet angle, une matrice A peut être comprise comme un système de coordonnées. En d'autres termes, les vecteurs eux-mêmes sont uniques, mais nous définissons un système de coordonnées pour les décrire. Étant donné qu'on utilise différents systèmes de coordonnées, les coordonnées des vecteurs varieront. Dans ce cas, pour le même vecteur, il a des coordonnées différentes dans différents systèmes de coordonnées.
La matrice A décrit précisément un système de coordonnées, et la matrice B décrit un autre système de coordonnées. Lorsque ces deux systèmes de coordonnées sont appliqués à x et y, ils donnent le même résultat, ce qui signifie que x et y sont essentiellement le même vecteur mais avec différents systèmes de coordonnées.

En combinant ces deux perspectives, on peut conclure que l'essence d'une matrice est de décrire un mouvement.

Dans le contexte d'OpenGL, il existe une matrice responsable des transformations de rendu, connue sous le nom de mode de matrice en OpenGL.

Comme mentionné précédemment, une matrice peut décrire à la fois la transformation d'un objet et le système de coordonnées dans lequel il existe. Par conséquent, lorsqu'on traite différentes opérations, on doit définir différents modes de matrice en OpenGL. Cela est réalisé à l'aide de la fonction glMatrixMode().

Cette fonction accepte trois modes différents : GL_PROJECTION pour les opérations de projection, GL_MODELVIEW pour les opérations de vue-modèle, et GL_TEXTURE pour les opérations de texture.

GL_PROJECTION indique à OpenGL que des opérations de projection seront effectuées, projetant l'objet sur un plan. Lorsque ce mode est activé, la matrice doit être définie comme la matrice identité à l'aide de glLoadIdentity(), suivies d'opérations telles que la définition de la perspective à l'aide de gluPerspective (nous expliquerons cette fonction en détail plus tard lorsque nous aborderons le concept de vue en OpenGL).

GL_MODELVIEW indique à OpenGL que les instructions suivantes seront utilisées pour décrire une opération basée sur un modèle, comme la définition du point de vue de la caméra. De même, après avoir activé ce mode, nous devons définir le mode de matrice OpenGL comme la matrice identité.

GL_TEXTURE est utilisé pour les opérations liées aux textures, que nous ne détaillerons pas pour le moment.

Si vous n'êtes pas familier avec le concept de matrices, vous pouvez simplement comprendre glMatrixMode() comme des déclarations à OpenGL sur les opérations à venir. Avant de rendre ou de faire tourner un objet, nous devons utiliser glPushMatrix pour enregistrer l'environnement de matrice actuel ; sinon, des erreurs de dessin mystérieuses peuvent se produire.

✨ Vérifier la solution et pratiquer

API courantes de dessin d'images en OpenGL

OpenGL fournit de nombreuses API courantes liées au dessin graphique. Voici quelques-unes d'entre elles que nous allons brièvement présenter et utiliser dans le code suivant :

  • glEnable(GLenum cap): Cette fonction est utilisée pour activer diverses fonctionnalités offertes par OpenGL. Le paramètre cap est une macro interne à OpenGL, qui fournit des effets tels que l'éclairage, le brouillard et la diffusion.
  • glPushMatrix() et glPopMatrix(): Ces fonctions enregistrent la matrice actuelle au sommet de la pile (enregistrant la matrice actuelle).
  • glRotatef(alpha, x, y, z): Représente la rotation de la forme actuelle dans le sens inverse des aiguilles d'une montre de alpha degrés le long de l'axe (x, y, z).
  • glTranslatef(distance, x, y): Représente la translation de la forme actuelle de distance le long de la direction (x, y).
  • glutSolidSphere(GLdouble radius, GLint slices, GLint stacks): Dessine une sphère, où radius est le rayon, slices est le nombre de lignes longitudinales et stacks est le nombre de lignes latitudinales.
  • glBegin() et glEnd(): Lorsque nous voulons dessiner une forme, nous devons appeler ces deux fonctions avant et après le dessin. glBegin() spécifie le type de forme à dessiner. Par exemple, GL_POINTS représente le dessin de points, GL_LINES représente le dessin de points connectés par des lignes, GL_TRIANGLES complète un triangle avec chaque groupe de trois points, et GL_POLYGON dessine un polygone du premier point au n-ième point, etc. Par exemple, lorsque nous devons dessiner un cercle, nous pouvons l'imiter en utilisant un polygone avec de nombreux côtés :
// r est le rayon, n est le nombre de côtés
glBegin(GL_POLYGON);
    for(i=0; i<n; ++i)
        glVertex2f(r*cos(2*PI/n*i), r*sin(2*PI/n*i));
glEnd();
✨ Vérifier la solution et pratiquer

Coordonnées de perspective en OpenGL

Dans la section précédente, nous avons défini neuf variables membres dans la classe SolarSystem en OpenGL :

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

Pour comprendre ces neuf variables, nous devons tout d'abord établir le concept de perspective de la caméra en programmation 3D avec OpenGL.

Imaginez que les scènes que nous regardons habituellement dans les films sont en fait tournées depuis la perspective de la caméra. Par conséquent, OpenGL a également un concept similaire. Si nous imaginons la caméra comme étant notre propre tête, alors :

  1. viewX, viewY, viewZ correspondent aux coordonnées de la tête (caméra) dans le système de coordonnées mondial OpenGL ;
  2. centerX, centerY, centerZ correspondent aux coordonnées de l'objet observé (tel que vu par la caméra) ;
  3. upX, upY, upZ correspondent au vecteur de direction pointant vers le haut depuis le sommet de la tête (sommet de la caméra) (car nous pouvons incliner notre tête pour observer un objet).

Avec cette compréhension, vous avez maintenant un concept du système de coordonnées en OpenGL.

Pour cette expérience, supposons que la perspective initiale est aux coordonnées (x, -x, x). Par conséquent, nous avons :

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

La position de l'objet observé (soleil) est à (0,0,0), donc dans le constructeur de la classe SolarSystem, nous initialisons la perspective comme suit :

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

Ensuite, nous pouvons définir les neuf paramètres de la perspective à l'aide de la fonction gluLookAt :

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

Ensuite, jetons un coup d'œil à gluPerspective(GLdouble fovy,GLdouble aspect,GLdouble zNear,GLdouble zFar).

Cette fonction créera un volume de vision de perspective symétrique, et avant d'utiliser cette fonction, le mode de matrice d'OpenGL doit être défini sur GL_PROJECTION.

Comme le montre la figure suivante :

OpenGL perspective projection diagram

L'image dans la fenêtre est capturée par la caméra, et le contenu réel capturé est sur le plan lointain, tandis que le contenu affiché est sur le plan proche. Par conséquent, cette fonction nécessite quatre paramètres :

  • Le premier paramètre est la taille de l'angle de perspective.
  • Le second paramètre est le rapport d'aspect de la fenêtre réelle, comme montré dans la figure aspect=w/h.
  • Le troisième paramètre est la distance au plan proche.
  • Le quatrième paramètre est la distance au plan lointain.
✨ Vérifier la solution et pratiquer

Effets d'éclairage en OpenGL

OpenGL divise le système d'éclairage en trois parties : les sources de lumière, les matériaux et l'environnement d'éclairage.

Comme son nom l'indique, une source de lumière est la source de lumière, comme le soleil ;
Les matériaux désignent les surfaces de divers objets qui reçoivent la lumière, comme les planètes et les satellites du système solaire autres que le soleil ;
L'environnement d'éclairage comprend des paramètres supplémentaires qui déterminent l'effet d'éclairage final, comme la réflexion des rayons lumineux, qui peut être contrôlée en définissant un paramètre appelé "luminosité ambiante" pour rendre l'image finale plus réaliste.

En physique, lorsqu'une lumière parallèle frappe une surface lisse, la lumière réfléchie reste parallèle. Ce type de réflexion est appelée "réflexion spéculaire". D'un autre côté, la réflexion causée par une surface irrégulière est appelée "réflexion diffuse".

OpenGL lighting effects example

Sources de lumière

Pour implémenter le système d'éclairage en OpenGL, la première chose à faire est de configurer les sources de lumière. Il est important de noter que OpenGL prend en charge un nombre limité de sources de lumière (huit au total), représentées par les macros GL_LIGHT0 à GL_LIGHT7. Elles peuvent être activées avec la fonction glEnable et désactivées avec la fonction glDisable. Par exemple : glEnable(GL_LIGHT0);

La position de la source de lumière est définie à l'aide de la fonction glLightfv, par exemple :

GLfloat light_position[] = {0.0f, 0.0f, 0.0f, 1.0f};
glLightfv(GL_LIGHT0, GL_POSITION, light_position); // Spécifie la position de la source de lumière 0

La position est représentée par quatre valeurs, (x, y, z, w). Lorsque w est 0, cela signifie que la source de lumière est infiniment loin. Les valeurs de x, y et z spécifient la direction de cette source de lumière infiniment distante.
Lorsque w n'est pas 0, il s'agit d'une source de lumière positionnelle, et sa position est (x/w, y/w, z/w).

Matériaux

Pour définir le matériau d'un objet, cinq attributs sont généralement requis :

  1. L'intensité de la lumière laissée dans l'environnement après de multiples réflexions.
  2. L'intensité de la lumière après réflexion diffuse.
  3. L'intensité de la lumière après réflexion spéculaire.
  4. L'intensité de la lumière émise par les objets non émetteurs de lumière en OpenGL, qui est faible et n'affecte pas les autres objets.
  5. L'exposant spéculaire, qui représente la rugosité du matériau. Une valeur plus petite signifie un matériau plus rugueux, et lorsqu'une lumière émise par une source ponctuelle de lumière tombe sur lui, des taches lumineuses plus grandes sont produites. À l'inverse, une valeur plus grande signifie que le matériau est plus similaire à une surface spéculaire, produisant des taches lumineuses plus petites.

OpenGL fournit deux versions de fonctions pour définir les matériaux :

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

La différence entre elles est que seul une valeur doit être définie pour l'exposant spéculaire, donc glMaterialf est utilisé. Pour les autres paramètres de matériau qui nécessitent plusieurs valeurs, un tableau est utilisé, et la version avec des paramètres de vecteur pointeur, glMaterialfv, est utilisée. Par exemple :

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

Environnement d'éclairage

Par défaut, OpenGL ne gère pas l'éclairage. Pour activer la fonctionnalité d'éclairage, vous devez utiliser la macro GL_LIGHTING, c'est-à-dire glEnable(GL_LIGHTING);

✨ Vérifier la solution et pratiquer

Dessiner les planètes

Lorsque l'on dessine une planète, on doit tout d'abord considérer son angle de révolution et son angle de rotation. Par conséquent, on peut tout d'abord implémenter la fonction membre Star::update(long timeSpan) de la classe Star dans le fichier ~/project/stars.cpp :

void Star::update(long timeSpan) {
    alpha += timeSpan * speed;  // Mettre à jour l'angle de révolution
    alphaSelf += selfSpeed;     // Mettre à jour l'angle de rotation
}

Après avoir mis à jour les angles de révolution et de rotation, on peut dessiner la planète spécifique en fonction des paramètres :

void Star::drawStar() {

    glEnable(GL_LINE_SMOOTH);
    glEnable(GL_BLEND);

    int n = 1440;

    // Enregistrer l'environnement de matrice OpenGL actuel
    glPushMatrix();
    {
        // Révolution

        // Si c'est une planète et que la distance n'est pas 0, alors la translater à l'origine par un rayon
        // Cette partie est utilisée pour les satellites
        if (parentStar!= 0 && parentStar->distance > 0) {
            // Faire tourner le graphique à dessiner autour de l'axe z par alpha
            glRotatef(parentStar->alpha, 0, 0, 1);
            // Translater dans la direction de l'axe x par distance, tandis que les directions y et z restent inchangées
            glTranslatef(parentStar->distance, 0.0, 0.0);
        }
        // Dessiner l'orbite
        glBegin(GL_LINES);
        for(int i=0; i<n; ++i)
            glVertex2f(distance * cos(2 * PI * i / n),
                       distance * sin(2 * PI * i / n));
        glEnd();
        // Faire tourner autour de l'axe z par alpha
        glRotatef(alpha, 0, 0, 1);
        // Translater dans la direction de l'axe x par distance, tandis que les directions y et z restent inchangées
        glTranslatef(distance, 0.0, 0.0);

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

        // Dessiner la couleur de la planète
        glColor3f(rgbaColor[0], rgbaColor[1], rgbaColor[2]);
        glutSolidSphere(radius, 40, 32);
    }
    // Restaurer l'environnement de matrice avant le dessin
    glPopMatrix();

}

Ce code utilise les fonctions sin() et cos(), qui nécessitent l'inclusion de #include<cmath>.

✨ Vérifier la solution et pratiquer

Dessin de l'éclairage

Pour la classe Planet, qui représente un corps céleste non lumineux, nous devons dessiner son effet d'éclairage. Ajoutez le code suivant au fichier ~/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);
}

Quant à la classe LightPlanet, qui représente un corps céleste lumineux, nous devons non seulement définir son matériau d'éclairage, mais également définir la position de sa source de lumière :

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); // Spécifie la position de la source de lumière 0
    glLightfv(GL_LIGHT0, GL_AMBIENT,  light_ambient);  // Représente l'intensité des rayons lumineux provenant de diverses sources qui atteignent le matériau, après de multiples réflexions et traçages
    glLightfv(GL_LIGHT0, GL_DIFFUSE,  light_diffuse);  // Intensité de la lumière après réflexion diffuse
    glLightfv(GL_LIGHT0, GL_SPECULAR, light_specular); // Intensité de la lumière après réflexion spéculaire

}
✨ Vérifier la solution et pratiquer

Dessiner la fenêtre

Dans la section précédente, nous avons mentionné les deux fonctions les plus importantes pour gérer l'affichage d'images : glutDisplayFunc et glutIdleFunc. glutDisplayFunc exécute la fonction de rappel lorsque GLUT détermine que le contenu de la fenêtre doit être mis à jour, tandis que glutIdleFunc gère le rappel lorsque la boucle d'événements est inactif.

Pour faire bouger l'ensemble du système solaire, nous devons considérer quand mettre à jour les positions des planètes et quand rafraîchir la vue.

Évidemment, glutDisplayFunc devrait se concentrer sur le rafraîchissement de la vue, et lorsque l'événement est inactif, nous pouvons commencer à mettre à jour les positions des planètes. Après que les positions ont été mises à jour, nous pouvons ensuite appeler la fonction de rafraîchissement de la vue pour rafraîchir l'affichage.

Par conséquent, nous pouvons tout d'abord implémenter la fonction membre SolarSystem::onUpdate() appelée dans glutDisplayFunc :

#define TIMEPAST 1 // Supposons que chaque mise à jour prenne un jour
void SolarSystem::onUpdate() {

    for (int i=0; i<STARS_NUM; i++)
        stars[i]->update(TIMEPAST); // Mettre à jour les positions des étoiles

    this->onDisplay(); // Rafraîchir l'affichage
}

Ensuite, le rafraîchissement de la vue d'affichage est implémenté dans SolarSystem::onDisplay() :

void SolarSystem::onDisplay() {

    // Effacer le tampon de la vue
    glClear(GL_COLOR_BUFFER_BIT  |  GL_DEPTH_BUFFER_BIT);
    // Effacer et définir le tampon de couleur
    glClearColor(.7f,.7f,.7f,.1f);
    // Spécifier la matrice actuelle comme matrice de projection
    glMatrixMode(GL_PROJECTION);
    // Spécifier la matrice spécifiée comme matrice identité
    glLoadIdentity();
    // Spécifier le volume de vue actuel
    gluPerspective(75.0f, 1.0f, 1.0f, 40000000);
    // Spécifier la matrice actuelle comme matrice de vue modèle
    glMatrixMode(GL_MODELVIEW);
    // Spécifier la matrice actuelle comme matrice identité
    glLoadIdentity();
    // Définir la matrice de vue et la multiplier avec la matrice actuelle
    gluLookAt(viewX, viewY, viewZ, centerX, centerY, centerZ, upX, upY, upZ);

    // Activer la première source de lumière (source de lumière 0)
    glEnable(GL_LIGHT0);
    // Activer l'éclairage
    glEnable(GL_LIGHTING);
    // Activer le test de profondeur, masquer automatiquement les graphiques couverts en fonction des coordonnées
    glEnable(GL_DEPTH_TEST);

    // Dessiner les planètes
    for (int i=0; i<STARS_NUM; i++)
        stars[i]->draw();

    // Nous avons utilisé GLUT_DOUBLE lors de l'initialisation du mode d'affichage dans la fonction principale
    // Nous devons utiliser glutSwapBuffers pour implémenter l'échange de tampons pour le double-buffering après que le dessin est terminé
    glutSwapBuffers();
}
✨ Vérifier la solution et pratiquer

Constructeurs et destructeurs de classes

Les constructeurs des classes définies dans stars.hpp doivent initialiser les variables membres des classes. Cette partie est relativement simple et on peut même utiliser les destructeurs par défaut, donc veuillez implémenter ces constructeurs vous-même :

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

Indication : Notez que lors de l'initialisation de la variable de vitesse, convertissez-la en vitesse angulaire. La formule de conversion est : alpha_speed = 360/speed.

Pour le constructeur de solarsystem.cpp, nous devons initialiser tous les planètes. Ici, nous fournissons les paramètres entre les planètes pour la commodité :

// Rayon de révolution
#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

// Distance au soleil
#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

// Vitesse de mouvement
#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

// Vitesse de rotation sur soi-même
#define SELFROTATE 3

// Définir une macro pour faciliter la définition d'un tableau multidimensionnel
#define SET_VALUE_3(name, value0, value1, value2) \
                   ((name)[0])=(value0), ((name)[1])=(value1), ((name)[2])=(value2)

// Dans l'expérience précédente, nous avons défini l'énum des planètes
enum STARS {Sun, Mercury, Venus, Earth, Moon,
    Mars, Jupiter, Saturn, Uranus, Neptune};

Indication :

Nous définissons une macro SET_VALUE_3 ici. Vous pourriez penser que nous pourrions écrire une fonction pour atteindre le même but.

En fait, les macros effectuent le travail de substitution globale lors du processus de compilation, tandis que la définition de fonctions

Cela nécessite des opérations de pile de fonction lors de l'appel, ce qui est beaucoup moins efficace que le traitement par macro lors du processus de compilation.

Par conséquent, les macros peuvent être plus efficaces.

Cependant, il est important de noter que bien que les macros puissent être plus efficaces, une utilisation excessive peut conduire à du code laid et moins lisible. Par contre, une utilisation appropriée des macros est encouragée.

Par conséquent, nous pouvons implémenter le constructeur de la classe SolarSystem, où les couleurs des planètes sont sélectionnées aléatoirement. Les lecteurs peuvent changer les couleurs des planètes eux-mêmes :

SolarSystem::SolarSystem() {

    // Définir la vue en perspective, comme nous avons discuté de l'initialisation de la vue en perspective auparavant
    viewX = 0;
    viewY = REST_Y;
    viewZ = REST_Z;
    centerX = centerY = centerZ = 0;
    upX = upY = 0;
    upZ = 1;

    // Soleil
    GLfloat rgbColor[3] = {1, 0, 0};
    stars[Sun]     = new LightPlanet(SUN_RADIUS, 0, 0, SELFROTATE, 0, rgbColor);
    // Mercure
    SET_VALUE_3(rgbColor,.2,.2,.5);
    stars[Mercury] = new Planet(MER_RADIUS, MER_DIS, MER_SPEED, SELFROTATE, stars[Sun], rgbColor);
    // Vénus
    SET_VALUE_3(rgbColor, 1,.7, 0);
    stars[Venus]   = new Planet(VEN_RADIUS, VEN_DIS, VEN_SPEED, SELFROTATE, stars[Sun], rgbColor);
    // Terre
    SET_VALUE_3(rgbColor, 0, 1, 0);
    stars[Earth]   = new Planet(EAR_RADIUS, EAR_DIS, EAR_SPEED, SELFROTATE, stars[Sun], rgbColor);
    // Lune
    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);
    // Saturne
    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);
    // Neptune
    SET_VALUE_3(rgbColor,.5,.5, 1);
    stars[Neptune] = new Planet(NEP_RADIUS, NEP_DIS, NEP_SPEED, SELFROTATE, stars[Sun], rgbColor);

}

En outre, n'oubliez pas de libérer la mémoire allouée dans le destructeur :

SolarSystem::~SolarSystem() {
    for(int i = 0; i<STARS_NUM; i++)
        delete stars[i];
}
✨ Vérifier la solution et pratiquer

Implémentation du changement de perspective avec les touches du clavier

Pour contrôler le changement de perspective, nous pouvons utiliser les cinq touches w, a, s, d, x du clavier, et utiliser la touche r pour réinitialiser la perspective. Tout d'abord, nous devons déterminer l'amplitude du changement de perspective après chaque pression de touche. Ici, nous définissons une macro OFFSET. Ensuite, nous pouvons déterminer le comportement de pression de touche de l'utilisateur en inspectant le paramètre key passé.

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

    switch (key)    {
        case 'w': viewY += OFFSET; break; // Augmenter la position de l'axe Y de la caméra de 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;
    }

}
✨ Vérifier la solution et pratiquer

Exécution et test

Nous avons principalement implémenté le code dans les fichiers stars.cpp et solarsystem.cpp.

Le code de stars.cpp est le suivant :

Cliquez pour voir le code complet
//
//  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);

}

Le code de solarsystem.cpp est le suivant :

Cliquez pour voir le code complet
//
// 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];
}

Exécutez dans le terminal :

make && ./solarsystem

Le résultat est montré dans la figure :

Aperçu de la simulation du système solaire

Étant donné que les couleurs des planètes sont uniques, l'effet d'éclairage n'est pas très évident mais reste visible. Par exemple, du côté droit du Jupiter jaune, il y a une apparence blanchâtre.

✨ Vérifier la solution et pratiquer

Sommaire

Dans ce projet, nous avons réalisé un modèle simple du système solaire. En utilisant le système d'éclairage d'OpenGL, nous sommes capables de voir les effets d'éclairage des planètes lorsqu'elles tournent autour du soleil. De plus, nous pouvons ajuster la perspective à l'aide du clavier pour observer le système solaire sous différents angles.