créer son propre système solaire

Génèse est un logiciel de simulation visant à créer son propre système planétaire de manière intuitive.

Stack technique

OpenGL + GLSL + C++ sous Visual Studio

Etape 1 : l'implémentation d’un contexte OpenGL et SDL

Comment faire fonctionner SDL et OpenGL ensemble ? Rien de plus simple : SDL offre un contexte pour OpenGL. Ainsi, l’architecture de base de mon programme est celle d’un programme SDL avec une boucle de rendu OpenGL. Cette boucle contient une fonction d’affichage avec les traitements OpenGL correspondants. Pour mieux comprendre comment cela fonctionne et afin de ne pas répéter ce que d’autres ont très bien expliqué, je vous invite à aller sur le site d'OpenClassRooms: creer des programmes en 3d avec Opengl

Etape 2 : les shaders

Mon désir de travailler réellement l’aspect visuel de mes programmes m’a emmené sur la piste des shaders. Et c’est donc ce point qui m’a demandé le plus de travail. L’utilisation des shaders conjointement avec OpenGL s’appelle le GLSL (OpenGL Shading Language).

Les shaders sont le « plus » de la 3D : ils permettent de reconfigurer le pipeline de la carte graphique pour en modifier son fonctionnement. De ce fait, ils interviennent sur tous les rendus en fonction de ce qui est activé ou non : affichage de formes, de lumière, de profondeur, ect…. Deux types de shader existent : le pixel shader et le vertex shader. Pour faire simple : le vertex shader intervient sur les sommets qui composent une forme et le pixel shader intervient sur chaque pixel de cette même forme. Le but étant de reconfigurer la manière dont la carte graphique intervient sur le rendu final, ces deux types de shaders sont indissociables. Ils sont donc linkés entre eux dans ce qui s’appelle un program. Afin d’avoir une explication complète du fonctionnement des shaders, je vous conseille fortement ces adresses: site d'OpenClassRooms - les shaders en glsl et lighthouse3d - exemple GLSL.

Une autre particularité des shaders est qu’ils ne sont pas compilés au même moment que votre programme. Ils sont compilés au lancement de l’exécutable (et non à sa compilation). Cela implique qu’il faut implémenter leur chargement et leur compilation (même si pour ca il y a pas mal de fonctions)

Où est la difficulté?

  • Ce n’est pas l’implémentation pour la compilation : la compilation des shaders répond à une méthodologie très précise et très bien expliquée (conf. lien plus haut).
  • Ce n’est pas non plus l’utilisation du shader : on l’active et le désactive à souhait.
  • Et enfin, le codage du shader pose peu de problèmes. En effet, 4 lignes de code dans votre shader peuvent suffire à affecter grandement vos rendus OpenGL. Il est cependant vrai que la programmation d’un shader demande de bonnes notions mathématiques, logiques et physiques.

La grosse difficulté du codage des shaders réside dans l’interprétation par OpenGL du shader. En effet, le shader agit de manière globale sur le rendu final, mais cela dépend grandement de ce qui est activé ou non dans votre programme.

Dans la vidéo servant d’exemple, j’ai implémenté des shaders pour l’affichage des boutons et menus (effets de fondu) et pour le soleil (effet de magma et de flamme).

Astuces:

  • L’implémentation d’une variable Uniform (variable qui permet de transmettre une valeur de l’application OpenGl vers le shader) qui est initialisée mais pas utilisée déclenche une erreur.
  • On ne peut pas recoder qu’une partie de ce que fais la carte graphique : Si l’on souhaite reconfigurer la couleur d’un objet avec un shader : il faut aussi reconfigurer les textures et les lumières, sinon elles ne seront pas présentes. Pour ce qui est du fonctionnement des textures, c’est presque facile. Pour ce qui est des lumières, cela demande de bien comprendre le fonctionnement de la lumières, en OpenGL d’abord, puis dans la théorie et enfin, dans son utilisation par la carte graphique.
  • L’utilisation des shaders présente l’avantage de ne pas ralentir l’ordinateur : c’est la carte graphique qui effectue les calculs et non pas le processeur. Cet avantage est un réel inconvénient lorsque l’ordinateur ne possède pas de carte graphique performante ou que celle-ci ne possède pas une version d’OpenGL récente. J’ai eu le problème et j’ai mis ma carte graphique Ati Radeon à jour sur ce site : Pilote ATI radeon mobility.
  • Pour transmettre plusieurs textures à un shader (multitexturing),  il faut bien sûr utiliser les variables « uniform ». Si vous avez appris à utiliser le GLSL, vous savez aussi que la fonction linkUniformVariableGLint() vous permet d’envoyer un entier au shader. Cette fonction va donc nous permettre d’envoyer nos textures.Il faut vous être assuré d’avoir au préalable liés vos textures au program ainsi:U = glGetUniformLocationARB(ShaderProgramName, varUniform) où:

    • U est l’entier servant d’identifiant dans le programme OpenGL -ShaderProgramName: le program (conf cour GLSL) -varUniform: la variable Uniform dans le shader

