Tutoriel de création d’un minimoteur de rendu 3D en Java avec LWJGL

Introduction : La 3D, LWJGL, installation, Hello-World

Gérard Bourriaud

Le but de ce tutoriel est de vous donnez des informations utiles afin de faire un moteur de rendu 3D en Java avec LWJGL. Ce tutoriel sera découpé en plusieurs articles. Ici c’est le premier donnant des informations générales sur la 3D, comment installer son environnement de travail, faire sa première fenêtre et un Hello World.

Cet article suppose que vous connaissiez le langage Java mais ne demande pas de connaissance en 3D, puisqu’il les fournis.

- Titre : Tutoriel de création d’un minimoteur de rendu 3D en Java avec LWJGL

- Auteur : Gérard Bourriaud

- Parution : 7 février 2021

- Mise à jour : 7 février 2021

- Public visé :

- Licence : 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 œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2020 JackHack. 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'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Voir également :

- Tutotriel sur un moteur 3D en Java avec LWJGL - Le graphe de scène

- Tutotriel sur un moteur 3D en Java avec LWJGL - Interaction avec l'utulisateur

Sommaire

- I. Introduction

- II. Rappel sur les concepts de la 3D

- III. La 3D et Java

- IV. Informations sur l’OpenGL

- V. Développement

- V-A. Créer un environnement de travail

- V-B. Remarque pour Mac

- V-C. Créer une fenêtre d’affichage

- VI. Hello world

- VI-A. Dessinons un triangle rouge

- VI-B. Un petit peu plus

- VII. Conclusion et remerciements

I - Introduction

Code source :

Code Source

Téléchargez le code source complet

La 3D en informatique est la projection d’un monde en trois dimensions sur un monde en deux dimensions (l’écran de l’appareil comme un ordinateur, un téléphone, une tablette.). Tout ce passe comme si on regardait ce monde à travers une fenêtre, c’est l’habitude de voir des perspectives, et un monde en 3D qui permet à notre cerveau de reconstituer l’effet de profondeur. C’est une chose qui m’a frappée lors de l’écriture de mon premier moteur 3D à une époque ou avoir une carte 3D ou un chipset intégré capable d’en faire était un luxe. À l’époque j’avais du tout faire en 100% Java.

Fort heureusement pour vous désormais les cartes 3D existent partout désormais ce qui simplifiera grandement ce tutoriel vous évitant des mathématiques sur la géométrie projective et des tas de problèmes de performances. Tout cela étant désormais géré par la carte 3D. Il y aura tout de même un peu de mathématiques auxquels on ne peut y échapper.

Pour adresser la carte 3D et lui demander d’afficher ce que l’on veut, il existe deux interfaces, DirectX (Disponible uniquement pour Windows) et OpenGL (Disponible pour toutes les plateformes : Windows, Mac, Linux). OpenGL ES (Pour les mobiles et tablettes : Android, IOS). OpenGL ES, par rapport à OpenGL est plus léger, et permet moins de choses en général. Ces interfaces sont des conventions que les constructeurs de cartes 3D s’engagent a respecté lors de l’écriture de leur driver pour la carte 3D. Pour être compatible avec les trois plate-formes Windows, Mac et Linux nous passeront par OpenGL. OpenGL reste assez bas niveau dans le sens, où on ne peut s’y adresser directement qu’à travers des librairies en C/C++. Fut une époque ou j’étais obligé de créer un binding en JNI pour OpenGL. Un binding est une traduction en Java, par exemple, des fonctions de la librairie en C. Puis à travers JNI (Java Native Interface), traduire chaque commande de Java vers C et chaque réponse de C vers Java. Fort heureusement pour vous, tout ce travail et la gestion de la mémoire et des problèmes de pointeurs est déjà faite désormais. Les binding les plus connus sont JOGL et LWJGL. Grâce à eux, vous n’aurez pas à taper une seule ligne en C/C++, ni du JNI. Il a été choisi LWJGL pour ce tutoriel.

Le site d’OpenGL vous permettra d’avoir une liste les commandes OpenGL. Ce qui vous permettra de progresser en dehors de ce tutoriel, qui ne peut pas prétendre tout couvrir.

II - Rappel sur les concepts de la 3D

Nous allons présenter ici, des notions et des définitions propre à la 3D. La vue 3D est faite à travers la fenêtre de l’écran. Comme dans la réalité, le champ s’agrandit plus les objets sont loin. Ils sont bien entendu perçu plus petit. Le champ de vison peut être représenté par une pyramide tronquée appelée « frustum ».

L’écran jaune sur le schéma sera la fenêtre sur laquelle sera dessinée la 3D. Les objets visibles à l’écran se trouvent dans le « frustum ». Il y a une limite à la profondeur que l’on peut voir. L’utilisateur est représenté ici par une caméra. Autre illustration plus imagée :

Comme dans la réalité, il y a trois dimensions :

- L’horizontale (orientée de gauche à droite), appelée l’axe des abscisses, noté X

- La verticale (orientée de bas en haut), appelée l’axe des ordonnées, noté Y

- La profondeur (orientée de très loin devant l’observateur jusqu’à l’utilisateur), appelée l’axe des profondeurs, noté Z

Le point central est l’observateur.

- Les abscisses X négatives seront à gauche de l’observateur

- Les abscisses X positives seront à droite de l’observateur

- Les ordonnées Y négatives seront sous de l’observateur

- Les ordonnées Y positives seront au-dessus de l’observateur

- Les profondeurs Z négatives seront face à l’observateur

- Les profondeurs Z positives seront derrière l’observateur

Pour résumé :

L’observateur se trouvant au centre de ce repère. Un tel repère s’appelle un repère Euclidien. Nom venant du mathématicien Euclide qui les a beaucoup étudiés dans ces travaux sur la géométrie traditionnelle. Pour les plus exigeants, le repère est orthonormé. Orthonormé veut dire orthogonal et normé :

- Un repère est orthogonal quand chaque axe fait un angle droit les uns part rapport aux autres

- Un repère est normé si sur ces axes on les découpe en intervalles réguliers pouvant ainsi avoir une graduation sur ceux-ci. Propriété nous permettant d’avoir des coordonnées, c’est-à-dire des valeurs pour X, Y et Z

Avec les coordonnées (X, Y Z) on peut placer des points dans le repère. En reliant les points on obtient des polygones. Si on colorie l’intérieur des contours polygones on obtient des faces. Un objet 3D sera formé d’une ou plusieurs faces.

Placer des objets dans la 3D est bien, pouvoir les bouger est mieux. Une transformation va changer de place un objet 3D, le changer de taille, voir le déformer. Toute transformation peut être représentée par une matrice, mais il est plus simple, dans la pratique, de manipulation de découper en transformations de base. À savoir qu’OpenGL vous permet les deux. Ici nous allons vous présenter les transformations des bases. En les combinant on pourra faire tout ce dont nous avons besoin.

La translation est bougée un objet dans une direction donnée. On précise de combien en X, Y et Z de combien l’objet doit bouger. Ce triplet (X, Y, Z) est appelé un vecteur. C’est un peu comme si on disait à l’objet :

- Avec X, avance de tant de pas à droite (ou à gauche)

- Avec Y, avance de tant de pas vers le haut (ou le bas)

- Avec Z, avance de tant de pas vers l’arrière (ou l’avant)

Les choix entre parenthèses sont pour les valeurs négatives de X, Y ou Z. Ce qui peut être illustré par :

Le cube bleu est l’objet que l’on bouge, la flèche verte, la direction qu’on veut qu’il prenne.

La rotation est le fait de faire tourner un objet autour d’un axe virtuel. C’est comme si on avait une barre et que l’on décidait de tourner autour.

Le changement d’échelle ou « scale » en anglais, permet de changer les proportions d’un objet. C’est un peu comme si l’objet pouvait s’étirer ou se compresser en X, Y et Z. Le facteur d’étirement ou de compression peut être différent selon l’axe.

