Tutoriel de création d'un mini-moteur de rendu 3D en Java avec LWJGL

Interaction avec l’utilisateur

Gérard Bourriaud

Cet article suppose que vous ayez suivit l’Introduction et va fortement utiliser le graphe de scène.

Il a pour but d’ajouter de l’interaction entre l’utilisateur et la scène 3D à travers le clavier, la souris et la manette de jeu.

- Titre : Tutoriel de création d'un mini-moteur de rendu 3D en Java avec LWJGL

- Auteur : Gérard Bourriaud

- Parution : 1 décembre 2020

- Mise à jour : 1 décembre 2020

- 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 Gérard Bourriaud. 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 :

- Tutoriel sur le moteur 3D en Java avec LWJGL - Introduction

- Tutoriel sur le moteur 3D en Java avec LWJGL - Le graphe de scène

Sommaire

- I. Introduction

- II. Le gestionnaire d’événements

- II-A. Les événements clavier

- II-B. Les événements de la souris

- II-B-1. L’entrée ou la sortie de la fenêtre par la souris

- II-B-2. La position de la souris dans la fenêtre

- II-B-3. L’appuie/le relâchement des boutons souris

- II-B-4. Les événements de scroll

- II-B-5. Gestion des événements

- II-C. La manette de jeu

- III. Capture d’un objet à une position dans l’écran

- III-A. La technique de picking

- III-B. Mise en place du picking

- III-B-1. Attribution du nom et d’une couleur de picking aux nœuds

- III-B-2. Capture de la couleur aux coordonnées demandées

- III-C. Mise en place d’un observateur de nœud

- IV. Conclusion

- V. Annexe

- V-A. Les buffers dans java.nio

I - Introduction

Code source du tutoriel. Nous recommandons de le télécharger dés maintenant car il contient des outils que cet article utilise .

Code source

Téléchargez le code source

Afin de récupérer les événements clavier, souris ou manette, nous utiliserons GLFW qui contient toute les commandes nécessaires.

Les commandes GLFW que nous appellerons ainsi que le retour des listeners sur lesquels nous allons nous abonner, seront dans le thread OpenGL.

Afin que l’utilisateur de notre moteur ne se retrouve jamais dans ce thread, nous allons créer une couche par dessus qui appellera nos écouteurs et observateurs dans un thread à part.

C’est pour cela que dans les outils fournis avec le code source vous trouverez un manager de thread basé sur un Executor.

Ce tutoriel fera des choix basés sur les besoins les plus fréquemment rencontrés. Néanmoins il fournira suffisamment d’informations pour que vous puissiez adapter votre code à d’autres besoins.

Il faut entendre souris au sens large, en effet les événements souris captureront aussi les événements touchpad.

II - Le gestionnaire d’événements

Nous allons gérer tous les événements à travers une classe dédié qui recevra les événements claviers, souris et manette.

Elle en fera des éventuels traitements.

C’est ici que nos écouteurs et observateurs s’enregistreront.

C’est elle qu’il les avertira au bon moment. (Nous verrons qu’il y a un moment privilégié)

Cette classe dédié nous l’appelleront le gestionnaire d’événements.

Afin de ne pas perturber l’affichage avec nos traitements il faut le faire en fin de boucle de rendu. Afin d’être sûr que GLFW à finis de rapporter tous les événements survenus pendant le rendu de la frame courante, il faudra appeler nos écouteurs et observateurs après le GLFW.glfwPollEvents(); de Render3D.

Il sera pratique que notre gestionnaire d’événements soit un singleton, on pourra ainsi s’y abonner ou désabonner de n’importe où. Ce qui donne au minimum :

EventManager

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

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

5. /**

6. * Manageur des évènements utilisateur. Se brancher dessus afin de pouvoir les traiter

7. */

8. public class EventManager

9. {

10. public static final EventManager EVENT_MANAGER = new EventManager();

12. private EventManager()

13. {

14. }

16. @ThreadOpenGL

17. void eventProcess()

18. {

19. }

20. }

Et dans Render3D

Render3D

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

import fr.developez.tutorial.java.dimension3.tool.GLU;
import fr.developez.tutorial.java.dimension3.tool.NonNull;
import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;
import java.util.Objects;
import org.lwjgl.glfw.GLFW;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;

/**
* Boucle de rendu
*/
class Render3D
{
// …

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

// …

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

EventManager.EVENT_MANAGER.eventProcess();

// …
}
// …
}

II-A - Les événements clavier

Pour s’enregistrer aux événements clavier, nous utiliseront le méthode GLFW : GLFW.glfwSetKeyCallback(long window, GLFWKeyCallbackI callback)

Cette méthode doit être appelée dans le thread OpenGL. Nous conseillons même de l’appeler avant l’affichage de la fenêtre, au même moment où dans l’introduction on enregistrait l’écouteur de fermeture de la fenêtre. Comme nous le ferons.

On ne peut enregistrer qu’un seul écouteur, ce n’est pas gênant, il suffira que se soit notre manager d’événements qui reçoive les informations, de là on pourra faire le traitement que l’on veut dont avertir des écouteurs.

Créons notre écouteur et enregistrons le.

EventManager

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

// …
import org.lwjgl.glfw.GLFWKeyCallbackI;
// …

/**
* Manageur des évènements utilisateur. Se brancher dessus afin de pouvoir les traiter
*/
public class EventManager
{
// …
final GLFWKeyCallbackI keyCallBack =
(long window, int key, int scanCode, int action, int modifiers) ->
{
// …
};
// …
}

Et dans Window3D

Window3D

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

import fr.developez.tutorial.java.dimension3.thread.ThreadManager;
import fr.developez.tutorial.java.dimension3.tool.NonNull;
import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;
import java.nio.IntBuffer;
import java.util.Objects;
import org.lwjgl.glfw.Callbacks;
import org.lwjgl.glfw.GLFW;
import org.lwjgl.glfw.GLFWErrorCallback;
import org.lwjgl.glfw.GLFWVidMode;
import org.lwjgl.opengl.GL;
import org.lwjgl.system.MemoryStack;
import org.lwjgl.system.MemoryUtil;

