Réalisation d'un First Person Shooter en 3D avec JAVA, JOGL, JOGG et JORBIS

Date de publication : 20 mai 2008

Par gouessej (tuer.developpez.com)
 

Cette liste d'articles et tutoriels s'appuie sur un jeu nommé T.U.E.R., développé par Julien Gouesse.

1. Prérequis
2. Histoire
3. Principe du jeu
4. Suivi du projet
5. Techniques utilisées dans le jeu
5-1. Capture d'écran
5-1-1. Le tampon logiciel
5-1-2. AWT
5-1-3. JOGL
5-2. Mode plein écran
5-3. Gestion de la souris pour un First Person Shooter (curseur transparent, centrage, focus...)
5-3-1. MouseListener, MouseMotionListener et Robot
5-3-2. MouseListener, MouseMotionListener, MouseInfo, PointerInfo et Robot
5-3-3. JInput
5-4. Déploiement avec Java Webstart
5-5. Tampon de profondeur
5-6. Rendu textuel
5-7. Double buffering
5-8. Matrices
5-9. Attributs
5-10. Dessin hiérarchique
5-11. Transformations géométriques (mise à l'échelle, translation, rotation, ...)
5-12. Mode de rendu (immediate mode, display list, vertex array, compiled vertex array, VBO)
5-13. Rendu multiple d'ensemble de point
5-14. Textures (multipass rendering et multitexturing à l'étude)
5-15. Affichage de surfaces colinéaires superposées (glPolygonOffset)
5-16. Alpha blending
5-17. Suppression des faces arrières
5-18. Requête d'occlusion (à l'étude)
5-19. Shader (à l'étude)
5-20. Chargement de modèles au format MD3, OBJ et 3DS
5-21. Son (OGG, WAV, MIDI)
5-22. Animations (basé sur le temps avec un "timer")
5-23. Modèle MVC (wip)
5-24. Architecture de composants (un seul composant pour le moment)
5-25. Composants graphiques de base en JOGL (barre de progression, menu)
5-26. Script ANT
5-27. Détection des collisions (volumes englobants, voxels)
5-28. Algorithme de subdivision de l'espace
5-29. Mode multijoueur avec JGN et/ou Project Darkstar (à l'étude)
5-30. Capture "vidéo" (premier jet plutôt lent, pas encore fini)
5-31. Système anti verrou de Cardan (quaternions et transformations non eulériennes, à l'étude)
5-32. Micro-optimisations en Java


1. Prérequis

Anglais technique (les sources du jeu étant commentées en anglais).
Expérience solide de Java.
Respect de la licence GNU GPL si vous comptez réutiliser le code source du jeu pour vos projets.


2. Histoire

En juillet 2006, j'ai commencé à écrire mon premier FPS en Java à partir d'ébauche en C++, un projet d'infographie où j'avais calé à cause du problème de verrou de Cardan. Un jour, mon disque dur m'a lâché, je n'avais pas internet et le projet n'était pas encore open source. J'ai été tellement déçu d'avoir perdu des semaines de boulot que j'ai laissé tomber.

En octobre 2006, un enseignant a dit que le second semestre comprenait un projet semestriel, que les enseignants proposeraient des sujets mais qu'exceptionnellement, les élèves pourraient aussi soumettre les leurs. Je voulais d'abord traduire le moteur du jeu "Cube" en Java mais j'ai vite renoncé quand j'ai découvert au fil de mes recherches sur l'ancêtre de TUER : Art Attack de Vincent Stahl. Il s'était inspiré de Java Maze de Jonathan Thomas.

Je trouvais cette appliquette (applet) sympathique mais rudement lente. J'arrivais à peine à avoir une image à la seconde sur certaines machines à l'université. L'auteur a reconnu que son jeu n'a jamais dépassé les 36 images par seconde même sur des foudres de guerre. J'ai entrepris de le réécrire sous forme d'application en mode plein écran et utilisant l'accélération graphique.

Cette fois, pour éviter tout déboire, pour ne pas perdre le fruit de mon travail et pour respecter la licence choisie par M. Stahl, le projet a été mis sous GNU GPL et le code source a été mis en ligne très tôt.

En mars 2006, l'utilisation de Java WebStart a été envisagé après une réflexion cinglante d'un membre de javagaming quand j'ai expliqué comment l'installer. Cela a permis à de jeunes enfants de pouvoir le tester, chose impensable pour beaucoup de jeux open source à l'heure actuelle.

Jusqu'en mai 2007, le projet a été conduit dans le cadre universitaire. A ce moment, le jeu était encore assez lent. J'ai donc décidé de poursuivre son développement dans le cadre personnel. Il ne respecte plus strictement l'original, certains éléments ont été ajoutés dont le viseur. Aujourd'hui, le jeu tourne de 8 à 400 images par seconde.


3. Principe du jeu

Je vous le concède, c'est aussi peu fin qu'un doom-like traditionnel. Détruisez tout ce qui se trouve sur votre passage. Pour l'instant, ce n'est pas très varié, seuls les robots ne vont pas se laisser faire. Le niveau actuel servira uniquement de tutoriel, ça fait un peu bizarre de commencer directement avec un lance-roquettes.


4. Suivi du projet

Le site officiel du jeu est le suivant :
http://tuer.tuxfamily.org

Le code source se trouve à l'adresse suivante :
http://download.tuxfamily.org/tuer/tuer.zip

Quelques captures d'écran se trouvent à l'adresse suivante :
http://tuer.tuxfamily.org/screenshot.html

La configuration minimale, la configuration du clavier et de la souris, la liste des bogues connus et la liste des requêtes de modification se trouvent à l'adresse suivante :
http://tuer.tuxfamily.org/project.html

Le lien permettant de lancer le jeu se trouve à l'adresse suivante (regardez la configuration minimale avant de le tester s'il vous plaît) :
http://tuer.tuxfamily.org/tuer.php



5. Techniques utilisées dans le jeu

Les techniques utilisées dans le jeu sont les suivantes :

  • tampon de profondeur
  • rendu textuel
  • double buffering
  • matrices
  • attributs
  • dessin hiérarchique
  • transformations géométriques (mise à l'échelle, translation, rotation, ...)
  • mode de rendu (immediate mode, display list, vertex array, compiled vertex array, VBO)
  • rendu multiple d'ensemble de point (glMultiDrawArrays)
  • textures (multipass rendering et multitexturing à l'étude)
  • affichage de surfaces colinéaires superposées (glPolygonOffset)
  • alpha blending
  • suppression des faces arrières
  • capture d'écran
  • requête d'occlusion (à l'étude)
  • shader (à l'étude)
  • chargement de modèles au format MD3, OBJ et 3DS
  • son (OGG, WAV, MIDI)
  • mode plein écran (exclusive fullscreen mode, software fullscreen mode)
  • gestion de la souris pour un First Person Shooter (curseur transparent, centrage, focus...)
  • animations (basé sur le temps avec un "timer")
  • modèle MVC (pas encore fini)
  • architecture de composants (un seul composant pour le moment)
  • composants graphiques de base en JOGL (barre de progression, menu)
  • script ANT
  • détection des collisions (volumes englobants, voxels)
  • algorithme de subdivision de l'espace (pas encore fini)
  • mode multijoueur avec JGN et/ou Project Darkstar (à l'étude)
  • capture "vidéo" (premier jet plutôt lent, pas encore fini)
  • système anti verrou de Cardan (quaternions et transformations non eulériennes, à l'étude)
  • déploiement avec Java Webstart

5-1. Capture d'écran

Il existe trois manières d'effectuer une capture d'écran directement depuis un programme Java:

  • Récupérer les données du tampon logiciel dans un moteur de raycasting ou de raytracing
  • Passer par AWT, utiliser la méthode createScreenCapture de la classe Robot
  • Passer par JOGL, utiliser la classe Screenshot

5-1-1. Le tampon logiciel

Supposons que vous réalisez un moteur de raytracing et que vous stockez les pixels dans un tableau d'entiers. Il est possible de construire une image bufferisée (avec la classe BufferedImage, la méthode setRGB(int startX, int startY, int w, int h, int[] rgbArray, int offset, int scansize) ) puis d'utiliser la méthode write( RenderedImage im, String formatName, File output ) de la classe ImageIO pour sauvegarder le contenu dans un fichier image. Cette façon de procéder est particulièrement peu efficace et coûteuse en ressource mémoire.


5-1-2. AWT

La classe Robot peut être utilisé pour effectuer des tests automatiques, pour simuler les clics d'un utilisateur et pour effectuer des captures d'écran. La classe ImageIO permet entre autres de lire et d'écrire des fichiers image. Cette façon de procéder est un peu lente et quand même coûteuse en ressource mémoire. Je vais vous montrer comment effectuer une capture d'écran avec ces classes :
Toolkit toolkit = Toolkit.getDefaultToolkit();
Dimension screenSize = toolkit.getScreenSize(); //récupère la taille de l'écran
Rectangle screenRect = new Rectangle( screenSize );
try {
    Robot robot = new Robot(); //crée le robot
    BufferedImage image = robot.createScreenCapture( screenRect ); //effectue une capture de tout l'écran
    ImageIO.write( image, "png" , new File( "capture.png" ) ); //écris le contenu dans un fichier au format PNG
}
catch( AWTException awte ) //La création peut échouer si l'accès au contrôle des entrées est refusé
{ awte.printStackTrace(); }

5-1-3. JOGL

La classe com.sun.opengl.util.Screenshot permet d'effectuer très simplement et efficacement des captures d'écran. Néanmoins, il faut appeler ses méthodes quand le contexte OpenGL est accessible, c'est-à-dire essentiellement dans la méthode display( GLAutoDrawable glad ) de l'instance de la classe GLEventListener utilisée en passive rendering ou bien dans toute méthode où vous rendez le contexte OpenGL courant en active rendering. Si vous voulez pouvoir effectuer des captures très rapidement sans trop dégrader les performances, utiliser plutôt une des méthodes writeToTargaFile au lieu d'une des méthodes writeToFile comme dans l'exemple suivant :
Toolkit toolkit = Toolkit.getDefaultToolkit();
//récupère la taille de l'écran
Dimension screenSize = toolkit.getScreenSize();
//capture et sauvegarde
Screenshot.writeToTargaFile( new File( "capture.tga" ) , screenSize.getWidth() , screenSize.getHeight() );
Il est tout à fait possible de convertir une image au format TGA dans un autre format une fois que les performances sont moins critiques.


5-2. Mode plein écran

Il existe deux types de mode plein écran :

  • le mode plein écran exclusif
  • le mode plein écran dit "simulé"
Le mode plein écran exclusif est disponible depuis Java 1.4. Une seule fenêtre à la fois peut être dans ce mode, elle est alors considérée comme au-dessus de toutes les autres fenêtres au niveau du Z-Order. L'impact positif sur les performances de ce mode est très souvent surestimé, il n'est absolument pas nécessaire pour profiter de l'accélération matérielle. Il permet de forcer la profondeur de la couleur et la résolution plutôt que d'être soumis aux réglages du bureau de l'utilisateur ce qui peut s'avérer utile pour les jeux. Il peut ne pas être supporté par votre système. Pour cela, vous pouvez (vous devez même) faire le test suivant :
Frame frame = new Frame();
GraphicsEnvironment graphicsEnvironment = GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsDevice graphicsDevice = graphicsEnvironment.getDefaultScreenDevice();
if( graphicsDevice.isFullScreenSupported() )
    {
     System.out.println("mode plein écran exclusif supporté");
    }
else
    {
     System.out.println("mode plein écran exclusif non supporté");
    }
Malheureusement, cela ne suffit pas. Sous Linux, isFullScreenSupported() renvoie parfois true alors que ce mode n'est pas supporté. Le plus simple est de ne pas utiliser le mode plein écran exclusif sous Linux. Il faut pour cela détecter le système d'exploitation ainsi :
if( System.getProperty( "os.name" ).compareToIgnoreCase( "Linux" ) == 0 )
    {
     System.out.println( "Vous êtes sous Linux" );
    }
else
    {
     System.out.println( "Vous n'êtes pas sous Linux" );
    }
Finalement, voici comment vous pouvez passer en mode plein écran exclusif de façon relativement sûre :
Frame frame = new Frame();
frame.setUndecorated( true ); //désactive la décoration de la fenêtre
GraphicsEnvironment graphicsEnvironment = GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsDevice graphicsDevice = graphicsEnvironment.getDefaultScreenDevice();
if( graphicsDevice.isFullScreenSupported() 
    && System.getProperty( "os.name" ).compareToIgnoreCase( "Linux" ) != 0 )
    {
	 System.out.println("mode plein écran exclusif supporté");
     graphicsDevice.setFullScreenWindow(frame);
    }
else
    {
	 System.out.println("mode plein écran exclusif non supporté");
     //dans ce cas, utilisez plutôt le mode plein écran simulé
    }
frame.setVisible( true );
Si vous n'avez pas besoin du mode plein écran exclusif ou bien si ce dernier n'est pas supporté là où vous voulez utiliser votre application, vous pouvez vous contenter du mode plein écran simulé. Dans le cas de mon jeu, c'est ce mode que j'utilise comme je n'ai vu absolument aucune différence de performance en utilisant le mode plein écran exclusif aussi bien sous Windows que sous Linux. Voici comment l'implémenter :
Frame frame = new Frame();
//place la fenêtre dans le coin supérieur gauche de l'écran
frame.setLocation( 0 , 0 ); 
//désactive la décoration de la fenêtre 
//(le cadre et la barre supérieure avec les boutons pour réduire, agrandir et fermer)
frame.setUndecorated( true );
//ignore les messages "paint" venant du système d'exploitation
frame.setIgnoreRepaint( true );
Toolkit toolkit = Toolkit.getDefaultToolkit();
Dimension screenSize = toolkit.getScreenSize();
int screenWidth=(int)screenSize.getWidth();
int screenHeight=(int)screenSize .getHeight();
//redimensionne la fenêtre pour qu'elle occupe tout l'écran
frame.setSize( screenWidth , screenHeight );
//frame.setExtendedState( JFrame.MAXIMIZED_BOTH ); peut faire la même chose, à vérifier
//Sous Linux, à cause d'un bug de metacity, la barre des tâches peut apparaître sur votre fenêtre 
//quand même si vous mettez false ci-dessous
frame.setResizable( System.getProperty( "os.name" ).compareToIgnoreCase( "Linux" ) == 0 );
frame.setVisible( true );
warning la ligne suivante : frame.setIgnoreRepaint( true );
utilisé dans le code ci-dessus est à utiliser avec précautions uniquement dans les cas suivants :
  • active rendering (par exemple en utilisant getPaintGraphics() et en faisant vous-mêmes les opérations de dessin dans une boucle, c'est tout à fait possible en AWT)
  • OpenGL (le dessin est délégué à une instance de GLEventListener, elle ne doit pas être dérangée par des appels à paint() n'importe quand)
  • les deux à la fois
Pour conclure, n'utilisez le mode plein écran exclusif que si vous en avez vraiment besoin.


5-3. Gestion de la souris pour un First Person Shooter (curseur transparent, centrage, focus...)

Un FPS nécessite de pouvoir déplacer la souris sans limite, pas comme un pointeur de souris dans une fenêtre. Soit on essaie d'accéder directement (nativement) aux changements de la souris en tant que périphérique, soit on essaie de suivre le pointeur de souris et de le repositionner quand il arrive près du bord de l'écran. Il existe alors plusieurs façons de gérer la souris :

  • Utiliser les écouteurs d'AWT et la classe Robot pour le repositionnement
  • Utiliser les écouteurs d'AWT uniquement pour les boutons, la classe MouseInfo pour le pointeur et la classe Robot pour le repositionnement
  • Utiliser JInput pour obtenir un accès très bas niveau

5-3-1. MouseListener, MouseMotionListener et Robot

Je ne donnerai pas d'implémentation détaillée de cette méthode car elle est très difficile à faire fonctionner correctement. La méthode mouseMouse(int x,int y) de la classe Robot peut décomposer un mouvement de souris à effectuer en plusieurs mouvements plus petits ce qui rend les choses assez complexes. Il faut différencier les mouvements de l'utilisateur et ceux du robot dans la méthode mouseMoved(MouseEvent me) de l'écouteur (MouseMotionListener) pour calculer correctement le déplacement du joueur en se basant sur le mouvement courant si le joueur en est à l'origine et sur le dernier positionnement du pointeur de souris du mouvement du joueur précédant celui-ci. La méthode getSource() de la classe MouseEvent permet de connaître la source de l'événement en question. Les boutons de la souris se gèrent assez simplement comme le montre l'exemple ci-dessous :
public void mousePressed(MouseEvent me){       
    switch(me.getButton())
        {case MouseEvent.BUTTON1:
	        {if(gameGLEventController.getCycle()==GameCycle.GAME)
	            {if(!gameModel.getPlayerHit())
	                gameModel.tryLaunchPlayerRocket();
	            }				                   
	            break;
	        }
	    case MouseEvent.BUTTON2:
	        {break;}
	    case MouseEvent.BUTTON3:
	        {break;}
	    }	
}

public void mouseMoved(MouseEvent me){
    //TODO
}

5-3-2. MouseListener, MouseMotionListener, MouseInfo, PointerInfo et Robot

Je précise que je dois cette méthode à deux membres de www.javagaming.org, Riven et Bienator. Elle a permis d'améliorer de façon notable la jouabilité de TUER. Elle marche très bien, je vous recommande vivement de vous en servir. Elle n'a pas à être appelée dans un écouteur. Elle est d'une simplicité presque déroutante comme vous pouvez le voir :
public final Point getDelta(){       
    Point pointer=MouseInfo.getPointerInfo().getLocation();
    int xDelta=pointer.x-centerx;
    int yDelta=pointer.y-centery;
    if(xDelta==0 && yDelta==0) 
        {// robot caused this OR user did not do anything
         return(new Point(0,0));
        }
    else
        {robot.mouseMove(centerx, centery);
         return(new Point(xDelta, yDelta));
        }      
}

5-3-3. JInput

JInput permet d'accéder directement au mouvement de l'axe et de la boule de la souris. La souris est représentée par une instance de la classe Controller et contient des sous-contrôleurs qui sont l'axe et la boule. Je vous laisse aller voir sur ce site :

exemple de gestion des mouvements de souris avec JInput

Evitez d'appeler trop souvent ce genre de méthodes dans un jeu en réseau. Cumulez plutôt les deltas côté client et laissez le cache côté serveur se remettre à jour 5 à 10 fois par seconde au maximum.


5-4. Déploiement avec Java Webstart

Java Webstart permet d'un point de vue utilisateur d'installer très simplement un programme Java. Java Webstart est intégré de base dans Java 1.4 en 2001. Il permet de paramétrer l'installation de l'application avec un fichier au format JNLP. On peut alors préciser quelle version de la machine virtuelle doit être utilisée et comment se déploie l'application. Le plus important est de bien décrire les ressources à utiliser. Je vais commenter le fichier JNLP utilisé pour le jeu TUER.
<?xml version="1.0" encoding="utf-8"?> 
<jnlp spec="1.0+" codebase="http://tuer.tuxfamily.org/" href="tuer.jnlp">
  <information>
    <title>TUER</title>
    <vendor>Julien GOUESSE</vendor>
    <homepage href="http://tuer.tuxfamily.org"/>
    <description>Small Quake-like written in Java + JOGL</description>
    <description kind="short">kill them all!!</description>
    <icon href="tuerLogo.png"/>
    <icon kind="splash" href="tuerLogo.png"/>
    <offline-allowed/>
  </information>
  <security> 
    <all-permissions />
  </security>
  <update check="always" policy="always"/>
  <property name="sun.java2d.noddraw" value="true"/>
  <resources>
    <j2se version="1.6+" initial-heap-size="90m" max-heap-size="256m"/>  
    <extension name="jogl" href="http://download.java.net/media/jogl/builds/archive/jsr-231-1.1.0/webstart/jogl.jnlp" />
    <jar href="tuer.jar" download="eager" main="true"/>
  </resources>
  <application-desc main-class="connection.GameServiceProvider" />
  <component-desc/>
</jnlp>
Il se peut que le serveur ne fasse pas l'association entre le fichier JNLP et son type MIME. Alors, quand l'utilisateur clique sur le lien, il verra le contenu du fichier et Java Webstart ne lancera pas l'application. Vous avez deux solutions :

  • modifier le fichier mime.types ou équivalent sur le serveur web
  • créer un fichier avec l'extension ".php" contenant la même chose que votre fichier JNLP mais avec les 3 lignes ci-dessous en entête (et l'appeler dans le lien à la place du fichier JNLP biensûr ) :
<?php header("Content-type: application/x-java-jnlp-file");
      echo "<?xml version=\"1.0\" encoding=\"utf-8\"?>";    
?>
Voici le lien à mettre finalement dans votre page web :
<a href="tuer.php">Click here to run TUER!</a>
Vous pouvez aussi utiliser une image cliquable.


5-5. Tampon de profondeur

Le tampon de profondeur est utilisé pour résoudre en partie les problèmes de visibilité dans des scènes en trois dimensions.


5-6. Rendu textuel

Le rendu textuel avec JOGL se fait via les classes GLUT et TextRenderer. Le sous-ensemble de GLUT disponible dans JOGL est assez pauvre. Par la suite, j'utiliserai uniquement la classe TextRenderer.


5-7. Double buffering

Le double buffering permet d'éliminer le scintillement et quelques autres effets désagréables qui se manifestent quand on change ce qui s'affiche à l'écran lors d'une animation par exemple. En fait, cela consiste à utiliser deux tampons, l'un est accédé en lecture (affichage) pendant que l'autre est accédé en écriture (le programme le remplit); une fois que la lecture reprend, on permute les tampons, celui qui servait en lecture sert en écriture et vice versa. Cela évite d'afficher une modification incomplète par exemple.


5-8. Matrices

OpenGL utilise principalement 2 matrices de transformation :

  • GL_PROJECTION
  • GL_MODELVIEW
La matrice de projection permet de paramétrer le frustum, autrement dit le type de projection. On peut faire soit une projection orthogonale (cf. glOrtho et gluOrtho2D), soit une projection en perspective (glFrustum et gluPerspective). C'est surtout la projection en perspective qui nous intéresse. La matrice modèle-vue permet de paramétrer l'observation, pour placer la caméra en définissant la position de l'observateur, le point de référence (le point qu'il regarde) et le vecteur dirigé vers le haut (appelé up vector). On utilise la méthode gluLookAt pour la modifier (en sélectionnant la matrice modèle-vue d'abord avec glMatrixMode).


5-9. Attributs

A Venir


5-10. Dessin hiérarchique

A Venir


5-11. Transformations géométriques (mise à l'échelle, translation, rotation, ...)

A Venir


5-12. Mode de rendu (immediate mode, display list, vertex array, compiled vertex array, VBO)

Le mode de rendu immédiat est le plus explicite et le plus simple à appréhender mais le plus lent. Chaque séquence de tracé commence par un appel à glBegin et se termine par un appel à glEnd. Ce mode a disparu d'OpenGL-ES et il en sera de même dans une future version d'OpenGL.

Les listes d'affichage (display lists ou call lists) permettent de déplacer les données sur la carte et de les utiliser ensuite via un identifiant. Une liste d'affichages comporte un certain nombre de primitive géométriques (par exemple des tracés en mode immédiat). Une liste doit être "compilée" pour ê ensuite exécuté, cette opération est coûteuse donc il est recommandé d'utiliser des listes d'affichage pour des données qui ne changent pas (ou très peu, auquel cas il faut recompiler la liste). Le gain de performances est important par rapport au mode immédiat surtout pour de gros ensembles de sommets. Ce mode a disparu de certaines versions d'OpenGL-ES et il en sera de même dans une future version d'OpenGL.

Les tableaux de sommets (vertex arrays, extension GL_EXT_vertex_array apparue dans OpenGL 1.1 en 1995) permettent de traiter les primitives géométriques sous forme de tableaux ce qui est plus rapide que le mode immédiat (qui envoie les primitives une par une à la carte graphique) mais plus lent que les listes d'affichage (car les données ne résident pas sur la carte pour les vertex arrays, elles sont envoyées à chaque affichage). Ce mode de rendu disparaîtra dans une future version d'OpenGL.

Les tableaux de sommets compilés (extension GL_EXT_compiled_vertex_array) permettent d'accélérer le rendu en utilisant le cache de la carte graphique pour afficher un même ensemble de primitives graphiques plusieurs fois par frame (par exemple un motif à reproduire un grand nombre de fois). Ce mode de rendu disparaîtra dans une future version d'OpenGL.

Les VBO (vertex buffer object, extension GL_ARB_vertex_buffer_object apparu en 2003 dans OpenGL 1.4 mais disponible sur certaines cartes compatibles OpenGL 1.3 et optionnellement dans OpenGL-ES 2.0, extension GL_ATI_vertex_array_object apparu en 2001, extension GL_NV_vertex_array_range apparu en 2001) font partie du projet OpenGL LM de l'ARB visant à simplifier l'écriture de pilotes de cartes graphiques et le rendu de primitives groupées. Ils peuvent tout à fait remplacer les autres méthodes de rendu et disposent d'une API simple, homogène et flexible tirant le meilleur des display lists et le meilleur des vertex arrays non compilés et compilés. Cependant, contrairement à ce qui est véhiculé sur beaucoup de sites internet, l'utilisation des VBO n'améliore qu'imperceptiblement les performances par rapport à une utilisation adéquate des "anciens" modes de rendu. Pour vous en convaincre, modifiez TUER pour qu'il n'utilise pas les VBO, il va alors se servir des display lists et des vertex arrays (il utilise de gros ensembles de sommets donc c'est un exemple pertinent), les performances seront alors quasiment identiques avec et sans les VBO.


5-13. Rendu multiple d'ensemble de point

L'extension GL_EXT_multi_draw_arrays apparu en 1999 (disponible depuis OpenGL 1.1, parfois appelé GL_SUN_multi_draw_arrays, disponible en standard depuis OpenGL 1.4) permet d'afficher plusieurs sections d'un VBO ou d'un vertex array en un seul appel via les méthodes glMultiDrawArraysEXT et glMultiDrawElementsEXT. Le gain de performance est très faible sur les premièmes cartes à l'implémenter.

L'extension GL_EXT_draw_instanced apparu en 2008 (disponible depuis OpenGL 2.0) permet de faire du geometry instancing avec les méthodes glDrawArraysInstancedEXT et glDrawElementsInstancedEXT. On peut utiliser un VBO ou un vertex array. L'identifiant d'instance (instance ID) peut être utilisé dans un vertex shader en tant qu'entier signé sur 32 bits "vertex.instance". Cet identifiant est incrémenté en interne quand OpenGL traite chaque itération. Ainsi, en passant un VBO une seule fois, on peut le dessiner de différentes positions en se servant du vertex shader.


5-14. Textures (multipass rendering et multitexturing à l'étude)

A venir


5-15. Affichage de surfaces colinéaires superposées (glPolygonOffset)

Quand deux objets sont très proches (par exemple un impact de balle sur un mur), des artefacts graphiques peuvent apparaître. Cela est dû à une limitation de la précision du tampon de profondeur. On parle alors de Z-buffer fighting. On peut introduire programmatiquement un décalage dans le tampon de profondeur d'OpenGL en utilisant la méthode glPolygonOffset.

Il n'y a pas de méthode simple pour calculer le décalage à passer en paramètre. Il est conseillé d'en tester plusieurs jusqu'à obtenir le résultat voulu tout en étant le plus petit possible.


5-16. Alpha blending

Cette technique permet entre autres d'afficher des objets dont une partie de certaines textures est transparente.


5-17. Suppression des faces arrières

La suppression des faces arrières (backface culling) est un moyen complémentaire d'éliminer des surfaces cachées qui se concentre sur les faces d'un objet comme l'indique son nom. Par exemple, prenons un cube, il n'y a aucun intérêt à afficher ses faces arrières si on le regarde depuis l'extérieur.


5-18. Requête d'occlusion (à l'étude)

Les requête d'occlusion peuvent à la fois être utilisées dans le système de gestion des collisions et pour construire une hiérarchie d'occludeurs (appelée Hierarchical Occlusion Map) pour déterminer les surfaces visibles surtout dans des espaces ouverts où d'autres techniques s'avèrent peu efficaces. Cependant, les requêtes d'occlusion sont asynchrones. Dans TUER, il est envisagé d'utiliser des antiportals, autrement dit des objets qui de part leur envergure peuvent cacher beaucoup de surfaces et donc qui sont de bons candidats comme occludeurs.


5-19. Shader (à l'étude)

Il existe 3 types de shader :

  • Les vertex shaders
  • Les geometry shaders
  • Les pixel shaders (appelé aussi fragment shaders)
Les shaders permettent de paramétrer certaines étapes de rendu jusqu'alors cablées en dur ce qui donne plus de flexibilité, cela évite de se contenter des manipulations du frame buffer en fin de processus.

Les vertex shaders permettent de modifier les transformations et l'éclairage alors que les pixel shaders permettent de modifier la couleur d'un pixel. Les geometry shaders (via l'extension GL_EXT_geometry_shader4) permettent d'ajouter et de retirer des sommets d'une primitive.


5-20. Chargement de modèles au format MD3, OBJ et 3DS

A venir


5-21. Son (OGG, WAV, MIDI)

Les bibliothèques JOGG et JORBIS permettent de lire et de jouer des morceaux au format OGG. Pour lire d'autres formats, on peut soit passer par JavaSound qui fait partie du Java de base, soit utiliser JOAL qui offre plus de fonctionnalités.


5-22. Animations (basé sur le temps avec un "timer")

A venir


5-23. Modèle MVC (wip)

Le patron de conception MVC permet de séparer une application logicielle en 3 parties que sont la vue, le modèle et le contrôleur. Le modèle contient les données, le comportement du programme. La vue contient la présentation des données, la partie qui interagit avec l'utilisateur. Le contrôleur permet de faire communiquer les deux parties. Dans ce patron de conception, on tolère que la vue accède au modèle directement en lecture mais pas en écriture. De ce point de vue, TUER est plus strict puisque qu'il oblige la vue à toujours passer par un contrôleur (cela s'avèrera utile dans le mode de jeu en ligne).


5-24. Architecture de composants (un seul composant pour le moment)

Une telle architecture permet de concevoir l'application comme un circuit électronique. Ainsi, dans l'idéal, il serait pratique de pouvoir changer un composant logiciel quand il s'avère défectueux ou pas assez performant par exemple. Un composant logiciel doit être documenté, intégrable, et robuste. Pour qu'il soit utilisable, il faut déjà que la documentation explique clairement ses services (traitements) et ses performances. Elle doit mentionner les points d'entrée et de sortie du composant et préciser ses contrats (cf. Aspect-Oriented Programming). Il est également envisageable de pouvoir compiler le composant séparément comme c'est le cas dans TUER. L'utilisation de composants logiciels améliore la qualité du logiciel. Il est plus facile de maintenir un logiciel quand la séparation des considérations (separation of concerns dans la littérature UML) est claire, quand le rôle de chaque composant est identifié. Cela rend le code plus réutilisable.


5-25. Composants graphiques de base en JOGL (barre de progression, menu)

Le mélange de composants lourds et légers peut poser des problèmes. Par exemple, si vous utilisez un JPopupMenu avec un GLCanvas, il se peut que le résultat final ne soit pas celui que vous attendiez, le menu scintille et est inutilisable. JOGL permet d'utiliser des composants Swing avec la classe GLJPanel mais c'est moins fiable qu'un simple GLCanvas. Du coup, vous pourriez être tentés de mélanger les deux. Pour résoudre ce problème, vous pouvez utiliser la bibliothèque FengGUI ou bien réécrire vous-mêmes les composants dont vous avez besoin avec JOGL.


5-26. Script ANT

ANT est un outil open source créé par la fondation Apache qui permet de précompiler un projet Java, un peu comme un makefile en C. Cet outil est portable, il permet quand même d'appeler des programmes natifs propres à la plateforme d'exécution. Il est très complet, pratique et extensible. Je l'utilise dans TUER pour précompiler les sources mais aussi pour créer des archives ZIP et des JARs, pour signer un JAR, pour lancer des programmes externes, etc... De plus, ANT peut être utilisé dans plusieurs IDE comme Eclipse et Netbeans mais aussi en ligne de commande. Cela permet de ne facher personne dans un projet open source, chacun restant libre de son choix d'é pour coder. ANT utilise XML. Pour s'en servir, il faut écrire un fichier "build.xml" en décrivant chaque cible dans son dialecte.


5-27. Détection des collisions (volumes englobants, voxels)

Pour des raisons de performance, il est rare que les jeux vidéo utilisent des systèmes de détection de collisions exacts et cela ne s'avève pas nécessaire sauf pour certaines simulations. Il y a au moins 4 types d'approximations :

  • La géométrie des objets
  • La fréquence des tests de collision (par rapport au frame rate)
  • Le nombre d'instants discrets considérés entre la position de départ et la position d'arrivée
  • Le modèle physique (les approximations des interpolations des trajectoires par exemple)
On peut simplifier la géométrie des objets en utilisant des volumes englobants, c'est-à-dire des polyèdres comportant moins de sommets que les objets qu'ils contiennent, par exemple des sphères, des ellipsoides, des parallélépipèdes (alignés ou pas avec les axes du repère).
Un voxel est un élément unitaire de volume, par analogie avec un pixel. Une "grille" de voxels peut être stockée à l'aide de deux types abstraits de données, soit une matrice creuse (rapide à parcourir mais gourmand en mémoire sauf si on se sert d'une liste bilinéaire au lieu d'un tableau multidimensionnel) soit un graphe sous forme de liste d'adjacence (moins rapide à parcourir mais beaucoup moins gourmand en mémoire), soit un arbre. On distingue alors les voxels vides et les voxels pleins.

Réduire la fréquence des tests de collision doit être fait avec grande prudence car plus l'objet se déplace vite, plus la correction à posteriori sera difficile à calculer, il se peut même que le joueur passe à travers les décors.
Pour des déplacements très courts, on peut se contenter de considérer seulement les deux instants discrets (le départ et l'arrivée), sinon on peut considérer un nombre fini d'instants discrets intermédiaires voire tenter d'approximer tous les instants en calculant le test avec un volume qui englobe toute la trajectoire, la forme de celle-ci dépendant du type de déplacement supposé (une translation rectiligne uniformément variée par exemple).

Un système de collisions peut aussi traiter les solides déformables mais là, ça se complique sérieusement. Je vous conseille l'algorithme VCLIP pour ce faire, il représente les solides déformables sous forme de hiérarchies de polyèdres convexes.
Il y a aussi diverses moyens d'optimiser les tests de collision :

  • Les subdivisions de l'espace
  • La séparation des objets qui n'interviennent pas dans les collisions des autres par une étape de présélection (n-body pruning)
  • L'utilisation de plusieurs couches de test pour ne pas appliquer des algorithmes très coûteux entre des objets distants


5-28. Algorithme de subdivision de l'espace

Pour déterminer les surfaces visibles, on peut laisser faire la carte graphique qui fait du frustum culling puis en activant le test de profondeur et/ou le backface culling mais cela peut s'avérer insuffisant quand on commence à avoir des scènes complexes. Alors, on peut explorer des pistes pour réduire la partie de la géométrie que l'on va effectivement envoyer à la carte graphique afin qu'elle décide quelles surfaces doivent être effectivement affichées. En réduisant la quantité d'information envoyée, on peut notablement améliorer la vitesse du moteur et donc le frame rate.

Pour ce faire, on peut découper la scène en petits bouts, choisir quels petits bouts doivent être envoyés à la carte graphique en fonction de la position du point d'observation et de la direction. Il existe un nombre important d'algorithmes de subdivision de l'espace. Il n'y en a pas un vraiment mieux que les autres, les jeux modernes combinent plusieurs d'entre eux. Le choix de l'algorithme dépend des scènes à gérer. Un critère important est la dynamicité des décors, c'est-à-dire la propension de vos décors à changer au cours de la partie. Par exemple, Red Faction 2 a des décors très dynamiques puisque vous pouvez casser presque tous les murs du jeu alors que Quake 3 a des décors plutôt statiques. La dynamicité des décors implique des contraintes différentes. Des décors statiques ne nécessiteront pas de recalculer les subdivisions de l'espace pendant la partie donc on peut choisir un type abstrait de données qui a des performances médiocres en mise à jour (et à la création) mais moins coûteux à parcourir. Par contre, pour des décors dynamiques, on doit choisir un type abstrait de données moins coûteux à mettre à jour quitte à ce que le parcours soit un peu plus coûteux.
Un autre critère important est le caractère ouvert des décors. Si la scène se joue en extérieur, mieux vaut se référer à la section sur les requêtes d'occlusion. Si vous mélangez les scènes d'intérieur et d'extérieur, vous pouvez tout à fait mélanger plusieurs techniques de subdivision de l'espace.

Il existe deux grandes catégories de type abstrait de données pour hiérarchiser l'espace :

  • les arbres
  • les graphes
Pour que le coût de parcours d'un arbre soit intéressant, il faut le maintenir équilibré au fil des modifications pour éviter qu'il ne se dégrade en liste. Ces rééquilibrages ont un coût parfois important et difficilement prévisible, il se peut qu'un nouveau plan de partitionnement fragmente beaucoup de surfaces, cela pose un problème de mémoire et de vitesse d'exécution. Un graphe ne nécessite pas de tels rééquilibrages, c'est pourquoi il est préférable d'utiliser ce type abstrait de données pour des scènes dynamiques. Toutefois, si vous souhaitez absolument utiliser des arbres n-aires pour ce faire, privilégiez des degrés plus élevés pour qu'une modification locale soit moins coûteuse et ne rééquilibrez pas l'arbre si la scène est modérément dynamique car le gain de vitesse de parcours ne compenserait pas le coût des rééquilibrages, ce ne serait pas rentable.

Il existe plusieurs types d'arbre de partitionnement de l'espace :

  • les arbres binaires
  • les arbres quaternaires
  • les arbres octaires
  • les arbres K
  • les arbres KD
On utilise un ou plusieurs plans de partitionnement pour construire un arbre de partitionnement de l'espace. Par exemple, pour un arbre binaire de partionnement de l'espace(BSP tree), on utilise un seul plan de partionnement, on subdivise récursivement l'espace en plaçant les surfaces devant ce plan d'un côté, les surfaces derrière ce plan de l'autre.

Une approche possible pour construire un graphe de partionnement de l'espace est d'utiliser l'algorithme de subdivision de l'espace en cellules et en portails (cells-and-portals algorithm). On appelle une méthode qui permet de précalculer un tel graphe une méthode de calcul des PVS (Potentially Visible Sets) ou plus précisément dans notre cas une méthode de calcul des PVC (Potentially Visible Cells).

Je vais vous présenter l'algorithme utilisé dans le scénographe de TUER. Celui-ci n'est pas optimal, il ne marche que dans un cas restreint mais cela donne quelques bases avant d'aller plus loin sachant que vous aurez beaucoup de mal à trouver des informations à ce sujet, c'est d'ailleurs pour cette raison que j'ai dû écrire le mien. Les papiers que j'ai lus ne détaillaient pas le fonctionnement des algorithmes et il existe peu de moteurs open source qui s'en servent.


5-29. Mode multijoueur avec JGN et/ou Project Darkstar (à l'étude)

Les problèmes de latence de RMI m'ont vite poussé à envisager d'autres solutions. Pour un jeu en temps réel, on a besoin d'un temps de réponse très court. Une alternative possible est d'utiliser JGN qui dispose d'un équivalent de RMI.

Il y a quelques précautions à prendre (ceci est valable pour de nombreuses méthodes d'accés distant) :

  • La partie modèle (au sens du patron de conception MVC) accessible à distance doit être thread-safe pour éviter qu'un seul client puisse bloquer le serveur
  • Il faut choisir un algorithme de ramasse-miettes adapté, le concurrent low pause collector et fixer une durée maximale des pauses
  • Si la méthode choisie permet d'utiliser des stubs plutôt que de recréer l'objet côté client, il faut faire le nécessaire pour rendre cela possible, implémenter l'interface Remote pour RMI par exemple
  • Il faut créer des caches côté client pour éviter d'aller chercher côté serveur des informations que nous avons déjà
  • Il faut renseigner côté serveur une table indiquant quand les informations disponibles à distance ont été modifiées pour la dernière fois et permettre alors au client de ne raffraichir ses caches uniquement quand c'est nécessaire (le client doit comparer l'ancienne version de la table et la version courante pour savoir ce qui a changé)


5-30. Capture "vidéo" (premier jet plutôt lent, pas encore fini)

Le système de capture vidéo de TUER est rudimentaire pour le moment, il produit une image par frame. J'envisage d'utiliser FMJ à l'avenir pour capter le flux et encoder la vidéo en temps réel si possible. Cependant, il existe une solution plus simple mais moins performante: utiliser le système de capture d'écran actuel puis appeler mencoder via jmencode pour fabriquer une vidéo à partir des fichiers image au format TGA.


5-31. Système anti verrou de Cardan (quaternions et transformations non eulériennes, à l'étude)

Le verrou de Cardan est dû à une limitation des transformations eulériennes qui se manifeste quand on combine 3 rotations autour de 3 axes différents. On perd un degré de liberté quand deux des trois axes sont portés dans la mê direction. Le seul moyen précis de résoudre ce problème est d'utiliser les transformations non eulériennes. Elles nécessitent de pouvoir exprimer des rotations autour d'axes quelconques ce qui est plus simple avec des quaternions. Contrairement à une idée très largement répandue, les quaternions à eux seuls ne résolvent pas ce problème s'ils sont utilisés avec des transformations eulériennes classiques (en combinant roulis, tangage et lacet).
Autrement dit, si on note R[Ox,a](objet) la rotation autour de l'axe Ox et d'angle a de l'objet, R[Oy,b] la rotation autour de l'axe Oy et d'angle b et R[Oz,c] la rotation autour de l'axe Oz d'angle c, pour combiner les 3 rotations axiales, on fait :

R(objet)=R[R[R[Ox,a](Oy),b]R[Ox,a](Oz),c](R[R[Ox,a](Oy),b](R[Ox,a](objet)))

On conserve ainsi le repère local associé à l'objet qui change après chaque rotation appliqué à celui-ci, on garde ainsi tous les degrés de liberté.


Il existe une solution imprécise qui se base sur les équivalences de Young-Hoo.


5-32. Micro-optimisations en Java

Tout d'abord, n'en arrivez là que quand vous aurez épuisé les autres pistes d'optimisation de plus haut niveau. Généralement, vous gagnerez beaucoup plus avec les macro-optimisations (optimisations des algorithmes et de leurs implémentations propres) qu'avec les micro-optimisations. De plus, utiliser des optimisations de bas niveau quand les optimisations de haut niveau n'ont pas été envisagées revient à mettre des rustines sur un pneu crevé, il se peut que vous passiez à côté de problèmes de performance bien réelles voire de bogues en procédant ainsi.
Il existe donc plusieurs pistes de micro-optimisations en Java :

  • La classe Unsafe
  • Le fine tuning du ramasse-miettes
  • L'interfaçage avec du code natif
  • L'allocation sur la pile
La classe sun.misc.Unsafe permet de simuler l'arithmétique des pointeurs et d'accéder directement à la mémoire mais de manière portable. Une des applications possibles est de se passer des array bound checks. Le gain de performance est minime (5%) par rapport à des tableaux classiques alors autant utiliser des NIO buffers. Vous pouvez aussi forcer la libération de la mémoire. La documentation de cette classe se trouve en ici.

Le garbage collector dispose de plusieurs algorithmes. Par exemple, le concurrent low pause collector effectue des pauses plus courtes mais utilise globalement plus de temps CPU que l'algorithme par défaut.



Valid XHTML 1.1!Valid CSS!

Les sources présentées sur cette page sont libres de droits, et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une oeuvre intellectuelle protégée par les droits d'auteurs. Copyright © 2008 Julien GOUESSE. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à 3 ans de prison et jusqu'à 300 000 E de dommages et intérêts. Cette page est déposée à la SACD.

Vos questions techniques : forum d'entraide 2D - 3D - Jeux - Publiez vos articles, tutoriels et cours
et rejoignez-nous dans l'équipe de rédaction du club d'entraide des développeurs francophones
Nous contacter - Hébergement - Participez - Copyright © 2000-2009 www.developpez.com - Legal informations.