Ainsi, il faut pour envoyer plusieurs textures au shader:

//Activer le numéro de texture 0.

glActiveTexture(GL_TEXTURE0);

//Appliquer la texture A voulu (appliqué en tant que texture 0).

glBindTexture(GL_TEXTURE_2D,A);

//Activer le numéro de texture 1

glActiveTexture(GL_TEXTURE1);

//Appliquer la texture B voulu (appliqué en tant que texture 1).

glBindTexture(GL_TEXTURE_2D,B);

//On fait à ce moment appel à note shader.

_shader.active();

//Puis on envoi enfin au shader la texture A au shader.

glUniform1iARB(TexA, 0);

//Puis on envoi enfin au shader la texture B au shader.

glUniform1iARB(TexB, 1);

J’attire votre attention sur les 2 dernières lignes. Contrairement à se qu’on croit, il ne faut pas envoyer les textures A et B au shader mais le numéro correspondant en OpenGL. Ainsi A étant affecté à GL_TEXTURE0, on envoi 0 et B étant affecté à GL_TEXTURE1, on envoi 1.

Mais que se passe-t-il du coté de notre shader ?

uniform sampler2D texA récupère 0. uniform sampler2D texB récupère 1. Ainsi la fonctionGLSL texture2D(texA,gl_TexCoord[0].st) saura exactement qu’elle travaille sur la texture 0, soit GL_TEXTURE0 soit A.

Afin de pouvoir aider ceux qui se lancent dans l’utilisation des shaders, vous pouvez télécharger ces différents shaders que j'ai créé :

Damien Leroux Genesis Magma

Damien Leroux Genesis UI Apparition Progressive

Damien Leroux Genesis UI Full2

Damien Leroux Genesis Rotation

Etape 3 : la skybox

Damien Leroux Genesis Skybox2

Pour pouvoir représenter l’espace autour des astres, j’ai opté pour une skybox. Ce procédé nécessite l’utilisation d’extensions OpenGL. Glew permet de facilement vérifier si l’implémentation de votre système les supporte. La skybox est en fait un cube (GL_TEXTURE_CUBE_MAP_ARB) dans lequel la caméra se trouve, toujours au centre, ce qui donne la sensation d’être dans l’espace.J’ai donc créé moi-même les textures qui représentent l’espace : texture pour une skybox d'espace. Il fallait ensuite les charger (Pour plus d’informations sur l’utilisation des skybox : le système solaire étape 6)

Etapes 4 : les textures

Pour ce qui est des textures, j’ai choisi le format jpeg.  C’est donc en utilisant SDL que je procède à ce chargement puis en utilisant OpenGL que je convertie ces surfaces en textures. (Pour plus d’informations sur l’utilisation des textures: le système solaire étape 3).

Etape 5 : caméra

Afin de pouvoir se déplacer dans l’espace, on utilise un trackball. Ce trackball réagit fonction des mouvements de la souris et du scrolling (pour se rapprocher et s’éloigner).

  • Ce trackball permet de se déplacer dans l’espace en ciblant différent astres mais ce n’est pas tout. En effet, la camera est programmé pour changer de comportement en fonction de l’astre qu’elle cible. De ce fait, lorsque l’on clique sur un astre, on fige l’astre qui sert de point référent. Par exemple, si l’on clique sur la terre : le soleil ne bouge pas ou si l’on clique sur la lune, la terre ne bouge pas.
  • Lorsque la caméra change de cible, la transition entre la cible précédente A et la suivante B se fait graduellement d’un point à un autre. Ce procédé est vraiment simple :coordonneesA x (1-t) + coordonneesB x (t) = positionVoulu (t allant de 0 à 1).Je m’explique. Si on veut aller d’un point A à un point B alors :Au début, on est au point A et t=0 donc coordonneesA x (1-0)+ coordonneesB x 0 = coordonneesA. On est bien en A.A la fin, on doit être au point B et t=1 donc coordonneesA x (1-1)+ coordonneesB x 1 = coordonneesB. On est bien en B. Faire varier t de 0 à 1 nous assure de partir de A pour arriver à B de manière linéaire.