La représentation des couleurs sur un écran est décomposée en trois couleurs de base : le rouge, le vert et le bleu. En jouant sur l’intensité des chacune de ces couleurs on obtient une très grande palette de couleurs possible. Sur les écrans modernes, on a 256 niveaux de rouge, 256 de vert et 256 de bleu. Ce qui donne couleurs possibles pour chaque pixel. À ce triplet on ajoute un niveau de transparence. De complètement opaque à totalement transparent. Cette information n’est pas directement dessinée à l’écran, mais permet de calculer la couleur finale quand un objet semi-transparent passe devant un autre. Imaginez une vitre teintée de rouge, la scène derrière elle sera plus ou moins rougie selon la transparence de cette fenêtre.

III - La 3D et Java

Le rendu 3D est calculée par la carte 3D. Pour que le rendu soit dessiné à l’écran dans une fenêtre, il faut demander à l’OS de réserver une fenêtre, à laquelle on demandera de dessiner le rendu calculé par la carte 3D. Pour donner des instructions à la carte 3D, il faut un ou des drivers. Par-dessus le/les drivers il y a une couche OpenGL. L’OpenGL quant à lui est manipulé par un librairie en C/C++. Le C/C++ échange avec Java via JNI. Par-dessus la couche Java vient notre code.

Sur le schéma, en jaune la partie gérée par LWJGL. On pourrait craindre que l’on perde des performances avec toutes ces couches que l’on doit traverser. Mais en réalité :

1. On n’envoie que des instructions à la carte 3D, c’est elle qui fait tout le travail qui coûte cher du calcul de rendu. Que les instructions veinent de Java, ou directement du C, ça ne change rien au temps du calcul du rendu.

2. Que se soit JOGL ou LWJGL, un travail remarquable a été fait pour optimiser les échanges à travers la couche JNI. Nous verrons plus loin comment en profiter au mieux.

3. Si on respecte le thread qui sera dédié au rendu et qu’il ne fait qu’une chose, envoyer des instructions, il faudra une scène très complexe pour commencer à sentir des ralentissements (Que l’on aurait également en C).

IV - Informations sur l’OpenGL

Le rendu fonctionne par frame. Une frame est le rendu à un instant donné. Comme une vidéo, le rendu n’est pas continu, mais en plusieurs images les une derrière les autres. C’est le défilement rapide de ces images qui donne l’impression de continuité. Pour le rendu, nous allons avoir une boucle qui à chaque tour va dessiner la nouvelle frame. Si nous voulons que le rendu paraisse fluide, nous ayons intérêt à ce que ce rendu soit rapide et non perturbé par des événements extérieurs. C’est pour cela que nous créerons un thread dédié à ce rendu qui ne fera qu’une chose le rendu OpenGL. Tous les éventuels écouteurs, événements, seront appelés dans des threads à part. C’est pour cela, que nous allons faire en sorte que le développeur qui utilisera notre code n’est pas à intervenir dans le thread OpenGL.

L’OpenGL fonctionne comme une machine à état. Ce qui veut dire que chaque instruction influence les suivantes. Si on veut annuler l’effet d’une instruction, il faut faire son instruction opposée. La conséquence de cette machine à état, est que les effets peuvent s’accumuler d’un tour de boucle sur l’autre. C’est pour cela que nous ferons en sorte qu’un tour de boucle n’a pas d’influence sur son suivant.

L’ordre des instructions est important, surtout quand il s’agit de cumuler des transformations afin de positionner et/ou déformer un objet. Par exemple ce n’est pas la même chose de tourner d’un quart de tour sur la droite, puis avancer de trois mètres. Et avancé de trois mètres, puis tourner d’un quart de tour sur la droite.

Avec ce que nous avons dit, pour positionner un cube et une sphère, on ferait à chaque tour de boucle

1. Liste de transformations pour placer le cube

2. Dessiner le cube

3. Liste de transformations inverse de celles pour placer le cube

4. Liste de transformations pour placer la sphère

5. Dessiner la sphère

6. Liste de transformations inverse de celles pour placer la sphère

La liste inverse des transformations, est assez pénible à gérer, heureusement l’OpenGL à un mécanisme pour simplifier cette gestion. Elle à de plus le mérite d’être plus rapide que de refaire le chemin inverse. Pour gérer la cumulation des transformations, l’OpenGL passe par une matrice. Chaque transformation modifie la matrice courante. l’OpenGL se sert ensuite de cette matrice résultante pour placer les objets. Il est possible de sauvegarder l’état de la matrice courante afin de la restaurer dans la pile des matrices. Ainsi plus besoin de revenir en arrière, il suffit de sauvegarder l’état auquel on veut pouvoir retourner. De faire nos modifications, puis de restaurer la matrice. Pour notre exemple, il devient

1. Sauvegarde de la matrice actuelle

2. Liste de transformations pour placer le cube

3. Dessiner le cube

4. Restaurer la matrice sauvegardée

5. Sauvegarde de la matrice actuelle

6. Liste de transformations pour placer la sphère

7. Dessiner la sphère

8. Restaurer la matrice sauvegardée

La pile des matrices, s’appelle ainsi, car elle fonctionne comme une pile en informatique. Cela permet de sauvegarder plusieurs états. Illustrons ce comportement. Au départ la pile est vide

Ensuite on sauvegarde un état

Puis un autre

On remarque, que le second état est placé sur le premier. À chaque sauvegarde, cela se comporte ainsi

et

Quand on restaure une matrice, on restaure la matrice d’en haut de la pile et cet état est enlevé de la pile. Dans notre schéma une restauration va rétablir l’état 4 et la pile devient

Une autre restauration va rétablir l’état 3, en changeant la pile

À retenir

Chaque sauvegarde met l’état courant de la matrice en haut de la pile

Chaque restauration, restaure l’état de la matrice en haut de la pile et l’enlève de la pile.

L’intérêt d’un tel mécanisme est particulièrement intéressant lorsqu’un objet doit être placé par rapport à un autre. Par exemple, si on souhaite dessiner une voiture, On va dessiner la carrosserie, puis chaque roue par rapport à la carrosserie. Pile au départ supposée vide.

Sauvegarde de la matrice principale

Placer la carrosserie. Dessiner la carrosserie. Sauvegarde de la matrice de la carrosserie

Placer la roue avant droite. Dessiner la roue avant droite. Restaurer la matrice de la carrosserie

Sauvegarde de la matrice de la carrosserie

Placer la roue avant gauche. Dessiner la roue avant gauche. Restaurer la matrice de la carrosserie

Sauvegarde de la matrice de la carrosserie

Placer la roue arrière droite. Dessiner la roue arrière droite. Restaurer la matrice de la carrosserie

Sauvegarde de la matrice de la carrosserie

Placer la roue arrière gauche. Dessiner la roue arrière gauche. Restaurer la matrice de la carrosserie

Restaurer la matrice principale

Chaque objet est découpé en une ou plusieurs faces. Chaque face étant un polygone simple, souvent des triangles ou des trapèzes. Bien que possible, il est rare d’utiliser des polygones de plus de quatre cotés. l’important est d’utiliser des polygones dit convexes. Ce découpage en faces s’appelle une maille. Pour simplifier la définition de convexe, c’est un polygone dont aucun coté ne se croisent et qui n’a pas de creux. Exemples de polygones convexes

Exemples de polygones non convexes, appelés aussi concaves

Remarque

Tous les triangles sont naturellement convexes

Pour les polygones non convexes, on les découpe en plusieurs polygones convexes

Pour les objets ronds, comme un ballon par exemple, on va dessiner des faces suffisamment petites pour approximer l’objet, puis grâce à l’éclairage donner l’illusion d’arrondi. Nous parlerons plus tard de l’éclairage et de son fonctionnement. Maille d’une sphère

V - Développement

Nous allons maintenant commencer à faire notre première fenêtre qui va afficher de la 3D. D’autres notions de 3D seront développées dans les tutoriels suivants celui-ci. Nous allons tout d’abord créer un environnement de travail, puis faire une fenêtre qui accueillera notre 3D et enfin, dessiner un triangle rouge.

V-A - Créer un environnement de travail

