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

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

Interaction avec l’utilisateur

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.

Article lu   fois.

L'auteur

Profil ProSite personnelJHelp

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

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échargerSélectionnez
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
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
package fr.developez.tutorial.java.dimension3.render;

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

/**
 * Manageur des évènements utilisateur. Se brancher dessus afin de pouvoir les traiter
 */
public class EventManager
{
    public static final EventManager EVENT_MANAGER = new EventManager();

    private EventManager()
    {
    }

    @ThreadOpenGL
    void eventProcess()
    {
    }
}

Et dans Render3D

Render3D
Sélectionnez
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
Sélectionnez
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
Sélectionnez
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.

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

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

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

/**
 * Écouteur des événements clavier
 */
@FunctionalInterface
public interface KeyboardEventListener
{
    /**
     * Appelé régulièrement pour donner l'état actuel des touches du clavier<br/>
     * Les entiers sont les codes des touches actuellement appuyées.<br/>
     * Leur signification sont dans les constantes de <b>GLFW</b> commençant par <b>GLFW.GLFW_KEY_</b>
     *
     * @param keys Liste des touches actuellement appuyées par l'utilisateur
     */
    public void currentKeyDown(@NonNull int[] keys);
}

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

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

import fr.developez.tutorial.java.dimension3.geometry.Box;
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    TRANSLATE_STEP    = 0.01f;
    private static final Box      box               = new Box();
    private static final Window3D window3D          = new Window3D(800, 600, "Tutoriel 3D - Boîte réagissant aux actions de l’utilisateur");

    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::currentKeyDown);
    }

    private static void currentKeyDown(@NonNull int[] keys)
    {
        for (final int key : keys)
        {
            switch (key)
            {
                case GLFW.GLFW_KEY_UP:
                    Main.box.y += Main.TRANSLATE_STEP;
                    break;
                case GLFW.GLFW_KEY_DOWN:
                    Main.box.y -= Main.TRANSLATE_STEP;
                    break;
                case GLFW.GLFW_KEY_RIGHT:
                    Main.box.x += Main.TRANSLATE_STEP;
                    break;
                case GLFW.GLFW_KEY_LEFT:
                    Main.box.x -= Main.TRANSLATE_STEP;
                    break;
            }
        }
    }
}

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

/**
 * Observateur de la sortie/entrée de la souris de l'écran
 */
@FunctionalInterface
public interface MouseInsideWindowObserver
{
    public void mouseInsideWidowChanged(boolean insideWindow);
}
EventManager
Sélectionnez
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
Sélectionnez
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
Sélectionnez
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
Sélectionnez
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
Sélectionnez
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
Sélectionnez
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
Sélectionnez
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
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
package fr.developez.tutorial.java.dimension3.mouse;

import org.lwjgl.glfw.GLFW;

/**
 * Ensemble des modifiers actuellement activés
 */
public final class Modifiers
{
    public final boolean numberLockDown;
    public final boolean capsLockDown;
    public final boolean superDown;
    public final boolean altDown;
    public final boolean controlDown;
    public final boolean shiftDown;

    public Modifiers(int modifiers)
    {
        this.numberLockDown = (modifiers & GLFW.GLFW_MOD_NUM_LOCK) != 0;
        this.capsLockDown   = (modifiers & GLFW.GLFW_MOD_CAPS_LOCK) != 0;
        this.superDown      = (modifiers & GLFW.GLFW_MOD_SUPER) != 0;
        this.altDown        = (modifiers & GLFW.GLFW_MOD_ALT) != 0;
        this.controlDown    = (modifiers & GLFW.GLFW_MOD_CONTROL) != 0;
        this.shiftDown      = (modifiers & GLFW.GLFW_MOD_SHIFT) != 0;
    }

    @Override
    public String toString()
    {
        return "NumberLock=" + this.numberLockDown +
                " CapsLock=" + this.capsLockDown +
                " Super=" + this.superDown +
                " Alt=" + this.altDown +
                " Control=" + this.controlDown +
                " Shift=" + this.shiftDown;
    }
}

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

MouseStatusEvent
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
package fr.developez.tutorial.java.dimension3.mouse;

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

/**
 * Décrit l'état actuel de la souris
 */
public class MouseStatusEvent
{
    /**
     * Abscisse de la position de la souris sur la fenêtre
     */
    public final double x;
    /**
     * Ordonnée de la position de la souris sur la fenêtre
     */
    public final double y;

    /**
     * Pas du scroll en X horizontal
     */
    public final double scrollX;
    /**
     * Pas du scroll en Y vertical
     */
    public final double scrollY;

    /**
     * Indique si le bouton gauche est enfoncé par l'utilisateur.
     */
    public final boolean buttonLeftDown;
    /**
     * Indique si le bouton du milieu est enfoncé par l'utilisateur.
     */
    public final boolean buttonMiddleDown;
    /**
     * Indique si le bouton droit est enfoncé par l'utilisateur.
     */
    public final boolean buttonRightDown;

    /**
     * Ensemble de modifiers actifs
     */
    @NonNull
    public final Modifiers modifiers;

