Retour table des matières Java 3D
Nous avons vu jusqu'à présent des scènes 3D statiques sans mouvement, sans animation. Nous allons voir dans ce chapitre comment animer une scène 3D et en particulier comment faire réagir un objet 3D ou la caméra à l'action de l'utilisateur.
1. Interaction avec la souris
Java 3D fournit ce que l'on appelle des comportements, c'est à dire des classes gérant des mécanismes d'interaction entre l'action de l'utilisateur (clavier ou souris) et la scène 3D.
Nous allons étudier dans un premier exemple, 3 comportements de base qui répondent à une action de la souris et que fournit Java 3D. Ces 3 comportements sont décrits par les classes : MouseRotate, MouseTranslate et MouseZoom. Ces 3 classes héritent de la classe Behavior comme tous les comportements en Java 3D.
- Le comportement MouseRotate permet de faire tourner un objet 3D autour des axes X et Y du repère 3D :
Un mouvement vertical de la souris en maintenant le bouton gauche enfoncé fait tourner l'objet autour de X.
Un mouvement horizontal de la souris en maintenant la bouton gauche enfoncé fiat tourner l'objet autour de Y.
Le constructeur de la classe MouseRotate que nous allons utiliser dans notre exemple est le suivant :
public MouseRotate(TransformGroup transformGroup)
transformGroup est le groupe de transformation auquel va s'appliquer le comportement.
- Le comportement MouseTranslate permet de déplacer un objet selon les axes X et Y du repère 3D :
Un mouvement vertical de la souris en maintenant le bouton droit enfoncé fait se déplacer l'objet 3D le long de l'axe Y.
Un mouvement horizontal de la souris en maintenant le bouton droit enfoncé fait se déplacer l'objet 3D le long de l'axe X.
Le constructeur de la classe MouseTranslate que nous allons utiliser dans notre exemple est le suivant :
public MouseTranslate(TransformGroup transformGroup)
transformGroup est le groupe de transformation auquel va s'appliquer le comportement.
- Le comportement MouseZoom permet de zoomer ou dezoomer sur un objet 3D selon la direction de l'axe Z du repère 3D.
Un mouvement vertical vers le bas de la souris avec le bouton gauche enfoncé et la touche ALT enfoncée permet de zoomer sur l'objet 3D (déplacement selon l'axe des Z dans la direction des Z > 0)
Un mouvement vertical vers le haut de la souris avec le bouton gauche enfoncé et la touche ALT enfoncée permet de s'éloigner de l'objet 3D (déplacement selon l'axe des Z dans la direction des Z < 0).
Le constructeur de la classe MouseZoom que nous allons utiliser dans notre exemple est le suivant :
public MouseZoom(TransformGroup transformGroup)
transformGroup est le groupe de transformation auquel va s'appliquer le comportement.
Pour illustrer ces 3 comportements, nous allons construire une scène 3D toute simple qui contient un cube couleur construit à l'aide de la classe utilitaire ColorCube.
L'idée consiste à créer un groupe de transformation qui va contenir le cube et qui sera soumis aux 3 comportements. Ce sont ces comportements qui vont donc agir sur le cube en modifiant le groupe de transformation auquel il appartient. Il convient donc impérativement de donner les bonnes capabilités en écriture et en lecture au groupe de transformation :
// Creation du groupe de transformation sur lequel vont s'appliquer les
// comportements
TransformGroup mouseTransform = new TransformGroup();
// Le groupe de transformation sera modifie par le comportement de la
// souris
mouseTransform.setCapability(TransformGroup.ALLOW_TRANSFORM_READ);
mouseTransform.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
Nous construisons ensuite les 3 comportements, sans oublier les limites d'influence (méthode setSchedulingBounds() héritée de la classe Behavior) :
// Creation de l'objet parent qui contiendra tous les autres objets 3D
BranchGroup parent = new BranchGroup();
// Creation comportement rotation a la souris
MouseRotate rotate = new MouseRotate(mouseTransform);
rotate.setSchedulingBounds(new BoundingSphere());
parent.addChild(rotate);
// Creation comportement deplacement a la souris
MouseTranslate translate = new MouseTranslate(mouseTransform);
translate.setSchedulingBounds(new BoundingSphere());
parent.addChild(translate);
// Creation comportement zoom a la souris
MouseZoom zoom = new MouseZoom(mouseTransform);
zoom.setSchedulingBounds(new BoundingSphere());
parent.addChild(zoom);
Il est interessant à ce niveau de dessiner le graphe de scène de notre exemple afin de bien montrer où se situent les comportements dans l'arborescence :
Arborescence d'une scène avec 3 comportements de base
Le fichier MouseBehaviorTest.java présente le code source complet de cet exemple.
Exécutez l'applet MouseBehaviorTest.html pour voir le résultat.
Nous allons voir dans ce paragraphe qu'il est possible de régler la sensibilité de la souris par rapport au mouvement imprimé à notre objet 3D. Pour cela nous pouvons utiliser la méthode setFactor() des classes MouseRotate, MouseTranslate et MouseRotate :
public void setFactor(double factor)
factor est un facteur multiplicatif du mouvement selon les axes X (MouseRotate, MouseTranslate), Y (MouseRotate, MouseTranslate) et Z (MouseZoom).
Par défaut, ce facteur est de 0.03 pour un objet de type MouseRotate, 0.02 pour un objet de type MouseTranslate et 0.04 pour un objet de type MouseZoom
L'exemple de ce paragraphe définit les 3 comportements de base de la souris avec des sensibilités des valeurs par défaut que l'on peut ajuster avec un curseur.
Le fichier MouseSensibilite.java présente le code source complet de cet exemple.
Exécutez l'applet MouseSensibilite.html pour voir le résultat.
Nous allons voir dans ce paragraphe que nous pouvons naviguer dans la scène 3D sans que la position de l'objet ne soit modifiée. C'est la caméra elle-même que nous allons déplacer grâce à la souris.
Pour cela, nous allons récupérer le groupe de
transformation associé à la caméra et non pas à l'objet 3D en appelant en cascade les méthodes suivantes sur un objet de type SimpleUniverse :
TransformGroup tg = su.getViewingPlatform().getViewPlatformTransform();
Ensuite, nous allons construire notre comportement de la façon suivante :
MouseRotate mouseRotate = new MouseRotate(MouseBehavior.INVERT_INPUT);
mouseRotate.setFactor(0.005);
mouseRotate.setTransformGroup(tg);
On utilise le constructeur de MouseRotate avec le champ MouseBehavior.INVERT_INPUT car un mouvement de la souris vers la droite doit faire tourner la caméra vers la droite et par conséquent l'objet 3D vers la gauche.
On diminue également la sensibilité du déplacement pour que le mouvement de la caméra soit plus lent et enfin on associe le groupe de transformation tg au comportement mouseRotate.
Le fichier MouseNavigation.java présente le code source complet de cet exemple.
Exécutez l'applet MouseNavigation.html pour voir le résultat.
2. Interaction avec le clavier
Le comportement de base du clavier que nous allons étudier ici n'influe pas sur la position de l'objet mais sur celle de la caméra.
La première difficulté va donc consister à récupérer le groupe de transformation associé à la caméra et non plus à l'objet, exactement comme nous venons juste de le faire dans l'exemple précédent. C'est ensuite ce groupe de transformation qui sera modifié par l'action de l'utilisateur sur les touches du clavier.
Les actions possibles du clavier sur la caméra sont les suivantes :
- Flèches gauche et droite : rotation de la caméra autour de l'axe Y
- Flèches haut et bas : déplacement de la caméra selon l'axe des Z
- Touches PgUp et PgDown : rotation de la caméra autour de l'axe X
- Touche ALT + flèches gauche et droite : déplacement de la caméra selon l'axe des X
- Touche ALT + touches PgUp et PgDown : déplacement de la caméra selon l'axe des Y
- La touche majuscule (Shift) permet d'accélérer les 5 actions énumérées ci-dessus.
- La touche = permet de réinitialiser la position de la caméra au centre de la vue.
Nous allons étudier un exemple simple mettant en oeuvre ce comportement de base :
Pour récupérer une référence de ce groupe de transformation, il faut appeler en cascade les méthodes suivantes sur un objet de type SimpleUniverse :
TransformGroup tg = simpleUniverse.getViewingPlatform().getViewPlatformTransform();
KeyNavigatorBehavior keyNavigatorBehavior = new KeyNavigatorBehavior(tg);
Le comportement de base au clavier est donc maintenant associé au groupe de transformation correspondant à la position de la caméra.
Le fichier KeyBehaviorTest.java présente le code source complet de cet exemple.
Exécutez l'applet KeyBehaviorTest.html pour voir le résultat.
Remarque :
Il y a deux bugs dans la version Windows de Java 3D, bugs qui sont toujours présents dans la dernière version 1.3.2 mais que je n'ai pas observés dans la version Linux.
Premièrement, lorsque l'on appuie sur les touches du clavier dans un délai compris entre 0 et 2 secondes après le lancement de l'application, notre objet 3D disparaît carrément de la scène 3D.
Deuxièmement, si l'on appuie puis relache la touche ALT tout en maintenant une autre touche pour déplacer l'objet 3D, on constate que le mouvement de l'objet devient perpétuel et qu'on ne peut plus l'arrêter !!!
Nous verrons dans le paragraphe suivant comment corriger ces deux bugs.
Contrairement à la souris, il n'existe pas pour le clavier de méthode setFactor() pour la classe KeyNavigatorBehavior permettant de régler la sensibilité. Nous allons donc devoir réécrire cette classe.
Malheureusement, nous ne pouvons pas étendre cette classe car la seule façon de régler la sensibilité est d'avoir accès à un champ privé de la classe KeyNavigatorBehavior qui est l'objet keyNavigator de type KeyNavigator.
C'est dans cette classe KeyNavigator que nous allons également corriger les deux bugs que nous avons mentionnés dans le paragraphe précédent.
Pour régler la sensibilité du clavier, il faut donc que l'on réécrive deux classes :
KeyNavigator et KeyNavigatorBehavior.
Ces deux classes se trouvent dans le package com.sun.j3d.utils.behaviors.keyboard de Java 3D.
- La classe KeyNavigator
Le mouvement de la caméra est indirectement calculé à partir de l'écart de temps qu'il y a entre 2 images (ou frames) consécutives. C'est l'appui sur les touches du clavier qui impriment une accélération. Ensuite, les physiciens se rappeleront sans problème que la vitesse et la position de l'objet (ici la caméra) se calculent à partir de l'accélération.
Ainsi, pour régler la sensibilité du clavier, une méthode consiste à modifier cet écart de temps entre 2 frames consécutives. Cet écart de temps est une variable privée nommée deltaTime.
L'idée consiste donc à introduire une nouvelle variable dans la classe KeyNavigator que nous allons appeler speedFactor et le fait de modifier deltaTime par ce facteur speedFactor permettra de régler la sensibilité de la navigation au clavier.
Nous allons donc remplacer deltaTime par deltaTime*speedFactor dans une méthode qui sera appelée à chaque fois qu'une frame sera rendue par le moteur de Java 3D. Cette méthode est la méthode integrateTransformChanges() de la classe KeyNavigator.
La libriairie originale contient l'instruction :
deltaTime *= 0.001;
que nous allons remplacer par :
deltaTime *= 0.001 * Math.abs(speedFactor);
Pour introduire ce facteur speedFactor, il est nécessaire de rajouter une méthode (publique) dans la classe KeyNavigator que nous avons intitulée setSpeedFactor() :
public void setSpeedFactor(float speedFactor) {
this.speedFactor = speedFactor;
}
- La classe KeyNavigatorBehavior
C'est bien entendu dans la classe KeyNavigatorBehavior que nous allons appeler la méthode setSpeedFactor() sur l'objet keyNavigator en rajoutant également une méthode publique setSpeedFactor() dans la classe KeyNavigatorBehavior cette fois :
public void setSpeedFactor(float speedFactor) {
keyNavigator.setSpeedFactor(speedFactor);
}
Il suffira ensuite d'appeler cette méthode setSpeedFactor() dans notre programme exactement comme nous avions appelé la méthode setFactor() pour les classes MouseRotate, MouseTranslate et MouseZoom.
Avant de passer à un exemple illustrant la sensibilité réglable du clavier, je vous propose de supprimer les 2 bugs que nous avions énoncés à la fin du paragraphe précédent car cette correction peut être faite dans la classe KeyNavigator.
- Correction des 2 bugs dans la classe KeyNavigator
Le premier bug concernait la disparition de l'objet 3D si on utilisait les touches du clavier moins de 2 secondes après le lancement de l'application. Ce problème (sous Windows) est dû au mauvais calcul de la variable deltaTime qui est fait dans la méthode getDeltaTime() de la classe KeyNavigator.
Cette méthode getDeltaTime() est appelée à chaque fois qu'une image est affichée (rendue) dans le canvas 3D, et c'est lors du premier appel que ce produit l'erreur.
Le code original de Sun donne :
private long getDeltaTime() {
long newTime = System.currentTimeMillis();
long deltaTime = newTime - time;
time = newTime;
if (deltaTime > 2000)
return 0;
else
return deltaTime;
}
Or si on appuie sur une touche du clavier 1,9 seconde après le début du lancement de l'application, deltaTime vaudra 1900 (millisecondes) ce qui est une valeur énorme (le temps de rendu entre 2 frames consécutives est de l'ordre de quelques millisecondes) et le programme ne passera pas le test suivant : if (deltaTime > 2000).
C'est pourquoi nous avons réécrit cette méthode getDeltaTime() :
private long getDeltaTime() {
long newTime = System.currentTimeMillis();
long deltaTime = newTime - time;
time = newTime;
if (firstTime) {
deltaTime = 0;
firstTime = false;
}
if (deltaTime > 2000)
return 0;
else
return deltaTime;
}
Nous avons simplement rajouté un attribut firstTime initialisé à true et qui devient false dès que la première image de notre application 3D a été affichée. Et lors du calcul de cette première image, on force deltaTime à 0.
Ainsi, ce premier bug dû au mauvais calcul de deltaTime disparaît.
Le second bug se produit si l'on appuie et relache la touche ALT pendant que les flèches de direction sont utilisées : le mouvement de la caméra devient perpétuel !!
Une façon simple de corriger ce bug consiste à arrêter le mouvement de la caméra en réinitialisant l'attribut key_state à 0 lorsqu'on relache la touche ALT. Ceci doit se faire dans la méthode publique processKeyEvent() de KeyNavigator :
case KeyEvent.VK_ALT: key_state = 0; break;
Remarque :
On constate qu'une fois la touche ALT relachée la caméra reste immobile et que les touches du clavier n'agissent plus : il faut cliquer une fois dans la fenêtre de notre application 3D pour réactiver le mouvement (alors qu'avec la version originale de Sun de la classe KeyNavigator le mouvement de la caméra devenait perpétuel). Ceci est dû au système de fenêtrage de Windows qui intercepte la touche ALT. Notre bug n'est donc qu'à motié corrigé ...
Nous avons écrit un petit exemple où nous utilisons les classes CustomKeyNavigator et CustomKeyNavigatorBehavior en lieu et place des classes KeyNavigator et KeyNavigatorBehavior fournie par Java 3D. La sensibilité du clavier peut se régler par un curseur.
Les fichiers KeyboardSensibilite.java, CustomKeyNavigator.java et CustomKeyNavigatorBehavior.java présentent le code source complet de cet exemple.
Exécutez l'applet KeyboardSensibilite.html pour voir le résultat.
3. Comportements personnalisés
Nous avons vu dans les paragraphes précédents que Java 3D fournit des comportements de base prédéfinis permettant de manipuler des objets 3D ou la caméra elle même.
Pour mieux comprendre le mécanisme de fonctionnement de ces comportements, nous allons étudier dans ce paragraphe plusieurs comportement personnalisés.
Un comportement personnalisé très simple
Dans ce premier exemple simple, nous allons créer un comportement permettant de faire tourner un objet 3D autour de l'axe X en appuyant sur les flèches UP et DOWN (du clavier) et autour de l'axe Y en appuyant sur les flèches LEFT et RIGHT.
La première chose à faire avant d'écrire un comportement personnalisé est de définir à quelle condition va se déclencher le comportement. Une condition peut être constituée de un ou plusieurs critères. Les critères possibles sont définies dans les sous-classes de la classe abstraite javax.media.j3d.WakeupCriterion. On se reportera à la documentation de Java 3D pour une description précise de chacun de ces critères. Dans l'exemple qui concerne ce paragraphe nous étudierons le critère WakeupOnAWTEvent qui correspond à un évènement AWT (souris ou clavier). Rassurez-vous, nous étudierons bien d'autres critères dans ce chapitre.
Comme nous l'avons dit, une condition qui va déclencher le comportement peut être constituée d'un seul critère ou d'une combinaison logique de plusieurs critères. Les combinaisons logiques de ces critères sont définies dans les sous-classes de la classe abstraite WakeupCondition. Les deux combinaisons les plus utilisées sont :
WakeupAnd : le comportement est déclenché si tous les critères sont vérifiés
WakeupOr : le comportement est déclenché si l'un des critère au moins est vérifié.
Dans l'exemple de ce paragraphe, notre comportement sera déclenché si un seul critère (WakeupOnAWTEvent) est vérifié, nous n'utiliserons pas les conditions. Toutefois, les conditions sont très utiles pour décrire les collisions, nous en verrons en exemple dans le paragraphe consacré aux collisions d'objets.
Voici les étapes à suivre pour créer un comportement personnalisé très simple réagissant à un seul et unique critère :
La première chose à faire consiste à écrire une classe dérivée qui va étendre la classe abstraite Behavior.
Cette classe abstraite possède deux méthodes abstraites qui sont initialize() et processStimulus() que nous allons devoir redéfinir.
Cependant, avant de parler de ces deux méthodes à redéfinir, il est nécessaire de créer notere critère qui va déclencher le comportement :
WakeupCondition wakeupCondition = new WakeupOnAWTEvent(KeyEvent.KEY_PRESSED);
Dans ce cas précis, le comportement sera déclenché dès qu'une touche du clavier sera pressée.
Cette méthode est appelée automatiquement au lancement de notre application.
C'est dans cette méthode que nous allons appeler pour la première fois la méthode wakeupOn() de la classe Behavior, avec en paramètre le critère qui va déclencher notre comportement :
public void initialize() {
this.wakeupOn(wakeupCondition);
}
Cette méthode est automatiquement appelée à chaque fois que notre condition wakeupCondition est vérifiée. Ici cela correspond à la pression d'une touche du clavier.
C'est dans processStimulus() que nous allons définir les actions à entreprendre à chaque fois que l'on appuie sur une touche du clavier.
Dans cette méthode, on boucle sur les critères ayant déclenché le comportement, et on ne va traiter que le critère de type WakeupOnAWTEvent :
public void processStimulus(Enumeration criteria) {
WakeupCriterion critere;
// On boucle sur les criteres ayant declenche le comportement
while (criteria.hasMoreElements()) {
// On recupere le premier critere de l'enumeration
critere = (WakeupCriterion)criteria.nextElement();
// On ne traite que les criteres correspondant a un evenement AWT
if (critere instanceof WakeupOnAWTEvent) { ...
On récupère ensuite l'évènement qui correspond à la pression d'une touche du clavier pour ne traiter que le cas où les touches pressées sont les flèches UP, DOWN, RIGHT et LEFT :
// On ne traite que les criteres correspondant a un evenement AWT
if (critere instanceof WakeupOnAWTEvent) {
// On recupere le tableau des evements AWT correspondant au critere
events = ((WakeupOnAWTEvent)critere).getAWTEvent();
if (events.length > 0) {
// On recupere l'evement (le tableau d'evenements ne contient en
// fait qu'un seul evenement)
evt = (KeyEvent)events[events.length-1];
// Traitement au cas par cas selon la touche du clavier pressee
switch(evt.getKeyCode()) { ...
Enfin, ne JAMAIS oublier à la fin de la méthode processStimulus(), de rappeler this.wakeupOn(wakeupCondition) pour que processStimulus() soit rappelée si une nouvelle touche du clavier est pressée :
// Une fois le stimulus traite, on reinitialise le comportement
this.wakeupOn(wakeupCondition);
Le fichier SimpleBehavior .java présente le code source complet de cet exemple.
Exécutez l'applet SimpleBehavior.html pour voir le résultat.
Changement du modificateur (Ctrl au lieu de Alt) associé au zoom de la souris
Nous avons vu parmi les 3 comportements de base de la souris que le zoom pouvait se faire en déplaçant la souris tout en maintenant le bouton gauche et la touche ALT enfoncées, ceci grâce à la classe MouseZoom.
Mais supposons maintenant que nous voulions zoomer en maintenant la touche CTRL enfoncée au lieu de ALT. Et bien ceci ne peut se faire sans que nous réécrivions les 3 classes MouseRotate, MouseTranslate et MouseZoom.
Le comportement décrit dans la classe MouseRotate ne fonctionne que si aucune des touches META et ALT ne sont enfoncées (sur un PC, la touche META est équivalente au bouton droit de la souris et la touche ALT au bouton du milieu). Il faut donc rajouter la condition : "la touche CTRL ne doit pas être enfoncée".
Nous remplaçons donc le test de la version originale de MouseRotate de Sun :
if ((id == MouseEvent.MOUSE_DRAGGED) && !evt.isMetaDown() && ! evt.isAltDown()){ ...
Par :
if ((id == MouseEvent.MOUSE_DRAGGED) && !evt.isMetaDown() && !evt.isAltDown() &&
!evt.isControlDown()){ ...
Si cette condition est vérifiée, alors le comportement se déclenche. Ce test se trouve dans la méthode doProcess() qui est appelée à partir de la méthode processStimulus().
Le comportement décrit dans la classe MouseTranslate ne se déclenche que si la touche ALT n'est pas enfoncée et si la touche META (équivalente au bouton droit de la souris) est enfoncée. Il faut là aussi rajouter la condition :
"la touche CTRL ne doit pas être enfoncée".
Nous remplaçons donc le test de la version originale de MouseTranslate de Sun :
if ((id == MouseEvent.MOUSE_DRAGGED) && !evt.isAltDown() && evt.isMetaDown()) { ...
Par :
if ((id == MouseEvent.MOUSE_DRAGGED) && !evt.isAltDown() && evt.isMetaDown() && !evt.isControlDown()) {
Ce test se trouve également dans la méthode doProcess() appelée dans processStimulus().
Dans la version originale de Sun, le zoom n'est possible qui si la touche ALT est enfoncée en même temps que l'on déplace la souris en maintenant le bouton gauche enfoncé. Cela se traduit par la portion de code suivante :
if ((id == MouseEvent.MOUSE_DRAGGED) && evt.isAltDown() && !evt.isMetaDown()){ ...
Nous voulons que le comportement se produise alors que la touche CTRL est enfoncée au lieu de la touche ALT, il faut donc modifier le test comme suit :
if ((id == MouseEvent.MOUSE_DRAGGED) && !evt.isAltDown() && !evt.isMetaDown() &&
evt.isControlDown()){
Nous avons écrit un exemple avec les 3 classes modifiées que nous avons nommées :
CustomMouseRotate, CustomMouseTranslate et CustomMouseZoom.
Les fichiers MouseZoomCtrl.java, CustomMouseRotate.java, CustomMouseTranslate.java et CustomMouseZoom.java présentent le code source complet de cet exemple.
Exécutez l'applet MouseZoomCtrl.html pour voir le résultat.
Calcul du nombre d'images par seconde (FPS) d'une animation 3D
Nous allons étudier dans ce paragraphe un comportement personnalisé vraiment très sympathique qui va nous permettre d'estimer le nombre d'images par seconde affichées dans une animation Java 3D.
L'idée consiste à mesurer le temps écoulé entre l'affichage d'un groupe d'images (ou frames). Pour cela nous avons besoin d'un comportement qui serait déclenché à chaque fois qu'un certain nombre d'image aura été affiché.
C'est le critère décrit par la classe WakeupOnElapsedFrames qui permet de faire cela, voici le constructeur que nous allons utiliser dans notre exemple :
public WakeupOnElapsedFrames(int frameCount)
frameCount représente le nombre d'images qui sont affichées avant que le comportement ne soit déclenché.
Pour notre exemple, nous allons créer une classe FpsBehavior (qui étend Behavior bien entendu). La condition qui va déclencher le comportement FpsBehavior se résume au seul critère WakeupOnElapsedFrames, la méthode processStimulus() de FpsBehavior sera donc appelée à chaque fois qu'un certain nombre de frames aura été affiché. Connaissant le nombre d'images affichées entre deux appels consécutifs à processStimulus(), il faut que l'on détermine le temps écoulé entre deux appels consécutifs à processStimulus() pour connaître le nombre d'images affichées par seconde.
Notre exemple se résume à une petite animation 3D d'un cube couleur tournant autour de l'axe des Y, le nombre d'images par secondss déterminé par la méthode processStimulus() de notre comportement personnalisé s'affiche dans le canvas 3D :
Le fichier CompteurImage.java présente le code source complet de cet exemple.
Exécutez l'applet CompteurImage.html pour voir le résultat.
4. Interception d'objets avec la souris
Rotation, translation et zoom d'un objet sélectionné à la souris
Pour aller plus loin dans les comportements, nous allons voir dans ce paragraphe que lorsqu'une scène 3D contient plusieurs objets, il est possible de sélectionner un des objets en particulier et de lui appliquer (spécifiquement à lui sans toucher aux autres objets) des comportements comme MouseRotate, MouseTranslate et MouseZoom que nous venons de voir.
Pour cela, nous allons utiliser les comportements décrits par les classes PickRotateBehavior, PickTranslateBehavior et PickZoomBehavior qui permettent donc d'appliquer un comportement à un objet spécifique d'une scène 3D sélectionné au préalable.
Dans l'exemple que nous allons étudier dans ce paragraphe, les constructeurs que nous allons utiliser pour ces 3 comportements sont les suivants :
public PickRotateBehavior(BranchGroup root, Canvas3D canvas, Bounds bounds)
root représente le noeud parent
du comportement
canvas représente le canvas 3D dans lequel la scène 3D est affichée
bounds représente la zone d'influence du comportement
public PickTranslateBehavior(BranchGroup root, Canvas3D canvas, Bounds bounds)
root représente le noeud parent du comportement
canvas représente le canvas 3D dans lequel la scène 3D est affichée
bounds représente la zone d'influence du comportement
public PickZoomBehavior(BranchGroup root, Canvas 3D canvas, Bounds bounds)
root représente le noeud parent du comportement
canvas représente le canvas 3D dans lequel la scène 3D est affichée
bounds représente la zone d'influence du comportement
Remarque :
Lorsque nous pourcourons la documentation Javadoc sur Java 3D, nous voyons que
les 3 classes ci dessus existent en double dans Java 3D. C'est tout simplement parce qu'il y en a une qui est dépréciée. Les classes qu'il faut considérer se situent dans le package com.sun.j3d.utils.picking.behaviors et non pas dans com.sun.j3d.utils.behaviors.picking qui lui est déprécié.
Le fichier MousePickTest.java présente le code source complet de cet exemple.
Exécutez l'applet MousePickTest.html pour voir le résultat.
Sélection et déplacement d'un objet avec le bouton gauche de la souris
Nous allons modifier le comportement PickTranslateBehavior que nous venons de voir dans l'exemple précédent afin que la sélection et le déplacement de l'objet sélectionné à la souris se fasse bouton gauche enfoncé au lieu du bouton droit par défaut.
Nous allons d'abord réécrire le comportement MouseTranslate en une nouvelle classe CustomMouseTranslate dans laquelle le test de la méthode doProcess() :
if ((id == MouseEvent.MOUSE_DRAGGED) && !evt.isAltDown() && evt.isMetaDown()) { ...
est remplacé par :
if ((id == MouseEvent.MOUSE_DRAGGED) && !evt.isAltDown() && !evt.isMetaDown()) {
Ceci afin que le déplacement de l'objet 3D se fasse avec le bouton gauche enfoncé au lieu du bouton droit par défaut.
Or le problème réside dans le fait que la classe PickTranslateBehavior utilise la classe MouseTranslate, or nous voudrions qu'elle utilise la classe CustomMouseTranslate. Pour cela, la seule solution consiste à réimplémenter la classe PickTranslateBehavior en remplaçant les objets de type MouseTranslate par des objets de notre propre type CustomMouseTranslate.
Nous profitons d'ailleurs de régler la sensibilité du déplacement de la souris dans le constructeur de notre nouvelle classe CustomPickTranslateBehavior en appelant la méthode setFactor() de la classe CustomMouseTranslate avec un argument de 0.008 afin de diminuer cette sensibilité.
Nous n'en avons pas fini avec la classe CustomPickTranslateBehavior. En effet, nous avons réglé le problème du déplacement de l'objet 3D avec le bouton gauche, il faut aussi régler le problème de la sélection de l'objet avec le bouton gauche (la sélection se faisant elle aussi avec le bouton droit par défaut). Pour cela il faut dans la méthode publique updateScene() remplacer le test :
if (!mevent.isAltDown() && mevent.isMetaDown()) { ...
par :
if (!mevent.isAltDown() && !mevent.isMetaDown()) {
Une fois que nous avons écrit notre classe CustomPickTranslateBehavior (qui étend PickMouseBehavior) un autre problème se pose avec la méthode freePickResult() qui est déclarée et implémentée dans PickMousebehavior mais que nous ne pouvons pas utiliser dans CustomPickTranslateBehavior car la visibilité de freePickResult() n'est pas publique.
Pour faire marcher notre exemple, nous sommes donc obliger de rapatrier la classe PickMouseBehavior dans le package courant de notre exemple, mais sans la modifier toutefois, le problème étant un simple problème de visibilité.
Après ces "quelques" modifications, notre bel exemple devrait fonctionner normalement.
Les fichiers MousePickDeplace.java, CustomPickTranslateBehavior.java, CustomMouseTranslate.java présentent le code source complet de cet exemple.
Exécutez l'applet MousePickDeplace.html pour voir le résultat.
5. Collision d'objets
Dans ce dernier paragraphe consacré aux comportements, nous allons voir que ces derniers permettent de gérer les collisions entre objets 3D.
Nous allons étudier les collisions au travers d'un exemple complet dans lequel nous allons construire 3 objets 3D :
2 sphères blanches immobiles et un petit cube que l'on peut déplacer à l'aide de la souris. Dès que le cube heurte l'une des sphères, celle-ci prend aussitot une couleur et dès que le cube s'en éloigne elle redevient blanche.
Pour déplacer le cube que nous sélectionnons avec le bouton gauche de la souris, nous allons utiliser le comportement de la classe CustomPickTranslateBehavior que nous avons développée dans le paragraphe précédent. Jusque là, rien de bien nouveau.
Par contre, pour gérer les collisions, nous allons écrire une nouvelle classe nommée DetecteurCollision. Nous avons choisi 2 critères pour gérer les collisions :
-
lorsque le cube se dirige vers une des sphères et percute la sphère, on utilise un évènement de type WakeupOnCollisionEntry.
- lorsque le cube sort de la sphère et s'en éloigne, un utilise un évènement de type WakeupOnCollisionExit
Bien entendu le comportement de type DetecteurCollision devra se déclencher lorsqu'un l'un ou l'autre de ces 2 critères est vérifié.
La condition permettant de déclencher le comportement au cas ou l'un ou l'autre de ces 2 critères est vérifié se traduit à l'aide d'un objet de type WakeupOr.
Un objet de type WakeupOr se construit de la façon suivante :
public WakeupOr(WakeupCriterion[] conditions)
conditions est la liste des critères. Si l'un des critères est vérifié, le comportement sera déclenché.
La méthode initialize() de la classe DetecteurCollision va utiliser un objet de type WakeupOr.
La méthode processStimulus() de la classe DetecteurCollision va quant à elle décrire l'action du comportmeent lorsqu'il est déclenché. Dans notre application, nous avons deux sphères que nous voulons soumettre au comportement de collision. Mais on souhaiterait que ce comportement soit légèrement différent pour les 2 sphères : la sphère gauche prend la couleur rouge lorsque le cube la percute tandis que la sphère droite prend la couleur bleue quand elle est percutée.
Ici, la difficulté vient du fait que comme nous allons appliquer le même comportement aux deux sphères, il faut impérativement que dans la méthode processStimulus() on sache sur quelle sphère le comportement s'est déclenché. Sinon, il faudrait écrire un comportement spécifique pour chacune des sphères, et nous allons essayer de nous en passer pour le cas qui nous préoccupe.
Une façon de s'en sortir consiste à utiliser la méthode setUserData() de la classe SceneGraphObject (qui est la super classe ou classe de base de Java 3D, tout comme Object pour Java), on pourrait de la sorte distinguer les sphères lors de leur construction grâce à ces meta data :
Sphere sphereGauche = new Sphere(0.3f);
sphereGauche.setUserData("Sphere gauche");
Sphere sphereDroite = new Sphere(0.3f);
sphereDroite.setUserData("Sphere droite");
Ainsi, dans la méthode processStimulus() de notre comportement DetecteurCollision, on peut récupérer ces donnees pour savoir sur quelle sphère le comportement s'est déclenché :
if (node.getUserData().toString().equals("Sphere gauche")) {
appearance.setMaterial(rouge);
}
else if (node.getUserData().toString().equals("Sphere droite")) {
appearance.setMaterial(bleu);
}
Les deux sphères sont ainsi distinguées et on peut appliquer à chacune une couleur différente dans la méthode processStimulus().
Les sphères changent de couleur lorsque le petit cube les percute
Le fichier Collision.zip contient tous les fichiers source de cet exemple.
Exécutez l'applet Collision.html pour voir le résultat.