La première chose à faire est de télécharger la version de LWJGL sous forme de zip sur leur Github : LWJGL. Une fois télécharger, dézipper le tout dans un dossier vide. Je vous conseille d’avoir un dossier ou il n’y a que ça dedans. Avec IntelliJ créez un projet Java : File -> New -> project…

Puis sur Next

Choisir un nom de projet, puis sur Finish. Maintenant il faut ajouter la librairie LWJGL à notre projet. Comme vous l’aurez remarqué, le zip de LWJGL contient tout un tas de dossiers.

Chaque dossier est une librairie que peut adresser LWJGL, ce qui permet de choisir ce dont le projet a besoin. La description officielle du contenu se trouve à Décription des librairies au sein de LWJGL. Nous allons configurer notre projet pour se lier aux librairies nécessaires. Ce dont on a besoin pour le moment :

1. La librairie core, commune à toutes les autres, vous en aurez toujours besoin, quelque soit la librairie LWJGL que vous souhaitez utiliser. Elle se trouve dans le dossier : « lwjgl »

2. La librairie OpenGL, qui permet de communiquer avec la carte graphique via OpenGL : « lwjgl-opengl »

3. La librairie GLFW, qui va nous permettre d’afficher une fenêtre dans laquelle on affichera la 3D : « lwjgl-glfw »

Copions dans un dossier lib de notre projet les librairies ci-dessus citées :

Remarque :

Afin de respecter l’open source, nous copions également les licences.

Nous vous invitons à les lire afin de décider si vous êtes en accord avec celles-ci.

La plupart vous laissent libre de l’utilisation, commerciale ou non.

Elle empêche seulement qu’un tiers s’en réclame propriétaire.

Pour bien comprendre, ouvrons le dossier de la librairie core :

Remarque

Pratiquement tous les dossiers de chaque librairie ont la même structure.

On y voit le jar principal : « lwjgl.jar », sa javadoc « lwjgl-javadoc.jar » et ces sources « lwjgl-sources.jar ». Les autres JARs qui commencent par « lwgj-natives » sont les libraires pour chaque plate-forme. Comme je vous l’ai dit plus haut, pour communiquer avec le matériel, on passe par des librairies en C/C++, ce qui implique l’utilisation du JNI. La partie C/C++ n’étant pas multiplate-forme, il faut compiler une librairie pour chaque plate-forme adressée. Bien sur c’est fait pour vous, mais vous devez les intégrer à votre programme. LWJGL à son lancement va détecter l’OS et déployer les bonnes librairies. Maintenant que vous savez à quoi sert les JARs, configurons notre « build.gradle »

build.gradle

1. dependencies {

2. // ...

3. // LWJGL core

4. implementation files("src/lib/core/lwjgl.jar")

5. runtime files("src/lib/core/lwjgl-natives-linux.jar")

6. runtime files("src/lib/core/lwjgl-natives-linux-arm32.jar")

7. runtime files("src/lib/core/lwjgl-natives-linux-arm64.jar")

8. runtime files("src/lib/core/lwjgl-natives-macos.jar")

9. runtime files("src/lib/core/lwjgl-natives-windows.jar")

10. runtime files("src/lib/core/lwjgl-natives-windows-x86.jar")

12. // LWJGL OpenGL

13. implementation files("src/lib/opengl/lwjgl-opengl.jar")

14. runtime files("src/lib/opengl/lwjgl-opengl-natives-linux.jar")

15. runtime files("src/lib/opengl/lwjgl-opengl-natives-linux-arm32.jar")

16. runtime files("src/lib/opengl/lwjgl-opengl-natives-linux-arm64.jar")

17. runtime files("src/lib/opengl/lwjgl-opengl-natives-macos.jar")

18. runtime files("src/lib/opengl/lwjgl-opengl-natives-windows.jar")

19. runtime files("src/lib/opengl/lwjgl-opengl-natives-windows-x86.jar")

21. // LWJGL GLWF

22. implementation files("src/lib/glfw/lwjgl-glfw.jar")

23. runtime files("src/lib/glfw/lwjgl-glfw-natives-linux.jar")

24. runtime files("src/lib/glfw/lwjgl-glfw-natives-linux-arm32.jar")

25. runtime files("src/lib/glfw/lwjgl-glfw-natives-linux-arm64.jar")

26. runtime files("src/lib/glfw/lwjgl-glfw-natives-macos.jar")

27. runtime files("src/lib/glfw/lwjgl-glfw-natives-windows.jar")

28. runtime files("src/lib/glfw/lwjgl-glfw-natives-windows-x86.jar")

29. // ...

30. }

Nous mettons en « implementation » les JARs nécessaires pour taper le code et en « runtime » ceux qui sont nécessaires seulement pendant l’exécution du programme.

Bien garder les noms des JARs contenant les librairies afin que LWJGL puisse faire son travail de déploiement des librairies adaptées à l’OS.

Pensez à synchroniser les sources afin que les modifications faites dans le build.gradle se répercutent dans le projet. Dans la dernière version d’IntelliJ, le bouton est un éléphant avec une roue bleue.

Au passage, parfois ce bouton n’est pas suffisant, dans ce cas, un « clean » puis un « build » complète le rafraîchissement.

Maintenant que notre environnent de travail est prêt on va pouvoir commencer des choses plus intéressantes.

V-B - Remarque pour Mac

Cette partie vous intéresse :

- Si vous utiliser un mac pour suivre ce tutoriel

- Au moment de la publication de votre application pour qu’elle soit également compatible Mac

Pour Mac, LWJGL à besoin que l’on précise l’option « -XstartOnFirstThread » à la JVM. Votre code qui permet de lancer l’application devra donc lancer à un moment le ligne de commande :

lancement du code

java -XstartOnFirstThread -jar MyApplication.jar

Cette option ne doit être activée que sur Mac. Le script de lancement va donc devoir faire un test de l’OS pour savoir si on est sur Mac ou pas.

Pour spécifier cette option dans Inteliji afin de pouvoir lancer les différents exemples de ce tutoriel, les utilisateurs de Mac vont devoir ce qui va suivre pour chaque classe main qu’ils créeront. D’abord créer un classe Main :

Main

1. package fr.developez.tutorial.java.dimension3;

3. public class Main

4. {

5. public static void main(String[] args)

6. {

7. }

8. }

Pour faire générer ce qu’il faut par défaut à Inteliji on clique sur la petite flèche verte :

Une fois lancé, apparaît en haut la configuration de Main. Nous allons modifie celle-ci pour ajouter notre option.

On clique sur le bouton pour faire apparaître les options.

Puis on clique sur « Edit Configurations … »

On choisit notre Main Et dans la zone de saisie « VM options » on y met l’option « -XstartOnFirstThread ». Ensuite à chaque fois qu’on lancera le Main l’option sera utilisée.

C’est le seul point spécifique pour Mac

V-C - Créer une fenêtre d’affichage

Nous allons créer et initialiser notre fenêtre et OpenGL pour accueillir nos futures scènes 3D. Comme nous n’avons dit plus haut, le thread OpenGL est extrêmement important. Il doit s’y effectuer toutes les commandes OpenGL et seulement celles-ci afin de ne pas nuire à l’affichage. Afin de se rappeler qu’une méthode s’exécute dans le thread OpenGL, nous créons une annotation à mettre à la déclaration de ces méthodes

ThreadOpenGL

1. package fr.developez.tutorial.java.dimension3.tool;

3. import java.lang.annotation.Documented;

4. import java.lang.annotation.ElementType;

5. import java.lang.annotation.Retention;

6. import java.lang.annotation.RetentionPolicy;

7. import java.lang.annotation.Target;

9. @Retention(RetentionPolicy.SOURCE)

10. @Target(ElementType.METHOD)

11. @Documented

12. public @interface ThreadOpenGL

13. {

14. }

Avant de pouvoir afficher la 3D à l’écran, il nous faut une fenêtre dans laquelle celle-ci sera dessinée. Nous allons donc commencer par créer cette fenêtre. Pour cela nous allons utiliser « GLFW ». Une chose à comprendre sur le GLFW et l’OpenGL, c’est que le Thread qui va initialiser la fenêtre doit être le même que celui qui sera utilisé pour faire le rendu OpenGL. Nous l’appelleront le Thread OpenGL. Dans ce Thread on ne doit faire que du rendu, tous les « listeners » que nous appellerons seront appelés dans un Thread à part. Ceci est important pour garder la fluidité du rendu. Un développeur utilisant notre fenêtre, ne devra jamais pouvoir ou devoir utiliser ce Thread directement. Cela évitera pas mal d’erreurs et des problèmes de rendu.