    public MouseStatusEvent(double x, double y,
                            double scrollX, double scrollY,
                            boolean buttonLeftDown, 
                            boolean buttonMiddleDown, 
                            boolean buttonRightDown,
                            @NonNull final Modifiers modifiers)
    {
        this.x                = x;
        this.y                = y;
        this.scrollX          = scrollX;
        this.scrollY          = scrollY;
        this.buttonLeftDown   = buttonLeftDown;
        this.buttonMiddleDown = buttonMiddleDown;
        this.buttonRightDown  = buttonRightDown;
        this.modifiers        = Objects.requireNonNull(modifiers, "modifiers must not be null");
    }
}

Puis l’écouteur lui-même

MouseStatusListener
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
/**
 * Écouteur du status courant de la souris
 */
@FunctionalInterface
public interface MouseStatusListener
{
    /**
     * Appelé régulièrement afin de reporter l'état courant de la souris
     */
    public void mouseStatus(@NonNull MouseStatusEvent mouseStatusEvent);
}

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

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

/**
 * Type de button/axe
 */
public enum JoystickInputType
{
    /**
     * Axe allant dans le sens positif
     */
    AXIS_POSITIVE,
    /**
     * Axe allant dans le sens négatif
     */
    AXIS_NEGATIVE,
    /**
     * Bouton
     */
    BUTTON,
    /**
     * Indéfini
     */
    NONE
}

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
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
161.
162.
163.
164.
165.
166.
167.
168.
169.
170.
171.
172.
173.
174.
175.
176.
177.
178.
179.
180.
181.
182.
183.
184.
185.
186.
187.
188.
189.
190.
191.
192.
193.
194.
195.
196.
197.
198.
199.
200.
201.
202.
203.
204.
205.
206.
207.
208.
209.
210.
211.
212.
213.
214.
215.
216.
package fr.developez.tutorial.java.dimension3.joystick;

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

/**
 * Code d'un élément du joystick : bouton, axe allant de le sens positif, axe allant dans le sens négatif
 */
public enum JoystickCode
{
    /**
     * Axe 1 allant dans le sens positif
     */
    AXIS_1_POSITIVE(0, JoystickInputType.AXIS_POSITIVE),
    /**
     * Axe 1 allant dans le sens négatif
     */
    AXIS_1_NEGATIVE(0, JoystickInputType.AXIS_NEGATIVE),
    /**
     * Axe 2 allant dans le sens positif
     */
    AXIS_2_POSITIVE(1, JoystickInputType.AXIS_POSITIVE),
    /**
     * Axe 2 allant dans le sens négatif
     */
    AXIS_2_NEGATIVE(1, JoystickInputType.AXIS_NEGATIVE),
    /**
     * Axe 3 allant dans le sens positif
     */
    AXIS_3_POSITIVE(2, JoystickInputType.AXIS_POSITIVE),
    /**
     * Axe 3 allant dans le sens négatif
     */
    AXIS_3_NEGATIVE(2, JoystickInputType.AXIS_NEGATIVE),
    /**
     * Axe 4 allant dans le sens positif
     */
    AXIS_4_POSITIVE(3, JoystickInputType.AXIS_POSITIVE),
    /**
     * Axe 4 allant dans le sens négatif
     */
    AXIS_4_NEGATIVE(3, JoystickInputType.AXIS_NEGATIVE),
    /**
     * Axe 5 allant dans le sens positif
     */
    AXIS_5_POSITIVE(4, JoystickInputType.AXIS_POSITIVE),
    /**
     * Axe 5 allant dans le sens négatif
     */
    AXIS_5_NEGATIVE(4, JoystickInputType.AXIS_NEGATIVE),
    /**
     * Axe 6 allant dans le sens positif
     */
    AXIS_6_POSITIVE(5, JoystickInputType.AXIS_POSITIVE),
    /**
     * Axe 6 allant dans le sens négatif
     */
    AXIS_6_NEGATIVE(5, JoystickInputType.AXIS_NEGATIVE),
    /**
     * Axe 7 allant dans le sens positif
     */
    AXIS_7_POSITIVE(6, JoystickInputType.AXIS_POSITIVE),
    /**
     * Axe 7 allant dans le sens négatif
     */
    AXIS_7_NEGATIVE(6, JoystickInputType.AXIS_NEGATIVE),
    /**
     * Axe 8 allant dans le sens positif
     */
    AXIS_8_POSITIVE(7, JoystickInputType.AXIS_POSITIVE),
    /**
     * Axe 8 allant dans le sens négatif
     */
    AXIS_8_NEGATIVE(7, JoystickInputType.AXIS_NEGATIVE),
    /**
     * Joystick bouton 1
     */
    BUTTON_1(0, JoystickInputType.BUTTON),
    /**
     * Joystick bouton 2
     */
    BUTTON_2(1, JoystickInputType.BUTTON),
    /**
     * Joystick bouton 3
     */
    BUTTON_3(2, JoystickInputType.BUTTON),
    /**
     * Joystick bouton 4
     */
    BUTTON_4(3, JoystickInputType.BUTTON),
    /**
     * Joystick bouton 5
     */
    BUTTON_5(4, JoystickInputType.BUTTON),
    /**
     * Joystick bouton 6
     */
    BUTTON_6(5, JoystickInputType.BUTTON),
    /**
     * Joystick bouton 7
     */
    BUTTON_7(6, JoystickInputType.BUTTON),
    /**
     * Joystick bouton 8
     */
    BUTTON_8(7, JoystickInputType.BUTTON),
    /**
     * Joystick bouton 9
     */
    BUTTON_9(8, JoystickInputType.BUTTON),
    /**
     * Joystick bouton 10
     */
    BUTTON_10(9, JoystickInputType.BUTTON),
    /**
     * Joystick bouton 11
     */
    BUTTON_11(10, JoystickInputType.BUTTON),
    /**
     * Joystick bouton 12
     */
    BUTTON_12(11, JoystickInputType.BUTTON),
    /**
     * Joystick bouton 13
     */
    BUTTON_13(12, JoystickInputType.BUTTON),
    /**
     * Joystick bouton 14
     */
    BUTTON_14(13, JoystickInputType.BUTTON),
    /**
     * Joystick bouton 15
     */
    BUTTON_15(14, JoystickInputType.BUTTON),
    /**
     * Joystick bouton 16
     */
    BUTTON_16(15, JoystickInputType.BUTTON),
    /**
     * Pas d'événement joystick
     */
    NONE(-1, JoystickInputType.NONE);

