IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

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

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

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.

Article lu   fois.

L'auteur

Profil ProSite personnelJHelp

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Introduction

Code source :

Code Source
TéléchargerSélectionnez
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’OpenGLSite officiel 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 ».

Image non disponible

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 :

Image non disponible

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é :

Image non disponible

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 :

Image non disponible

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.

Image non disponible

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.

Image non disponible

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 kitxmlcodeinlinelatexdvp256 * 256 * 256 = 16 777 216finkitxmlcodeinlinelatexdvpcouleurs 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.

Image non disponible

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.

Image non disponible

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

Image non disponible

Ensuite on sauvegarde un état

Image non disponible

Puis un autre

Image non disponible

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

Image non disponible

et

Image non disponible

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

Image non disponible

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

Image non disponible

À 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.

Image non disponible

Sauvegarde de la matrice principale

Image non disponible

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

Image non disponible

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

Image non disponible

Sauvegarde de la matrice de la carrosserie

Image non disponible

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

Image non disponible

Sauvegarde de la matrice de la carrosserie

Image non disponible

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

Image non disponible

Sauvegarde de la matrice de la carrosserie

Image non disponible

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

Image non disponible

Restaurer la matrice principale

Image non disponible

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

Image non disponible

Exemples de polygones non convexes, appelés aussi concaves

Image non disponible

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

Image non disponible

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 : LWJGLGithub de 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

Image non disponible

Puis sur Next

Image non disponible

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.

Image non disponible

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 LWJGLDé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 :

Image non disponible

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 :

Image non disponible

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
TéléchargerSélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
dependencies {
// ...
    // LWJGL core
    implementation files("src/lib/core/lwjgl.jar")
    runtime files("src/lib/core/lwjgl-natives-linux.jar")
    runtime files("src/lib/core/lwjgl-natives-linux-arm32.jar")
    runtime files("src/lib/core/lwjgl-natives-linux-arm64.jar")
    runtime files("src/lib/core/lwjgl-natives-macos.jar")
    runtime files("src/lib/core/lwjgl-natives-windows.jar")
    runtime files("src/lib/core/lwjgl-natives-windows-x86.jar")

    // LWJGL OpenGL
    implementation files("src/lib/opengl/lwjgl-opengl.jar")
    runtime files("src/lib/opengl/lwjgl-opengl-natives-linux.jar")
    runtime files("src/lib/opengl/lwjgl-opengl-natives-linux-arm32.jar")
    runtime files("src/lib/opengl/lwjgl-opengl-natives-linux-arm64.jar")
    runtime files("src/lib/opengl/lwjgl-opengl-natives-macos.jar")
    runtime files("src/lib/opengl/lwjgl-opengl-natives-windows.jar")
    runtime files("src/lib/opengl/lwjgl-opengl-natives-windows-x86.jar")

    // LWJGL GLWF
    implementation files("src/lib/glfw/lwjgl-glfw.jar")
    runtime files("src/lib/glfw/lwjgl-glfw-natives-linux.jar")
    runtime files("src/lib/glfw/lwjgl-glfw-natives-linux-arm32.jar")
    runtime files("src/lib/glfw/lwjgl-glfw-natives-linux-arm64.jar")
    runtime files("src/lib/glfw/lwjgl-glfw-natives-macos.jar")
    runtime files("src/lib/glfw/lwjgl-glfw-natives-windows.jar")
    runtime files("src/lib/glfw/lwjgl-glfw-natives-windows-x86.jar")
// ...
}

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.

Image non disponible

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

Image non disponible

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
Sélectionnez
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
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
package fr.developez.tutorial.java.dimension3;

public class Main
{
    public static void main(String[] args)
    {
    }
}

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

Image non disponible

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

Image non disponible

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

Image non disponible

Puis on clique sur « Edit Configurations … »

Image non disponible

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
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
package fr.developez.tutorial.java.dimension3.tool;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.METHOD)
@Documented
public @interface ThreadOpenGL
{
}

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
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
package fr.developez.tutorial.java.dimension3.render;

import java.util.Objects;
import fr.developez.tutorial.java.dimension3.tool.NonNull;
import fr.developez.tutorial.java.dimension3.tool.Tools;
import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;