Widow3D

1. package fr.developez.tutorial.java.dimension3.render;

3. import java.util.Objects;

4. import fr.developez.tutorial.java.dimension3.tool.NonNull;

5. import fr.developez.tutorial.java.dimension3.tool.Tools;

6. import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;

8. public class Window3D

9. {

10. public final  int                  width;

11. public final  int                  height;

12. public final  String               title;

14. public Window3D(int width, int height, @NonNull String title)

15. {

16. this.title = Objects.requireNonNull(title, "title must not be null");

17. // Évitons les fenêtres trop petite

18. this.width  = Math.max(128, width);

19. this.height = Math.max(128, height);

21. // Création et lancement du Thread OpenGL

22. (new Thread(this::startOpenGLThread)).start();

23. }

25. @ThreadOpenGL

26. private void startOpenGLThread()

27. {

28. // TODO Initialiser la fenêtre, la lier à OpenGL, l’afficher, lancer le rendu

29. }

30. }

La première chose à faire est de capturer les erreurs GLFW pour avoir des informations en cas de problèmes. Pour le moment, redirigeons ces erreurs sur la console.

Window3D

1. // ...

2. import org.lwjgl.glfw.GLFWErrorCallback;

3. import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;

4. // ...

6. public class Window3D

7. {

8. // ....

10. @ThreadOpenGL

11. private void startOpenGLThread()

12. {

13. // Capture les erreurs LWJGL et les affichent sur la console

14. // Une gestion de erreurs différente est possible, mais sort du cadre du tutoriel

15. GLFWErrorCallback.createPrint(System.err)

16. .set();

18. // TODO Initialiser la fenêtre, la lier à OpenGL, l’afficher, lancer le rendu

19. }

20. }

On doit ensuite initialiser GLFW:

Window3D

1. // ...

2. import org.lwjgl.glfw.GLFWErrorCallback;

3. import org.lwjgl.glfw.GLFW;

4. import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;

5. // ...

7. public class Window3D

8. {

9. // ....

11. @ThreadOpenGL

12. private void startOpenGLThread()

13. {

14. // Capture les erreurs LWJGL et les affichent sur la console

15. // Une gestion de erreurs différente est possible, mais sort du cadre du tutoriel

16. GLFWErrorCallback.createPrint(System.err)

17. .set();

19. if (!GLFW.glfwInit())

20. {

21. // On désinscrit la gestion d’erreurs puisque nous ne pouvons pas continuer

22. GLFW.glfwSetErrorCallback(null)

23. .free();

24. System.err.println("GLFW initialisation failed");

25. return;

26. }

28. // TODO Initialiser la fenêtre, la lier à OpenGL, l’afficher, lancer le rendu

29. }

30. }

Nous allons ensuite configurer la fenêtre. GLFW va nous donner une référence sur cette fenêtre. Cette référence devra être utilisée pour communiquer avec la fenêtre. Nous devons donc garder cette référence. Cette référence est sous la forme d’un long

Note (Pour les plus curieux) :

Cette valeur est ce qu’on appelle une référence opaque. C’est-à-dire quelque chose qui n’a pas de sens en Java. Le développeur Java n’a pas à se soucier de ce qu’elle signifie vraiment, mais doit la garder telle qu’elle. Rappelons-nous ce qui a été dit dans l’introduction, en réalité nous parlons à une librairie en C/C++. Notre fenêtre, à sa création, sera référencée par un pointeur sur une adresse mémoire. Le long est la valeur de cette adresse. Il faut un long pour une adresse mémoire compatible à une architecture 64 bits. Et comme qui peut le plus, peut le moins, ça marche aussi sur des architectures 32 bits. Ici nous ne préoccupons pas de ces détails, mais c’est toujours intéressant de savoir d’où viennent les choses.

Window3D

1. // ...

2. import org.lwjgl.glfw.GLFWErrorCallback;

3. import org.lwjgl.glfw.GLFW;

4. import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;

5. // ...

7. public class Window3D

8. {

9. // ...

10. /**

11. * Référence sur la fenêtre permettant à GLFW de la gréée

12. */

13. private       long                 window;

14. // ....

16. @ThreadOpenGL

17. private void startOpenGLThread()

18. {

19. // Capture les erreurs LWJGL et les affichent sur la console

20. // Une gestion de erreurs différente est possible, mais sort du cadre du tutoriel

21. GLFWErrorCallback.createPrint(System.err)

22. .set();

24. if (!GLFW.glfwInit())

25. {

26. // On désinscrit la gestion d’erreurs puisque nous ne pouvons pas continuer

27. GLFW.glfwSetErrorCallback(null)

28. .free();

29. System.err.println("GLFW initialisation failed");

30. return;

31. }

33. // Configure GLFW

34. GLFW.glfwDefaultWindowHints();

35. // On empêche, pour le moment, que la fenêtre s’affiche afin d’avoir le temps de la configurée

36. GLFW.glfwWindowHint(GLFW.GLFW_VISIBLE, GLFW.GLFW_FALSE);

38. // La fenêtre aura un contour

39. GLFW.glfwWindowHint(GLFW.GLFW_DECORATED, GLFW.GLFW_TRUE);

40. // La fenêtre ne pourra pas être redimensionnée

41. // Un redimensionnement obligerait à écouter le changement de taille et à y réagir.

42. // Comme nous voulons faire simple, nous ne gérons pas ce cas tout de suite

43. GLFW.glfwWindowHint(GLFW.GLFW_RESIZABLE, GLFW.GLFW_FALSE);

44. // On veut que la fenêtre fasse au plus proche possible la dimension demandée

45. GLFW.glfwWindowHint(GLFW.GLFW_MAXIMIZED, GLFW.GLFW_FALSE);

47. // Création de la fenêtre

48. this.window = GLFW.glfwCreateWindow(this.width, this.height, this.title, MemoryUtil.NULL, MemoryUtil.NULL);

50. if (this.window == MemoryUtil.NULL)

51. {

52. // La fenêtre n’a pas pu être réservée en mémoire, on ne peut pas continuer

53. this.closeGLFW();

54. System.err.println("GLFW can’t create window");

55. return;

56. }

59. // TODO Initialiser la fenêtre, la lier à OpenGL, l’afficher, lancer le rendu

60. }

62. /**

63. * Appeler à la sortie de la fenêtre pour nettoyer proprement les divers états

64. */

65. private void closeGLFW()

66. {

67. // Termine la gestion de la fenêtre par GLFW

68. GLFW.glfwTerminate();

69. // Arrête de traquer les erreurs

70. GLFW.glfwSetErrorCallback(null)

71. .free();

72. }

73. }

Maintenant que notre référence est créée, nous allons ajouter un listener qui va réagir au plus important événement : La fermeture de la fenêtre, afin de bien libérer la mémoire. Pour cela créons une classe qui va réagir aux événements de la fenêtre :

Window3DCloseEventManager

1. package fr.developez.tutorial.java.dimension3.render;

3. import org.lwjgl.glfw.GLFW;

4. import org.lwjgl.glfw.GLFWWindowCloseCallbackI;

6. class Window3DCloseEventManager

7. implements GLFWWindowCloseCallbackI

8. {

9. /**

10. * Appeler au moment qu’une fenêtre est sur le point de se fermer

11. * @param window Référence sur la fenêtre entrain de se fermer

12. */

13. @Override

14. public void invoke(long window)

15. {

16. this.closeWidow(window);

17. }

19. /**

20. * Gère la fermeture d’une fenêtre

21. * @param window Référence sur la fenêtre à fermer

22. */

23. void closeWidow(long window)

24. {

25. // Pour le moment on autorise la fenêtre à se fermer dans tous les cas

26. GLFW.glfwSetWindowShouldClose(window, true);

27. }

28. }