    /**
     * Numéro d'axe maximum que l'on gère pour le joystick
     */
    public static final int MAX_AXIS_INDEX   = 7;
    /**
     * Numéro de bouton maximum que l'on gère pour le joystick
     */
    public static final int MAX_BUTTON_INDEX = 15;

    @NonNull
    private static JoystickCode obtain(int elementNumber, JoystickInputType joystickInputType)
    {
        for (final JoystickCode joystickCode : JoystickCode.values())
        {
            if (joystickCode.number == elementNumber && joystickCode.joystickInputType == joystickInputType)
            {
                return joystickCode;
            }
        }

        return JoystickCode.NONE;
    }

    /**
     * Cherche un axe par son numéro et son sens d'appui
     *
     * @param axisNumber Numéro de l'axe
     * @param positive   Direction de l'axe
     * @return Axe trouvé ou {@link JoystickCode#NONE} si l'axe n'existe pas
     */
    @NonNull
    public static JoystickCode obtainAxis(int axisNumber, boolean positive)
    {
        if (positive)
        {
            return JoystickCode.obtain(axisNumber, JoystickInputType.AXIS_POSITIVE);
        }

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

    /**
     * Cherche un bouton par son numéro
     *
     * @param buttonNumber Numéro du bouton
     * @return Bouton trouvé ou {@link JoystickCode#NONE} si le bouton n'existe pas
     */
    @NonNull
    public static JoystickCode obtainButton(int buttonNumber)
    {
        return JoystickCode.obtain(buttonNumber, JoystickInputType.BUTTON);
    }

    /**
     * Numéro du bouton/axe
     */
    public final int               number;
    /**
     * Type de bouton/axe
     */
    public final JoystickInputType joystickInputType;

    JoystickCode(int number, JoystickInputType joystickInputType)
    {
        this.number            = number;
        this.joystickInputType = joystickInputType;
    }

    @Override
    public String toString()
    {
        return this.name() + " (" + this.number + ") " + this.joystickInputType;
    }
}

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

JoystickStatus
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
package fr.developez.tutorial.java.dimension3.joystick;

/**
 * État d'un bouton/direction d'axe
 */
public enum JoystickStatus
{
    /**
     * Pressée
     */
    PRESSED,
    /**
     * Relâchée
     */
    RELEASED;
}

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

Joystick
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
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.NonNull;
import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.lwjgl.glfw.GLFW;

/**
 * Représente un Joystick
 */
public enum Joystick
{
    JOYSTICK_1(GLFW.GLFW_JOYSTICK_1),
    JOYSTICK_2(GLFW.GLFW_JOYSTICK_2),
    JOYSTICK_3(GLFW.GLFW_JOYSTICK_3),
    JOYSTICK_4(GLFW.GLFW_JOYSTICK_4),
    JOYSTICK_5(GLFW.GLFW_JOYSTICK_5),
    JOYSTICK_6(GLFW.GLFW_JOYSTICK_6),
    JOYSTICK_7(GLFW.GLFW_JOYSTICK_7),
    JOYSTICK_8(GLFW.GLFW_JOYSTICK_8),
    JOYSTICK_9(GLFW.GLFW_JOYSTICK_9),
    JOYSTICK_10(GLFW.GLFW_JOYSTICK_10),
    JOYSTICK_11(GLFW.GLFW_JOYSTICK_11),
    JOYSTICK_12(GLFW.GLFW_JOYSTICK_12),
    JOYSTICK_13(GLFW.GLFW_JOYSTICK_13),
    JOYSTICK_14(GLFW.GLFW_JOYSTICK_14),
    JOYSTICK_15(GLFW.GLFW_JOYSTICK_15),
    JOYSTICK_16(GLFW.GLFW_JOYSTICK_16);