public class Window3D
{
    public final  int                  width;
    public final  int                  height;
    public final  String               title;

    public Window3D(int width, int height, @NonNull String title)
    {
        this.title = Objects.requireNonNull(title, "title must not be null");
        // Évitons les fenêtres trop petite
        this.width  = Math.max(128, width);
        this.height = Math.max(128, height);

        // Création et lancement du Thread OpenGL
        (new Thread(this::startOpenGLThread)).start();
    }

    @ThreadOpenGL
    private void startOpenGLThread()
    {
        // TODO Initialiser la fenêtre, la lier à OpenGL, l’afficher, lancer le rendu         
    }
}

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
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
// ... 
import org.lwjgl.glfw.GLFWErrorCallback;
import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;
// ...

public class Window3D
{
    // ....

    @ThreadOpenGL
    private void startOpenGLThread()
    {
        // Capture les erreurs LWJGL et les affichent sur la console
        // Une gestion de erreurs différente est possible, mais sort du cadre du tutoriel
        GLFWErrorCallback.createPrint(System.err)
                         .set();

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

On doit ensuite initialiser GLFW:

Window3D
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
// ... 
import org.lwjgl.glfw.GLFWErrorCallback;
import org.lwjgl.glfw.GLFW;
import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;
// ...

public class Window3D
{
    // ....

    @ThreadOpenGL
    private void startOpenGLThread()
    {
        // Capture les erreurs LWJGL et les affichent sur la console
        // Une gestion de erreurs différente est possible, mais sort du cadre du tutoriel
        GLFWErrorCallback.createPrint(System.err)
                         .set();

        if (!GLFW.glfwInit())
        {
            // On désinscrit la gestion d’erreurs puisque nous ne pouvons pas continuer
            GLFW.glfwSetErrorCallback(null)
                .free();
            System.err.println("GLFW initialisation failed");
            return;
        }

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

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
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
// ... 
import org.lwjgl.glfw.GLFWErrorCallback;
import org.lwjgl.glfw.GLFW;
import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;
// ...

public class Window3D
{
    // ...
    /**
     * Référence sur la fenêtre permettant à GLFW de la gréée
     */
    private       long                 window;
    // ....
    
    @ThreadOpenGL
    private void startOpenGLThread()
    {
        // Capture les erreurs LWJGL et les affichent sur la console
        // Une gestion de erreurs différente est possible, mais sort du cadre du tutoriel
        GLFWErrorCallback.createPrint(System.err)
                         .set();

        if (!GLFW.glfwInit())
        {
            // On désinscrit la gestion d’erreurs puisque nous ne pouvons pas continuer
            GLFW.glfwSetErrorCallback(null)
                .free();
            System.err.println("GLFW initialisation failed");
            return;
        }

        // Configure GLFW
        GLFW.glfwDefaultWindowHints();
        // On empêche, pour le moment, que la fenêtre s’affiche afin d’avoir le temps de la configurée
        GLFW.glfwWindowHint(GLFW.GLFW_VISIBLE, GLFW.GLFW_FALSE);

        // La fenêtre aura un contour
        GLFW.glfwWindowHint(GLFW.GLFW_DECORATED, GLFW.GLFW_TRUE);
        // La fenêtre ne pourra pas être redimensionnée
        // Un redimensionnement obligerait à écouter le changement de taille et à y réagir.
        // Comme nous voulons faire simple, nous ne gérons pas ce cas tout de suite
        GLFW.glfwWindowHint(GLFW.GLFW_RESIZABLE, GLFW.GLFW_FALSE);
        // On veut que la fenêtre fasse au plus proche possible la dimension demandée
        GLFW.glfwWindowHint(GLFW.GLFW_MAXIMIZED, GLFW.GLFW_FALSE);

        // Création de la fenêtre
        this.window = GLFW.glfwCreateWindow(this.width, this.height, this.title, MemoryUtil.NULL, MemoryUtil.NULL);

        if (this.window == MemoryUtil.NULL)
        {
            // La fenêtre n’a pas pu être réservée en mémoire, on ne peut pas continuer
            this.closeGLFW();
            System.err.println("GLFW can’t create window");
            return;
        }


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

    /**
     * Appeler à la sortie de la fenêtre pour nettoyer proprement les divers états
     */
    private void closeGLFW()
    {
        // Termine la gestion de la fenêtre par GLFW
        GLFW.glfwTerminate();
        // Arrête de traquer les erreurs
        GLFW.glfwSetErrorCallback(null)
            .free();
    }
}

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
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
package fr.developez.tutorial.java.dimension3.render;
 
import org.lwjgl.glfw.GLFW;
import org.lwjgl.glfw.GLFWWindowCloseCallbackI;

class Window3DCloseEventManager
        implements GLFWWindowCloseCallbackI
{
    /**
     * Appeler au moment qu’une fenêtre est sur le point de se fermer
     * @param window Référence sur la fenêtre entrain de se fermer
     */
    @Override
    public void invoke(long window)
    {
        this.closeWidow(window);
    }

    /**
     * Gère la fermeture d’une fenêtre 
     * @param window Référence sur la fenêtre à fermer
     */
    void closeWidow(long window)
    {
        // Pour le moment on autorise la fenêtre à se fermer dans tous les cas
        GLFW.glfwSetWindowShouldClose(window, true);
    }
}

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
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
// ...
import org.lwjgl.glfw.Callbacks;
import org.lwjgl.glfw.GLFW;
import org.lwjgl.glfw.GLFWErrorCallback;
import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;
// ...
public class Window3D
{
    // ...
    /**
     * Référence sur la fenêtre permettant à GLFW de la gérer
     */
    private       long                      window;
    private final Window3DCloseEventManager window3DCloseEventManager = new Window3DCloseEventManager();
    // ...

    @ThreadOpenGL
    private void startOpenGLThread()
    {
        // Capture les erreurs LWJGL et les affichent sur la console
        // Une gestion de erreurs différente est possible, mais sort du cadre du tutoriel
        GLFWErrorCallback.createPrint(System.err)
                         .set();

        if (!GLFW.glfwInit())
        {
            // On désinscrit la gestion d’erreurs puisque nous ne pouvons pas continuer
            GLFW.glfwSetErrorCallback(null)
                .free();
            System.err.println("GLFW initialisation failed");
            return;
        }

        // Configure GLFW
        GLFW.glfwDefaultWindowHints();
        // On empêche, pour le moment, que la fenêtre s’affiche afin d’avoir le temps de la configurée
        GLFW.glfwWindowHint(GLFW.GLFW_VISIBLE, GLFW.GLFW_FALSE);

        // La fenêtre aura un contour
        GLFW.glfwWindowHint(GLFW.GLFW_DECORATED, GLFW.GLFW_TRUE);
        // La fenêtre ne pourra pas être redimensionnée
        // Un redimensionnement obligerait à écouter le changement de taille et à y réagir.
        // Comme nous voulons faire simple, nous ne gérons pas ce cas tout de suite
        GLFW.glfwWindowHint(GLFW.GLFW_RESIZABLE, GLFW.GLFW_FALSE);
        // On veut que la fenêtre fasse au plus proche possible la dimension demandée
        GLFW.glfwWindowHint(GLFW.GLFW_MAXIMIZED, GLFW.GLFW_FALSE);

        // Création de la fenêtre
        this.window = GLFW.glfwCreateWindow(this.width, this.height, this.title, MemoryUtil.NULL, MemoryUtil.NULL);

        if (this.window == MemoryUtil.NULL)
        {
            // La fenêtre n’a pas pu être réservée en mémoire, on ne peut pas continuer
            this.closeGLFW();
            System.err.println("GLFW can’t create window");
            return;
        }

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

        // Management de la fermeture de la fenêtre pour libérer proprement la mémoire
        GLFW.glfwSetWindowCloseCallback(this.window, this.window3DCloseEventManager);
        
        // ...
    }
    
    public void closeWindow()
    {
        this.window3DCloseEventManager.closeWidow(this.window);
    }

    void exitWidow()
    {
        // On libère tous les listeners
        Callbacks.glfwFreeCallbacks(this.window);
        // On finalise la destruction de la fenêtre
        GLFW.glfwDestroyWindow(this.window);
        // On ferme les événements GLFW
        this.closeGLFW();
    }

    /**
     * Appeler à la sortie de la fenêtre pour nettoyer proprement les divers états
     */
    private void closeGLFW()
    {
        // Termine la gestion de la fenêtre par GLFW
        GLFW.glfwTerminate();
        // Arrête de traquer les erreurs
        GLFW.glfwSetErrorCallback(null)
            .free();
    }
}

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
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
// ... 
import org.lwjgl.glfw.Callbacks;
import org.lwjgl.glfw.GLFW;
import org.lwjgl.glfw.GLFWErrorCallback;
import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;
// ...
public class Window3D
{
    // ...
    /**
     * Référence sur la fenêtre permettant à GLFW de la gérer
     */
    private       long                      window;
    private final Window3DCloseEventManager window3DCloseEventManager = new Window3DCloseEventManager();
    // ...

    @ThreadOpenGL
    private void startOpenGLThread()
    {
        // Capture les erreurs LWJGL et les affichent sur la console
        // Une gestion de erreurs différente est possible, mais sort du cadre du tutoriel
        GLFWErrorCallback.createPrint(System.err)
                         .set();

        if (!GLFW.glfwInit())
        {
            // On désinscrit la gestion d’erreurs puisque nous ne pouvons pas continuer
            GLFW.glfwSetErrorCallback(null)
                .free();
            System.err.println("GLFW initialisation failed");
            return;
        }

        // Configure GLFW
        GLFW.glfwDefaultWindowHints();
        // On empêche, pour le moment, que la fenêtre s’affiche afin d’avoir le temps de la configurée
        GLFW.glfwWindowHint(GLFW.GLFW_VISIBLE, GLFW.GLFW_FALSE);

        // La fenêtre aura un contour
        GLFW.glfwWindowHint(GLFW.GLFW_DECORATED, GLFW.GLFW_TRUE);
        // La fenêtre ne pourra pas être redimensionnée
        // Un redimensionnement obligerait à écouter le changement de taille et à y réagir.
        // Comme nous voulons faire simple, nous ne gérons pas ce cas tout de suite
        GLFW.glfwWindowHint(GLFW.GLFW_RESIZABLE, GLFW.GLFW_FALSE);
        // On veut que la fenêtre fasse au plus proche possible la dimension demandée
        GLFW.glfwWindowHint(GLFW.GLFW_MAXIMIZED, GLFW.GLFW_FALSE);

        // Création de la fenêtre
        this.window = GLFW.glfwCreateWindow(this.width, this.height, this.title, MemoryUtil.NULLMemoryUtil.NULL, MemoryUtil.NULL);

        if (this.window == MemoryUtil.NULL)
        {
            // La fenêtre n’a pas pu être réservée en mémoire, on ne peut pas continuer
            this.closeGLFW();
            System.err.println("GLFW can’t create window");
            return;
        }

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

        // Management de la fermeture de la fenêtre pour libérer proprement la mémoire
        GLFW.glfwSetWindowCloseCallback(this.window, this.window3DCloseEventManager);
        
        // Associe le contexte Open GL à la fenêtre
        GLFW.glfwMakeContextCurrent(this.window);
        // Activation du mode v-sync
        GLFW.glfwSwapInterval(1);

        // Maintenant que tout est configuré la fenêtre peut se montrer
        GLFW.glfwShowWindow(this.window);

        // Cet appel est crucial pour que la management d’Open GL avec GLFW se passe bien
        GL.createCapabilities();

        // TODO Boucle de rendue
    }
}

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
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
package fr.developez.tutorial.java.dimension3.render;
 
import fr.developez.tutorial.java.dimension3.tool.NonNulltool.NonNull;
import fr.developez.tutorial.java.dimension3.tool.Tools;
import java.util.Objects;
import org.lwjgl.glfw.GLFW;
import org.lwjgl.opengl.GL11;
import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;
 
class Render3D
{
    Render3D()
    {
    }

    /**
     * Lance la boucle de rendu
     *
     * @param window3D Fenêtre où se dessine la 3D
     * @param window   Référence sur la fenêtrefenêtre
     */
    @ThreadOpenGL
    void rendering(@NonNull final Window3D window3D, final long window)
    {
        Objects.requireNonNull(window3D, "window3D must not be null");

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

        // Tant que la fenêtre est présente, rafraîchir la 3D
        while (!GLFW.glfwWindowShouldClose(window))
        {
            // Nettoyage de la fenêtre en buffer pour la prochaine frame
            GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT);

            // Dessine la frame courante
            this.drawScene();

            // Rend la fenêtre en buffer dessinée visible à l’écran
            GLFW.glfwSwapBuffers(window);

            // Récupération des événements fenêtre arrivée pendant le dessin de la frame.
            // Les listeners sur les événements claviers seront appelés par cette méthode
            GLFW.glfwPollEvents();

            // TODO traité les évènements survenus
        }

        // La fenêtre de rendu se ferme ou est fermée, on quitte tout proprement
        window3D.exitWidow();
    }

    /**
     * Initialise le rendu OpenGL
     *
     * @param width  Largeur de la fenêtre de dessin
     * @param height Hauteur de la fenêtre de dessin
     */
    @ThreadOpenGL
    private void initialize3D(final int width, final int height)
    {
        // TODO
    }

    /**
     * Dessine la scène 3D
     */
    @ThreadOpenGL
    private void drawScene()
    {
        // TODO
    }
}

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
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
// ... 
import org.lwjgl.glfw.Callbacks;
import org.lwjgl.glfw.GLFW;
import org.lwjgl.glfw.GLFWErrorCallback;
import org.lwjgl.opengl.GL;
import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;
// ...
public class Window3D
{
    // ...
    /**
     * Référence sur la fenêtre permettant à GLFW de la gérer
     */
    private       long                      window;
    private final Window3DCloseEventManager window3DCloseEventManager = new Window3DCloseEventManager();
    private final Render3D                  render3D                  = new Render3D();
 
    // ... 
    @ThreadOpenGL
    private void startOpenGLThread()
    {
        // Capture les erreurs LWJGL et les affichent sur la console
        // Une gestion de erreurs différente est possible, mais sort du cadre du tutoriel
        GLFWErrorCallback.createPrint(System.err)
                         .set();

        if (!GLFW.glfwInit())
        {
            // On désinscrit la gestion d’erreurs puisque nous ne pouvons pas continuer
            GLFW.glfwSetErrorCallback(null)
                .free();
            System.err.println("GLFW initialisation failed");
            return;
        }

        // Configure GLFW
        GLFW.glfwDefaultWindowHints();
        // On empêche, pour le moment, que la fenêtre s’affiche afin d’avoir le temps de la configurée
        GLFW.glfwWindowHint(GLFW.GLFW_VISIBLE, GLFW.GLFW_FALSE);

        // La fenêtre aura un contour
        GLFW.glfwWindowHint(GLFW.GLFW_DECORATED, GLFW.GLFW_TRUE);
        // La fenêtre ne pourra pas être redimensionnée
        // Un redimensionnement obligerait à écouter le changement de taille et à y réagir.
        // Comme nous voulons faire simple, nous ne gérons pas ce cas tout de suite
        GLFW.glfwWindowHint(GLFW.GLFW_RESIZABLE, GLFW.GLFW_FALSE);
        // On veut que la fenêtre fasse au plus proche possible la dimension demandée
        GLFW.glfwWindowHint(GLFW.GLFW_MAXIMIZED, GLFW.GLFW_FALSE);

        // Création de la fenêtre
        this.window = GLFW.glfwCreateWindow(this.width, this.height, this.title, MemoryUtil.NULL, MemoryUtil.NULL);

        if (this.window == MemoryUtil.NULL)
        {
            // La fenêtre n’a pas pu être réservée en mémoire, on ne peut pas continuer
            this.closeGLFW();
            System.err.println("GLFW can’t create window");
            return;
        }

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

        // Management de la fermeture de la fenêtre pour libéré proprement la mémoire
        GLFW.glfwSetWindowCloseCallback(this.window, this.window3DCloseEventManager);

        // Associe le contexte OpenGL à la fenêtre
        GLFW.glfwMakeContextCurrent(this.window);
        // Activation du mode v-sync
        GLFW.glfwSwapInterval(1);

        // Maintenant que tout est configuré la fenêtre peut se montrer
        GLFW.glfwShowWindow(this.window);

        // Cet appel est crucial pour que la management d’OpenGL avec GLFW se passe bien
        GL.createCapabilities();

        //Lancement du rendu OpenGL
        this.render3D.rendering(this, this.window);
    }
    // ...
}

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

Main
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
package fr.developez.tutorial.java.dimension3;

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

public class Main
{
    public static void main(String[] args)
    {
        final Window3D window3D = new Window3D(800, 600, "Tutoriel 3D - Hello world");
    }
}

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

Image non disponible

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
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
package fr.developez.tutorial.java.dimension3.render;

import fr.developez.tutorial.java.dimension3.tool.GLU;
import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;

class Render3D
{
    private final Window3D window3D;
    private final long     window;

    // ... 

    /**
     * Initialise le rendu OpenGL
     *
     * @param width  Largeur de la fenêtre de dessin
     * @param height Hauteur de la fenêtre de dessin
     */
    @ThreadOpenGL
    private void initialize3D(final int width, final int height)
    {
        // *** Gestion de la transparence ***
        // On indique que l’on souhaite activer la  transparence
        GL11.glEnable(GL11.GL_ALPHA_TEST);
        // Spécification de la précision de la transparence
        GL11.glAlphaFunc(GL11.GL_GREATER, 0.01f);
        // Façon dont la transparence est calculée
        GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);

        /// *** Apparence globale ***
        // Les matériaux peuvent être colorés
        GL11.glEnable(GL11.GL_COLOR_MATERIAL);
        // Pour des raisons de performances, on désactive les textures de manière globale, on les activera au cas par cas
        GL11.glDisable(GL11.GL_TEXTURE_2D);
        // On active la possibilité de combiné des effets
        GL11.glEnable(GL11.GL_BLEND);

        // ** Réglage de la perspective  ***
        // On fixe le rectangle de la fenêtre où la 3D sera dessinée.
        // Ici on veut dessiner sur toute la fenêtre
        // Sur le "frustum", c’est la zone proche de l’utilisateur
        // S’appelle aussi ’view port’
        GL11.glViewport(0, 0, width, height);
        // On veut une projection normalisée
        GL11.glEnable(GL11.GL_NORMALIZE);

        // Réglage de la projection
        GL11.glMatrixMode(GL11.GL_PROJECTION);
        GL11.glLoadIdentity();
        final double ratio = (double) width / (double) height;
        GLU.gluPerspective(45.0, ratio, 0.1, 5000.0);
        // Réglage du modèle de la vue
        GL11.glMatrixMode(GL11.GL_MODELVIEW);
        GL11.glLoadIdentity();

        // Au départ on met un écran blanc
        GL11.glClearColor(1f, 1f, 1f, 1f);
        //On active le test de profondeur
        GL11.glEnable(GL11.GL_DEPTH_TEST);

        // Active les face visible que d’un côté (Pour éviter de dessiner des choses qui seront toujours cachés)
        GL11.glEnable(GL11.GL_CULL_FACE);
        GL11.glCullFace(GL11.GL_FRONT);

        // Éclairage de base pour un effet agréable
        GL11.glLightModeli(GL11.GL_LIGHT_MODEL_LOCAL_VIEWER, GL11.GL_TRUE);
        GL11.glShadeModel(GL11.GL_SMOOTH);
        GL11.glLightModeli(GL12.GL_LIGHT_MODEL_COLOR_CONTROL, GL12.GL_SEPARATE_SPECULAR_COLOR);
        GL11.glLightModeli(GL11.GL_LIGHT_MODEL_TWO_SIDE, 1);

        // Active l’éclairage
        // GL11.glEnable(GL11.GL_LIGHTING);
    }
    // ...
}

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
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
// ...
import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;
import org.lwjgl.opengl.GL11;
// ...

class Render3D
{
    // ...
    /**
     * Dessine la scène 3D
     */
    @ThreadOpenGL
    private void drawScene()
    {
        // Efface l’écran en blanc
        GL11.glClearColor(1f, 1f, 1f, 1f);

        // Dessin du triangle

        // Utilise la couleur rouge
        GL11.glColor4f(1f, 0f, 0f, 1f);


        // Début du polygone
        GL11.glBegin(GL11.GL_POLYGON);
    
        // Chaque point du polygone dans le sens des aiguilles d’une montre
        GL11.glVertex3f(0.0f, 0.5f, -1.0f);
        GL11.glVertex3f(0.5f, -0.5f, -1.0f);
        GL11.glVertex3f(-0.5f, -0.5f, -1.0f);

        // Fin du polygone
        GL11.glEnd();
    }
}

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

À chaque glBegin doit toujours correspondre un glEnd

Un glEnd doit avoir un glBegin pré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 »

Image non disponible

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
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;
import org.lwjgl.opengl.GL11;
// ...

class Render3D
{
    // ...

    // Coordonnées de l’objet
    private float triangleX = 0f;
    private float triangleY = 0f;
    private float triangleZ = -1f;
    // ...

    /**
     * Dessine la scène 3D
     */
    @ThreadOpenGL
    private void drawScene()
    {
        // Efface l’écran en blanc
        GL11.glClearColor(1f, 1f, 1f, 1f);

        // Dessin du triangle

        // Utilise la couleur rouge
        GL11.glColor4f(1f, 0f, 0f, 1f);

        // Sauvegarde la matrice courante dans la pile des matrices
        GL11.glPushMatrix();
        // Change la matrice courant afin de positionner le triangle à l’écran
        GL11.glTranslatef(this.triangleX, this.triangleY, this.triangleZ);

        // Début du polygone
        GL11.glBegin(GL11.GL_POLYGON);

        // Chaque point du polygone dans le sens des aiguilles d’une montre
        // Définit en relatif à la matrice courante
        GL11.glVertex3f(0.0f, 0.5f, 0.0f);
        GL11.glVertex3f(0.5f, -0.5f, 0.0f);
        GL11.glVertex3f(-0.5f, -0.5f, 0.0f);

        // Fin du polygone
        GL11.glEnd();
        // Restaure la matrice sauvegardée
        GL11.glPopMatrix();
    }
}

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
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;
import org.lwjgl.opengl.GL11;
// ….

class Render3D
{
    // ...

    // Coordonnées de l’objet
    private float triangleX = 0f;
    private float triangleY = 0f;
    private float triangleZ = -1f;
   // Pas de translation
   private final float step = 0.01f;
   // Sens de translation
   private       int   way  = 1;
    // ...

    /**
     * Dessine la scène 3D
     */
    @ThreadOpenGL
    private void drawScene()
    {
        // Efface l’écran en blanc
        GL11.glClearColor(1f, 1f, 1f, 1f);

        // Dessin du triangle

        // Utilise la couleur rouge
        GL11.glColor4f(1f, 0f, 0f, 1f);

        // Sauvegarde la matrice courante dans la pile des matrices
        GL11.glPushMatrix();
        // Change la matrice courant afin de positionner le triangle à l’écran
        GL11.glTranslatef(this.triangleX, this.triangleY, this.triangleZ);

        // Début du polygone
        GL11.glBegin(GL11.GL_POLYGON);

        // Chaque point du polygone dans le sens des aiguilles d’une montre
        // Définit en relatif à la matrice courante
        GL11.glVertex3f(0.0f, 0.5f, 0.0f);
        GL11.glVertex3f(0.5f, -0.5f, 0.0f);
        GL11.glVertex3f(-0.5f, -0.5f, 0.0f);

        // Fin du polygone
        GL11.glEnd();
        // Restaure la matrice sauvegardée
        GL11.glPopMatrix();
       // On met à jour la position pour le prochain tour
       this.triangleX += this.step * this.way;
       if (this.triangleX >= 1.5f)
       {
           this.way = -1;
       }
       if (this.triangleX <= -1.5f)
       {
           this.way = 1;
       }
    }
}

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 BaronMickael Baron pour sa relecture technique ainsi de m’avoir proposé de rédiger ce tutoriel.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

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 ni 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.