Plus d’informations surglfwSetWindowShouldClose : Il s’agit d’un flag qui permet d’autoriser ou non la fermeture de la fenêtre. Le mettre à true, comme ici, va permettre la fermeture de celle-ci. Dans certains cas il peut être utile de le mettre à false pour, par exemple, afficher une boîte de dialogue qui demande : « Êtes-vous sur de vouloir quitter ? ». Nous nous servirons également de ce flag, pour savoir quand arrêter la boucle rendue OpenGL. Maintenant, nous pouvons brancher notre listener de fermeture.

Window3D

1. // ...

2. import org.lwjgl.glfw.Callbacks;

3. import org.lwjgl.glfw.GLFW;

4. import org.lwjgl.glfw.GLFWErrorCallback;

5. import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;

6. // ...

7. public class Window3D

8. {

9. // ...

10. /**

11. * Référence sur la fenêtre permettant à GLFW de la gérer

12. */

13. private       long                      window;

14. private final Window3DCloseEventManager window3DCloseEventManager = new Window3DCloseEventManager();

15. // ...

17. @ThreadOpenGL

18. private void startOpenGLThread()

19. {

20. // Capture les erreurs LWJGL et les affichent sur la console

21. // Une gestion de erreurs différente est possible, mais sort du cadre du tutoriel

22. GLFWErrorCallback.createPrint(System.err)

23. .set();

25. if (!GLFW.glfwInit())

26. {

27. // On désinscrit la gestion d’erreurs puisque nous ne pouvons pas continuer

28. GLFW.glfwSetErrorCallback(null)

29. .free();

30. System.err.println("GLFW initialisation failed");

31. return;

32. }

34. // Configure GLFW

35. GLFW.glfwDefaultWindowHints();

36. // On empêche, pour le moment, que la fenêtre s’affiche afin d’avoir le temps de la configurée

37. GLFW.glfwWindowHint(GLFW.GLFW_VISIBLE, GLFW.GLFW_FALSE);

39. // La fenêtre aura un contour

40. GLFW.glfwWindowHint(GLFW.GLFW_DECORATED, GLFW.GLFW_TRUE);

41. // La fenêtre ne pourra pas être redimensionnée

42. // Un redimensionnement obligerait à écouter le changement de taille et à y réagir.

43. // Comme nous voulons faire simple, nous ne gérons pas ce cas tout de suite

44. GLFW.glfwWindowHint(GLFW.GLFW_RESIZABLE, GLFW.GLFW_FALSE);

45. // On veut que la fenêtre fasse au plus proche possible la dimension demandée

46. GLFW.glfwWindowHint(GLFW.GLFW_MAXIMIZED, GLFW.GLFW_FALSE);

48. // Création de la fenêtre

49. this.window = GLFW.glfwCreateWindow(this.width, this.height, this.title, MemoryUtil.NULL, MemoryUtil.NULL);

51. if (this.window == MemoryUtil.NULL)

52. {

53. // La fenêtre n’a pas pu être réservée en mémoire, on ne peut pas continuer

54. this.closeGLFW();

55. System.err.println("GLFW can’t create window");

56. return;

57. }

59. // TODO : Insérer ici les listeners pour les événements souris, clavier et joystick

61. // Management de la fermeture de la fenêtre pour libérer proprement la mémoire

62. GLFW.glfwSetWindowCloseCallback(this.window, this.window3DCloseEventManager);

64. // ...

65. }

67. public void closeWindow()

68. {

69. this.window3DCloseEventManager.closeWidow(this.window);

70. }

72. void exitWidow()

73. {

74. // On libère tous les listeners

75. Callbacks.glfwFreeCallbacks(this.window);

76. // On finalise la destruction de la fenêtre

77. GLFW.glfwDestroyWindow(this.window);

78. // On ferme les événements GLFW

79. this.closeGLFW();

80. }

82. /**

83. * Appeler à la sortie de la fenêtre pour nettoyer proprement les divers états

84. */

85. private void closeGLFW()

86. {

87. // Termine la gestion de la fenêtre par GLFW

88. GLFW.glfwTerminate();

89. // Arrête de traquer les erreurs

90. GLFW.glfwSetErrorCallback(null)

91. .free();

92. }

93. }

Nous avons ajouté une méthode pour pouvoir fermer la fenêtre à partir du code, ainsi que préparée la fermeture propre de la fenêtre. En appelant closeWindow du listener, on s’assure de pouvoir gérer la fermeture de la fenêtre toujours au même endroit. Ce qui évitera de dupliquer du code le moment ou l’on voudra ajouter une logique plus complexe à la fermeture. Il ne nous reste à attacher le rendu OpenGL à la fenêtre, et préparé la boucle de rendue. Tout d’abord, attachons OpenGL et montrons la fenêtre :

Window3D

1. // ...

2. import org.lwjgl.glfw.Callbacks;

3. import org.lwjgl.glfw.GLFW;

4. import org.lwjgl.glfw.GLFWErrorCallback;

5. import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;

6. // ...

7. public class Window3D

8. {

9. // ...

10. /**

11. * Référence sur la fenêtre permettant à GLFW de la gérer

12. */

13. private       long                      window;

14. private final Window3DCloseEventManager window3DCloseEventManager = new Window3DCloseEventManager();

15. // ...

17. @ThreadOpenGL

18. private void startOpenGLThread()

19. {

20. // Capture les erreurs LWJGL et les affichent sur la console

21. // Une gestion de erreurs différente est possible, mais sort du cadre du tutoriel

22. GLFWErrorCallback.createPrint(System.err)

23. .set();

25. if (!GLFW.glfwInit())

26. {

27. // On désinscrit la gestion d’erreurs puisque nous ne pouvons pas continuer

28. GLFW.glfwSetErrorCallback(null)

29. .free();

30. System.err.println("GLFW initialisation failed");

31. return;

32. }

34. // Configure GLFW

35. GLFW.glfwDefaultWindowHints();

36. // On empêche, pour le moment, que la fenêtre s’affiche afin d’avoir le temps de la configurée

37. GLFW.glfwWindowHint(GLFW.GLFW_VISIBLE, GLFW.GLFW_FALSE);

39. // La fenêtre aura un contour

40. GLFW.glfwWindowHint(GLFW.GLFW_DECORATED, GLFW.GLFW_TRUE);

41. // La fenêtre ne pourra pas être redimensionnée

42. // Un redimensionnement obligerait à écouter le changement de taille et à y réagir.

43. // Comme nous voulons faire simple, nous ne gérons pas ce cas tout de suite

44. GLFW.glfwWindowHint(GLFW.GLFW_RESIZABLE, GLFW.GLFW_FALSE);

45. // On veut que la fenêtre fasse au plus proche possible la dimension demandée

46. GLFW.glfwWindowHint(GLFW.GLFW_MAXIMIZED, GLFW.GLFW_FALSE);

48. // Création de la fenêtre

49. this.window = GLFW.glfwCreateWindow(this.width, this.height, this.title, MemoryUtil.NULLMemoryUtil.NULL, MemoryUtil.NULL);

51. if (this.window == MemoryUtil.NULL)

52. {

53. // La fenêtre n’a pas pu être réservée en mémoire, on ne peut pas continuer

54. this.closeGLFW();

55. System.err.println("GLFW can’t create window");

56. return;

57. }

59. // TODO : Insérer ici les listeners pour les événements souris, clavier et joystick

61. // Management de la fermeture de la fenêtre pour libérer proprement la mémoire

62. GLFW.glfwSetWindowCloseCallback(this.window, this.window3DCloseEventManager);

64. // Associe le contexte Open GL à la fenêtre

65. GLFW.glfwMakeContextCurrent(this.window);

66. // Activation du mode v-sync

67. GLFW.glfwSwapInterval(1);

69. // Maintenant que tout est configuré la fenêtre peut se montrer

70. GLFW.glfwShowWindow(this.window);

72. // Cet appel est crucial pour que la management d’Open GL avec GLFW se passe bien

73. GL.createCapabilities();

75. // TODO Boucle de rendue

76. }

77. }