    private final int                               joystickID;
    private       boolean                           connected               = false;
    private       boolean                           connectionStatusChanged = false;
    /**
     * Maintient à jour l'état des boutons et axes du joystick
     */
    private final Map<JoystickCode, JoystickStatus> joystickElementsStatus  = new HashMap<>();
    private       String                            joystickName            = "";
    private       String                            joystickGUID            = "";

    Joystick(int joystickID)
    {
        this.joystickID = joystickID;
        this.resetStatus();
    }

    /**
     * Nom du Joystick. <br/>
     * Si le joystick n'est pas branché, son nom sera vide.
     * En effet on ne peut lire son nom que si il est branché.<br/>
     * Si malgré le fait qu'il soit branché, il est toujours vide, c'est que GLFW n'arrive pas à le lire.
     */
    public String joystickName()
    {
        return this.joystickName;
    }

    /**
     * GUID du joystick<br/>
     * Si le joystick n'est pas branché, son GUID sera vide.
     * En effet on ne peut lire son GUID que si il est branché.<br/>
     * Si malgré le fait qu'il soit branché, il est toujours vide, c'est que GLFW n'arrive pas à le lire.<br/>
     * <br/>
     * Le GUID représente un model de joystick, mais ne permet pas de distingué deux joysticks du même model.<br/>
     * Cette valeur est tout de même utile pour associé des correspondances numéros de bouton/axe à une action sur le joystick<br/>
     * En effet pour un même model, la flèche du haut aura toujours le même numéro de bouton par exemple.
     */
    public String joystickGUID()
    {
        return this.joystickGUID;
    }


    /**
     * Indique si le joystick est actuellement branché
     */
    public boolean connected()
    {
        return this.connected;
    }

    /**
     * Liste des boutons/axes du joystick actuellement appuyés par l'utilisateur
     */
    @NonNull
    JoystickCode[] currentPressedElements()
    {
        final List<JoystickCode> currentPressedElements = new ArrayList<>();

        for (final Map.Entry<JoystickCode, JoystickStatus> entry : this.joystickElementsStatus.entrySet())
        {
            if (entry.getValue() != JoystickStatus.RELEASED)
            {
                currentPressedElements.add(entry.getKey());
            }
        }

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

    /**
     * Indique si le status de connection viens de changer
     */
    boolean connectionStatusChanged()
    {
        return this.connectionStatusChanged;
    }

    /**
     * Met à jour l'état des boutons/axes du joystick
     */
    @ThreadOpenGL
    void updateInformation()
    {
         // TODO Lire l’état courant de la manette et le mettre à jour
    }

    private void resetStatus()
    {
        this.joystickName = "";
        this.joystickGUID = "";

        for (final JoystickCode joystickCode : JoystickCode.values())
        {
            this.joystickElementsStatus.put(joystickCode, JoystickStatus.RELEASED);
        }
    }

    @Override
    public String toString()
    {
        return this.name() + " : " + this.joystickName + " : " + this.joystickGUID;
    }
}

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

import fr.developez.tutorial.java.dimension3.render.Joystick;
import fr.developez.tutorial.java.dimension3.tool.NonNull;
import java.util.Objects;

/**
 * Évènement de changement d'état de connexion d'un joystick
 */
public class JoystickConnectionEvent
{
    /**
     * Joystick connecté/déconnecté
     */
    @NonNull
    public final Joystick joystick;
    /**
     * Nouvel état de connection
     */
    public final boolean  connected;

    public JoystickConnectionEvent(@NonNull final Joystick joystick, boolean connected)
    {
        this.joystick  = Objects.requireNonNull(joystick, "joystick must not be null");
        this.connected = connected;
    }
}
JoystickConnectionObserver
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
package fr.developez.tutorial.java.dimension3.joystick;

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

/**
 * Observateur de l'état de connection des joystick.<br/>
 * Il est appelée à chaque fois qu'un joystick est branché ou débranché.<br/>
 * Il est également appelé au premier enregistrement pour avoir l'état courant des divers joystick possibles.<br/>
 * Voir {@link fr.developez.tutorial.java.dimension3.render.EventManager#register(JoystickConnectionObserver)}
 */
@FunctionalInterface
public interface JoystickConnectionObserver
{
    public void joystickConnectionChanged(@NonNull JoystickConnectionEvent joystickConnectionEvent);
}
JoystickEvent
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
package fr.developez.tutorial.java.dimension3.joystick;

import fr.developez.tutorial.java.dimension3.render.Joystick;
import fr.developez.tutorial.java.dimension3.tool.NonNull;

/**
 * Évènement joystick
 */
public final class JoystickEvent
{
    /**
     * Joystick où à lieu l'événement
     */
    @NonNull
    public final Joystick       joystick;
    /**
     * Liste des boutons/axes actuellement appuyer
     */
    @NonNull
    public final JoystickCode[] currentPressed;