public class Window3D
{
// …
@ThreadOpenGL
private void startOpenGLThread()
{
// …
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;
}

// Écoute les événements claviers
GLFW.glfwSetKeyCallback(this.window, EventManager.EVENT_MANAGER.keyCallBack);

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

Maintenant que notre écouteur est branché, expliquons en ses paramètres.

final GLFWKeyCallbackI keyCallBack =
(long window, int key, int scanCode, int action, int modifiers) ->

Paramétre

Explication

window

Pointeur opaque représentant la fenêtre qui a capturé l’événement clavier.
Ici nous avons une seule fenêtre, nous pourrons donc ignorer ce paramètre

key

Code de la touche qui a déclenché l’événement.
Les codes des touches capturées par GLFW sont les constantes commençant par GLFW.GLFW_KEY_.
Par exemaple, la touche A a pour code GLFW_KEY_A, la touche flèche du haut, GLFW_KEY_UP, …

scanCode

Code de la touche système dépendant. Utile pour les touches non standard non définis par les constantes GLFW. Ici nous nous limiterons aux touches standards pour nos interactions.

action

Action qui vient de se produire sure la touche, voir le tableau plus bas pour la description des ces événements

modifiers

Combinaison binaire de touches dite modifiante. Par exemple ici on peut savoir si shift ou control sont appuyés pendant l’événement. Plus de détails un peu plus bas

Les actions pouvant arriver à une touche sont

Action

Valeur GLFW

Description

Appuyée

GLFW.GLFW_PRESS

Déclenché dès l’appui sur la touche

Répétée

GLFW.GLFW_REPEAT

Quand on appuie longtemps sur une touche, le système génère une répétition de celle-ci. Cette action est appelée à chaque répétition déclenchée par le système.

Relâchée

GLFW.GLFW_RELEASE

Déclenchée dès qu’on relâche la touche

Les touches modifiantes sont appelées ainsi,, car elles peuvent changer le comportement de la touche. Par exemple appuyer sur une lettre dans un éditeur de texte écrit une lettre minuscule. Mais, si on appuie sur la même lettre avec shift enfoncé en même temps, on aura une lettre majuscule.

Les touches qualifiées de modifiantes sont stockées dans modifiers sous forme de combinaison binaire. Dit autrement, chaque bit de l’entier correspond à une de ces touches, 1 signifiant la touche est enfoncée, tandis que 0 signifie qu’elle est relâchée.

Seuls les six derniers bits ont une signification.

Bit

Constante GLFW

Description

Bit 5

GLFW_MOD_NUM_LOCK

La touche verrouillage numérique.
Ne marche pas pour tous les systèmes/clavier

Bit 4

GLFW_MOD_CAPS_LOCK

La touche verrouillage majuscule.
Ne marche pas pour tous les systèmes/claviers

Bit 3

GLFW_MOD_SUPER

Cette touche correspond sous windows à la touche windows, sous mac : pomme ou command. Comme cette touche correspond souvent à une action système, sa capture peut être absorbée par l'action qui se déclenche.

Bit 2

GLFW_MOD_ALT

Touche Alt

Bit 1

GLFW_MOD_CONTROL

Touche control

Bit 0

GLFW_MOD_SHIFT

Touche shift

A savoir que la touche AtlGr sera vue comme si Alt et control étaient appuyés en même temps.

Par exemple pour faire une action si control est enfoncé :

if((modifiers & GLFW.GLFW_MOD_CONTROL)!= 0)
{
// Action à faire si control est enfoncé
}

Maintenant que nous savons ce que nous recevons, décidons de ce que nous voulons faire des événements clavier.

On pourrait se contenter de créer un écouteur qui redirige ces informations de manière brute, mais et laisser l’utilisateur du moteur le soin d’y mettre sa propre logique. Mais ici nous allons un peu plus loin en y ajoutant un prétraitement.

L’idée est d’envoyé au travers de notre écouteur la liste des touches en train d’être appuyées. Ainsi, quand l’utilisateur recevra l’événement, il pourra facilement réagir à l’événement.

C’est pour cela, que nous exposerons l’écouteur suivant :

KeyboardEventListener

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

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

5. /**

6. * Écouteur des événements clavier

7. */

8. @FunctionalInterface

9. public interface KeyboardEventListener

10. {

11. /**

12. * Appelé régulièrement pour donner l'état actuel des touches du clavier<br/>

13. * Les entiers sont les codes des touches actuellement appuyées.<br/>

14. * Leur signification sont dans les constantes de <b>GLFW</b> commençant par <b>GLFW.GLFW_KEY_</b>

15. *

16. * @param keys Liste des touches actuellement appuyées par l'utilisateur

17. */

18. public void currentKeyDown(@NonNull int[] keys);

19. }

Nous allons maintenant ajouter la possibilité de s’abonner ou désabonner des événements clavier.

EventManager

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

// …
import fr.developez.tutorial.java.dimension3.keyboard.KeyboardEventListener;
import fr.developez.tutorial.java.dimension3.tool.NonNull;
import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
// …

/**
* Manageur des évènements utilisateur. Se brancher dessus afin de pouvoir les traiter
*/
public class EventManager
{
// …
private final List<KeyboardEventListener>      keyboardEventListeners      = new ArrayList<>();
// …
/**
* Enregistre un écouteur des événements clavier.<br/>
* Il sera appelé régulièrement pour donner l'état actuel des touches du clavier
*/
public void register(@NonNull KeyboardEventListener keyboardEventListener)
{
Objects.requireNonNull(keyboardEventListener, "keyboardEventListener must not be null");

synchronized (this.keyboardEventListeners)
{
if (!this.keyboardEventListeners.contains(keyboardEventListener))
{
this.keyboardEventListeners.add(keyboardEventListener);
}
}
}

/**
* Oublie un écouteur des événements clavier.<br/>
* Il ne sera plus appelé
*/
public void unregister(KeyboardEventListener keyboardEventListener)
{
synchronized (this.keyboardEventListeners)
{
this.keyboardEventListeners.remove(keyboardEventListener);
}
}
// …
}

Afin d’avoir la liste des touches actuellement appuyées, nous allons la maintenir à jour en respectant ceci :

- Au moment de l’appuie d’une touche, on l’ajoute dans la liste

- Au moment du relâchement d’une touche, on l’enlève de la liste

Ce qui donne :

EventManager

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

// …
import java.util.TreeSet;
import org.lwjgl.glfw.GLFW;
import org.lwjgl.glfw.GLFWKeyCallbackI;
// …

/**
* Manageur des évènements utilisateur. Se brancher dessus afin de pouvoir les traiter
*/
public class EventManager
{
// …
private final TreeSet<Integer> currentKeysDown           = new TreeSet<>();
// …

/**
* Écouteur GLFW des événements clavier.<br/>
* <br/>
* A chaque événement sur une touche, une <b>action</b> est associée.<br/>
* L'action peut être de trois types :
* <ul>
*     <li>Appuyée : Arrive au moment ou l'utilisateur enfonce la touche. Valeur : GLFW.GLFW_PRESS</li>
*     <li>Répétée : Arrive quand la touche est restée appuyé depuis un certain temps (Temps dépendant de l'OS et de ses réglages/préférences). Valeur : GLFW.GLFW_REPEAT</li>
*     <li>Relâchée : Arrive au moment où l'utilisateur relâche la touche. Valeur : GLFW.GLFW_RELEASE</li>
* </ul>
* <p>
* Les <b>modifiers</b> associés est la cumulation binaire. À chaque bit correspond une touche d'action de type Ctrl, Alt. ...
* Ci dessous la signification des bits et leur masque GLFW
* <table>
*     <tr><th>bit 5</th><td>La touche lock numérique est enfoncée. Ne marche pas pour tous les systèmes/claviers</td><td>GLFW.GLFW_MOD_NUM_LOCK</td></tr>
*     <tr><th>bit 4</th><td>La touche mise an majuscule est enfoncée. Ne marche pas pour tous les systèmes/claviers</td><td>GLFW.GLFW_MOD_CAPS_LOCK</td></tr>
*     <tr><th>bit 3</th><td>Cette touche correspond sous windows à la touche windows, sous mac pomme ou command. Comme cette touche correspond souvent à une action système, sa capture peut être absorbée par l'action qui se déclenche.</td><td>GLFW.GLFW_MOD_SUPER</td></tr>
*     <tr><th>bit 2</th><td>Touche Alt enfoncée</td><td>GLFW.GLFW_MOD_ALT</td></tr>
*     <tr><th>bit 1</th><td>Touche Control enfoncée</td><td>GLFW.GLFW_MOD_CONTROL</td></tr>
*     <tr><th>bit 0</th><td>Touche Shift enfoncée</td><td>GLFW.GLFW_MOD_SHIFT</td></tr>
* </table>
* A noter, que la touche AltGr est mappé de la même façon que si Alt et Control sont enfoncées en même temps.
*
* @param window    Fenêtre qui à déclenchée l'événement
* @param key       Numéro de la touche enfoncé. Correspondance retrouvable grâce aux constantes <b>GLFW</b> qui commencent par <b>GLFW.GLFW_KEY_</b>
* @param scanCode  Code de la touche d'un point de vue du système
* @param action    Action sur la touche. Voir plus haut
* @param modifiers Modifiers pressées en même temps que la touche, voir plus haut
*/
final GLFWKeyCallbackI keyCallBack =
(long window, int key, int scanCode, int action, int modifiers) ->
{
switch (action)
{
case GLFW.GLFW_PRESS:
this.currentKeysDown.add(key);
break;
case GLFW.GLFW_RELEASE:
this.currentKeysDown.remove(key);
break;
}
};
// …
}

Il ne nous reste plus qu’à avertir les écouteurs des événements clavier.

EventManager

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

// …
import fr.developez.tutorial.java.dimension3.keyboard.KeyboardEventListener;
import fr.developez.tutorial.java.dimension3.thread.ThreadManager;
import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;
import java.util.TreeSet;
// …

/**
* Manager des évènements utilisateur. Se brancher dessus afin de pouvoir les traiter
*/
public class EventManager
{
// …
@ThreadOpenGL
void eventProcess()
{
this.updateKeyboardEvents();
}

@ThreadOpenGL
private void updateKeyboardEvents()
{
// Si, il y a au moins une touche du clavier actuellement enfoncée,
// on alerte les écouteurs
final int size = this.currentKeysDown.size();

if (size > 0)
{
final int[] keysDown = new int[size];
int         index    = 0;

for (final Integer integer : this.currentKeysDown)
{
keysDown[index] = integer;
index++;
}

synchronized (this.keyboardEventListeners)
{
for (final KeyboardEventListener keyboardEventListener : this.keyboardEventListeners)
{
// Les écouteurs sont appelés dans un thread à part afin que l'exécution de leur code ne ralentisse pas le thread OpenGL
ThreadManager.execute(keysDown, keyboardEventListener::currentKeyDown);
}
}
}
}
// …
}

L’appel ThreadManager.execute fait parti des outils que nous fournissons dans le code source du tutoriel. Il permet, de lancer une tâche dans un thread à part.

Nous allons illustrer l’utilisation de ceci à travers un exemple :

Main

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

3. import fr.developez.tutorial.java.dimension3.geometry.Box;

4. import fr.developez.tutorial.java.dimension3.render.Color4f;

5. import fr.developez.tutorial.java.dimension3.render.EventManager;

6. import fr.developez.tutorial.java.dimension3.render.SceneGraph;

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

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

9. import org.lwjgl.glfw.GLFW;

11. public class Main

12. {

13. private static final float    TRANSLATE_STEP    = 0.01f;

14. private static final Box      box               = new Box();

15. private static final Window3D window3D          = new Window3D(800, 600, "Tutoriel 3D - Boîte réagissant aux actions de l’utilisateur");

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

18. {

19. Main.drawScene(Main.window3D.getSceneGraph());

20. }

22. private static void drawScene(@NonNull final SceneGraph sceneGraph)

23. {

24. sceneGraph.setBackground(Color4f.BLACK);

26. sceneGraph.root.addChild(Main.box);

28. Main.box.z      = -2f;

29. Main.box.angleX = 22.5f;

30. Main.box.scaleX = 2f;

31. Main.box.scaleZ = 0.5f;

32. Main.box.setColor(Color4f.LIGHT_GREEN);

34. EventManager.EVENT_MANAGER.register(Main::currentKeyDown);

35. }

37. private static void currentKeyDown(@NonNull int[] keys)

38. {

39. for (final int key : keys)

40. {

41. switch (key)

42. {

43. case GLFW.GLFW_KEY_UP:

44. Main.box.y += Main.TRANSLATE_STEP;

45. break;

46. case GLFW.GLFW_KEY_DOWN:

47. Main.box.y -= Main.TRANSLATE_STEP;

48. break;

49. case GLFW.GLFW_KEY_RIGHT:

50. Main.box.x += Main.TRANSLATE_STEP;

51. break;

52. case GLFW.GLFW_KEY_LEFT:

53. Main.box.x -= Main.TRANSLATE_STEP;

54. break;

55. }

56. }

57. }

58. }

Nous obtenons une boîte verte qui bouge selon les flèches qu’appuie l’utilisateur. Si il appuie sur deux touches en même temps, par exemple haut et gauche, la combinaison des deux événements se produit et la boîte à un mouvement diagonal allant vers le haut et la gauche.

II-B - Les événements de la souris

Comme dit au début, il faut entendre souris au sens large, car les événements touchpad seront aussi capturés. Ainsi que l’utilisateur utilise une souris ou un touchpad, le comportement sera le même.

GLFW distingue plusieurs événements souris :

- La souris entre ou sort de la fenêtre 3D

- La position de la souris au sein de la fenêtre 3D

- L’appuie ou le relâchement d’un des boutons souris

- Le scroll vertical ou horizontal. Pour une souris ordinaire on aura seulement le scroll vertical qui correspond aux mouvements de la molette. Pour le touchpad, cela dépendra du glisser du doigt.

Quand nous exposerons les événements souris, nous regrouperons la position, l’état des boutons ainsi que des scroll dans un seul événement.

Voyons en détails chaque événement :

II-B-1 - L’entrée ou la sortie de la fenêtre par la souris

Savoir quand la souris sort ou entre dans la fenêtre, peut servir dans certaines ergonomies. C’est pour cela que nous exposerons un observateur sur l’événement.

De plus nous nous serviront de cet état pour savoir si il faut reporté ou non les événements souris.

Pour écouter l’événement on utilisera la méthode GLFW.glfwSetCursorEnterCallback(long window, GLFWCursorEnterCallbackI callback);

Il faut donc un observateur qui implémentera GLFWCursorEnterCallbackI

EventManager

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

// …
import org.lwjgl.glfw.GLFW;
import org.lwjgl.glfw.GLFWCursorEnterCallbackI;
// …

/**
* Manager des évènements utilisateur. Se brancher dessus afin de pouvoir les traiter
*/
public class EventManager
{
// …
private       boolean          mouseInsideWindow         = false;
// …
/**
* Détecte l'entrée/sortie de la souris de la fenêtre
*/
final GLFWCursorEnterCallbackI cursorEnterCallback =
(long window, boolean entered) ->
{
this.mouseInsideWindow = entered;
};
// …
}

Et l’ajouter dans Window3D

Window3D

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

// …
import org.lwjgl.glfw.GLFW;
import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;
// …

public class Window3D
{
// …
@ThreadOpenGL
private void startOpenGLThread()
{
// …

// Écoute les événements claviers
GLFW.glfwSetKeyCallback(this.window, EventManager.EVENT_MANAGER.keyCallBack);
// Écoute les entrées/sortie de la souris de la fenêtre
GLFW.glfwSetCursorEnterCallback(this.window, EventManager.EVENT_MANAGER.cursorEnterCallback);

// …
}
// …
}

Cet observateur, n’est appelé que si l’état entrée/sortie de la fenêtre change.

Du coup si la fenêtre, lors du lancement, apparaît sous le curseur de la souris, nous ne saurons pas que l’on est dans la fenêtre et donc la valeur de mouseInsideWindow serait fausse tant que la souris n’est pas sorte au moins une fois de la fenêtre.

Afin d’avoir une valeur correcte le plus tôt possible, nous mettrons aussi la valeur à jour quand la position de la souris est signalée par GLFW. En effet la position n’est donnée que si la souris est dans la fenêtre et dés que la fenêtre apparaît sous la souris. .

Créons notre observateur, permettons de l’enregistrer et signalons les observateurs du changement.

MouseInsideWindowObserver

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

3. /**

4. * Observateur de la sortie/entrée de la souris de l'écran

5. */

6. @FunctionalInterface

7. public interface MouseInsideWindowObserver

8. {

9. public void mouseInsideWidowChanged(boolean insideWindow);

10. }

EventManager

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

// …
import fr.developez.tutorial.java.dimension3.mouse.MouseInsideWindowObserver;
import fr.developez.tutorial.java.dimension3.thread.ThreadManager;
import fr.developez.tutorial.java.dimension3.tool.NonNull;
import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import org.lwjgl.glfw.GLFWCursorEnterCallbackI;
// …

/**
* Manager des évènements utilisateur. Se brancher dessus afin de pouvoir les traiter
*/
public class EventManager
{
// …
private       boolean          previousMouseInsideWindow = false;
private       boolean          mouseInsideWindow         = false;
// …
private final List<MouseInsideWindowObserver> mouseInsideWindowObservers  = new ArrayList<>();
// …
/**
* Enregistre un observateur de détection d'entrée/sortie de la fenêtre <br/>
* L'écouteur est appelé immédiatement avec l'état actuel de la souris par rapport à la fenêtre.<br/>
* Il sera appelé a chaque entrée/sortie de la fenêtre
*/
public void register(@NonNull MouseInsideWindowObserver mouseInsideWindowObserver)
{
Objects.requireNonNull(mouseInsideWindowObserver, "mouseInsideWindowObserver must not be null");

mouseInsideWindowObserver.mouseInsideWidowChanged(this.mouseInsideWindow);

synchronized (this.mouseInsideWindowObservers)
{
if (!this.mouseInsideWindowObservers.contains(mouseInsideWindowObserver))
{
this.mouseInsideWindowObservers.add(mouseInsideWindowObserver);
}
}
}

/**
* Oublie un observateur de détection d'entrée/sortie de la fenêtre <br/>
* Il ne sera plus appelé
*/
public void unregister(MouseInsideWindowObserver mouseInsideWindowObserver)
{
synchronized (this.mouseInsideWindowObservers)
{
this.mouseInsideWindowObservers.remove(mouseInsideWindowObserver);
}
}
// …

@ThreadOpenGL
void eventProcess()
{
this.updateKeyboardEvents();
this.updateMouseEvents();
// …
}
// …
@ThreadOpenGL
private void updateMouseEvents()
{
// On regarde si l'état entrée/sortie de al souris a changé
final boolean mouseEnterExitChanged = this.previousMouseInsideWindow != this.mouseInsideWindow;
this.previousMouseInsideWindow = this.mouseInsideWindow;

// Si l'état a changé, on prévient les observateurs
if (mouseEnterExitChanged)
{
synchronized (this.mouseInsideWindowObservers)
{
final boolean inside = this.mouseInsideWindow;

for (final MouseInsideWindowObserver mouseInsideWindowObserver : this.mouseInsideWindowObservers)
{
// Les observateurs sont appelés dans un thread à part afin que l'exécution de leur code ne ralentisse pas le thread OpenGL
ThreadManager.execute(inside, mouseInsideWindowObserver::mouseInsideWidowChanged);
}
}
}
// …
}
// …
}

II-B-2 - La position de la souris dans la fenêtre

Pour être signalé de la position de la souris, nous utiliserons : GLFW.glfwSetCursorPosCallback(long window, GLFWCursorPosCallbackI callback)

EventManager

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

// …
import org.lwjgl.glfw.GLFWCursorPosCallbackI;
// …

/**
* Manager des évènements utilisateur. Se brancher dessus afin de pouvoir les traiter
*/
public class EventManager
{
// …
private       double           mouseX                    = 0.0;
private       double           mouseY                    = 0.0;
private       boolean          mouseInsideWindow         = false ;
// …
/**
* Écoute les modifications de la position de la souris dans la fenêtre
*
* @param window La fenêtre ou est survenu l'événement souris
* @param cursorX Abscisse de la souris
* @param cursorY Ordonnée de la souris
*/
final GLFWCursorPosCallbackI cursorPosCallback =
(long window, double cursorX, double cursorY) ->
{
this.mouseInsideWindow = true;
this.mouseX            = cursorX;
this.mouseY            = cursorY;
};
// …
]

Comme dit plus haut, si on a une position de la souris dans la fenêtre, alors celle-ci est dans la fenêtre.

D’où la mise à jour de l’état d’entrée/sortie de la fenêtre

Les coordonnées sont les coordonnées à l’intérieur de la fenêtre. Ici les X vont de gauche à droite et varient de 0 à la largeur de la fenêtre. Les Y vont du haut vers le bas, de 0 à la hauteur de la fenêtre.

La position, sera combinées avec d’autres informations dans un même évènement définit un peu plus loin.

II-B-3 - L’appuie/le relâchement des boutons souris

Pour nous abonner aux évènements boutons de la souris, nous utiliseront :

GLFW.glfwSetMouseButtonCallback(long window, GLFWMouseButtonCallbackI callback)

EventManager

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

// …
import org.lwjgl.glfw.GLFWMouseButtonCallbackI;
// …

/**
* Manager des évènements utilisateur. Se brancher dessus afin de pouvoir les traiter
*/
public class EventManager
{
// …
private       boolean          mouseLeftButtonDown       = false;
private       boolean          mouseMiddleButtonDown     = false;
private       boolean          mouseRightButtonDown      = false;
// …
/**
* Écouteur des événements sur les boutons de la souris.
* <br/>
* A chaque événement sur un bouton, une <b>action</b> est associée.<br/>
* L'action peut être de trois types :
* <ul>
*     <li>Appuyée : Arrive au moment ou l'utilisateur enfonce le bouton. Valeur : GLFW.GLFW_PRESS</li>
*     <li>Répétée : Arrive quand le bouton est restée appuyé depuis un certain temps (Temps dépendant de l'OS et de ses réglages/préférences). Valeur : GLFW.GLFW_REPEAT</li>
*     <li>Relâchée : Arrive au moment où l'utilisateur relâche le bouton. Valeur : GLFW.GLFW_RELEASE</li>
* </ul>
* <p>
* Les <b>modifiers</b> associés est la cumulation binaire. À chaque bit correspond une touche d'action de type Ctrl, Alt. ...
* Ci dessous la signification des bits et leur masque GLFW
* <table>
*     <tr><th>bit 5</th><td>La touche lock numérique est enfoncée. Ne marche pas pour tous les systèmes/claviers</td><td>GLFW.GLFW_MOD_NUM_LOCK</td></tr>
*     <tr><th>bit 4</th><td>La touche mise an majuscule est enfoncée. Ne marche pas pour tous les systèmes/claviers</td><td>GLFW.GLFW_MOD_CAPS_LOCK</td></tr>
*     <tr><th>bit 3</th><td>Cette touche correspond sous windows à la touche windows, sous mac pomme ou command. Comme cette touche correspond souvent à une action système, sa capture peut être absorbée par l'action qui se déclenche.</td><td>GLFW.GLFW_MOD_SUPER</td></tr>
*     <tr><th>bit 2</th><td>Touche Alt enfoncée</td><td>GLFW.GLFW_MOD_ALT</td></tr>
*     <tr><th>bit 1</th><td>Touche Control enfoncée</td><td>GLFW.GLFW_MOD_CONTROL</td></tr>
*     <tr><th>bit 0</th><td>Touche Shift enfoncée</td><td>GLFW.GLFW_MOD_SHIFT</td></tr>
* </table>
* A noter, que la touche AltGr est mappé de la même façon que si Alt et Control sont enfoncées en même temps.
* <p>
* Les boutons sont distingués grace aux constantes:
* <ul>
*     <li>GLFW.GLFW_MOUSE_BUTTON_LEFT : Le bouton gauche</li>
*     <li>GLFW.GLFW_MOUSE_BUTTON_MIDDLE : Le bouton du milieu</li>
*     <li>GLFW.GLFW_MOUSE_BUTTON_RIGHT : Le bouton droit</li>
* </ul>
*
* @param window    La fenêtre ou est survenu l'événement souris
* @param button    Le code du bouton qui a déclenché l'événement. Voir plus haut
* @param action    L'action faite su le bouton. Voir plus haut
* @param modifiers Les modifiers appuyé en même temps. Voir plus haut
*/
final GLFWMouseButtonCallbackI mouseButtonCallback =
(long window, int button, int action, int modifiers) ->
{
switch (action)
{
case GLFW.GLFW_PRESS:
switch (button)
{
case GLFW.GLFW_MOUSE_BUTTON_LEFT:
this.mouseLeftButtonDown = true;
break;
case GLFW.GLFW_MOUSE_BUTTON_MIDDLE:
this.mouseMiddleButtonDown = true;
break;
case GLFW.GLFW_MOUSE_BUTTON_RIGHT:
this.mouseRightButtonDown = true;
break;
}
break;
case GLFW.GLFW_RELEASE:
switch (button)
{
case GLFW.GLFW_MOUSE_BUTTON_LEFT:
this.mouseLeftButtonDown = false;
break;
case GLFW.GLFW_MOUSE_BUTTON_MIDDLE:
this.mouseMiddleButtonDown = false;
break;
case GLFW.GLFW_MOUSE_BUTTON_RIGHT:
this.mouseRightButtonDown = false;
break;
}
break;
}
};
// …
}

Le paramètre bouton est le code du bouton qui a déclenché l’action.

Les paramètres action et modifiers ont exactement le même rôle et la même signification que pour les événements clavier.

Branchons notre écouteur

Window3D

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

// …
import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;
import org.lwjgl.glfw.GLFW;
// …

public class Window3D
{
// …

@ThreadOpenGL
private void startOpenGLThread()
{
// …
// Écoute les événements claviers
GLFW.glfwSetKeyCallback(this.window, EventManager.EVENT_MANAGER.keyCallBack);
// Écoute les entrées/sortie de la souris de la fenêtre
GLFW.glfwSetCursorEnterCallback(this.window, EventManager.EVENT_MANAGER.cursorEnterCallback);
// Écoute les événements boutons de souris
GLFW.glfwSetMouseButtonCallback(this.window, EventManager.EVENT_MANAGER.mouseButtonCallback);
// …
}
// …
}

Pouir la souris, il est intéressant de récupérer le modificateur.

Mais les récupérés ici, nous permettrait d’avoir leur valeur au moment d’un appuie ou un relevé de bouton, alors qu’il peut être intéressant de savoir que l’un deux n’est plus appuyé ou appuyé entre les événements boutons.

C’est pour cela que nous allons plutôt récupérer les modificateurs depuis les événements clavier.

Pour les modificateurs pas toujours gérés (le verrouillage numérique et le verrouillage majuscule), nous allons même enrichir ces modificateurs nous-même avant de les envoyer aux événements souris

EventManager

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

// …
import org.lwjgl.glfw.GLFW;
import org.lwjgl.glfw.GLFWKeyCallbackI;
// …

/**
* Manager des évènements utilisateur. Se brancher dessus afin de pouvoir les traiter
*/
public class EventManager
{
// …
private       int              modifiers                 = 0;
// …
final GLFWKeyCallbackI keyCallBack =
(long window, int key, int scanCode, int action, int modifiers) ->
{
this.modifiers = modifiers;

switch (action)
{
case GLFW.GLFW_PRESS:
this.currentKeysDown.add(key);
break;
case GLFW.GLFW_RELEASE:
this.currentKeysDown.remove(key);
break;
}
};
// …
@ThreadOpenGL
void eventProcess(final Node3D pickedNode)
{
// Le traitement du clavier est mis avant celui de la souris afin de pouvoir enrichir les modifiers
this.updateKeyboardEvents();
this.updateMouseEvents();
// …
}
// …
@ThreadOpenGL
private void updateKeyboardEvents()
{
// Si il y a au moins une touche du clavier actuellement enfoncée,
// on alerte les listeners
final int size = this.currentKeysDown.size();

if (size > 0)
{
final int[] keysDown = new int[size];
int         index    = 0;

for (final Integer integer : this.currentKeysDown)
{
keysDown[index] = integer;

// Sur certains système/clavier, le number lock et caps lock ne sont pas capturés en tant que modifier.
// On ajoute donc l'information ici.
switch (integer)
{
case GLFW.GLFW_KEY_NUM_LOCK:
this.modifiers |= GLFW.GLFW_MOD_NUM_LOCK;
break;
case GLFW.GLFW_KEY_CAPS_LOCK:
this.modifiers |= GLFW.GLFW_MOD_CAPS_LOCK;
break;
}

index++;
}

synchronized (this.keyboardEventListeners)
{
for (final KeyboardEventListener keyboardEventListener : this.keyboardEventListeners)
{
// Les écouteurs sont appelés dans un thread à part afin que l'exécution de leur code ne ralentisse pas le thread OpenGL
ThreadManager.execute(keysDown, keyboardEventListener::currentKeyDown);
}
}
}
}
// …
}

II-B-4 - Les événements de scroll

Le scroll s’écoute via GLFW.glfwSetScrollCallback(long window, GLFWScrollCallbackI callback)

EventManager

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

// …
import org.lwjgl.glfw.GLFWScrollCallbackI;
// …

/**
* Manager des évènements utilisateur. Se brancher dessus afin de pouvoir les traiter
*/
public class EventManager
{
// …
private       double           scrollX                   = 0.0;
private       double           scrollY                   = 0.0;
// …
/**
* Écoute les événements du scroll molette ou glisser sur un touch pad
*/
final GLFWScrollCallbackI scrollCallback =
(long window, double xOffset, double yOffset) ->
{
this.scrollX = xOffset;
this.scrollY = yOffset;
};
// …
}

Window3D

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

// …
import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;
import org.lwjgl.glfw.GLFW;
// …

public class Window3D
{
// …
@ThreadOpenGL
private void startOpenGLThread()
{
// …
// Écoute les événements claviers
GLFW.glfwSetKeyCallback(this.window, EventManager.EVENT_MANAGER.keyCallBack);
// Écoute les entrées/sortie de la souris de la fenêtre
GLFW.glfwSetCursorEnterCallback(this.window, EventManager.EVENT_MANAGER.cursorEnterCallback);
// Écoute les événements boutons de souris
GLFW.glfwSetMouseButtonCallback(this.window, EventManager.EVENT_MANAGER.mouseButtonCallback);
// Écoute les changements de position souris
GLFW.glfwSetCursorPosCallback(this.window, EventManager.EVENT_MANAGER.cursorPosCallback);
// Écoute les événements de scrolling, pour la souris c'est la molette, le touch pad un glisser du doigt
GLFW.glfwSetScrollCallback(this.window, EventManager.EVENT_MANAGER.scrollCallback);
// …
}
// …
}

GLFW ne nous indique pas quand le scroll est fini, nous devrons le remettre à zéro nous même une fois que l’événement sera reporté

II-B-5 - Gestion des événements

Maintenant nous avons toutes les informations nécessaires afin de construire notre événement souris.

Juste avant, nous allons construire un objet pour faciliter la lecture des modificateurs

Modifiers

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

3. import org.lwjgl.glfw.GLFW;

5. /**

6. * Ensemble des modifiers actuellement activés

7. */

8. public final class Modifiers

9. {

10. public final boolean numberLockDown;

11. public final boolean capsLockDown;

12. public final boolean superDown;

13. public final boolean altDown;

14. public final boolean controlDown;

15. public final boolean shiftDown;

17. public Modifiers(int modifiers)

18. {

19. this.numberLockDown = (modifiers & GLFW.GLFW_MOD_NUM_LOCK) != 0;

20. this.capsLockDown   = (modifiers & GLFW.GLFW_MOD_CAPS_LOCK) != 0;

21. this.superDown      = (modifiers & GLFW.GLFW_MOD_SUPER) != 0;

22. this.altDown        = (modifiers & GLFW.GLFW_MOD_ALT) != 0;

23. this.controlDown    = (modifiers & GLFW.GLFW_MOD_CONTROL) != 0;

24. this.shiftDown      = (modifiers & GLFW.GLFW_MOD_SHIFT) != 0;

25. }

27. @Override

28. public String toString()

29. {

30. return "NumberLock=" + this.numberLockDown +

31. " CapsLock=" + this.capsLockDown +

32. " Super=" + this.superDown +

33. " Alt=" + this.altDown +

34. " Control=" + this.controlDown +

35. " Shift=" + this.shiftDown;

36. }

37. }

Maintenant créons un objet représentant notre événement souris

MouseStatusEvent

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

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

4. import java.util.Objects;

6. /**

7. * Décrit l'état actuel de la souris

8. */

9. public class MouseStatusEvent

10. {

11. /**

12. * Abscisse de la position de la souris sur la fenêtre

13. */

14. public final double x;

15. /**

16. * Ordonnée de la position de la souris sur la fenêtre

17. */

18. public final double y;

20. /**

21. * Pas du scroll en X horizontal

22. */

23. public final double scrollX;

24. /**

25. * Pas du scroll en Y vertical

26. */

27. public final double scrollY;

29. /**

30. * Indique si le bouton gauche est enfoncé par l'utilisateur.

31. */

32. public final boolean buttonLeftDown;

33. /**

34. * Indique si le bouton du milieu est enfoncé par l'utilisateur.

35. */

36. public final boolean buttonMiddleDown;

37. /**

38. * Indique si le bouton droit est enfoncé par l'utilisateur.

39. */

40. public final boolean buttonRightDown;

42. /**

43. * Ensemble de modifiers actifs

44. */

45. @NonNull

46. public final Modifiers modifiers;

48. public MouseStatusEvent(double x, double y,

49. double scrollX, double scrollY,

50. boolean buttonLeftDown,

51. boolean buttonMiddleDown,

52. boolean buttonRightDown,

53. @NonNull final Modifiers modifiers)

54. {

55. this.x                = x;

56. this.y                = y;

57. this.scrollX          = scrollX;

58. this.scrollY          = scrollY;

59. this.buttonLeftDown   = buttonLeftDown;

60. this.buttonMiddleDown = buttonMiddleDown;

61. this.buttonRightDown  = buttonRightDown;

62. this.modifiers        = Objects.requireNonNull(modifiers, "modifiers must not be null");

63. }

64. }

Puis l’écouteur lui-même

MouseStatusListener

1. /**

2. * Écouteur du status courant de la souris

3. */

4. @FunctionalInterface

5. public interface MouseStatusListener

6. {

7. /**

8. * Appelé régulièrement afin de reporter l'état courant de la souris

9. */

10. public void mouseStatus(@NonNull MouseStatusEvent mouseStatusEvent);

11. }

Il ne nous reste plus qu’a pouvoir s’enregistrer et traiter les événements souris

EventManager

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

// …
import fr.developez.tutorial.java.dimension3.mouse.Modifiers;
import fr.developez.tutorial.java.dimension3.mouse.MouseInsideWindowObserver;
import fr.developez.tutorial.java.dimension3.mouse.MouseStatusEvent;
import fr.developez.tutorial.java.dimension3.mouse.MouseStatusListener;
import fr.developez.tutorial.java.dimension3.thread.ThreadManager;
import fr.developez.tutorial.java.dimension3.tool.NonNull;
import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
// …

/**
* Manager des évènements utilisateur. Se brancher dessus afin de pouvoir les traiter
*/
public class EventManager
{
// …
private       boolean          mouseLeftButtonDown       = false;
private       boolean          mouseMiddleButtonDown     = false;
private       boolean          mouseRightButtonDown      = false;
private       double           mouseX                    = 0.0;
private       double           mouseY                    = 0.0;
private       double           scrollX                   = 0.0;
private       double           scrollY                   = 0.0;
private       int              modifiers                 = 0;
private       boolean          previousMouseInsideWindow = false;
private       boolean          mouseInsideWindow         = false;
private final List<MouseInsideWindowObserver>  mouseInsideWindowObservers  = new ArrayList<>();
private final List<MouseStatusListener>        mouseStatusListeners = new ArrayList<>();
// …
/**
* Enregistre un écouteur des événements souris dans la fenêtre.<br/>
* Il sera appelé régulièrement pour donné l'état actuel de la souris.
*/
public void register(@NonNull MouseStatusListener mouseStatusListener)
{
Objects.requireNonNull(mouseStatusListener, "mouseStatusListener must not be null");

synchronized (this.mouseStatusListeners)
{
if (!this.mouseStatusListeners.contains(mouseStatusListener))
{
this.mouseStatusListeners.add(mouseStatusListener);
}
}
}

/**
* Oublie un écouteur des événements souris dans la fenêtre.<br/>
* Il ne sera plus appelé
*/
public void unregister(MouseStatusListener mouseStatusListener)
{
synchronized (this.mouseStatusListeners)
{
this.mouseStatusListeners.remove(mouseStatusListener);
}
}
// …
@ThreadOpenGL
void eventProcess()
{
// Le traitement du clavier est mis avant celui de la souris afin de pouvoir enrichir les modifiers
this.updateKeyboardEvents();
this.updateMouseEvents();
}
// …
@ThreadOpenGL
private void updateMouseEvents()
{
// On regarde si l'état entrée/sortie de al souris a changé
final boolean mouseEnterExitChanged = this.previousMouseInsideWindow != this.mouseInsideWindow;
this.previousMouseInsideWindow = this.mouseInsideWindow;

// Si l'état a changé, on prévient les observateurs
if (mouseEnterExitChanged)
{
synchronized (this.mouseInsideWindowObservers)
{
final boolean inside = this.mouseInsideWindow;

for (final MouseInsideWindowObserver mouseInsideWindowObserver : this.mouseInsideWindowObservers)
{
// Les observateurs sont appelés dans un thread à part afin que l'exécution de leur code ne ralentisse pas le thread OpenGL
ThreadManager.execute(inside, mouseInsideWindowObserver::mouseInsideWidowChanged);
}
}
}

// Si la souris est dans la fenêtre, on donne aux écouteurs l'état courant de la souris
if (this.mouseInsideWindow)
{
synchronized (this.mouseStatusListeners)
{
final MouseStatusEvent mouseStatusEvent =
new MouseStatusEvent(this.mouseX, this.mouseY,
this.scrollX, this.scrollY,
this.mouseLeftButtonDown,
this.mouseMiddleButtonDown,
this.mouseRightButtonDown,
new Modifiers(this.modifiers));

for (final MouseStatusListener mouseStatusListener : this.mouseStatusListeners)
{
// Les écouteurs sont appelés dans un thread à part afin que l'exécution de leur code ne ralentisse pas le thread OpenGL
ThreadManager.execute(mouseStatusEvent, mouseStatusListener::mouseStatus);
}
}
}

// Reinitialize le scroll, GLFW ne nous avertit pas si il est finit
this.scrollX = 0;
this.scrollY = 0;
}
// …
}

Pour illustrer ce que nous venons de voir, ajoutons des événements souris à notre exemple. Ici nous allons faire tourner l’objet sur lui-même si l’utilisateur appuie sur le bouton gauche de la souris et bouge la souris en même temps. Et faire un zoom aux mouvements molette

Main

package fr.developez.tutorial.java.dimension3;

// ...
import fr.developez.tutorial.java.dimension3.geometry.Box;
import fr.developez.tutorial.java.dimension3.mouse.MouseStatusEvent;
import fr.developez.tutorial.java.dimension3.render.Color4f;
import fr.developez.tutorial.java.dimension3.render.EventManager;
import fr.developez.tutorial.java.dimension3.render.SceneGraph;
import fr.developez.tutorial.java.dimension3.render.Window3D;
import fr.developez.tutorial.java.dimension3.tool.NonNull;
import org.lwjgl.glfw.GLFW;
// …

public class Main
{
private static final float    ROTATE_STEP       = 0.5f;
private static final float    TRANSLATE_STEP    = 0.01f;
private static final Box      box               = new Box("Boîte");
private static       boolean  wasButtonLeftDown = false;
private static       double   oldMouseX         = 0.0;
private static       double   oldMouseY         = 0.0;
private static final Window3D window3D          = new Window3D(800, 600, "Tutoriel 3D - Boîte qui tourne");

public static void main(String[] args)
{
Main.drawScene(Main.window3D.getSceneGraph());
}

private static void drawScene(@NonNull final SceneGraph sceneGraph)
{
sceneGraph.setBackground(Color4f.BLACK);

sceneGraph.root.addChild(Main.box);

Main.box.z      = -2f;
Main.box.angleX = 22.5f;
Main.box.scaleX = 2f;
Main.box.scaleZ = 0.5f;
Main.box.setColor(Color4f.LIGHT_GREEN);
EventManager.EVENT_MANAGER.register(Main::mouseInsideWidowChanged);
EventManager.EVENT_MANAGER.register(Main::mouseStatus);
EventManager.EVENT_MANAGER.register(Main::currentKeyDown);
}
// …
private static void mouseInsideWidowChanged(boolean insideWindow)
{
if (insideWindow)
{
System.out.println("*** INSIDE ***");
}
else
{
System.out.println("*** OUTSIDE ***");
}
}

private static void mouseStatus(@NonNull final MouseStatusEvent mouseStatusEvent)
{
if (mouseStatusEvent.buttonLeftDown && Main.wasButtonLeftDown)
{
Main.box.angleY += (mouseStatusEvent.x - Main.oldMouseX) * Main.ROTATE_STEP;
Main.box.angleX += (mouseStatusEvent.y - Main.oldMouseY) * Main.ROTATE_STEP;
}

Main.box.z += mouseStatusEvent.scrollY * Main.TRANSLATE_STEP;

Main.oldMouseX         = mouseStatusEvent.x;
Main.oldMouseY         = mouseStatusEvent.y;
Main.wasButtonLeftDown = mouseStatusEvent.buttonLeftDown;
}
// …
}

II-C - La manette de jeu

GLFW n’a pas d’écouteur à branché pour être avertis du changement de l’état des manettes branchées. Par contre il possède des méthodes permettant d’aller lire leur état actuel.

GLFW permet d’aller lire l’état de 16 manettes « en même temps ».

Afin de gérer l’état de chaque manette nous aurons un objet Joystick qui maintiendra à jour l’état d’une manette.

Comme les manettes sont d’un nombre limitéon peut les représenté par une énumération.

A chaque manette actuellement branchée, GLFW leur attribut un numéro de 0 à 15.

Il ne faut pas assumer de la logique d’attribution de ces numéros. La plupart du temps, la première qu’il voit à le numéro 0, puis la seconde 1, … Par contre si la première manette est débranchée, il peut très bien réattribué les numéros, et ce n’est pas forcément dans le même ordre que la fois d’avant. Par exemple la troisième peut se retrouver en premier.

GLFW peut lire le nom et l’identifiant de la manette. L’identifiant permet d’identifier le modèle, c’est-à-dire que deux manettes du même modèle auront le même identifiant. Cela, permet de pouvoir associer un composant d’interaction à une action.

On peut distinguer deux types de composant d’interaction : les boutons et les axes.

- Les boutons ont deux états : appuyé ou relâché

- Les axes retournent une valeur réelle entre -1.0 et 1.0 qui correspond à une pression ou un sens d’inclinaison. Par exemple si on penche plus ou moins un stickpad vers la droite ou vers la gauche. On peut se dire que le stick pad peut de penché aussi vers le haut et le bas, cela est vu comme un autre axe d’un point de vue GLFW. Ce qui fait que le même composant d’interaction peut être représenté par deux axes. L’un pour le sens horizontal, l’autre le vertical.

Ici nous allons faire se comporter les axes comme deux boutons. L’un représentant les valeurs négatives, l’autre les valeurs positives. Ceux représentant les valeurs négatives seront considérés comme appuyés si la valeur de l’axe est plus petite que -0.25. Et ceux qui représentent une valeur positive seront considérés comme appuyé si la valeur de l’axe est plus grande que 0.25.

A chaque axe ou bouton est attribué un numéro. La position des boutons et axes dépend du modèle de la manette. D’où l’intérêt de pouvoir identifier le modèle de la manette. On peut regarder quel numéro est associé pour chaque axe et chaque bouton. Leur associé une fonctionnalité, par exemple le bouton numéro 4 sur cette manette est la flèche du haut.

Créons un type de composant d’interaction :

JoystickInputType

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

3. /**

4. * Type de button/axe

5. */

6. public enum JoystickInputType

7. {

8. /**

9. * Axe allant dans le sens positif

10. */

11. AXIS_POSITIVE,

12. /**

13. * Axe allant dans le sens négatif

14. */

15. AXIS_NEGATIVE,

16. /**

17. * Bouton

18. */

19. BUTTON,

20. /**

21. * Indéfini

22. */

23. NONE

24. }

Le case NONE est pratique parfois pour représenter le fait qu’il n’y a pas d’action.

Nous allons représenter chaque bouton et chaque axe partie négative et positive par un code.

JoystickCode

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

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

5. /**

6. * Code d'un élément du joystick : bouton, axe allant de le sens positif, axe allant dans le sens négatif

7. */

8. public enum JoystickCode

9. {

10. /**

11. * Axe 1 allant dans le sens positif

12. */

13. AXIS_1_POSITIVE(0, JoystickInputType.AXIS_POSITIVE),

14. /**

15. * Axe 1 allant dans le sens négatif

16. */

17. AXIS_1_NEGATIVE(0, JoystickInputType.AXIS_NEGATIVE),

18. /**

19. * Axe 2 allant dans le sens positif

20. */

21. AXIS_2_POSITIVE(1, JoystickInputType.AXIS_POSITIVE),

22. /**

23. * Axe 2 allant dans le sens négatif

24. */

25. AXIS_2_NEGATIVE(1, JoystickInputType.AXIS_NEGATIVE),

26. /**

27. * Axe 3 allant dans le sens positif

28. */

29. AXIS_3_POSITIVE(2, JoystickInputType.AXIS_POSITIVE),

30. /**

31. * Axe 3 allant dans le sens négatif

32. */

33. AXIS_3_NEGATIVE(2, JoystickInputType.AXIS_NEGATIVE),

34. /**

35. * Axe 4 allant dans le sens positif

36. */

37. AXIS_4_POSITIVE(3, JoystickInputType.AXIS_POSITIVE),

38. /**

39. * Axe 4 allant dans le sens négatif

40. */

41. AXIS_4_NEGATIVE(3, JoystickInputType.AXIS_NEGATIVE),

42. /**

43. * Axe 5 allant dans le sens positif

44. */

45. AXIS_5_POSITIVE(4, JoystickInputType.AXIS_POSITIVE),

46. /**

47. * Axe 5 allant dans le sens négatif

48. */

49. AXIS_5_NEGATIVE(4, JoystickInputType.AXIS_NEGATIVE),

50. /**

51. * Axe 6 allant dans le sens positif

52. */

53. AXIS_6_POSITIVE(5, JoystickInputType.AXIS_POSITIVE),

54. /**

55. * Axe 6 allant dans le sens négatif

56. */

57. AXIS_6_NEGATIVE(5, JoystickInputType.AXIS_NEGATIVE),

58. /**

59. * Axe 7 allant dans le sens positif

60. */

61. AXIS_7_POSITIVE(6, JoystickInputType.AXIS_POSITIVE),

62. /**

63. * Axe 7 allant dans le sens négatif

64. */

65. AXIS_7_NEGATIVE(6, JoystickInputType.AXIS_NEGATIVE),

66. /**

67. * Axe 8 allant dans le sens positif

68. */

69. AXIS_8_POSITIVE(7, JoystickInputType.AXIS_POSITIVE),

70. /**

71. * Axe 8 allant dans le sens négatif

72. */

73. AXIS_8_NEGATIVE(7, JoystickInputType.AXIS_NEGATIVE),

74. /**

75. * Joystick bouton 1

76. */

77. BUTTON_1(0, JoystickInputType.BUTTON),

78. /**

79. * Joystick bouton 2

80. */

81. BUTTON_2(1, JoystickInputType.BUTTON),

82. /**

83. * Joystick bouton 3

84. */

85. BUTTON_3(2, JoystickInputType.BUTTON),

86. /**

87. * Joystick bouton 4

88. */

89. BUTTON_4(3, JoystickInputType.BUTTON),

90. /**

91. * Joystick bouton 5

92. */

93. BUTTON_5(4, JoystickInputType.BUTTON),

94. /**

95. * Joystick bouton 6

96. */

97. BUTTON_6(5, JoystickInputType.BUTTON),

98. /**

99. * Joystick bouton 7

100. */

101. BUTTON_7(6, JoystickInputType.BUTTON),

102. /**

103. * Joystick bouton 8

104. */

105. BUTTON_8(7, JoystickInputType.BUTTON),

106. /**

107. * Joystick bouton 9

108. */

109. BUTTON_9(8, JoystickInputType.BUTTON),

110. /**

111. * Joystick bouton 10

112. */

113. BUTTON_10(9, JoystickInputType.BUTTON),

114. /**

115. * Joystick bouton 11

116. */

117. BUTTON_11(10, JoystickInputType.BUTTON),

118. /**

119. * Joystick bouton 12

120. */

121. BUTTON_12(11, JoystickInputType.BUTTON),

122. /**

123. * Joystick bouton 13

124. */

125. BUTTON_13(12, JoystickInputType.BUTTON),

126. /**

127. * Joystick bouton 14

128. */

129. BUTTON_14(13, JoystickInputType.BUTTON),

130. /**

131. * Joystick bouton 15

132. */

133. BUTTON_15(14, JoystickInputType.BUTTON),

134. /**

135. * Joystick bouton 16

136. */

137. BUTTON_16(15, JoystickInputType.BUTTON),

138. /**

139. * Pas d'événement joystick

140. */

141. NONE(-1, JoystickInputType.NONE);

143. /**

144. * Numéro d'axe maximum que l'on gère pour le joystick

145. */

146. public static final int MAX_AXIS_INDEX   = 7;

147. /**

148. * Numéro de bouton maximum que l'on gère pour le joystick

149. */

150. public static final int MAX_BUTTON_INDEX = 15;

152. @NonNull

153. private static JoystickCode obtain(int elementNumber, JoystickInputType joystickInputType)

154. {

155. for (final JoystickCode joystickCode : JoystickCode.values())

156. {

157. if (joystickCode.number == elementNumber && joystickCode.joystickInputType == joystickInputType)

158. {

159. return joystickCode;

160. }

161. }

163. return JoystickCode.NONE;

164. }

166. /**

167. * Cherche un axe par son numéro et son sens d'appui

168. *

169. * @param axisNumber Numéro de l'axe

170. * @param positive   Direction de l'axe

171. * @return Axe trouvé ou {@link JoystickCode#NONE} si l'axe n'existe pas

172. */

173. @NonNull

174. public static JoystickCode obtainAxis(int axisNumber, boolean positive)

175. {

176. if (positive)

177. {

178. return JoystickCode.obtain(axisNumber, JoystickInputType.AXIS_POSITIVE);

179. }

181. return JoystickCode.obtain(axisNumber, JoystickInputType.AXIS_NEGATIVE);

182. }

184. /**

185. * Cherche un bouton par son numéro

186. *

187. * @param buttonNumber Numéro du bouton

188. * @return Bouton trouvé ou {@link JoystickCode#NONE} si le bouton n'existe pas

189. */

190. @NonNull

191. public static JoystickCode obtainButton(int buttonNumber)

192. {

193. return JoystickCode.obtain(buttonNumber, JoystickInputType.BUTTON);

194. }

196. /**

197. * Numéro du bouton/axe

198. */

199. public final int               number;

200. /**

201. * Type de bouton/axe

202. */

203. public final JoystickInputType joystickInputType;

205. JoystickCode(int number, JoystickInputType joystickInputType)

206. {

207. this.number            = number;

208. this.joystickInputType = joystickInputType;

209. }

211. @Override

212. public String toString()

213. {

214. return this.name() + " (" + this.number + ") " + this.joystickInputType;

215. }

216. }

Pour chaque code on aura deux états : appuyé ou relâchée

JoystickStatus

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

3. /**

4. * État d'un bouton/direction d'axe

5. */

6. public enum JoystickStatus

7. {

8. /**

9. * Pressée

10. */

11. PRESSED,

12. /**

13. * Relâchée

14. */

15. RELEASED;

16. }

On peut désormais commencer à construire notre objet représentant une manette

Joystick

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

3. import fr.developez.tutorial.java.dimension3.joystick.JoystickCode;

4. import fr.developez.tutorial.java.dimension3.joystick.JoystickStatus;

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

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

7. import java.util.HashMap;

8. import java.util.ArrayList;

9. import java.util.List;

10. import java.util.Map;

11. import org.lwjgl.glfw.GLFW;

13. /**

14. * Représente un Joystick

15. */

16. public enum Joystick

17. {

18. JOYSTICK_1(GLFW.GLFW_JOYSTICK_1),

19. JOYSTICK_2(GLFW.GLFW_JOYSTICK_2),

20. JOYSTICK_3(GLFW.GLFW_JOYSTICK_3),

21. JOYSTICK_4(GLFW.GLFW_JOYSTICK_4),

22. JOYSTICK_5(GLFW.GLFW_JOYSTICK_5),

23. JOYSTICK_6(GLFW.GLFW_JOYSTICK_6),

24. JOYSTICK_7(GLFW.GLFW_JOYSTICK_7),

25. JOYSTICK_8(GLFW.GLFW_JOYSTICK_8),

26. JOYSTICK_9(GLFW.GLFW_JOYSTICK_9),

27. JOYSTICK_10(GLFW.GLFW_JOYSTICK_10),

28. JOYSTICK_11(GLFW.GLFW_JOYSTICK_11),

29. JOYSTICK_12(GLFW.GLFW_JOYSTICK_12),

30. JOYSTICK_13(GLFW.GLFW_JOYSTICK_13),

31. JOYSTICK_14(GLFW.GLFW_JOYSTICK_14),

32. JOYSTICK_15(GLFW.GLFW_JOYSTICK_15),

33. JOYSTICK_16(GLFW.GLFW_JOYSTICK_16);

35. private final int                               joystickID;

36. private       boolean                           connected               = false;

37. private       boolean                           connectionStatusChanged = false;

38. /**

39. * Maintient à jour l'état des boutons et axes du joystick

40. */

41. private final Map<JoystickCode, JoystickStatus> joystickElementsStatus  = new HashMap<>();

42. private       String                            joystickName            = "";

43. private       String                            joystickGUID            = "";

45. Joystick(int joystickID)

46. {

47. this.joystickID = joystickID;

48. this.resetStatus();

49. }

51. /**

52. * Nom du Joystick. <br/>

53. * Si le joystick n'est pas branché, son nom sera vide.

54. * En effet on ne peut lire son nom que si il est branché.<br/>

55. * Si malgré le fait qu'il soit branché, il est toujours vide, c'est que GLFW n'arrive pas à le lire.

56. */

57. public String joystickName()

58. {

59. return this.joystickName;

60. }

62. /**

63. * GUID du joystick<br/>

64. * Si le joystick n'est pas branché, son GUID sera vide.

65. * En effet on ne peut lire son GUID que si il est branché.<br/>

66. * Si malgré le fait qu'il soit branché, il est toujours vide, c'est que GLFW n'arrive pas à le lire.<br/>

67. * <br/>

68. * Le GUID représente un model de joystick, mais ne permet pas de distingué deux joysticks du même model.<br/>

69. * Cette valeur est tout de même utile pour associé des correspondances numéros de bouton/axe à une action sur le joystick<br/>

70. * En effet pour un même model, la flèche du haut aura toujours le même numéro de bouton par exemple.

71. */

72. public String joystickGUID()

73. {

74. return this.joystickGUID;

75. }

78. /**

79. * Indique si le joystick est actuellement branché

80. */

81. public boolean connected()

82. {

83. return this.connected;

84. }

86. /**

87. * Liste des boutons/axes du joystick actuellement appuyés par l'utilisateur

88. */

89. @NonNull

90. JoystickCode[] currentPressedElements()

91. {

92. final List<JoystickCode> currentPressedElements = new ArrayList<>();

94. for (final Map.Entry<JoystickCode, JoystickStatus> entry : this.joystickElementsStatus.entrySet())

95. {

96. if (entry.getValue() != JoystickStatus.RELEASED)

97. {

98. currentPressedElements.add(entry.getKey());

99. }

100. }

102. return currentPressedElements.toArray(new JoystickCode[0]);

103. }

105. /**

106. * Indique si le status de connection viens de changer

107. */

108. boolean connectionStatusChanged()

109. {

110. return this.connectionStatusChanged;

111. }

113. /**

114. * Met à jour l'état des boutons/axes du joystick

115. */

116. @ThreadOpenGL

117. void updateInformation()

118. {

119. // TODO Lire l’état courant de la manette et le mettre à jour

120. }

122. private void resetStatus()

123. {

124. this.joystickName = "";

125. this.joystickGUID = "";

127. for (final JoystickCode joystickCode : JoystickCode.values())

128. {

129. this.joystickElementsStatus.put(joystickCode, JoystickStatus.RELEASED);

130. }

131. }

133. @Override

134. public String toString()

135. {

136. return this.name() + " : " + this.joystickName + " : " + this.joystickGUID;

137. }

138. }

Maintenant lisons l’état de la manette à l’aide de GLFW. La lecture de l’état doit se faire dans le thread OpenGL.

Tout d’abord, regardons si la manette est branchée ou non et mettons à jour l’état de la connexion.

Ceci ce fait via GLFW.glfwJoystickPresent(int joystickID)

Joystick

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

// …
import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;
import org.lwjgl.glfw.GLFW;
// …
/**
* Représente un Joystick
*/
public enum Joystick
{
// …
/**
* Met à jour l'état des boutons/axes du joystick
*/
@ThreadOpenGL
void updateInformation()
{
// Le joystick est-il branché ?
if (GLFW.glfwJoystickPresent(this.joystickID))
{
// On essaie de le lire de nom du joystick
final String name = GLFW.glfwGetJoystickName(this.joystickID);

if (name != null)
{
this.joystickName = name;
}

// On essaie de lire l'identifiant du joystick
final String guid = GLFW.glfwGetJoystickGUID(this.joystickID);

if (guid != null)
{
this.joystickGUID = guid;
}

// On regarde si le joystick vient juste d'être branché
if (!this.connected)
{
this.connected               = true;
this.connectionStatusChanged = true;
}
else
{
this.connectionStatusChanged = false;
}

// TODO Mettre à jour les axes et le boutons

}
else
{
// Le joystick n'est pas branché
// On regarde si cela vient juste d'arriver
if (this.connected)
{
this.connected               = false;
this.connectionStatusChanged = true;
this.resetStatus();
}
else
{
this.connectionStatusChanged = false;
}
}
}
// …
}

On met au passage à jour le nom et l’identifiant du modèle.

Maintenant lisons les « valeurs de pressions » des axes.

Pour ce faire, on utilisera GLFW.glfwGetJoystickAxes(int joystickID)

Cette méthode peut retourner null si la manette n’a pas d’axe ou en cas de problème de lecture. Autrement elle retourne un java.nio.FloatBuffer qui contient les valeurs de pressions de chaque axe.

I

Ne pas supposer que les valeurs des axes soient écritent depuis le début du buffer. Néanmoins la position sur laquelle se trouve le buffer est la bonne pour lire les axes.

Une fois les valeurs lues, il faudra restaurer la position du buffer pour éviter tout interférences avec GLFW.

Si vous ne connaissez pas les buffers de java.nio ou que vous avez quelques difficultés à comprendre cette remarque, referez vous à l’annexe en fin du tutoriel qui donne plus d’explications.

Le nombre de valeurs que nous lirons sera le minimum entre le nombre d’axes que nous gérons et le nombre de valeurs lisibles dans le buffer.

Grâce ces valeurs lues, nous pourrons mettre à jour l’état des axes.

Joystick

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

// ...
import fr.developez.tutorial.java.dimension3.joystick.JoystickCode;
import fr.developez.tutorial.java.dimension3.joystick.JoystickStatus;
import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;
import java.nio.FloatBuffer;
import org.lwjgl.glfw.GLFW;
// …

/**
* Représente un Joystick
*/
public enum Joystick
{
// …
/**
* Met à jour l'état des boutons/axes du joystick
*/
@ThreadOpenGL
void updateInformation()
{
// Le joystick est-il branché ?
if (GLFW.glfwJoystickPresent(this.joystickID))
{
// …
// On récupère les valeurs des pressions appliquées aux axes
final FloatBuffer axes = GLFW.glfwGetJoystickAxes(this.joystickID);

if (axes != null)
{
// Lecture des pressions
final int     position  = axes.position();
final float[] axesValue = new float[axes.limit() - position];
axes.get(axesValue);
axes.position(position);

// Pour chaque pression récupérée on met à jour l'état de l'axe correspondant
final int max = Math.min(axesValue.length - 1, JoystickCode.MAX_AXIS_INDEX);

for (int axeNumber = 0; axeNumber <= max; axeNumber++)
{
if (axesValue[axeNumber] < -0.25f)
{
this.joystickElementsStatus.put(JoystickCode.obtainAxis(axeNumber, true),
JoystickStatus.RELEASED);
this.joystickElementsStatus.put(JoystickCode.obtainAxis(axeNumber, false),
JoystickStatus.PRESSED);
}
else if (axesValue[axeNumber] > 0.25f)
{
this.joystickElementsStatus.put(JoystickCode.obtainAxis(axeNumber, false),
JoystickStatus.RELEASED);
this.joystickElementsStatus.put(JoystickCode.obtainAxis(axeNumber, true),
JoystickStatus.PRESSED);
}
else
{
this.joystickElementsStatus.put(JoystickCode.obtainAxis(axeNumber, true),
JoystickStatus.RELEASED);
this.joystickElementsStatus.put(JoystickCode.obtainAxis(axeNumber, false),
JoystickStatus.RELEASED);
}
}
}
// …
}
// …
}
// …
}

Pour les boutons l’idée est la même, sauf que cette fois on récupère un ByteBuffer où chaque byte est l’état appuyé ou relâché d’un bouton

Joystick

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

// ...
import fr.developez.tutorial.java.dimension3.joystick.JoystickCode;
import fr.developez.tutorial.java.dimension3.joystick.JoystickStatus;
import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;
import java.nio.ByteBuffer;
import org.lwjgl.glfw.GLFW;
// …

/**
* Représente un Joystick
*/
public enum Joystick
{
// …
/**
* Met à jour l'état des boutons/axes du joystick
*/
@ThreadOpenGL
void updateInformation()
{
// Le joystick est-il branché ?
if (GLFW.glfwJoystickPresent(this.joystickID))
{
// …
// On récupère l'état des boutons
final ByteBuffer buttons = GLFW.glfwGetJoystickButtons(this.joystickID);

if (buttons != null)
{
// Lecture des états
final int    position      = buttons.position();
final byte[] buttonsStatus = new byte[buttons.limit() - position];
buttons.get(buttonsStatus);
buttons.position(position);

// On met à jour l'état des boutons
final int max = Math.min(buttonsStatus.length - 1, JoystickCode.MAX_BUTTON_INDEX);

for (int buttonNumber = 0; buttonNumber <= max; buttonNumber++)
{
if (buttonsStatus[buttonNumber] == GLFW.GLFW_PRESS)
{
this.joystickElementsStatus.put(JoystickCode.obtainButton(buttonNumber),
JoystickStatus.PRESSED);
}
else
{
this.joystickElementsStatus.put(JoystickCode.obtainButton(buttonNumber),
JoystickStatus.RELEASED);
}
}
}
// …
}
// …
}
// …
}

Désormais, notre manette peut se mettre à jour, il faut branché ceci dans le gestionnaire d’événements.

EventManager

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

// …
import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;
// …

/**
* Manager des évènements utilisateur. Se brancher dessus afin de pouvoir les traiter
*/
public class EventManager
{
// …
@ThreadOpenGL
void eventProcess()
{
// Le traitement du clavier est mis avant celui de la souris afin de pouvoir enrichir les modifiers
this.updateKeyboardEvents();
this.updateMouseEvents();
this.updateJoystickEvents();
}
// …
@ThreadOpenGL
private void updateJoystickEvents()
{
// pour chaque joystick possible
for (final Joystick joystick : Joystick.values())
{
// On met à jour les états du joystick
joystick.updateInformation();

// TODO Alerter les observateurs et les écouteurs
}
}
// …
}

Nous allons couper l’information des événements joystick en deux. Une partie dédiée à la connexion/déconnexion de la manette. Et l’autre aux états des boutons et/ou axes.

JoystickConnectionEvent

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

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

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

5. import java.util.Objects;

7. /**

8. * Évènement de changement d'état de connexion d'un joystick

9. */

10. public class JoystickConnectionEvent

11. {

12. /**

13. * Joystick connecté/déconnecté

14. */

15. @NonNull

16. public final Joystick joystick;

17. /**

18. * Nouvel état de connection

19. */

20. public final boolean  connected;

22. public JoystickConnectionEvent(@NonNull final Joystick joystick, boolean connected)

23. {

24. this.joystick  = Objects.requireNonNull(joystick, "joystick must not be null");

25. this.connected = connected;

26. }

27. }

JoystickConnectionObserver

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

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

5. /**

6. * Observateur de l'état de connection des joystick.<br/>

7. * Il est appelée à chaque fois qu'un joystick est branché ou débranché.<br/>

8. * Il est également appelé au premier enregistrement pour avoir l'état courant des divers joystick possibles.<br/>

9. * Voir {@link fr.developez.tutorial.java.dimension3.render.EventManager#register(JoystickConnectionObserver)}

10. */

11. @FunctionalInterface

12. public interface JoystickConnectionObserver

13. {

14. public void joystickConnectionChanged(@NonNull JoystickConnectionEvent joystickConnectionEvent);

15. }

JoystickEvent

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

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

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

6. /**

7. * Évènement joystick

8. */

9. public final class JoystickEvent

10. {

11. /**

12. * Joystick où à lieu l'événement

13. */

14. @NonNull

15. public final Joystick       joystick;

16. /**

17. * Liste des boutons/axes actuellement appuyer

18. */

19. @NonNull

20. public final JoystickCode[] currentPressed;

22. public JoystickEvent(Joystick joystick, JoystickCode[] currentPressed)

23. {

24. this.joystick       = joystick;

25. this.currentPressed = currentPressed;

26. }

27. }

JoystickEventListener

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

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

5. /**

6. * Écouteur des événements joystick

7. */

8. @FunctionalInterface

9. public interface JoystickEventListener

10. {

11. /**

12. * Appelé régulièrement pour informé de l'état courant des boutons/axes du joystick

13. *

14. * @param joystick       Joystick auquel les événements se réfèrent

15. * @param currentPressed Liste des Axe/boutons actuellement appuyés par l'utilisateur

16. */

17. public void joystickEventHappen(@NonNull JoystickEvent joystickEvent);

18. }

Il ne nous reste plus qu’à brancher tout ça dans le manager d’événements

EventManager

package fr.developez.tutorial.java.dimension3.render;
// …
import fr.developez.tutorial.java.dimension3.joystick.JoystickCode;
import fr.developez.tutorial.java.dimension3.joystick.JoystickConnectionEvent;
import fr.developez.tutorial.java.dimension3.joystick.JoystickConnectionObserver;
import fr.developez.tutorial.java.dimension3.joystick.JoystickEvent;
import fr.developez.tutorial.java.dimension3.joystick.JoystickEventListener;
import fr.developez.tutorial.java.dimension3.thread.ThreadManager;
import fr.developez.tutorial.java.dimension3.tool.NonNull;
import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
// …

/**
* Manager des évènements utilisateur. Se brancher dessus afin de pouvoir les traiter
*/
public class EventManager
{
// …
private final List<JoystickConnectionObserver> joystickConnectionObservers = new ArrayList<>();
private final List<JoystickEventListener> joystickEventListeners      = new ArrayList<>();
// …
/**
* Enregistre un observateur d'événements connexion/déconnexion d'un joystick.<br/>
* Il est appelé immédiatement avec l'état courant de chaque joystick possible.<br/>
* Il sera appelé à chaque changement d'état de l'un des joysticks possible
*/
public void register(@NonNull JoystickConnectionObserver joystickConnectionObserver)
{
Objects.requireNonNull(joystickConnectionObserver, "joystickConnectionObserver must not be null");

for (final Joystick joystick : Joystick.values())
{
joystickConnectionObserver.joystickConnectionChanged(
new JoystickConnectionEvent(joystick, joystick.connected()));
}

synchronized (this.joystickConnectionObservers)
{
if (!this.joystickConnectionObservers.contains(joystickConnectionObserver))
{
this.joystickConnectionObservers.add(joystickConnectionObserver);
}
}
}

/**
* Oublie un observateur d'événements connexion/déconnexion d'un joystick.<br/>
* Il ne sera plus appelé
*/
public void unregister(JoystickConnectionObserver joystickConnectionObserver)
{
synchronized (this.joystickConnectionObservers)
{
this.joystickConnectionObservers.remove(joystickConnectionObserver);
}
}

/**
* Enregistre un écouter d'événements joystick.<br/>
* Cet écouteur sera appelé régulièrement afin de reporté l'état actuels des boutons/axes des joysticks connectés
*/
public void register(@NonNull JoystickEventListener joystickEventListener)
{
Objects.requireNonNull(joystickEventListener, "joystickEventListener must not be null");

synchronized (this.joystickEventListeners)
{
if (!this.joystickEventListeners.contains(joystickEventListener))
{
this.joystickEventListeners.add(joystickEventListener);
}
}
}

/**
* Oublie n écouter d'événements joystick.<br/>
* Il ne sera plus appelé
*/
public void unregister(JoystickEventListener joystickEventListener)
{
synchronized (this.joystickEventListeners)
{
this.joystickEventListeners.remove(joystickEventListener);
}
}
// …
@ThreadOpenGL
void eventProcess()
{
// Le traitement du clavier est mis avant celui de la souris afin de pouvoir enrichir les modifiers
this.updateKeyboardEvents();
this.updateMouseEvents();
this.updateJoystickEvents();
}
// …
@ThreadOpenGL
private void updateJoystickEvents()
{
// pour chaque joystick possible
for (final Joystick joystick : Joystick.values())
{
// On met à jour les états du joystick
joystick.updateInformation();

// Si la connection à changée, on avertit les observateurs
if (joystick.connectionStatusChanged())
{
final JoystickConnectionEvent joystickConnectionEvent = new JoystickConnectionEvent(joystick,
joystick.connected());

synchronized (this.joystickConnectionObservers)
{
for (final JoystickConnectionObserver joystickConnectionObserver : this.joystickConnectionObservers)
{
// Les observateurs sont appelés dans un thread à part afin que l'exécution de leur code ne ralentisse pas le thread OpenGL
ThreadManager.execute(joystickConnectionEvent,
joystickConnectionObserver::joystickConnectionChanged);
}
}
}

// Si il y a au moins un bouton/axe d'appuyé, on averti les écouteurs
final JoystickCode[] currentPressed = joystick.currentPressedElements();

if (currentPressed.length > 0)
{
final JoystickEvent joystickEvent = new JoystickEvent(joystick, currentPressed);

synchronized (this.joystickEventListeners)
{
for (final JoystickEventListener joystickEventListener : this.joystickEventListeners)
{
// Les observateurs sont appelés dans un thread à part afin que l'exécution de leur code ne ralentisse pas le thread OpenGL
ThreadManager.execute(joystickEvent,
joystickEventListener::joystickEventHappen);
}
}
}
}
}
// …
}

Pour illustrer ceci, ajoutons une réaction à la manette à notre exemple.

Main

package fr.developez.tutorial.java.dimension3;

// …
import fr.developez.tutorial.java.dimension3.geometry.Box;
import fr.developez.tutorial.java.dimension3.joystick.JoystickCode;
import fr.developez.tutorial.java.dimension3.joystick.JoystickConnectionEvent;
import fr.developez.tutorial.java.dimension3.joystick.JoystickEvent;
// …

public class Main
{
// ...
private static final float    TRANSLATE_STEP    = 0.01f;
private static final Box      box               = new Box();
// …
private static void drawScene(@NonNull final SceneGraph sceneGraph)
{
sceneGraph.setBackground(Color4f.BLACK);

sceneGraph.root.addChild(Main.box);

Main.box.z      = -2f;
Main.box.angleX = 22.5f;
Main.box.scaleX = 2f;
Main.box.scaleZ = 0.5f;
Main.box.setColor(Color4f.LIGHT_GREEN);

EventManager.EVENT_MANAGER.register(Main::joystickConnectionChanged);
EventManager.EVENT_MANAGER.register(Main::joystickEventHappen);
EventManager.EVENT_MANAGER.register(Main::mouseInsideWidowChanged);
EventManager.EVENT_MANAGER.register(Main::mouseStatus);
EventManager.EVENT_MANAGER.register(Main::currentKeyDown);
}
// …
private static void joystickConnectionChanged(@NonNull JoystickConnectionEvent joystickConnectionEvent)
{
String header = "NOT CONNECTED : ";

if (joystickConnectionEvent.connected)
{
header = "CONNECTED : ";
}

System.out.print(header);
System.out.println(joystickConnectionEvent.joystick);
}

private static void joystickEventHappen(@NonNull JoystickEvent joystickEvent)
{
for (final JoystickCode joystickCode : joystickEvent.currentPressed)
{
switch (joystickCode)
{
case AXIS_1_NEGATIVE:
Main.box.x -= Main.TRANSLATE_STEP;
break;
case AXIS_1_POSITIVE:
Main.box.x += Main.TRANSLATE_STEP;
break;
case AXIS_2_NEGATIVE:
Main.box.y += Main.TRANSLATE_STEP;
break;
case AXIS_2_POSITIVE:
Main.box.y -= Main.TRANSLATE_STEP;
break;
}
}
}
// …
}

III - Capture d’un objet à une position dans l’écran

Le but ici est de savoir quel objet se trouve sous des coordonnées de l’écran.

Un des usages fréquents est d’utiliser les coordonnées de la souris pour savoir quel objet se trouve sous elle.

On peut voir le problème de manière géométrique. Puis l’utilisateur n’est pas coller à l’écran, une droite peut être tracée entre lui et le point de l’écran que l’on cherche. Le premier objet rencontré en suivant cette droite en s’éloignant de l’utilisateur est l’objet cherché. Le problème qui va se poser, est de comment calculer cette intersection :

- Il faut tenir compte de la position de l’objet selon ses propres coordonnées, rotation et échelles relativement à son parent, sui lui-même est placé selon son parent ...

- Pour les formes simple une tenu compte ci dessus, on va trouver une formule simple pour avoir si il y a intersection ou pas. Mais pour les formes complexes ça se gâte. Il faudrait tester pour chacun des polygones. Mais dans ce cas il faut les garder en mémoire et l’optimisation de scellé n’a plus de sens.

Tout ceci est assez complexe, va coûter cher en temps de calcul et abolit une optimisation. De plus il sera pas toujour possible d’être précis au pixel prêt.

Afin de résoudre ces différents problèmes nous allons passer par une astuce qu’on appelle le picking de couleur. Elle a pour avantages

- Coûté aussi cher quelque-soi la complexité des objets. Et permet de garder nos optimisations

- Rapide d’exécution et pas de mathématique compliqué

- Garantis une précision au pixel prêt

III-A - La technique de picking

Pour comprendre l’idée il faut s’avoir qu’OpenGL permet de capturer les pixels préparés pour l’affichage sur une zone plus ou moins grande. Si la zone est réduite à un pixel, ou capturera la couleur sous une coordonnée précise.

Donc en faisant ceci dans l’ordre :

1. On dessine chaque objet d’une couleur différente

2. On capture la couleur sous les coordonnées demandées

3. On dessine vraiment la scène

4. On échange les buffer

5. Ensuite avec la couleur capturée on déduit l’objet capturé puisque sa couleur était unique.

On a récupéré notre objet sans calcul complexe. On ne voit pas à l’écran le premier dessin des objets colorer, car il sera remplacé par le dessin de la vraie scène avant l’échange des buffers.

III-B - Mise en place du picking

III-B-1 - Attribution du nom et d’une couleur de picking aux nœuds

Ce que nous captureront ce sont soit des objets, soit des clones. Les nœuds n’ayant pas de représentation 3D, on ne les capturera pas.

Afin d ‘éviter de faire un caste pour cahger la couleur d’un objet, on va ajouter un objet intermédiaire entre le nœud et les objet ou les clones. Cet objet représentant les objets qui ont une représentation à l’écran.

La nouvelle hiérarchie sera

Afin de repérer plus facilement un nœud, nous allons ajouter lui ajouter un nom.

Comme nous voulons utiliser la technique de picking sur les nœuds, on leur attribuera une couleur de picking unique à leur création.

On va gérer la couleur de picking avec l’utilitaire suivant :

ColorPickingManager

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

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

4. import java.util.concurrent.atomic.AtomicInteger;

6. /**

7. * Manageur de couleurs dédiées pour la capture d'objet.<br/>

8. * Les couleurs sont séparés suffisament pour pouvoir les distinguées lors de la capture.

9. */

10. class ColorPickingManager

11. {

12. /**

13. * Couleur de capture courante fournie au prochain objet qui en demande une

14. */

15. private static final AtomicInteger CURRENT_PICKING_COLOR = new AtomicInteger(0xFF_00_00_00);

16. /**

17. * Séparation entre les couleurs fournies

18. */

19. private static final int           PICKING_PRECISION     = 8;

20. /**

21. * Précision utilisée pour comparer une couleur de capture et une couleur capturée

22. */

23. private static final float         PICK_EPSILON          = 1e-5f;

25. /**

26. * Donne la couleur de capture courante et calcul la prochaine

27. */

28. @NonNull

29. static Color4f newPickColor()

30. {

31. return new Color4f(

32. ColorPickingManager.CURRENT_PICKING_COLOR.accumulateAndGet(ColorPickingManager.PICKING_PRECISION,

33. Integer::sum));

34. }

36. /**

37. * Indique si la couleur capturée correspond à une couleur de capture

38. */

39. static boolean isSamePickColor(float pickRed, float pickGreen, float pickBlue,

40. @NonNull final Color4f color)

41. {

42. return Math.abs(pickRed - color.red) < ColorPickingManager.PICK_EPSILON

43. && Math.abs(pickGreen - color.green) < ColorPickingManager.PICK_EPSILON

44. && Math.abs(pickBlue - color.blue) < ColorPickingManager.PICK_EPSILON;

45. }

46. }

On sépare les couleurs de capture d’un pas de 8 dans le bleu, pour être sûr que les couleurs ne soient pas trop proches, afin de compenser les problèmes de précisions qui feraient confondre deux couleurs.

Malgré cela, il y a (256*256*256)/8 = 2 097 152 valeurs possibles. Ce qui nous laisse de la marge.

La valeur du epsilon pour déterminer si une couleur est celle capturée existe pour la même raison. Espérer une égalité stricte serait utopique du aux problèmes de précision.

Enfin les nœuds vont maintenant avoir deux méthodes de rendu une pour la scène, l’autre pour le picking.

Node3D

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

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

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

5. import java.util.ArrayList;

6. import java.util.List;

7. import java.util.Objects;

8. import java.util.Stack;

9. import org.lwjgl.opengl.GL11;

11. /**

12. * Noeud virtuel de la scene

13. */

14. public class Node3D

15. {

16. @NonNull

17. final Color4f pickColor = ColorPickingManager.newPickColor();

19. @NonNull

20. public final String name;

22. /**

23. * Coordonne X de la position dans la 3D

24. */

25. public float x = 0f;

26. /**

27. * Coordonne Y de la position dans la 3D

28. */

29. public float y = 0f;

30. /**

31. * Coordonne Z de la position dans la 3D

32. */

33. public float z = 0f;

35. /**

36. * Angle de la rotation autour de l'axe X

37. */

38. public float angleX = 0f;

39. /**

40. * Angle de la rotation autour de l'axe Y

41. */

42. public float angleY = 0f;

43. /**

44. * Angle de la rotation autour de l'axe Z

45. */

46. public float angleZ = 0f;

48. /**

49. * Facteur d'échelle le long dde l'axe X <br/>

50. * La valeur doit toujours être strictement positive

51. */

52. public float scaleX = 1f;

53. /**

54. * Facteur d'échelle le long dde l'axe Y <br/>

55. * La valeur doit toujours être strictement positive

56. */

57. public float scaleY = 1f;

58. /**

59. * Facteur d'échelle le long dde l'axe Z <br/>

60. * La valeur doit toujours être strictement positive

61. */

62. public float scaleZ = 1f;

64. /**

65. * Liste des noeuds placés relativement à celui-ci

66. */

67. private final List<Node3D> children = new ArrayList<>();

69. public Node3D(@NonNull final String name)

70. {

71. this.name = Objects.requireNonNull(name, "name must not be null");

72. }

74. /**

75. * Ajoute un noeud enfant

76. */

77. public final void addChild(@NonNull final Node3D node)

78. {

79. Objects.requireNonNull(node, "node must not be null");

81. // On synchronize pour éviter l'accès concurrent

82. synchronized (this.children)

83. {

84. this.children.add(node);

85. }

86. }

88. /**

89. * Dessine le noeud

90. */

91. @ThreadOpenGL

92. final void drawNode()

93. {

94. // On sauvegarde la matrice actuelle

95. GL11.glPushMatrix();

97. // *** Position du noeud ***

98. // Position aux coordonnées X, Y, Z

99. GL11.glTranslatef(this.x, this.y, this.z);

100. // Rotation autour de l'axe X

101. GL11.glRotatef(this.angleX, 1f, 0f, 0f);

102. // Rotation autour de l'axe Y

103. GL11.glRotatef(this.angleY, 0f, 1f, 0f);

104. // Rotation autour de l'axe Z

105. GL11.glRotatef(this.angleZ, 0f, 0f, 1f);

106. // Application des facteurs d'échelle

107. GL11.glScalef(this.scaleX, this.scaleY, this.scaleZ);

109. // Dessin specific

110. this.drawSpecific();

112. // Placement des noeuds enfants relativement à la position de ce noeud

113. // On synchronize pour éviter l'accès concurrent

114. synchronized (this.children)

115. {

116. for (final Node3D child : this.children)

117. {

118. child.drawNode();

119. }

120. }

122. // On restaure la matrice sauvegardée

123. GL11.glPopMatrix();

124. }

126. /**

127. * Dessine le noeud pour capturé sa couleur

128. */

129. @ThreadOpenGL

130. final void drawNodePicking()

131. {

132. // On sauvegarde la matrice actuelle

133. GL11.glPushMatrix();

135. // *** Position du noeud ***

136. // Position aux coordonnées X, Y, Z

137. GL11.glTranslatef(this.x, this.y, this.z);

138. // Rotation autour de l'axe X

139. GL11.glRotatef(this.angleX, 1f, 0f, 0f);

140. // Rotation autour de l'axe Y

141. GL11.glRotatef(this.angleY, 0f, 1f, 0f);

142. // Rotation autour de l'axe Z

143. GL11.glRotatef(this.angleZ, 0f, 0f, 1f);

144. // Application des facteurs d'échelle

145. GL11.glScalef(this.scaleX, this.scaleY, this.scaleZ);

147. this.pickColor.apply();

149. // Dessin specific

150. this.drawSpecificOnlyMesh();

152. // Placement des noeuds enfants relativement à la position de ce noeud

153. // On synchronize pour éviter l'accès concurrent

154. synchronized (this.children)

155. {

156. for (final Node3D child : this.children)

157. {

158. child.drawNodePicking();

159. }

160. }

162. // On restaure la matrice sauvegardée

163. GL11.glPopMatrix();

164. }

167. /**

168. * Cherche parmi ce noeud et ses descendants, celui dont le nom est donné en paramètre.<br/>

169. * Si aucun noeud n'est trouvé, <b>null</b> sera retourné

170. */

171. public Node3D nodeByName(@NonNull final String name)

172. {

173. Objects.requireNonNull(name, "name must not be null");

175. final Stack<Node3D> node3DStack = new Stack<>();

176. node3DStack.push(this);

177. Node3D node;

179. while (!node3DStack.empty())

180. {

181. node = node3DStack.pop();

183. if (name.equals(node.name))

184. {

185. return node;

186. }

188. for (final Node3D child : node.children)

189. {

190. node3DStack.push(child);

191. }

192. }

194. return null;

195. }

197. /**

198. * Cherche parmi ce noeud et ses descendants, celui qui correspond à la couleur capturée de la fenêtre.<br/>

199. * Si aucun noeud n'est trouvé, <b>null</b> sera retourné

200. */

201. Node3D nodeByPickColor(float pickRed, float pickGreen, float pickBlue)

202. {

203. final Stack<Node3D> node3DStack = new Stack<>();

204. node3DStack.push(this);

205. Node3D node;

207. while (!node3DStack.empty())

208. {

209. node = node3DStack.pop();

211. if (ColorPickingManager.isSamePickColor(pickRed, pickGreen, pickBlue, node.pickColor))

212. {

213. return node;

214. }

216. for (final Node3D child : node.children)

217. {

218. node3DStack.push(child);

219. }

220. }

222. return null;

223. }

226. /**

227. * Dessin supplémentaire pour le noeud

228. * <p>

229. * A surcharger par les classes enfants

230. * <p>

231. * Ne fait rien par défaut

232. */

233. @ThreadOpenGL

234. void drawSpecific()

235. {

236. // Rien à faire d'autres. Les classes enfants y mettrons leur code spécifique

237. }

239. /**

240. * Dessin supplémentaire pour le noeud n'affichant que les mailles

241. * <p>

242. * A surcharger par les classes enfants

243. * <p>

244. * Ne fait rien par défaut

245. */

246. @ThreadOpenGL

247. void drawSpecificOnlyMesh()

248. {

249. // Rien à faire d'autres. Les classes enfants y mettrons leur code spécifique

250. }

252. @Override

253. public final String toString()

254. {

255. return this.getClass()

256. .getName() + " : " + this.name;

257. }

258. }

DrawableNode

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

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

4. import java.util.Objects;

6. /**

7. * Noeud ayant une représentation visuelle à l'écran

8. */

9. public abstract class DrawableNode

10. extends Node3D

11. {

12. protected Color4f color = Color4f.GREY;

14. @NonNull

15. public final Color4f getColor()

16. {

17. return this.color;

18. }

20. public final void setColor(@NonNull final Color4f color)

21. {

22. this.color = Objects.requireNonNull(color, "color must not be null");

23. }

25. public DrawableNode(@NonNull final String name)

26. {

27. super(name);

28. }

29. }

Object3D

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

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

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

6. /**

7. * Objet 3D

8. */

9. public class Object3D

10. extends DrawableNode

11. {

12. @NonNull

13. public final Mesh    mesh  = new Mesh();

15. public Object3D(@NonNull final String name)

16. {

17. super(name);

18. }

21. @ThreadOpenGL

22. @Override

23. void drawSpecific()

24. {

25. this.color.apply();

26. this.mesh.drawMesh();

27. }

29. @ThreadOpenGL

30. @Override

31. void drawSpecificOnlyMesh()

32. {

33. this.mesh.drawMesh();

34. }

35. }

Clone3D

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

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

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

5. import java.util.Objects;

7. /**

8. * Clone d'un objet 3D

9. */

10. public class Clone3D

11. extends DrawableNode

12. {

13. private final Object3D cloned;

15. public Clone3D(@NonNull final String name, @NonNull final Object3D cloned)

16. {

17. super(name);

18. this.cloned = Objects.requireNonNull(cloned, "cloned must not be null");

19. }

21. @ThreadOpenGL

22. @Override

23. void drawSpecific()

24. {

25. this.color.apply();

26. // Ici on utilise la maille de l'objet cloné

27. this.cloned.mesh.drawMesh();

28. }

30. @ThreadOpenGL

31. @Override

32. void drawSpecificOnlyMesh()

33. {

34. // Ici on utilise la maille de l'objet cloné

35. this.cloned.mesh.drawMesh();

36. }

37. }

On vous laisse le soin de rajouter le nom dans Box

Maintent que nos loeuds ont une couleur de picking unique et que l’on peut les dessiner en mode picking de couleur, nous allons pouvoir faire le picking lui-même

III-B-2 - Capture de la couleur aux coordonnées demandées

Pour capturer la couleur, nous utiliserons l’instruction

GL11.glReadPixels(pickX, pickY,
1, 1,
GL11.GL_RGBA, GL11.GL_FLOAT,
BufferTools.TEMPORARY_FLOAT_BUFFER);

Dans l’ordre des paramètres :

1. (pickX, pickY) sont les coordonnées du point dont on souhaite récupéré la couleur

2. 1 x 1 sont respectivement la largeur et la hauteur de la zone à capturer, ici 1 pixel

3. GL11.GL_RGBA Indique que l’on souhaite la couleur sous la format rouge, vert, bleu, opacité

4. GL11.GL_FLOAT indique que chaque composante couleur sera sous forme de float entre 0 et 1

5. Le dernier paramètre est un buffer qui va recevoir les couleurs capturées. Ici nous n’en auront qu’une seule puisque nous ne lisons qu’un seul pixel.

Afin de gérer les buffer nous fournissons un outil qui permet de réutiliser toujours le même buffer, donc la même zone mémoire pour nos différents usages. Par anticipation au prochain article, il est assez grand pour accueillir une texture. Un buffer peut être vu sous plusieurs forme. Pour en savoir plus sur les buffer référez-vous à l’annexe en fin de cet article.

Sachez juste qu’ils sont une solution afin de transférez rapidement des tableaux à travers la couche JNI.

BufferTools

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

3. import java.nio.ByteBuffer;

4. import java.nio.ByteOrder;

5. import java.nio.DoubleBuffer;

6. import java.nio.FloatBuffer;

7. import java.nio.IntBuffer;

9. /**

10. * Buffers réservés en mémoire.<br/>

11. * Comme les buffers seront utilisés séquentiellement au sein du même thread, le thread OpenGL,

12. * on peut réutiliser toujours le même. <br/>

13. * Cela évite des problèmes de mémoire et du temps d'allocation/libération.<br/>

14. * On va fixer ce buffer à une taille suffisamment assez grande pour une grande texture

15. */

16. class BufferTools

17. {

18. /**

19. * Largeur maximum d'une texture

20. */

21. static final int          MAX_WIDTH               = 4096;

22. /**

23. * Hauteur maximum d'une texture

24. */

25. static final int          MAX_HEIGHT              = 4096;

26. /**

27. * Taille maximum du buffer en pixels

28. */

29. static final int          MAX_DIMENSION           = BufferTools.MAX_WIDTH * BufferTools.MAX_HEIGHT;

30. /**

31. * Taille maximum du buffer en octets

32. */

33. static final int          MAX_DIMENSION_IN_BYTES  = BufferTools.MAX_DIMENSION << 2;

34. /**

35. * Le buffer réservé sous forme d'un buffer d'octets

36. */

37. static final ByteBuffer   TEMPORARY_BYTE_BUFFER   =

38. ByteBuffer.allocateDirect(BufferTools.MAX_DIMENSION_IN_BYTES)

39. .order(ByteOrder.nativeOrder());

40. /**

41. * Le buffer vu comme un buffer d'entiers

42. */

43. static final IntBuffer    TEMPORARY_INT_BUFFER    =

44. BufferTools.TEMPORARY_BYTE_BUFFER.asIntBuffer();

45. /**

46. * Le buffer vu comme un buffer de floats

47. */

48. static final FloatBuffer  TEMPORARY_FLOAT_BUFFER  =

49. BufferTools.TEMPORARY_BYTE_BUFFER.asFloatBuffer();

50. /**

51. * Le buffer vu comme un buffer de doubles

52. */

53. static final DoubleBuffer TEMPORARY_DOUBLE_BUFFER =

54. BufferTools.TEMPORARY_BYTE_BUFFER.asDoubleBuffer();

55. }

Nous avons ce qu’il faut désormais pour faire capturer le nœud sous les coordonnées par notre boucle de rendu

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.NonNull;

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

6. import java.util.Objects;

7. import org.lwjgl.glfw.GLFW;

8. import org.lwjgl.opengl.GL11;

9. import org.lwjgl.opengl.GL12;

11. /**

12. * Boucle de rendu

13. */

14. class Render3D

15. {

16. private static final boolean LIGHT_MANAGED = false;

18. /**

19. * Graphe de scène associé

20. */

21. @NonNull

22. final SceneGraph sceneGraph = new SceneGraph();

23. int pickX = -1;

24. int pickY = -1;

26. Render3D()

27. {

28. }

30. /**

31. * Lance la boucle de rendu

32. *

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

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

35. */

36. @ThreadOpenGL

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

38. {

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

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

42. Node3D pickedNode;

44. // Tant que la fenêtre est présente, rafraichir la 3D

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

46. {

47. // Dessine la frame courante

48. pickedNode = this.drawScene();

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

51. GLFW.glfwSwapBuffers(window);

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

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

55. GLFW.glfwPollEvents();

57. EventManager.EVENT_MANAGER.eventProcess(pickedNode);

58. }

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

61. window3D.exitWidow();

62. }

64. /**

65. * Initialize le rendu OpenGL

66. *

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

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

69. */

70. @ThreadOpenGL

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

72. {

73. // *** Gestion de la transparence ***

74. // On indique que l'on souhaite activé la tranparence

75. GL11.glEnable(GL11.GL_ALPHA_TEST);

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

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

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

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

81. /// *** Apparence globale ***

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

83. GL11.glEnable(GL11.GL_COLOR_MATERIAL);

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

85. GL11.glDisable(GL11.GL_TEXTURE_2D);

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

87. GL11.glEnable(GL11.GL_BLEND);

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

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

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

92. // Sur le "frustum", c'est la zone proche de l'utilisateur

93. // S'appelle aussi 'view port'

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

95. // On veut une projection normalisée

96. GL11.glEnable(GL11.GL_NORMALIZE);

98. // Réglage de la projection

99. GL11.glMatrixMode(GL11.GL_PROJECTION);

100. GL11.glLoadIdentity();

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

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

103. // Réglage du model de la vue

104. GL11.glMatrixMode(GL11.GL_MODELVIEW);

105. GL11.glLoadIdentity();

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

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

109. //On active le test de profondeur

110. GL11.glEnable(GL11.GL_DEPTH_TEST);

112. // Active les face visible que d'un coté (Pour éviter de dessiner des choses qui seront toujours cachés)

113. GL11.glEnable(GL11.GL_CULL_FACE);

114. GL11.glCullFace(GL11.GL_FRONT);

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

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

118. GL11.glShadeModel(GL11.GL_SMOOTH);

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

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

122. // Active l'éclairage

123. if (Render3D.LIGHT_MANAGED)

124. {

125. GL11.glEnable(GL11.GL_LIGHTING);

126. }

127. }

129. /**

130. * Dessine la scène 3D

131. */

132. @ThreadOpenGL

133. private Node3D drawScene()

134. {

135. Node3D    pickedNode = null;

136. final int pickX      = this.pickX;

137. final int pickY      = this.pickY;

139. if (pickX >= 0 && pickY >= 0)

140. {

141. // Si on doit capturer un objet, on dessine la scène en mode picking

142. if (Render3D.LIGHT_MANAGED)

143. {

144. GL11.glDisable(GL11.GL_LIGHTING);

145. }

147. GL11.glDisable(GL11.GL_CULL_FACE);

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

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

151. // Dessine la frame courante en mode capture d'un noeud

152. pickedNode = this.sceneGraph.pickNode(pickX, pickY);

154. if (Render3D.LIGHT_MANAGED)

155. {

156. GL11.glEnable(GL11.GL_LIGHTING);

157. }

159. GL11.glEnable(GL11.GL_CULL_FACE);

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

161. }

163. this.sceneGraph.drawScene();

165. return pickedNode;

166. }

167. }

Changements au sein du graphe de scène

SceneGraph

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

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

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

5. import java.util.Objects;

6. import org.lwjgl.opengl.GL11;

8. /**

9. * Le graphe de scène

10. */

11. public class SceneGraph

12. {

13. /**

14. * Nom du noeud racine, ne pas utilisé pour les autres noeuds

15. */

16. public static final String ROOT_NODE_NAME = "-<(ROOT_NODE)>-";

18. @NonNull

19. public final Node3D  root            = new Node3D(SceneGraph.ROOT_NODE_NAME);

20. private      Color4f backgroundColor = Color4f.WHITE;

22. public SceneGraph()

23. {

24. }

26. @NonNull

27. public Color4f getBackground()

28. {

29. return this.backgroundColor;

30. }

32. public void setBackground(@NonNull final Color4f backgroundColor)

33. {

34. this.backgroundColor = Objects.requireNonNull(backgroundColor, "backgroundColor must not be null");

35. }

37. @ThreadOpenGL

38. void drawScene()

39. {

40. GL11.glClearColor(this.backgroundColor.red, this.backgroundColor.green, this.backgroundColor.blue, 1f);

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

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

44. this.root.drawNode();

45. }

47. /**

48. * Dessine la scène en mode picking et retourne le noeud capturé ou <b>null</b> si il n'y en a pas

49. */

50. @ThreadOpenGL

51. Node3D pickNode(int pickX, int pickY)

52. {

53. // Dessine la scène en mode picking

54. this.root.drawNodePicking();

56. // On capture la couleur sous les coordonnées du picking

57. // On se place au début du buffer

58. BufferTools.TEMPORARY_FLOAT_BUFFER.rewind();

59. // On capture dans le buffer la couleur de la position à l'écran

60. GL11.glReadPixels(pickX, pickY,

61. 1, 1,

62. GL11.GL_RGBA, GL11.GL_FLOAT, BufferTools.TEMPORARY_FLOAT_BUFFER);

63. //On remet le buffer au début afin de pouvoir lire ce que la capture a écrite.

64. BufferTools.TEMPORARY_FLOAT_BUFFER.rewind();

65. // On lit les informations de couleurs capturées

66. final float red   = BufferTools.TEMPORARY_FLOAT_BUFFER.get();

67. final float green = BufferTools.TEMPORARY_FLOAT_BUFFER.get();

68. final float blue  = BufferTools.TEMPORARY_FLOAT_BUFFER.get();

69. // On cherche et renvoie le noeud dont la couleur de capture correspond.

70. // Si il n'y a pas de tel noeud, null est renvoyé.

71. return this.root.nodeByPickColor(red, green, blue);

72. }

73. }

Donnons à notre fenêtre de rendu la cpacitée de préciser les coordonnées du point où l’on souhaite capturer le nœud. Il faut se rappeler que :

- Du point de vue de la fenêtre

- Les abscisses X vont de 0 à la largeur de la fenêtre de droite à gauche

- Les ordonnées Y vont de 0 à la hauteur de la fenêtre de haut en bas

- Du point de vue OpenGL

- Les abscisses X vont de 0 à la largeur de la fenêtre de droite à gauche

- Les ordonnées Y vont de 0 à la hauteur de la fenêtre de bas en haut

Les ordonnées sont donc inversées.

Window3D

package fr.developez.tutorial.java.dimension3.render;
// …
public class Window3D
{
// …
private final Render3D                  render3D                  = new Render3D();
// …
/**
* Définit les coordonnées sous lesquels on doit capturer les objets
*/
public void pickNode(int x, int y)
{
// Si les coordonnées sont dans l'écran on active le picking, sinon on le désactive
if (x >= 0 && x < this.width && y >= 0 && y < this.height)
{
this.render3D.pickX = x;
// En OpenGL, les Y vont du bas vers le haut, tandis que sur la fenêtre elles vont du haut vers le bas.
// Si bien qu'il faille inverser les Y demandés.
this.render3D.pickY = this.height - y;
}
else
{
this.render3D.pickX = -1;
this.render3D.pickY = -1;
}
}

/**
* On arrête la détection du picking
*/
public void stopPicking()
{
this.render3D.pickX = -1;
this.render3D.pickY = -1;
}
// ..
}

III-C - Mise en place d’un observateur de nœud

Maintenant que l’on peut capturer notre objet, créons un observateur pour que l’utilisateur puisse le recevoir et branchons-le dans le gestionnaire d’événements.

PickingObserver

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

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

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

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

7. /**

8. * Observateur des événement de picking.<br/>

9. * Quand un objet est détecté sous le point de la fenêtre dont les coordonnées sont définis par {@link fr.developez.tutorial.java.dimension3.render.Window3D#pickNode(int, int)},

10. * {@link #onPickedNode(DrawableNode)} sera appelé à chaque nouvel objet détecté.<br/>

11. * <br/>

12. * Au moment ou il n'y a plus d'objet sous les coordonnées, ou si on viens de désactiver le picking avec {@link Window3D#stopPicking()},

13. * {@link #nothingPicked()} est appelé une fois.<br/>

14. * Il ne sera appelé de nouveau que si un objet est de nouveau détecté puis reperdu.

15. */

16. @FunctionalInterface

17. public interface PickingObserver

18. {

19. /**

20. * Appelé dés qu'il n'y a plus d'objets détecté<br/>

21. * Ne fait rien par défaut

22. */

23. public default void nothingPicked()

24. {

25. }

28. /**

29. * Appelé dés qu'un nouvel objet est détecté

30. *

31. * @param drawableNode Objet détecté

32. */

33. public void onPickedNode(@NonNull final DrawableNode drawableNode);

34. }

EventManager

package fr.developez.tutorial.java.dimension3.render;
// …
import fr.developez.tutorial.java.dimension3.picking.PickingObserver;
import fr.developez.tutorial.java.dimension3.thread.ThreadManager;
import fr.developez.tutorial.java.dimension3.tool.NonNull;
import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
// …

/**
* Manager des évènements utilisateur. Se brancher dessus afin de pouvoir les traiter
*/
public class EventManager
{
// …
private       Node3D           pickedNode                = null;
// …
private final List<PickingObserver>            pickingObservers = new ArrayList<>();
// …

/**
* Enregistre un observateur d'objet capturé sous les coordonnées définis par {@link Window3D#pickNode(int, int)}<br/>
* Il sera appelé immédiatement avec l'objet couramment capturé où sera averti qu'il n'y a pas actuellement d'objet détecté.<br/>
* Il sera appelé à chaque nouvel objet détecté, ainsi qu'a chaque fois qu'il n'y a plus d'objet.
*/
public void register(@NonNull final PickingObserver pickingObserver)
{
Objects.requireNonNull(pickingObserver, "pickingObserver must not be null");
final Node3D pickedNode = this.pickedNode;

if (pickedNode == null)
{
pickingObserver.nothingPicked();
}
else
{
pickingObserver.onPickedNode((DrawableNode) pickedNode);
}

synchronized (this.pickingObservers)
{
if (!this.pickingObservers.contains(pickingObserver))
{
this.pickingObservers.add(pickingObserver);
}
}
}

/**
* Oublie  un observateur d'objet capturé sous les coordonnées définis par {@link Window3D#pickNode(int, int)}<br/>
* Il ne sera plus appelé
*/
public void unregister(final PickingObserver pickingObserver)
{
synchronized (this.pickingObservers)
{
this.pickingObservers.remove(pickingObserver);
}
}
// …
@ThreadOpenGL
void eventProcess(final Node3D pickedNode)
{
// Le traitement du clavier est mis avant celui de la souris afin de pouvoir enrichir les modifiers
this.updateKeyboardEvents();
this.updateMouseEvents();
this.updateJoystickEvents();
this.updatePickingEvents(pickedNode);
}
// …
private void updatePickingEvents(final Node3D pickedNode)
{
if (pickedNode != null)
{
if (this.pickedNode != pickedNode)
{
synchronized (this.pickingObservers)
{
for (final PickingObserver pickingObserver : this.pickingObservers)
{
// Les observateurs sont appelés dans un thread à part afin que l'exécution de leur code ne ralentisse pas le thread OpenGL
ThreadManager.execute((DrawableNode) pickedNode, pickingObserver::onPickedNode);
}
}
}
}
else if (this.pickedNode != null)
{
synchronized (this.pickingObservers)
{
for (final PickingObserver pickingObserver : this.pickingObservers)
{
// Les observateurs sont appelés dans un thread à part afin que l'exécution de leur code ne ralentisse pas le thread OpenGL
ThreadManager.execute(pickingObserver::nothingPicked);
}
}
}

this.pickedNode = pickedNode;
}
// …
}

Complétons notre example en changeant la couleur de la boîte quand la souris passe au-dessus de lui.

Main

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

3. import fr.developez.tutorial.java.dimension3.geometry.Box;

4. import fr.developez.tutorial.java.dimension3.joystick.JoystickCode;

5. import fr.developez.tutorial.java.dimension3.joystick.JoystickConnectionEvent;

6. import fr.developez.tutorial.java.dimension3.joystick.JoystickEvent;

7. import fr.developez.tutorial.java.dimension3.mouse.MouseStatusEvent;

8. import fr.developez.tutorial.java.dimension3.picking.PickingObserver;

9. import fr.developez.tutorial.java.dimension3.render.Color4f;

10. import fr.developez.tutorial.java.dimension3.render.DrawableNode;

11. import fr.developez.tutorial.java.dimension3.render.EventManager;

12. import fr.developez.tutorial.java.dimension3.render.SceneGraph;

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

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

15. import org.lwjgl.glfw.GLFW;

17. public class Main

18. {

19. private static final float    ROTATE_STEP       = 0.5f;

20. private static final float    TRANSLATE_STEP    = 0.01f;

21. private static final Box      box               = new Box("Boîte");

22. private static       boolean  wasButtonLeftDown = false;

23. private static       double   oldMouseX         = 0.0;

24. private static       double   oldMouseY         = 0.0;

25. private static final Window3D window3D          = new Window3D(800, 600, "Tutoriel 3D - Boîte qui tourne");

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

28. {

29. Main.drawScene(Main.window3D.getSceneGraph());

30. }

32. private static void drawScene(@NonNull final SceneGraph sceneGraph)

33. {

34. sceneGraph.setBackground(Color4f.BLACK);

36. sceneGraph.root.addChild(Main.box);

38. Main.box.z      = -2f;

39. Main.box.angleX = 22.5f;

40. Main.box.scaleX = 2f;

41. Main.box.scaleZ = 0.5f;

42. Main.box.setColor(Color4f.LIGHT_GREEN);

44. EventManager.EVENT_MANAGER.register(Main::joystickConnectionChanged);

45. EventManager.EVENT_MANAGER.register(Main::joystickEventHappen);

46. EventManager.EVENT_MANAGER.register(Main::mouseInsideWidowChanged);

47. EventManager.EVENT_MANAGER.register(Main::mouseStatus);

48. EventManager.EVENT_MANAGER.register(Main::currentKeyDown);

50. EventManager.EVENT_MANAGER.register(new PickingObserver()

51. {

52. @Override

53. public void onPickedNode(DrawableNode drawableNode)

54. {

55. drawableNode.setColor(Color4f.LIGHT_BLUE);

56. }

58. @Override

59. public void nothingPicked()

60. {

61. Main.box.setColor(Color4f.LIGHT_GREEN);

62. }

63. });

64. }

66. private static void joystickConnectionChanged(@NonNull JoystickConnectionEvent joystickConnectionEvent)

67. {

68. String header = "NOT CONNECTED : ";

70. if (joystickConnectionEvent.connected)

71. {

72. header = "CONNECTED : ";

73. }

75. System.out.print(header);

76. System.out.println(joystickConnectionEvent.joystick);

77. }

79. private static void joystickEventHappen(@NonNull JoystickEvent joystickEvent)

80. {

81. for (final JoystickCode joystickCode : joystickEvent.currentPressed)

82. {

83. switch (joystickCode)

84. {

85. case AXIS_1_NEGATIVE:

86. Main.box.x -= Main.TRANSLATE_STEP;

87. break;

88. case AXIS_1_POSITIVE:

89. Main.box.x += Main.TRANSLATE_STEP;

90. break;

91. case AXIS_2_NEGATIVE:

92. Main.box.y += Main.TRANSLATE_STEP;

93. break;

94. case AXIS_2_POSITIVE:

95. Main.box.y -= Main.TRANSLATE_STEP;

96. break;

97. }

98. }

99. }

101. private static void mouseInsideWidowChanged(boolean insideWindow)

102. {

103. if (insideWindow)

104. {

105. System.out.println("*** INSIDE ***");

106. }

107. else

108. {

109. Main.window3D.stopPicking();

110. System.out.println("*** OUTSIDE ***");

111. }

112. }

114. private static void mouseStatus(@NonNull final MouseStatusEvent mouseStatusEvent)

115. {

116. if (mouseStatusEvent.buttonLeftDown && Main.wasButtonLeftDown)

117. {

118. Main.box.angleY += (mouseStatusEvent.x - Main.oldMouseX) * Main.ROTATE_STEP;

119. Main.box.angleX += (mouseStatusEvent.y - Main.oldMouseY) * Main.ROTATE_STEP;

120. }

122. Main.box.z += mouseStatusEvent.scrollY * Main.TRANSLATE_STEP;

123. Main.window3D.pickNode((int) mouseStatusEvent.x, (int) mouseStatusEvent.y);

125. Main.oldMouseX         = mouseStatusEvent.x;

126. Main.oldMouseY         = mouseStatusEvent.y;

127. Main.wasButtonLeftDown = mouseStatusEvent.buttonLeftDown;

128. }

130. private static void currentKeyDown(@NonNull int[] keys)

131. {

132. for (final int key : keys)

133. {

134. switch (key)

135. {

136. case GLFW.GLFW_KEY_UP:

137. Main.box.y += Main.TRANSLATE_STEP;

138. break;

139. case GLFW.GLFW_KEY_DOWN:

140. Main.box.y -= Main.TRANSLATE_STEP;

141. break;

142. case GLFW.GLFW_KEY_RIGHT:

143. Main.box.x += Main.TRANSLATE_STEP;

144. break;

145. case GLFW.GLFW_KEY_LEFT:

146. Main.box.x -= Main.TRANSLATE_STEP;

147. break;

148. }

149. }

150. }

151. }

IV - Conclusion

Nous avons vu dans cet article comment recevoir des événements utilisateurs et comment détecter un objet.

Nous avons présenté un gestionnaire d’événements possible.

Il est possible d’ajouter un gestionnaire d’actions, dont l’idée serait, de se dire que l’utilisateur appuie sur la flèche du haut du clavier, ou qu’il appuie sur la bouton de la manette correspondant à la flèche du haut, ou qu’il actionne l’axe permettant de monter, ces trois événements déclenchent une seule et même action l’action d’aller vers le haut. Delà, la possibilité de changer le mapping des touches, ...

Dans le prochain article on va enfin ne plus dessiner des objets d’une seule couleur, mais pouvoir y mettre des images grâce aux textures et la première version des matériaux

V - Annexe

V-A - Les buffers dans java.nio

Dans le pcakgae java.nio se trouve la notion de buffer.

Ils sont de plusieurs type : CharBuffer, ByteBuffer, ShortBuffer, IntBuffer, LongBuffer, FloatBuffer et DoubleBuffer

Chacun de ses buffer reservent une zone de mémoire coté C du JNI et qui donc échappe au garbage collector.

Chaque buffer voit la zone de mémoire qu’on lui a réservé comme un tableau d’un type primitif. Le nom de la classe indique le type associé. Par exemple CharBuffer va voir cette zone de mémoire comme un tableau de char. ByteBuffer comme un tableau de byte. Et ainsi de suite.

Ils sont conçus pour faciliter et accélérer les transferts de tableau entre le C et le Java à travers le JNI.

On peut décider que la même zone de mémoire peut être partagée par un ByteBuffer et un IntBuffer, par exemple. Propriété que l’on utilise dans le BufferTools.

Les buffers fonctionnent tous de la même façon.

On a une position qui pointe sur une case de la mémoire, à chaque écriture ou lecture la position avance. Il est possible de connaître la position actuelle et de la modifié soit en précisant la nouvelle position, soit en lui disant de retourner au début.

Si on écrit des informations, il faut retourner au début de leur écriture pour pouvoir les lires.

Si on reprend ce que l’on fait lors du picking

BufferTools.TEMPORARY_FLOAT_BUFFER.rewind();
// On capture dans le buffer la couleur de la position à l'écran
GL11.glReadPixels(pickX, pickY,
1, 1,
GL11.GL_RGBA, GL11.GL_FLOAT, BufferTools.TEMPORARY_FLOAT_BUFFER);
//On remet le buffer au début afin de pouvoir lire ce que la capture a écrite.
BufferTools.TEMPORARY_FLOAT_BUFFER.rewind();
// On lit les informations de couleurs capturées
final float red   = BufferTools.TEMPORARY_FLOAT_BUFFER.get();
final float green = BufferTools.TEMPORARY_FLOAT_BUFFER.get();
final float blue  = BufferTools.TEMPORARY_FLOAT_BUFFER.get();

La première chose que l’on fait c’est un rewind qui à pour effet de positionner la position du buffer au début de la mémoire.

Ainsi, on est sûr que les composantes de la couleur capturées seront écrites au début du buffer

L’instruction glReadPixels va écrire les quatre composantes, du coup la position sera aprés la couleur.

Du coup pour pouvoir lire la couleur, on fait de nouveau un rewind afin de se retrouver au début.

Puis ont fait les get qui lisent une information et passe à la suivante. Comme on a demandé du RGBA, on sait que la première valeur sera rouge, puis vert, bleue et enfin opacité.

C’est aussi pour cela que pour les axes ou boutons du joystick on ne suppose pas qu’on est au début de la mémoire et que l’on repositionne le buffer donné à la même place que l’on a trouvé. Ne sachant pas ce que fait GLFW de ce buffer avant ou après. On reste neutre.