Le rôle de la boucle de rendu est dans l’ordre :

1. Initialiser Open GL avec les options que l’on désire utiliser

2. Rafraîchir l’affichage en boucle tant que la fenêtre est présente

3. Quitter proprement l’application quand l’utilisateur demande la fermeture de la fenêtre

Voici son état minimal :

Render3D

1. package fr.developez.tutorial.java.dimension3.render;

3. import fr.developez.tutorial.java.dimension3.tool.NonNulltool.NonNull;

4. import fr.developez.tutorial.java.dimension3.tool.Tools;

5. import java.util.Objects;

6. import org.lwjgl.glfw.GLFW;

7. import org.lwjgl.opengl.GL11;

8. import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;

10. class Render3D

11. {

12. Render3D()

13. {

14. }

16. /**

17. * Lance la boucle de rendu

18. *

19. * @param window3D Fenêtre où se dessine la 3D

20. * @param window   Référence sur la fenêtrefenêtre

21. */

22. @ThreadOpenGL

23. void rendering(@NonNull final Window3D window3D, final long window)

24. {

25. Objects.requireNonNull(window3D, "window3D must not be null");

27. this.initialize3D(window3D.width, window3D.height);

29. // Tant que la fenêtre est présente, rafraîchir la 3D

30. while (!GLFW.glfwWindowShouldClose(window))

31. {

32. // Nettoyage de la fenêtre en buffer pour la prochaine frame

33. GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT);

35. // Dessine la frame courante

36. this.drawScene();

38. // Rend la fenêtre en buffer dessinée visible à l’écran

39. GLFW.glfwSwapBuffers(window);

41. // Récupération des événements fenêtre arrivée pendant le dessin de la frame.

42. // Les listeners sur les événements claviers seront appelés par cette méthode

43. GLFW.glfwPollEvents();

45. // TODO traité les évènements survenus

46. }

48. // La fenêtre de rendu se ferme ou est fermée, on quitte tout proprement

49. window3D.exitWidow();

50. }

52. /**

53. * Initialise le rendu OpenGL

54. *

55. * @param width  Largeur de la fenêtre de dessin

56. * @param height Hauteur de la fenêtre de dessin

57. */

58. @ThreadOpenGL

59. private void initialize3D(final int width, final int height)

60. {

61. // TODO

62. }

64. /**

65. * Dessine la scène 3D

66. */

67. @ThreadOpenGL

68. private void drawScene()

69. {

70. // TODO

71. }

72. }

On remarque tout d’abord que l’on boucle sur : glfwWindowShouldClose. Ce qui permet d’arrêter la boucle de rendue dès qu’une requête est faite. Pour comprendre le contenu de la boucle, il faut savoir que le rendu à l’écran n’est, en réalité pas continu. OpenGL passe par une technique dite de double buffer. Cette technique consiste à calculer l’image en mémoire, puis une fois prête l’affichée à l’écran. Afin d’économiser de la mémoire (et du temps de création), il joue avec deux buffers. l’un affiché à l’écran, l’autre en mémoire. Et quand l’image en mémoire est prête, il échange les deux buffers. À noter que les cartes plus performantes peuvent gérer plus de deux buffers, mais le principe reste le même. Ce qui fait que le buffer que l’on a pour travailler n’est pas un page vierge mais contient les informations d’un précédent calcul. C’est pour cela que la première chose que l’on fait est de tout nettoyer afin d’être sûr de ne pas avoir d’artefacts des calculs précédents. On pourrait se demander pourquoi le buffer n’est pas nettoyé automatiquement. En fait pour certains effets il peut être utile de ne pas le nettoyer ou seulement en partie. Un buffer contient non seulement les informations de couleur des pixels à dessiner, mais également des informations sur la profondeur calculée. C’est pour cela qu’ici on nettoie les informations de couleurs et de profondeur, avec GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT). L’instruction glfwSwapBuffers permet de dire à la carte graphique que l’on a fini de dessiner et qu’elle peut échanger les buffers.glfwPollEvents va remplir les informations sur les événements survenu pendant le rendu, ce qui nous sera utile quand nous réagiront aux événements souris, clavier, manette de jeu. À la fin de la boucle, on n’oublie pas de fermer proprement la fenêtre. Il ne nous reste plus qu’à appeler notre boucle de rendu dans la fenêtre :

Window3D

1. // ...

2. import org.lwjgl.glfw.Callbacks;

3. import org.lwjgl.glfw.GLFW;

4. import org.lwjgl.glfw.GLFWErrorCallback;

5. import org.lwjgl.opengl.GL;

6. import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;

7. // ...

8. public class Window3D

9. {

10. // ...

11. /**

12. * Référence sur la fenêtre permettant à GLFW de la gérer

13. */

14. private       long                      window;

15. private final Window3DCloseEventManager window3DCloseEventManager = new Window3DCloseEventManager();

16. private final Render3D                  render3D                  = new Render3D();

18. // ...

19. @ThreadOpenGL

20. private void startOpenGLThread()

21. {

22. // Capture les erreurs LWJGL et les affichent sur la console

23. // Une gestion de erreurs différente est possible, mais sort du cadre du tutoriel

24. GLFWErrorCallback.createPrint(System.err)

25. .set();

27. if (!GLFW.glfwInit())

28. {

29. // On désinscrit la gestion d’erreurs puisque nous ne pouvons pas continuer

30. GLFW.glfwSetErrorCallback(null)

31. .free();

32. System.err.println("GLFW initialisation failed");

33. return;

34. }

36. // Configure GLFW

37. GLFW.glfwDefaultWindowHints();

38. // On empêche, pour le moment, que la fenêtre s’affiche afin d’avoir le temps de la configurée

39. GLFW.glfwWindowHint(GLFW.GLFW_VISIBLE, GLFW.GLFW_FALSE);

41. // La fenêtre aura un contour

42. GLFW.glfwWindowHint(GLFW.GLFW_DECORATED, GLFW.GLFW_TRUE);

43. // La fenêtre ne pourra pas être redimensionnée

44. // Un redimensionnement obligerait à écouter le changement de taille et à y réagir.

45. // Comme nous voulons faire simple, nous ne gérons pas ce cas tout de suite

46. GLFW.glfwWindowHint(GLFW.GLFW_RESIZABLE, GLFW.GLFW_FALSE);

47. // On veut que la fenêtre fasse au plus proche possible la dimension demandée

48. GLFW.glfwWindowHint(GLFW.GLFW_MAXIMIZED, GLFW.GLFW_FALSE);

50. // Création de la fenêtre

51. this.window = GLFW.glfwCreateWindow(this.width, this.height, this.title, MemoryUtil.NULL, MemoryUtil.NULL);

53. if (this.window == MemoryUtil.NULL)

54. {

55. // La fenêtre n’a pas pu être réservée en mémoire, on ne peut pas continuer

56. this.closeGLFW();

57. System.err.println("GLFW can’t create window");

58. return;

59. }

61. // TODO : Insérer ici les listeners pour les événements souris, clavier et joystick

63. // Management de la fermeture de la fenêtre pour libéré proprement la mémoire

64. GLFW.glfwSetWindowCloseCallback(this.window, this.window3DCloseEventManager);

66. // Associe le contexte OpenGL à la fenêtre

67. GLFW.glfwMakeContextCurrent(this.window);

68. // Activation du mode v-sync

69. GLFW.glfwSwapInterval(1);

71. // Maintenant que tout est configuré la fenêtre peut se montrer

72. GLFW.glfwShowWindow(this.window);

74. // Cet appel est crucial pour que la management d’OpenGL avec GLFW se passe bien

75. GL.createCapabilities();

77. //Lancement du rendu OpenGL

78. this.render3D.rendering(this, this.window);

79. }

80. // ...

81. }

Désormais tout est prêt pour l’affichage. Si on lance un « main » :

Main

1. package fr.developez.tutorial.java.dimension3;

3. import fr.developez.tutorial.java.dimension3.render.Window3D;

5. public class Main

6. {

7. public static void main(String[] args)

8. {

9. final Window3D window3D = new Window3D(800, 600, "Tutoriel 3D - Hello world");

10. }

11. }