Pour plus d’informations sur l’utilisation des textures : le système solaire étape 5

Etape 6 : les lumières

Damien Leroux Genesis Light

L’éclairage est orienté afin de simuler la lumière de l’étoile sur les planètes. Il est aussi régler pour éclairer les éléments chimiques servant à construire une planète dans le menu du choix des éléments. Cette même lumière est désactivée sur le soleil (il représente lui-même la lumière) et sur les parties « menu » (Pour plus d’informations sur l’utilisation des lumières : le système solaire étape 4)

Etape 7 : Le mouse picking

Le mouse picking est en deux parties. La première partie est dédiée à la reconnaissance des formes OpenGL. Par exemple, savoir si la sourie est sur une planète (une sphère). La deuxième partie est dédiée à la reconnaissance de formes voulues. Par exemple, sur un quad on plaque une texture dont l’image est un rond et bien que la sourie soit sur un quad (un carré), on souhaite que la détection ne se fasse que sur le rond.

  • Pour ce qui est de la reconnaissance des formes OpenGL, il faut d’abord dessiner une première fois la scène pour que le mouse picking enregistre dans un pile tous les objets qui se trouvent en dessous du pointeur de souris (inutile de mettre des couleurs, textures ou lumières : seules les formes comptent). On récupère alors dans cette pile l’identifiant de l’objet avec la profondeur la plus faible. Puis on redessine la scène pour l’afficher à l’écran cette fois ci. Pour connaitre la manière dont procéder, ce tutoriel est très bien expliqué (en anglais) :OpenGL mouse picking.
  • Pour ce qui est de la reconnaissance de formes voulues, le mouse picking est géré manuellement en fonction de nombreux calculs selon la forme à détecter. Par exemple un bouton rond est en fait une texture avec un rond dessus qui est placé sur un carré (on parle de quad). Il faut donc calculer la position de la souris par rapport au centre de ce cercle pour savoir si la souris est bien sur le bouton (le centre du cercle se trouvant lui-même au centre du carré de texture).

Etape 8 : La création de menus (UI)

Damien Leroux Genesis UI

L’implémentation des menus est segmentée en différentes classes: une partie « menu » et une partie « bouton ». Les boutons ont des propriétés et formes distinctes tandis que les menus regroupent plusieurs boutons. Dans l’exemple fourni, on affiche les boutons à l’aide des shaders.

Chaque bouton possède un état. Cet état peut être : « ouvert », « fermé», « survolé », « enfoncé », « en cours d’ouverture » ou « en cours de fermeture ».

Chaque bouton peut donc déclencher :

  • un changement de son état. Par exemple, le survole d’un bouton modifie son état de « ouvert » à « survolé », ce qui s’accompagne de l’appel d’un shader.
  • un changement des états des autres boutons et/ou menus. Par exemple, cliquer sur un bouton pour afficher un menu modifie l’état du menu à afficher de « fermé » à « en cours d’ouverture ».  Lorsqu’un menu est« en cours d’ouverture », un variable évolue graduellement de 0 à 1. Lorsque 1 est atteint, l’état du menu passe de « en cours d’ouverture » à « ouvert ».
  • des actions diverses dans le programme. Par exemple, cliquer sur le bouton de validation de création d’une planète édite automatiquement un fichier data afin de sauvegarder, en temps réel, l’apparition d’une nouvelle planète.

Etape 9 : La création d’astre

La classe mère ASTRE peut être dérivée en classes filles : soit une PLANETE, soit une ETOILE. Un astre possède : positions, trajectoire, identifiant, forme, taille, couleurs, textures, date de création, composition chimique et durée de vie et un nom. Une planète possède tous ce que possède un astre avec en plus de la vie Une étoile possède en plus une force de rayonnement. La création d’un astre constitue essentiellement l’enregistrement de nouvelles données au sein du programme. Pour des raisons pratiques, dans la démonstration que je vous ai faite, le choix du nom d’un astre nouvellement créé est généré automatiquement, de même que sa taille et sa vitesse. Par contre, la définition de la trajectoire et de la composition chimique de l’astre est paramétrable.

La création d'un astre fonctionne ainsi:

Damien Leroux Genesis UI Full3

Déterminer la composition chimique d’un astre n’a ici aucun effet, si ce n’est que la couleur est une moyenne des couleurs des différents éléments chimiques choisis.

Déterminer la trajectoire de l'astre consiste en 3 étapes:

Damien Leroux Genesis Planet