    public JoystickEvent(Joystick joystick, JoystickCode[] currentPressed)
    {
        this.joystick       = joystick;
        this.currentPressed = currentPressed;
    }
}
JoystickEventListener
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
package fr.developez.tutorial.java.dimension3.joystick;

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

/**
 * Écouteur des événements joystick
 */
@FunctionalInterface
public interface JoystickEventListener
{
    /**
     * Appelé régulièrement pour informé de l'état courant des boutons/axes du joystick
     *
     * @param joystick       Joystick auquel les événements se réfèrent
     * @param currentPressed Liste des Axe/boutons actuellement appuyés par l'utilisateur
     */
    public void joystickEventHappen(@NonNull JoystickEvent joystickEvent);
}

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

EventManager
Sélectionnez
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
Sélectionnez
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

Nouvelle hiérarchie des noeuds

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

import fr.developez.tutorial.java.dimension3.tool.NonNull;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Manageur de couleurs dédiées pour la capture d'objet.<br/>
 * Les couleurs sont séparés suffisament pour pouvoir les distinguées lors de la capture.
 */
class ColorPickingManager
{
    /**
     * Couleur de capture courante fournie au prochain objet qui en demande une
     */
    private static final AtomicInteger CURRENT_PICKING_COLOR = new AtomicInteger(0xFF_00_00_00);
    /**
     * Séparation entre les couleurs fournies
     */
    private static final int           PICKING_PRECISION     = 8;
    /**
     * Précision utilisée pour comparer une couleur de capture et une couleur capturée
     */
    private static final float         PICK_EPSILON          = 1e-5f;

    /**
     * Donne la couleur de capture courante et calcul la prochaine
     */
    @NonNull
    static Color4f newPickColor()
    {
        return new Color4f(
                ColorPickingManager.CURRENT_PICKING_COLOR.accumulateAndGet(ColorPickingManager.PICKING_PRECISION,
                                                                           Integer::sum));
    }

    /**
     * Indique si la couleur capturée correspond à une couleur de capture
     */
    static boolean isSamePickColor(float pickRed, float pickGreen, float pickBlue,
                                   @NonNull final Color4f color)
    {
        return Math.abs(pickRed - color.red) < ColorPickingManager.PICK_EPSILON
                && Math.abs(pickGreen - color.green) < ColorPickingManager.PICK_EPSILON
                && Math.abs(pickBlue - color.blue) < ColorPickingManager.PICK_EPSILON;
    }
}

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
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
161.
162.
163.
164.
165.
166.
167.
168.
169.
170.
171.
172.
173.
174.
175.
176.
177.
178.
179.
180.
181.
182.
183.
184.
185.
186.
187.
188.
189.
190.
191.
192.
193.
194.
195.
196.
197.
198.
199.
200.
201.
202.
203.
204.
205.
206.
207.
208.
209.
210.
211.
212.
213.
214.
215.
216.
217.
218.
219.
220.
221.
222.
223.
224.
225.
226.
227.
228.
229.
230.
231.
232.
233.
234.
235.
236.
237.
238.
239.
240.
241.
242.
243.
244.
245.
246.
247.
248.
249.
250.
251.
252.
253.
254.
255.
256.
257.
258.
package fr.developez.tutorial.java.dimension3.render;

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 java.util.Stack;
import org.lwjgl.opengl.GL11;

/**
 * Noeud virtuel de la scene
 */
public class Node3D
{
    @NonNull
    final Color4f pickColor = ColorPickingManager.newPickColor();

    @NonNull
    public final String name;

    /**
     * Coordonne X de la position dans la 3D
     */
    public float x = 0f;
    /**
     * Coordonne Y de la position dans la 3D
     */
    public float y = 0f;
    /**
     * Coordonne Z de la position dans la 3D
     */
    public float z = 0f;

    /**
     * Angle de la rotation autour de l'axe X
     */
    public float angleX = 0f;
    /**
     * Angle de la rotation autour de l'axe Y
     */
    public float angleY = 0f;
    /**
     * Angle de la rotation autour de l'axe Z
     */
    public float angleZ = 0f;

    /**
     * Facteur d'échelle le long dde l'axe X <br/>
     * La valeur doit toujours être strictement positive
     */
    public float scaleX = 1f;
    /**
     * Facteur d'échelle le long dde l'axe Y <br/>
     * La valeur doit toujours être strictement positive
     */
    public float scaleY = 1f;
    /**
     * Facteur d'échelle le long dde l'axe Z <br/>
     * La valeur doit toujours être strictement positive
     */
    public float scaleZ = 1f;

    /**
     * Liste des noeuds placés relativement à celui-ci
     */
    private final List<Node3D> children = new ArrayList<>();

    public Node3D(@NonNull final String name)
    {
        this.name = Objects.requireNonNull(name, "name must not be null");
    }

    /**
     * Ajoute un noeud enfant
     */
    public final void addChild(@NonNull final Node3D node)
    {
        Objects.requireNonNull(node, "node must not be null");

        // On synchronize pour éviter l'accès concurrent
        synchronized (this.children)
        {
            this.children.add(node);
        }
    }