On a une fenêtre vide noire prête à accueillir notre scène 3D :

Cette initialisation va nous permettre de régler les divers aspects généraux de notre 3D. Comme la taille du frustum (voir l’introduction). Le rectangle dans la fenêtre où dessiner la 3D, ici toute la fenêtre. Mais grâce à cela il est possible de montrer facilement plusieurs vues de la scène ou des différentes scènes. Cela sort du cadre du tutoriel.

Afin de simplifier certains calculs, ce tutoriel fournit dans son code source l’outil GLU (Récupéré de JOGL)

Render3D

1. package fr.developez.tutorial.java.dimension3.render;

3. import fr.developez.tutorial.java.dimension3.tool.GLU;

4. import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;

5. import org.lwjgl.opengl.GL11;

6. import org.lwjgl.opengl.GL12;

8. class Render3D

9. {

10. private final Window3D window3D;

11. private final long     window;

13. // ...

15. /**

16. * Initialise le rendu OpenGL

17. *

18. * @param width  Largeur de la fenêtre de dessin

19. * @param height Hauteur de la fenêtre de dessin

20. */

21. @ThreadOpenGL

22. private void initialize3D(final int width, final int height)

23. {

24. // *** Gestion de la transparence ***

25. // On indique que l’on souhaite activer la  transparence

26. GL11.glEnable(GL11.GL_ALPHA_TEST);

27. // Spécification de la précision de la transparence

28. GL11.glAlphaFunc(GL11.GL_GREATER, 0.01f);

29. // Façon dont la transparence est calculée

30. GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);

32. /// *** Apparence globale ***

33. // Les matériaux peuvent être colorés

34. GL11.glEnable(GL11.GL_COLOR_MATERIAL);

35. // Pour des raisons de performances, on désactive les textures de manière globale, on les activera au cas par cas

36. GL11.glDisable(GL11.GL_TEXTURE_2D);

37. // On active la possibilité de combiné des effets

38. GL11.glEnable(GL11.GL_BLEND);

40. // ** Réglage de la perspective  ***

41. // On fixe le rectangle de la fenêtre où la 3D sera dessinée.

42. // Ici on veut dessiner sur toute la fenêtre

43. // Sur le "frustum", c’est la zone proche de l’utilisateur

44. // S’appelle aussi ’view port’

45. GL11.glViewport(0, 0, width, height);

46. // On veut une projection normalisée

47. GL11.glEnable(GL11.GL_NORMALIZE);

49. // Réglage de la projection

50. GL11.glMatrixMode(GL11.GL_PROJECTION);

51. GL11.glLoadIdentity();

52. final double ratio = (double) width / (double) height;

53. GLU.gluPerspective(45.0, ratio, 0.1, 5000.0);

54. // Réglage du modèle de la vue

55. GL11.glMatrixMode(GL11.GL_MODELVIEW);

56. GL11.glLoadIdentity();

58. // Au départ on met un écran blanc

59. GL11.glClearColor(1f, 1f, 1f, 1f);

60. //On active le test de profondeur

61. GL11.glEnable(GL11.GL_DEPTH_TEST);

63. // Active les face visible que d’un côté (Pour éviter de dessiner des choses qui seront toujours cachés)

64. GL11.glEnable(GL11.GL_CULL_FACE);

65. GL11.glCullFace(GL11.GL_FRONT);

67. // Éclairage de base pour un effet agréable

68. GL11.glLightModeli(GL11.GL_LIGHT_MODEL_LOCAL_VIEWER, GL11.GL_TRUE);

69. GL11.glShadeModel(GL11.GL_SMOOTH);

70. GL11.glLightModeli(GL12.GL_LIGHT_MODEL_COLOR_CONTROL, GL12.GL_SEPARATE_SPECULAR_COLOR);

71. GL11.glLightModeli(GL11.GL_LIGHT_MODEL_TWO_SIDE, 1);

73. // Active l’éclairage

74. // GL11.glEnable(GL11.GL_LIGHTING);

75. }

76. // ...

77. }

L’important à comprendre dans cette initialisation:

- GL11.glViewport(0, 0, width, height) définit le view port. c’est-à-dire la zone dans la fenêtre où la 3D sera dessinée. Ici on prend toute la fenêtre.

- GLU.gluPerspective(45.0, ratio, 0.1, 5000.0) va créer le frustum.

- La première valeur, correspond à l’angle que fait visuellement l’axe des Z par rapport à l’axe des X. l’aspect le plus naturel est 45 degrés.

- La seconde valeur est la proportion largeur par rapport à la hauteur. Ici réglée pour que les proportions soit homogènes.

- La troisième valeur est la distance du plan le plus proche du frustum

- La dernière valeur est la distance du plan le plus éloigné du frustum

Une face a deux cotées, l’un dit de face, l’autre de dos. Pour déterminer si une face est de face ou de dos, OpenGL regarde l’ordre dans lequel sont spécifié les points du polygone. Si quand on les regarde, la définition des points spécifiés sont dans le sens des aiguilles d’une montre, alors on voit la face de la face. Si quand on les regarde, la définition des points spécifiés sont dans le sens contraire des aiguilles d’une montre, alors on voit le dos de la face. GL11.glEnable(GL11.GL_CULL_FACE);, indique que l’on souhaite distinguer le sens des face (savoir si on les regarde de face ou de dos). GL11.glCullFace(GL11.GL_FRONT);, indique de ne calculer et dessiner que les faces de face et ignorées celles de dos. L’intérêt de ce réglage est pour les objets dont on ne voit jamais l’intérieur comme les boîtes, les sphères… On évite ainsi qu’OpenGL calcule les faces intérieures qui seront de toute façon cachées par des faces extérieures. Nous mettons, pour le moment GL11.glEnable(GL11.GL_LIGHTING); en commentaire afin de ne pas activer l’éclairage. Quand nous auront appris comment l’éclairage fonctionne, nous réactiverons cette ligne. Désormais tout est prêt pour afficher de la 3D. Commençons par un Hello world

VI - Hello world

Ce « Hello world » va consister à afficher un triangle rouge. Il suppose que vous ayez suivi les précédentes étapes.

VI-A - Dessinons un triangle rouge

Nous verrons dans le tutoriel suivant la notion de graphe de scène (scene graph en Anglais) et comment le mettre en place. Pour l’instant, nous allons juste dessiner notre triangle directement dans la boucle de rafraîchissement du thread OpenGL. À chaque tour de la boucle on redessine le triangle. Comme nous l’avons vu dans l’introduction, les couleurs sont des valeurs d’intensité de rouge, vert et bleu. Avec un notion de transparence. Ici on veut du rouge, donc le rouge à fond, pas de bleu, pas de vert, et une couleur opaque. Il existe plusieurs façons de spécifier une couleur en OpenGL. Ici nous prendrons la version avec des float. Cette version fait varier les valeurs entre 0 et 1. Où 0 absence totale, 1 intensité complète. Cette notation à l’avantage d’être plus souple que la version avec des entiers et ne pas prendre trop de mémoire par rapport à la version en double.

Dans la mesure du possible on utilisera des int ou des float pour des raisons de mémoire et/ou de performances. Nous verrons plus tard comment optimiser les performances.

Render3D

1. // ...

2. import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;

3. import org.lwjgl.opengl.GL11;

4. // ...

6. class Render3D

7. {

8. // ...

9. /**

10. * Dessine la scène 3D

11. */

12. @ThreadOpenGL

13. private void drawScene()

14. {

15. // Efface l’écran en blanc

16. GL11.glClearColor(1f, 1f, 1f, 1f);

18. // Dessin du triangle

20. // Utilise la couleur rouge

21. GL11.glColor4f(1f, 0f, 0f, 1f);

24. // Début du polygone

25. GL11.glBegin(GL11.GL_POLYGON);

27. // Chaque point du polygone dans le sens des aiguilles d’une montre

28. GL11.glVertex3f(0.0f, 0.5f, -1.0f);

29. GL11.glVertex3f(0.5f, -0.5f, -1.0f);

30. GL11.glVertex3f(-0.5f, -0.5f, -1.0f);

32. // Fin du polygone

33. GL11.glEnd();

34. }

35. }

