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 .
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 :
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
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.
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
package
fr.developez.tutorial.java.dimension3.render;
import
fr.developez.tutorial.java.dimension3.thread.ThreadManager;
import
fr.developez.tutorial.java.dimension3.tool.NonNull;
import
fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;
import
java.nio.IntBuffer;
import
java.util.Objects;
import
org.lwjgl.glfw.Callbacks;
import
org.lwjgl.glfw.GLFW;
import
org.lwjgl.glfw.GLFWErrorCallback;
import
org.lwjgl.glfw.GLFWVidMode;
import
org.lwjgl.opengl.GL;
import
org.lwjgl.system.MemoryStack;
import
org.lwjgl.system.MemoryUtil;
public
class
Window3D
{
// …
@ThreadOpenGL
private
void
startOpenGLThread
(
)
{
// …
if
(
this
.window ==
MemoryUtil.NULL)
{
// La fenêtre n'a pas pu être réservée en mémoire, on ne peut pas continuer
this
.closeGLFW
(
);
System.err.println
(
"GLFW can't create window"
);
return
;
}
// Écoute les événements claviers
GLFW.glfwSetKeyCallback
(
this
.window, EventManager.EVENT_MANAGER.keyCallBack);
// Management de la fermeture de la fenêtre pour libérer proprement la mémoire
GLFW.glfwSetWindowCloseCallback
(
this
.window, this
.window3DCloseEventManager);
// …
}
// …
}
Maintenant que notre écouteur est branché, expliquons en ses paramètres.
final
GLFWKeyCallbackI keyCallBack =
(
long
window, int
key, int
scanCode, int
action, int
modifiers) ->
Paramétre |
Explication |
window |
Pointeur opaque représentant la fenêtre qui a capturé l’événement clavier. |
key |
Code de la touche qui a déclenché l’événement. |
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. |
Bit 4 |
GLFW_MOD_CAPS_LOCK |
La touche verrouillage majuscule. |
Bit 3 |
GLFW_MOD_SUPER |
Cette touche correspond sous windows à la touche windows, sous mac : pomme ou command. Comme cette touche correspond souvent à une action système, sa capture peut être absorbée par l'action qui se déclenche. |
Bit 2 |
GLFW_MOD_ALT |
Touche Alt |
Bit 1 |
GLFW_MOD_CONTROL |
Touche control |
Bit 0 |
GLFW_MOD_SHIFT |
Touche shift |
A savoir que la touche AtlGr sera vue comme si Alt et control étaient appuyés en même temps.
Par exemple pour faire une action si control est enfoncé :
if
((
modifiers &
GLFW.GLFW_MOD_CONTROL)!=
0
)
{
// Action à faire si control est enfoncé
}
Maintenant que nous savons ce que nous recevons, décidons de ce que nous voulons faire des événements clavier.
On pourrait se contenter de créer un écouteur qui redirige ces informations de manière brute, mais et laisser l’utilisateur du moteur le soin d’y mettre sa propre logique. Mais ici nous allons un peu plus loin en y ajoutant un prétraitement.
L’idée est d’envoyé au travers de notre écouteur la liste des touches en train d’être appuyées. Ainsi, quand l’utilisateur recevra l’événement, il pourra facilement réagir à l’événement.
C’est pour cela, que nous exposerons l’écouteur suivant :
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.
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 :
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.
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 :
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.01
f;
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 =
-
2
f;
Main.box.angleX =
22.5
f;
Main.box.scaleX =
2
f;
Main.box.scaleZ =
0.5
f;
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
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
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.
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);
}
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)
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)
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
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
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)
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;
}
;
// …
}
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
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
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
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
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
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.5
f;
private
static
final
float
TRANSLATE_STEP =
0.01
f;
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 =
-
2
f;
Main.box.angleX =
22.5
f;
Main.box.scaleX =
2
f;
Main.box.scaleZ =
0.5
f;
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 :
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.
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
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
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)
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.
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.25
f)
{
this
.joystickElementsStatus.put
(
JoystickCode.obtainAxis
(
axeNumber, true
),
JoystickStatus.RELEASED);
this
.joystickElementsStatus.put
(
JoystickCode.obtainAxis
(
axeNumber, false
),
JoystickStatus.PRESSED);
}
else
if
(
axesValue[axeNumber] >
0.25
f)
{
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
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.
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.
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;
}
}
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);
}
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;
}
}
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
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.
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.01
f;
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 =
-
2
f;
Main.box.angleX =
22.5
f;
Main.box.scaleX =
2
f;
Main.box.scaleZ =
0.5
f;
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 :
- On dessine chaque objet d’une couleur différente
- On capture la couleur sous les coordonnées demandées
- On dessine vraiment la scène
- On échange les buffer
- Ensuite avec la couleur capturée on déduit l’objet capturé puisque sa couleur était unique.
On a récupéré notre objet sans calcul complexe. On ne voit pas à l’écran le premier dessin des objets colorer, car il sera remplacé par le dessin de la vraie scène avant l’échange des buffers.
III-B. Mise en place du picking▲
III-B-1. Attribution du nom et d’une couleur de picking aux nœuds▲
Ce que nous captureront ce sont soit des objets, soit des clones. Les nœuds n’ayant pas de représentation 3D, on ne les capturera pas.
Afin d ‘éviter de faire un caste pour cahger la couleur d’un objet, on va ajouter un objet intermédiaire entre le nœud et les objet ou les clones. Cet objet représentant les objets qui ont une représentation à l’écran.
La nouvelle hiérarchie sera
Afin de repérer plus facilement un nœud, nous allons ajouter lui ajouter un nom.
Comme nous voulons utiliser la technique de picking sur les nœuds, on leur attribuera une couleur de picking unique à leur création.
On va gérer la couleur de picking avec l’utilitaire suivant :
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-5
f;
/**
* 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.
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 =
0
f;
/**
* Coordonne Y de la position dans la 3D
*/
public
float
y =
0
f;
/**
* Coordonne Z de la position dans la 3D
*/
public
float
z =
0
f;
/**
* Angle de la rotation autour de l'axe X
*/
public
float
angleX =
0
f;
/**
* Angle de la rotation autour de l'axe Y
*/
public
float
angleY =
0
f;
/**
* Angle de la rotation autour de l'axe Z
*/
public
float
angleZ =
0
f;
/**
* Facteur d'échelle le long dde l'axe X
<
br/
>
* La valeur doit toujours être strictement positive
*/
public
float
scaleX =
1
f;
/**
* Facteur d'échelle le long dde l'axe Y
<
br/
>
* La valeur doit toujours être strictement positive
*/
public
float
scaleY =
1
f;
/**
* Facteur d'échelle le long dde l'axe Z
<
br/
>
* La valeur doit toujours être strictement positive
*/
public
float
scaleZ =
1
f;
/**
* 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, 1
f, 0
f, 0
f);
// Rotation autour de l'axe Y
GL11.glRotatef
(
this
.angleY, 0
f, 1
f, 0
f);
// Rotation autour de l'axe Z
GL11.glRotatef
(
this
.angleZ, 0
f, 0
f, 1
f);
// 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, 1
f, 0
f, 0
f);
// Rotation autour de l'axe Y
GL11.glRotatef
(
this
.angleY, 0
f, 1
f, 0
f);
// Rotation autour de l'axe Z
GL11.glRotatef
(
this
.angleZ, 0
f, 0
f, 1
f);
// 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;
}
}
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);
}
}
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
(
);
}
}
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
GL11.glReadPixels
(
pickX, pickY,
1
, 1
,
GL11.GL_RGBA, GL11.GL_FLOAT,
BufferTools.TEMPORARY_FLOAT_BUFFER);
Dans l’ordre des paramètres :
- (pickX, pickY) sont les coordonnées du point dont on souhaite récupéré la couleur
- 1 x 1 sont respectivement la largeur et la hauteur de la zone à capturer, ici 1 pixel
- GL11.GL_RGBA Indique que l’on souhaite la couleur sous la format rouge, vert, bleu, opacité
- GL11.GL_FLOAT indique que chaque composante couleur sera sous forme de float entre 0 et 1
- 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.
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
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.01
f);
// 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
(
1
f, 1
f, 1
f, 1
f);
//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
(
1
f, 1
f, 1
f, 1
f);
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
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, 1
f);
// 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.
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.
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);
}
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.
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.5
f;
private
static
final
float
TRANSLATE_STEP =
0.01
f;
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 =
-
2
f;
Main.box.angleX =
22.5
f;
Main.box.scaleX =
2
f;
Main.box.scaleZ =
0.5
f;
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
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.