    /**
     * Dessine le noeud
     */
    @ThreadOpenGL
    final void drawNode()
    {
        // On sauvegarde la matrice actuelle
        GL11.glPushMatrix();

        // *** Position du noeud ***
        // Position aux coordonnées X, Y, Z
        GL11.glTranslatef(this.x, this.y, this.z);
        // Rotation autour de l'axe X
        GL11.glRotatef(this.angleX, 1f, 0f, 0f);
        // Rotation autour de l'axe Y
        GL11.glRotatef(this.angleY, 0f, 1f, 0f);
        // Rotation autour de l'axe Z
        GL11.glRotatef(this.angleZ, 0f, 0f, 1f);
        // Application des facteurs d'échelle
        GL11.glScalef(this.scaleX, this.scaleY, this.scaleZ);

        // Dessin specific
        this.drawSpecific();

        // Placement des noeuds enfants relativement à la position de ce noeud
        // On synchronize pour éviter l'accès concurrent
        synchronized (this.children)
        {
            for (final Node3D child : this.children)
            {
                child.drawNode();
            }
        }

        // On restaure la matrice sauvegardée
        GL11.glPopMatrix();
    }

    /**
     * Dessine le noeud pour capturé sa couleur
     */
    @ThreadOpenGL
    final void drawNodePicking()
    {
        // On sauvegarde la matrice actuelle
        GL11.glPushMatrix();

        // *** Position du noeud ***
        // Position aux coordonnées X, Y, Z
        GL11.glTranslatef(this.x, this.y, this.z);
        // Rotation autour de l'axe X
        GL11.glRotatef(this.angleX, 1f, 0f, 0f);
        // Rotation autour de l'axe Y
        GL11.glRotatef(this.angleY, 0f, 1f, 0f);
        // Rotation autour de l'axe Z
        GL11.glRotatef(this.angleZ, 0f, 0f, 1f);
        // Application des facteurs d'échelle
        GL11.glScalef(this.scaleX, this.scaleY, this.scaleZ);

        this.pickColor.apply();

        // Dessin specific
        this.drawSpecificOnlyMesh();

        // Placement des noeuds enfants relativement à la position de ce noeud
        // On synchronize pour éviter l'accès concurrent
        synchronized (this.children)
        {
            for (final Node3D child : this.children)
            {
                child.drawNodePicking();
            }
        }

        // On restaure la matrice sauvegardée
        GL11.glPopMatrix();
    }


    /**
     * Cherche parmi ce noeud et ses descendants, celui dont le nom est donné en paramètre.<br/>
     * Si aucun noeud n'est trouvé, <b>null</b> sera retourné
     */
    public Node3D nodeByName(@NonNull final String name)
    {
        Objects.requireNonNull(name, "name must not be null");

        final Stack<Node3D> node3DStack = new Stack<>();
        node3DStack.push(this);
        Node3D node;

        while (!node3DStack.empty())
        {
            node = node3DStack.pop();

            if (name.equals(node.name))
            {
                return node;
            }

            for (final Node3D child : node.children)
            {
                node3DStack.push(child);
            }
        }

        return null;
    }

    /**
     * Cherche parmi ce noeud et ses descendants, celui qui correspond à la couleur capturée de la fenêtre.<br/>
     * Si aucun noeud n'est trouvé, <b>null</b> sera retourné
     */
    Node3D nodeByPickColor(float pickRed, float pickGreen, float pickBlue)
    {
        final Stack<Node3D> node3DStack = new Stack<>();
        node3DStack.push(this);
        Node3D node;

        while (!node3DStack.empty())
        {
            node = node3DStack.pop();

            if (ColorPickingManager.isSamePickColor(pickRed, pickGreen, pickBlue, node.pickColor))
            {
                return node;
            }

            for (final Node3D child : node.children)
            {
                node3DStack.push(child);
            }
        }

        return null;
    }


    /**
     * Dessin supplémentaire pour le noeud
     * <p>
     * A surcharger par les classes enfants
     * <p>
     * Ne fait rien par défaut
     */
    @ThreadOpenGL
    void drawSpecific()
    {
        // Rien à faire d'autres. Les classes enfants y mettrons leur code spécifique
    }

    /**
     * Dessin supplémentaire pour le noeud n'affichant que les mailles
     * <p>
     * A surcharger par les classes enfants
     * <p>
     * Ne fait rien par défaut
     */
    @ThreadOpenGL
    void drawSpecificOnlyMesh()
    {
        // Rien à faire d'autres. Les classes enfants y mettrons leur code spécifique
    }