N’oublions pas que la méthode `drawScene` est appelée en boucle pour chaque frame. C’est pour cela que nous mettons l’écran en blanc au début afin de partir à chaque fois d’une page vierge. Si on ne le faisait pas on cumulerait avec l’affichage précédent. Ici on ne verrait pas la différence puisque l’on dessine à chaque fois exactement la même chose. Mais c’est rarement le cas. Donc prenons l’habitude dès maintenant en commençant par GL11.glClearColor(1f, 1f, 1f, 1f);. Les quatre nombres sont respectivement, la quantité de rouge, de vert et de bleu. Le quatrième est la transparence. La transparence de la couleur qui efface l’écran sera pour nous toujours à 1 (opaque). D’autre valeur de transparence sont dédiés à certains effets. On veut colorier le triangle en rouge, du coup on change la couleur de remplissage courante par du rouge avec GL11.glColor4f(1f, 0f, 0f, 1f);. Pour chaque face de la maille de notre objet, il faut dire à OpenGL quand la description de la face commence et quand celle-ci finit. C’est le rôle du couple GL11.glBegin(GL11.GL_POLYGON); , GL11.glEnd();

À chaqueglBegindoit toujours correspondre un glEnd

UnglEnddoit avoir unglBeginprécédent qui lui correspond

Les instructions GL11.glVertex3f définissent les coordonnées d’un sommet du polygone. On fait bien attention de définir dans le sens des aiguilles d’une montre. Car à cause de nos réglages OpenGL, le définir dans l’autre sens rendrait le triangle non visible. On remarque que le Z choisit est -1.0f et non pas 0.0f. En effet, on rappelle que c’est l’observateur qui se trouve en (0, 0, 0). Ensuite, le frustum, fait qu’il y a une distance entre l’observateur et l’écran. l’écran est sur la zone proche du frustum.

On a finalement notre « Hello world »

VI-B - Un petit peu plus

Avant de parler du graphe de scène, nous allons animer un peu notre triangle. Afin de simplifier l’animation, nous allons non plus placer notre triangle en absolu, mais en relatif par rapport à une position qui pourra changer.

Render3D

1. import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;

2. import org.lwjgl.opengl.GL11;

3. // ...

5. class Render3D

6. {

7. // ...

9. // Coordonnées de l’objet

10. private float triangleX = 0f;

11. private float triangleY = 0f;

12. private float triangleZ = -1f;

13. // ...

15. /**

16. * Dessine la scène 3D

17. */

18. @ThreadOpenGL

19. private void drawScene()

20. {

21. // Efface l’écran en blanc

22. GL11.glClearColor(1f, 1f, 1f, 1f);

24. // Dessin du triangle

26. // Utilise la couleur rouge

27. GL11.glColor4f(1f, 0f, 0f, 1f);

29. // Sauvegarde la matrice courante dans la pile des matrices

30. GL11.glPushMatrix();

31. // Change la matrice courant afin de positionner le triangle à l’écran

32. GL11.glTranslatef(this.triangleX, this.triangleY, this.triangleZ);

34. // Début du polygone

35. GL11.glBegin(GL11.GL_POLYGON);

37. // Chaque point du polygone dans le sens des aiguilles d’une montre

38. // Définit en relatif à la matrice courante

39. GL11.glVertex3f(0.0f, 0.5f, 0.0f);

40. GL11.glVertex3f(0.5f, -0.5f, 0.0f);

41. GL11.glVertex3f(-0.5f, -0.5f, 0.0f);

43. // Fin du polygone

44. GL11.glEnd();

45. // Restaure la matrice sauvegardée

46. GL11.glPopMatrix();

47. }

48. }

Désormais, on utilise la pile des matrices détaillée dans l’introduction. GL11.glPushMatrix(); sauvegarde en haut de la pile des matrices l’état de la matrice courante. GL11.glPopMatrix(); retire l’état de la matrice en haut de la pile et l’utilise comme état courant. GL11.glTranslatef applique à la matrice courante une translation, qui va bouger toutes les coordonnées du vecteur donné. On remarque que les Z dans les glVertex3f valent 0.0f en effet maintenant on est en relatif. Si on lance l’application, on ne constate aune différence visuelle.

Animons cela maintenant. Pour animé notre triangle, il suffit de faire varier les coordonnées du triangle entre chaque boucle.

Render3D

1. import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;

2. import org.lwjgl.opengl.GL11;

3. // ….

5. class Render3D

6. {

7. // ...

9. // Coordonnées de l’objet

10. private float triangleX = 0f;

11. private float triangleY = 0f;

12. private float triangleZ = -1f;

13. // Pas de translation

14. private final float step = 0.01f;

15. // Sens de translation

16. private       int   way  = 1;

17. // ...

19. /**

20. * Dessine la scène 3D

21. */

22. @ThreadOpenGL

23. private void drawScene()

24. {

25. // Efface l’écran en blanc

26. GL11.glClearColor(1f, 1f, 1f, 1f);

28. // Dessin du triangle

30. // Utilise la couleur rouge

31. GL11.glColor4f(1f, 0f, 0f, 1f);

33. // Sauvegarde la matrice courante dans la pile des matrices

34. GL11.glPushMatrix();

35. // Change la matrice courant afin de positionner le triangle à l’écran

36. GL11.glTranslatef(this.triangleX, this.triangleY, this.triangleZ);

38. // Début du polygone

39. GL11.glBegin(GL11.GL_POLYGON);

41. // Chaque point du polygone dans le sens des aiguilles d’une montre

42. // Définit en relatif à la matrice courante

43. GL11.glVertex3f(0.0f, 0.5f, 0.0f);

44. GL11.glVertex3f(0.5f, -0.5f, 0.0f);

45. GL11.glVertex3f(-0.5f, -0.5f, 0.0f);

47. // Fin du polygone

48. GL11.glEnd();

49. // Restaure la matrice sauvegardée

50. GL11.glPopMatrix();

51. // On met à jour la position pour le prochain tour

52. this.triangleX += this.step * this.way;

53. if (this.triangleX >= 1.5f)

54. {

55. this.way = -1;

56. }

57. if (this.triangleX <= -1.5f)

58. {

59. this.way = 1;

60. }

61. }

62. }

On a désormais un triangle qui bouge à l’écran. Comme on le remarque, ça ne change absolument pas le code de l’affichage de l’objet, c’est l’avantage d’utiliser une position relative. Autre remarque, les coordonnées auraient pu être modifiées depuis un autre thread. Ce qui permet de laisser le développeur modifier les coordonnées sans se préoccuper du thread OpenGL, de faire un moteur d’animation… Amusez-vous à faire vos propres essais.

Pour information.GL11.glRotatef(angle, x, y, z) effectue une rotation de l’angle demandé (en degré) autour de l’axe dont les coordonnés sont précisés avec (x, y, z). Pour une rotation autour de X : . Pour une rotation autour de Y : GL11.glRotatef(angleY, 0f, 1f, 0f). Pour une rotation autour de Z : GL11.glRotatef(angleZ, 0f, 0f, 1f).GL11.glScalef(scaleX, scaleY, scaleZ) change les proportions selon X, Y, Z. Pour une déformation homogène il suffit de mettre la même valeur pour X, Y et Z.

VII - Conclusion et remerciements

Vous venez de faire votre premier pas dans la 3D avec Java et LWJGL. Les tutoriels suivants vous permettront d’aller plus loin.

J’ai insisté sur l’importance du thread OpenGL, car par expérience ne pas le respecter c’est ce créer pas mal d’ennui. Comme je l’ai dit ce n’est pas mon premier moteur 3D. Ce tutoriel a aussi pour but de vous éviter certains écueils que j’ai rencontré ou vu. J’espère néanmoins que cette première partie vous a donnée envie d’explorer plus loin et découvrir ce qu’on peut faire avec cette puissante API qu’est LWJGL.

Je tiens remercier Mickael Baron pour sa relecture technique ainsi de m’avoir proposé de rédiger ce tutoriel.