Choisir un astre qui va servir de référentiel : un simple clic sur un astre fait de cet astre un point référent.

Damien Leroux Genesis Skybox

Choisir où positionner notre nouvel astre..

Damien Leroux Genesis Universe

Choisir l’inclinaison et le sens de parcours de l’astre nouvellement créé.

Etape 10 : afficher du texte

Ce n’est pas aussi simple que l’on pourrait le croire. En effet, SDL permet facilement d’afficher grâce à sa librairie TTF du texte à l’écran mais pas dans un contexte OpenGL. Si vous ne connaissez pas encore cette librairie, je vous invite à vous rendre sur : site d'OpenClassRooms: Texte SDL TTF. Pour pouvoir afficher du texte, il faut :

  • Générer une SDL_ surface avec le texte voulu.
  • Plaquer  la SDL_ surface obtenue que une surface OpenGL (de type quad).

Voici les deux fonctions permettant tout cela:

//Soit la texture qui contient le nom de l’astre: ce qui permet l'affichage de celui-ci.
GLuint _texte;

//nom est le texte que l’on souhait afficher
//police est la police que l’on souhaite utiliser pour afficher le texte
//couleur est la couleur du texte.
void setNom(string nom,TTF_Font *police,SDL_Color couleur){
    //si une texture OpenGL n'est pas déjà affectée à _texte
    if(_texte!=0){
    glDeleteTextures(1,&_texte); //on vide _texte
    _texte=0;// la variable _texte est vidée.
    }
    glEnable(GL_TEXTURE_2D); //on active la gestion de texture 2D OpenGL
    _texte= charge_texte(nom,police,couleur);
    glDisable(GL_TEXTURE_2D); //on desactive la gestion de texture 2D OpenGL
}

//cette fonction permet de récupérer une texture composée de texte afin de l'afficher en openGL.
//nom est le texte que l’on souhait afficher
//police est la police que l’on souhaite utiliser pour afficher le texte
//couleur est la couleur du texte
GLuint charge_texte(const char * name,TTF_Font *font, SDL_Color color){
    //la surface SDL
    SDL_Surface* gl_fliped_surface=NULL;
    GLuint glID=0; //La texture OpenGL qui va être renvoyée.
    //On applique sur la surface SDL notre texte et on vérifie que cela a fonctionné.
    if((gl_fliped_surface=TTF_RenderText_Blended(font, name, color)) !=NULL) {
        // On génère un entier pour la texture glID à renvoyer
        glGenTextures(1, &glID);
        //on spécifie la texture sur laquelle agiront les opérations futures.
        glBindTexture(GL_TEXTURE_2D, glID);
        //Création de la texture OpenGL grâce à la surface SDL
        glTexImage2D(GL_TEXTURE_2D, 0, 4, gl_fliped_surface->w, gl_fliped_surface->h, 0, GL_RGBA, GL_UNSIGNED_BYTE, gl_fliped_surface->pixels);
        //Paramétrage de l’affichage de la texture
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
        //libération des surfaces
        SDL_FreeSurface(gl_fliped_surface);
    }
    return glID;
}

Utilisation:

GLfloat textSize[2]; //les dimensions du texte à afficher (à vous de choisir)

glEnable(GL_TEXTURE_2D); //On active la gestion de texture 2D OpenGL.

glBindTexture(GL_TEXTURE_2D,_texte); //On souhaite utiliser notre texture qui contient notre texte.

glBegin(GL_QUADS);

glTexCoord2d(1,0);

glVertex3f(0, textSize[1],-textSize[0]);

glTexCoord2d(0,0);

glVertex3f(0, textSize[1], textSize[0]);

glTexCoord2d(0,1);

glVertex3f(0,-textSize[1], textSize[0]);

glTexCoord2d(1,1);

glVertex3f(0,-textSize[1],-textSize[0]);

glEnd();

glDisable(GL_TEXTURE_2D); on désactive la gestion de texture 2D Open GL

Etape 11 : la gestion du temps

J’ai créé une variable statique dans une classe de gestion du temps qui se calle sur la variable de gestion du temps de boucle. ui plus est, grâce à la gestion du temps de boucle et du FPS, le temps est en seconde. Ainsi, lorsque j’indique comme vitesse de trajectoire à un astre : 1, il fait 1 tour en 1 seconde. Lorsque je lui indique 1/10, il met 10 secondes pour faire un tour. Afin d’implémenter ce genre de système je vous conseille : site d'OpenClassRooms: le temps.

Résultat final

Damien Leroux Genesis

Damien Leroux Genesis UI Full1

Télécharger Génèse