    @Override
    public final String toString()
    {
        return this.getClass()
                   .getName() + " : " + this.name;
    }
}
DrawableNode
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
package fr.developez.tutorial.java.dimension3.render;

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

/**
 * Noeud ayant une représentation visuelle à l'écran
 */
public abstract class DrawableNode
        extends Node3D
{
    protected Color4f color = Color4f.GREY;

    @NonNull
    public final Color4f getColor()
    {
        return this.color;
    }

    public final void setColor(@NonNull final Color4f color)
    {
        this.color = Objects.requireNonNull(color, "color must not be null");
    }

    public DrawableNode(@NonNull final String name)
    {
        super(name);
    }
}
Object3D
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
package fr.developez.tutorial.java.dimension3.render;

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

/**
 * Objet 3D
 */
public class Object3D
        extends DrawableNode
{
    @NonNull
    public final Mesh    mesh  = new Mesh();

    public Object3D(@NonNull final String name)
    {
        super(name);
    }


    @ThreadOpenGL
    @Override
    void drawSpecific()
    {
        this.color.apply();
        this.mesh.drawMesh();
    }

    @ThreadOpenGL
    @Override
    void drawSpecificOnlyMesh()
    {
        this.mesh.drawMesh();
    }
}
Clone3D
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
package fr.developez.tutorial.java.dimension3.render;

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

/**
 * Clone d'un objet 3D
 */
public class Clone3D
        extends DrawableNode
{
    private final Object3D cloned;

    public Clone3D(@NonNull final String name, @NonNull final Object3D cloned)
    {
        super(name);
        this.cloned = Objects.requireNonNull(cloned, "cloned must not be null");
    }

    @ThreadOpenGL
    @Override
    void drawSpecific()
    {
        this.color.apply();
        // Ici on utilise la maille de l'objet cloné
        this.cloned.mesh.drawMesh();
    }

    @ThreadOpenGL
    @Override
    void drawSpecificOnlyMesh()
    {
        // Ici on utilise la maille de l'objet cloné
        this.cloned.mesh.drawMesh();
    }
}

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

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

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.DoubleBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;

/**
 * Buffers réservés en mémoire.<br/>
 * Comme les buffers seront utilisés séquentiellement au sein du même thread, le thread OpenGL,
 * on peut réutiliser toujours le même. <br/>
 * Cela évite des problèmes de mémoire et du temps d'allocation/libération.<br/>
 * On va fixer ce buffer à une taille suffisamment assez grande pour une grande texture
 */
class BufferTools
{
    /**
     * Largeur maximum d'une texture
     */
    static final int          MAX_WIDTH               = 4096;
    /**
     * Hauteur maximum d'une texture
     */
    static final int          MAX_HEIGHT              = 4096;
    /**
     * Taille maximum du buffer en pixels
     */
    static final int          MAX_DIMENSION           = BufferTools.MAX_WIDTH * BufferTools.MAX_HEIGHT;
    /**
     * Taille maximum du buffer en octets
     */
    static final int          MAX_DIMENSION_IN_BYTES  = BufferTools.MAX_DIMENSION << 2;
    /**
     * Le buffer réservé sous forme d'un buffer d'octets
     */
    static final ByteBuffer   TEMPORARY_BYTE_BUFFER   =
            ByteBuffer.allocateDirect(BufferTools.MAX_DIMENSION_IN_BYTES)
                      .order(ByteOrder.nativeOrder());
    /**
     * Le buffer vu comme un buffer d'entiers
     */
    static final IntBuffer    TEMPORARY_INT_BUFFER    =
            BufferTools.TEMPORARY_BYTE_BUFFER.asIntBuffer();
    /**
     * Le buffer vu comme un buffer de floats
     */
    static final FloatBuffer  TEMPORARY_FLOAT_BUFFER  =
            BufferTools.TEMPORARY_BYTE_BUFFER.asFloatBuffer();
    /**
     * Le buffer vu comme un buffer de doubles
     */
    static final DoubleBuffer TEMPORARY_DOUBLE_BUFFER =
            BufferTools.TEMPORARY_BYTE_BUFFER.asDoubleBuffer();
}

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

Render3D
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
161.
162.
163.
164.
165.
166.
167.
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
{
    private static final boolean LIGHT_MANAGED = false;

    /**
     * Graphe de scène associé
     */
    @NonNull
    final SceneGraph sceneGraph = new SceneGraph();
    int pickX = -1;
    int pickY = -1;

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

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

        // Tant que la fenêtre est présente, rafraichir la 3D
        while (!GLFW.glfwWindowShouldClose(window))
        {
            // Dessine la frame courante
            pickedNode = this.drawScene();

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

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

            EventManager.EVENT_MANAGER.eventProcess(pickedNode);
        }

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

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

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

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

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

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

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

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

        // Active l'éclairage
        if (Render3D.LIGHT_MANAGED)
        {
            GL11.glEnable(GL11.GL_LIGHTING);
        }
    }

    /**
     * Dessine la scène 3D
     */
    @ThreadOpenGL
    private Node3D drawScene()
    {
        Node3D    pickedNode = null;
        final int pickX      = this.pickX;
        final int pickY      = this.pickY;

        if (pickX >= 0 && pickY >= 0)
        {
            // Si on doit capturer un objet, on dessine la scène en mode picking
            if (Render3D.LIGHT_MANAGED)
            {
                GL11.glDisable(GL11.GL_LIGHTING);
            }

            GL11.glDisable(GL11.GL_CULL_FACE);
            GL11.glClearColor(1f, 1f, 1f, 1f);
            GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT);

            // Dessine la frame courante en mode capture d'un noeud
            pickedNode = this.sceneGraph.pickNode(pickX, pickY);

            if (Render3D.LIGHT_MANAGED)
            {
                GL11.glEnable(GL11.GL_LIGHTING);
            }

            GL11.glEnable(GL11.GL_CULL_FACE);
            // Nettoyage de la fenêtre en buffer pour la prochaine frame
        }

        this.sceneGraph.drawScene();

        return pickedNode;
    }
}

Changements au sein du graphe de scène

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

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

/**
 * Le graphe de scène
 */
public class SceneGraph
{
    /**
     * Nom du noeud racine, ne pas utilisé pour les autres noeuds
     */
    public static final String ROOT_NODE_NAME = "-<(ROOT_NODE)>-";

    @NonNull
    public final Node3D  root            = new Node3D(SceneGraph.ROOT_NODE_NAME);
    private      Color4f backgroundColor = Color4f.WHITE;

    public SceneGraph()
    {
    }

    @NonNull
    public Color4f getBackground()
    {
        return this.backgroundColor;
    }

    public void setBackground(@NonNull final Color4f backgroundColor)
    {
        this.backgroundColor = Objects.requireNonNull(backgroundColor, "backgroundColor must not be null");
    }

    @ThreadOpenGL
    void drawScene()
    {
        GL11.glClearColor(this.backgroundColor.red, this.backgroundColor.green, this.backgroundColor.blue, 1f);
        // Nettoyage de la fenêtre en buffer pour la prochaine frame
        GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT);

        this.root.drawNode();
    }

    /**
     * Dessine la scène en mode picking et retourne le noeud capturé ou <b>null</b> si il n'y en a pas
     */
    @ThreadOpenGL
    Node3D pickNode(int pickX, int pickY)
    {
        // Dessine la scène en mode picking
        this.root.drawNodePicking();

        // On capture la couleur sous les coordonnées du picking
        // On se place au début du buffer
        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();
        // On cherche et renvoie le noeud dont la couleur de capture correspond.
        // Si il n'y a pas de tel noeud, null est renvoyé.
        return this.root.nodeByPickColor(red, green, blue);
    }
}

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

import fr.developez.tutorial.java.dimension3.render.DrawableNode;
import fr.developez.tutorial.java.dimension3.render.Window3D;
import fr.developez.tutorial.java.dimension3.tool.NonNull;

/**
 * Observateur des événement de picking.<br/>
 * 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)},
 * {@link #onPickedNode(DrawableNode)} sera appelé à chaque nouvel objet détecté.<br/>
 * <br/>
 * 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()},
 * {@link #nothingPicked()} est appelé une fois.<br/>
 * Il ne sera appelé de nouveau que si un objet est de nouveau détecté puis reperdu.
 */
@FunctionalInterface
public interface PickingObserver
{
    /**
     * Appelé dés qu'il n'y a plus d'objets détecté<br/>
     * Ne fait rien par défaut
     */
    public default void nothingPicked()
    {
    }


    /**
     * Appelé dés qu'un nouvel objet est détecté
     *
     * @param drawableNode Objet détecté
     */
    public void onPickedNode(@NonNull final DrawableNode drawableNode);
}
EventManager
Sélectionnez
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
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
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;
import fr.developez.tutorial.java.dimension3.mouse.MouseStatusEvent;
import fr.developez.tutorial.java.dimension3.picking.PickingObserver;
import fr.developez.tutorial.java.dimension3.render.Color4f;
import fr.developez.tutorial.java.dimension3.render.DrawableNode;
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::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);

        EventManager.EVENT_MANAGER.register(new PickingObserver()
        {
            @Override
            public void onPickedNode(DrawableNode drawableNode)
            {
                drawableNode.setColor(Color4f.LIGHT_BLUE);
            }

            @Override
            public void nothingPicked()
            {
                Main.box.setColor(Color4f.LIGHT_GREEN);
            }
        });
    }

    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;
            }
        }
    }

    private static void mouseInsideWidowChanged(boolean insideWindow)
    {
        if (insideWindow)
        {
            System.out.println("*** INSIDE ***");
        }
        else
        {
            Main.window3D.stopPicking();
            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.window3D.pickNode((int) mouseStatusEvent.x, (int) mouseStatusEvent.y);

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

    private static void currentKeyDown(@NonNull int[] keys)
    {
        for (final int key : keys)
        {
            switch (key)
            {
                case GLFW.GLFW_KEY_UP:
                    Main.box.y += Main.TRANSLATE_STEP;
                    break;
                case GLFW.GLFW_KEY_DOWN:
                    Main.box.y -= Main.TRANSLATE_STEP;
                    break;
                case GLFW.GLFW_KEY_RIGHT:
                    Main.box.x += Main.TRANSLATE_STEP;
                    break;
                case GLFW.GLFW_KEY_LEFT:
                    Main.box.x -= Main.TRANSLATE_STEP;
                    break;
            }
        }
    }
}

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

 
Sélectionnez
        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.

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

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2020 Gérard Bourriaud. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.