Compteur :
Java 3D - Chapitre 9 : Quelques outils et aspects avancés de Java 3D

Retour table des matières Java 3D

 

Dans cette partie, nous allons étudier quelques possibilités avancées qu'offre la librairie Java 3D. Certains aspects ne vous seront sûrement pas étrangers (surtout pour les gros consommateurs de jeux vidéo) comme l'anticrénelage, le mipmapping, les effets de brouillard.
Nous allons également voir comment utiliser des objets 3D complexes créés à l'aide de logiciels tiers comme 3D Studio, Lightwave ou Wavefront dans une application Java 3D.

 

 

1. Réglage de la profondeur du champ de vision

 

Certains auront déjà remarqué que le champ de vision d'une application est limité dans l'espace. Par exemple, si l'on déplace un objet 3D vers le fond d'une scène 3D, on voit qu'au delà d'une certaine distance cet objet va brutalement disparaître pour réapparaître aussitôt qu'on le rapproche à nouveau du centre du repère 3D.
Par défaut, dans une application Java 3D le champ de vision s'étend jusqu'à Z = -10, tous les objets situés au delà du plan vertical situé à la coordonnée Z = -10 seront invisibles.

La position de ce plan (appelé clip plane en anglais) peut être modifiée grâce à la méthode setBackClipDistance() de la classe View :

public void setBackClipDistance(double distance)
distance représente la nouvelle distance par rapport à l'origine à laquelle le clip plane sera positionné. distance doit être une valeur positive mais le plan sera positionné à Z = -distance.

La seule difficulté ici consiste à récupérer une référence à un objet de type View dans notre application 3D.
Une façon de faire est d'appeler les 2 méthodes getViewer() et getView() en cascade sur un objet su de type SimpleUniverse :

View view = su.getViewer().getView();

Nous avons illustré cet aspect dans un petit exemple où nous pouvons déplacer un cube grâce aux flèches du clavier dans une scène où le plan arrière est situé à une distance 1000 de l'origine du repère.

L'archive src.zip contient le code source complet de cet exemple.
Exécutez l'applet ProfondeurChamp.html pour voir le résultat.

 

2. Scène 3D avec vues multiples

 

Jusqu'à présent, sur tous les exemples que nous avons étudiés, notre application se contentait d'afficher la scène 3D dans une unique fenêtre, que l'on peut appeler vue de face.
Or, si vous avez déjà utilisé des logiciels de modélisation 3D genre 3D Studio MAX, vous aurez sûrement remarqué que en plus de la vue de face, d'autres vues comme la vue de dessus ou de côté permettaient de mieux visualiser les objets 3D. Java 3D offre la possibilité de visualiser une scène 3D sous plusieurs angles, c'est ce que nous allons voir dans ce paragraphe.

On obtenait la vue de face par défaut en utilisant un objet de type SimpleUniverse qui fournissait les éléments nécessaires pour construire notre application 3D.
Voici pour rappel, l'arbre d'une scène 3D de base avec un objet de type SimpleUniverse :

Une arborescence de type SimpleUniverse avec une vue unique sur la scène 3D

Un objet de type SimpleUniverse encapsule donc tous les éléments nécessaires pour la création d'une scène 3D de base avec un seul point de vue.

Désormais, dans le cas qui nous préoccupe, nous ne pouvons plus utiliser un objet de type SimpleUniverse, il va falloir passer par la classe VirtualUniverse et par toutes les autres composantes nécessaires à la création de vues multiples.
Voici le schèma de l'arborescence que nous allons utiliser pour créer des vues multiples pour la même scène 3D :

Une arborescence de type VirtualUniverse  à laquelle nous pouvons greffer plusieurs vues sur la scène 3D

Dans ce type d'arborescence, notre objet de type SimpleUniverse a été éclaté en plusieurs objets qui sont de type :
VirtualUniverse, Locale ainsi q'un groupe d'objets que nous appelons Vue (rectangle en pointillés bleus). Cette classe Vue est une classe que nous avons créée personnellement pour plus de commodité, elle n'appartient pas à la librairie Java 3D de Sun.
Il va nous falloir créer autant d'objets de type Vue que nous voulons de points de vue sur notre scène 3D. Dans l'exemple que nous allons étudier, nous allons créer une vue de face, de gauche, de droite et de dessus. Il nous faudra donc créer 4 instances de la classe Vue.
Le placement de la vue (dessus, face, droite ou gauche) s'effectue grâce à l'objet de type TransformGroup (c'est le même objet qui nous permettait de modifier la position de la caméra lorsque nous avons étudié les comportements du clavier de type KeyBehavior par exemple.

 

Description de la classe Vue

// Les objets de type PhysicalBody et PhysicalEnvironment doivent etre
// communs a toutes les instances de la classe Vue. Ce sont les memes objets
// que l'on utilisera pour toutes les vues
protected static final PhysicalBody physBody = new PhysicalBody();
protected static final PhysicalEnvironment physEnv = new PhysicalEnvironment();

// Objets specifiques a chaque vue
protected BranchGroup parent = null;
protected TransformGroup vpTG = null;
protected ViewPlatform viewPlatform = null;
protected View view = null;
protected Canvas3D canvas = null;

 

GraphicsConfigTemplate3D gconfigTemplate = new GraphicsConfigTemplate3D();
GraphicsConfiguration gconfig = GraphicsEnvironment.getLocalGraphicsEnvironment().
    getDefaultScreenDevice().getBestConfiguration(gconfigTemplate);

 

canvas = new Canvas3D(gconfig);
viewPlatform = new ViewPlatform();
view = new View();

 

view.addCanvas3D(canvas);
view.attachViewPlatform(viewPlatform);
view.setPhysicalBody(physBody);
view.setPhysicalEnvironment(physEnv);

 

vpTG = new TransformGroup();
vpTG.setCapability(TransformGroup.ALLOW_TRANSFORM_READ);
vpTG.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
vpTG.addChild(viewPlatform);

 

parent = new BranchGroup();
parent.setCapability(BranchGroup.ALLOW_DETACH);
parent.addChild(vpTG);

 

public TransformGroup getViewPlatformTransformGroup() {
    return vpTG;
}

 

public BranchGroup getParent() {
    return parent;
}

 

public Canvas3D getCanvas3D() {
    return canvas;
}

 

Attachement des 4 instances de la classe Vue à l'univers virtuel VirtualUniverse

// Creation d'un univers et d'un objet Locale qui va contenir la scene
// 3D ainsi que les 4 cameras
VirtualUniverse universe = new VirtualUniverse();
Locale locale = new Locale(universe);

 

// Creation et placement des 4 vues
// Vue de face perpendiculaire a Z placee a Z = 3 metres
Vue vueFace = new Vue();
TransformGroup vpTG = vueFace.getViewPlatformTransformGroup();
Transform3D transform = new Transform3D();
transform.setTranslation(new Vector3f(0f, 0f, 3f));
vpTG.setTransform(transform);

// Vue de droite perpendiculaire a X placee a X = 3 metres
Vue vueDroite = new Vue();
vpTG = vueDroite.getViewPlatformTransformGroup();
transform = new Transform3D();
transform.setTranslation(new Vector3f(3f, 0f, 0f));
Transform3D transform2 = new Transform3D();
transform2.rotY(Math.PI / 2.0);
transform.mul(transform2);
vpTG.setTransform(transform);

// Vue de gauche perpendiculaire a X placee a X = -3 metres
Vue vueGauche = new Vue();
vpTG = vueGauche.getViewPlatformTransformGroup();
transform = new Transform3D();
transform.setTranslation(new Vector3f(-3f, 0f, 0f));
transform2 = new Transform3D();
transform2.rotY(-Math.PI / 2.0);
transform.mul(transform2);
vpTG.setTransform(transform);

// Vue de haut perpendiculaire a Y placee a Y = 3 metres
Vue vueHaut = new Vue();
vpTG = vueHaut.getViewPlatformTransformGroup();
transform = new Transform3D();
transform.setTranslation(new Vector3f(0f, 3f, 0f));
transform2 = new Transform3D();
transform2.rotX(-Math.PI / 2.0);
transform.mul(transform2);
vpTG.setTransform(transform);

 

locale.addBranchGraph(vueFace.getParent());
locale.addBranchGraph(vueDroite.getParent());
locale.addBranchGraph(vueGauche.getParent());
locale.addBranchGraph(vueHaut.getParent());

 

// Creation de la scene 3D qui contient tous les objets 3D que l'on veut visualiser
BranchGroup scene = createSceneGraph();

// Compilation de la scene 3D
scene.compile();

// Attachement de la scene 3D a l'objet locale
locale.addBranchGraph(scene);

 

Scène 3D avec 4 vues différentes

Le fichier VuesMultiples1.java présente le code source complet de cet exemple.
Exécutez l'applet VuesMultiples1.html pour voir le résultat.

 

Nous avons même rajouté des comportements de base de la souris (rotation, déplacement et zoom) qui fonctionnent dans les 4 vues. Or nous remarquons que si les comportements sont normaux dans la vue de face, ce n'est pas le cas pour les autres vues. Par exemple, si l'on veut déplacer le cube de bas en haut dans la vue de haut, et bien le cube va plutôt se déplacer vers nous alors que sur la vue de face (vue par défaut), le déplacement s'effectue bien de bas en haut.
En fait, même si l'on effectue le déplacement en ayant cliqué dans la vue de haut, tout se passe comme si l'on avait cliqué dans la vue de face, les comportements ne font pas la distinction entre les vues.
C'est ce que nous allons essayer de corriger dans le paragraphe suivant.

 

Modification des comportements de base de la souris dans le cas des vues multiples

Nous avons repris les 3 comportements de base de la souris :
MouseRotate, MouseTranslate et MouseZoom que nous avions étudiés dans le chapitre 7.

L'idée consiste ici à réécrire des 3 comportements avec des transformations qui vont dépendre de la vue dans laquelle on a cliqué. Par exemple, si l'on clique avec le bouton droit dans la vue de haut et que l'on déplace le cube vers le haut dans cette vue, et bien il faut que le cube se déplace selon les Z négatifs dans la vue de face. Comme tous les déplacements doivent être calculés selon cette vue de face qui est la vue par défaut pour le repère 3D, il faudra donc impérativement qu'un déplacement vers le haut dans la vue de haut se traduise par un déplacement selon les Z négatifs de notre objet.
Il faut modifier de la même façon les deux autres comportements concernant la rotation et le zoom, et écrire des actions spécifiques selon la vue sélectionnée.

Il est donc nécessaire que nos comportements connaissent la vue qui a été cliquée. Pour cela, le constructeur de chaque comportement va avoir comme paramètre une référence pour chacun des 4 canvas 3D correspondant à nos 4 vues :

public CustomMouseRotate(TransformGroup transformGroup,
                         Canvas3D haut, Canvas3D droite,
                         Canvas3D gauche, Canvas3D face)

public CustomMouseTranslate(TransformGroup transformGroup,
                            Canvas3D haut, Canvas3D droite,
                            Canvas3D gauche, Canvas3D face)

public CustomMouseZoom(TransformGroup transformGroup,
                       Canvas3D haut, Canvas3D droite,
                       Canvas3D gauche, Canvas3D face)

Ensuite, il faut modifier la méthode doProcess() qui est appelée à partir de processStimulus() pour chacun de ces 3 comportements :

 

Extrait de code de la méthode originale (classe MouseRotate) :

transformX.rotX(x_angle);
transformY.rotY(y_angle);

transformGroup.getTransform(currXform);

Matrix4d mat = new Matrix4d();
// Remember old matrix
currXform.get(mat);

// Translate to origin
currXform.setTranslation(new Vector3d(0.0,0.0,0.0));
if (invert) {
  currXform.mul(currXform, transformX);
  currXform.mul(currXform, transformY);
} else {
  currXform.mul(transformX, currXform);
  currXform.mul(transformY, currXform);
}

Extrait de code la méthode modifiée (classe CustomMouseRotate) :

// Transformations specifiques a chaque canvas 3D
if (evt.getSource().equals(haut)) {
  transformX.rotX(x_angle);
  transformY.setIdentity();
  transformZ.rotZ(-y_angle);
}
else if (evt.getSource().equals(droite)) {
  transformX.setIdentity();
  transformY.rotY(y_angle);
  transformZ.rotZ(-x_angle);
}
else if (evt.getSource().equals(gauche)) {
  transformX.setIdentity();
  transformY.rotY(y_angle);
  transformZ.rotZ(x_angle);
}
else if (evt.getSource().equals(face)) {
  transformX.rotX(x_angle);
  transformY.rotY(y_angle);
  transformZ.setIdentity();
}

transformGroup.getTransform(currXform);

Matrix4d mat = new Matrix4d();
// Remember old matrix
currXform.get(mat);

// Translate to origin
currXform.setTranslation(new Vector3d(0.0,0.0,0.0));
if (invert) {
  currXform.mul(currXform, transformX);
  currXform.mul(currXform, transformY);
  currXform.mul(currXform, transformZ);
} else {
  currXform.mul(transformX, currXform);
  currXform.mul(transformY, currXform);
  currXform.mul(transformZ, currXform);
}

A partir de l'évènement evt de type MouseEvent, on peut savoir dans quel canvas 3D (ou vue) on a cliqué pour déclencher le comportement. On applique donc la bonne transformation (currXform) en fonction du canvas 3D dans lequel on se trouve.

 

Extrait du code de la méthode originale (classe MouseTranslate) :

translation.x = dx*x_factor;
translation.y = -dy*y_factor;

transformX.set(translation);

if (invert) {
  currXform.mul(currXform, transformX);
} else {
  currXform.mul(transformX, currXform);
}

Extrait du code de la méthode modifiée (classe CustomMouseTranslate) :

// Transformations specifiques a chaque canvas 3D
if (evt.getSource().equals(haut)) {
  translation.x = dx*x_factor;
  translation.y = 0;
  translation.z = dy*y_factor;
}
else if (evt.getSource().equals(droite)) {
  translation.x = 0;
  translation.y = -dy*y_factor;
  translation.z = -dx*x_factor;
}
else if (evt.getSource().equals(gauche)) {
  translation.x = 0;
  translation.y = -dy*y_factor;
  translation.z = dx*x_factor;
}
else if (evt.getSource().equals(face)) {
  translation.x = dx*x_factor;
  translation.y = -dy*y_factor;
  translation.z = 0;
}

transformX.set(translation);

if (invert) {
  currXform.mul(currXform, transformX);
} else {
  currXform.mul(transformX, currXform);
}

 

Extrait du code de la méthode originale (classe MouseZoom) :

translation.z = dy*z_factor;

transformX.set(translation);

if (invert) {
  currXform.mul(currXform, transformX);
} else {
  currXform.mul(transformX, currXform);
}

Extrait du code de la méthode modifiée (classe CustomMouseZoom) :

// Transformations specifiques a chaque canvas 3D
if (evt.getSource().equals(haut)) {
  translation.x = 0;
  translation.y = dy*z_factor;
  translation.z = 0;
}
else if (evt.getSource().equals(droite)) {
  translation.x = dy*z_factor;
  translation.y = 0;
  translation.z = 0;
}
else if (evt.getSource().equals(gauche)) {
  translation.x = -dy*z_factor;
  translation.y = 0;
  translation.z = 0;
}
else if (evt.getSource().equals(face)) {
  translation.x = 0;
  translation.y = 0;
  translation.z = dy*z_factor;
}

transformX.set(translation);

if (invert) {
  currXform.mul(currXform, transformX);
} else {
  currXform.mul(transformX, currXform);
}

 

L'archive src.zip contient le code source complet de cet exemple.
Exécutez l'applet VuesMultiples2.html pour voir le résultat.

 

3. Choisir un objet à afficher dans une liste avec la classe Switch

 

La classe Switch représente un groupe d'objets et permet d'afficher un objet au choix dans groupe. Elle étend naturellement la classe Group (tout comme les classes BranchGroup, Primitive et TransformGroup par exemple).

Dans notre exemple qui va illustrer l'usage de cet classe nous allons utiliser le constructeur suivant :

public Switch(int whichChild)
whichChild est l'indice de l'objet que l'on veut afficher.

Pour ajouter un objet dans un groupe de type Switch, on utilise la méthode addChild() de la classe parent Group comme pour tous les groupes.
Enfin, pour sélectionner l'objet du groupe à afficher, on utilise la méthode setWhichChild() de la classe Switch :

public void setWhichChild(int child)
child est l'indice de l'objet à afficher

L'exemple de ce paragraphe montre l'utilisation d'un switch avec 3 objets : un cône, un cylindre et une sphère. Un simple appui sur un bouton permet de changer d'objet à afficher.

Le fichier SwitchTest.java présente le code source complet de cet exemple.
Exécutez l'applet SwitchTest.html pour voir le résultat.

 

4. Effets de brouillard

 

L'effets de brouillard sont très présents dans les jeux vidéos en particulier. Nous allons voir dans ce paragraphe que Java 3D offre une possibilité très simple de créer un effet de brouillard dans une scène 3D.
Nous avons la possibilité de créer un brouillard dit linéaire et un brouillard dit exponentiel.

 

Brouillard linéaire

C'est la classe LinearFog qui permet de créer un bouillard linéaire. Elle étend la classe abstraite Fog.
Le brouillard linéaire est basé sur un couple de distances par rapport à l'origine Z = 0 :
une distance frontDistance à partir de laquelle le brouillard commence à obscurcir la scène et une distance backDistance à partir de laquelle la scène est complètement obscurcie par le brouillard.

Le facteur de mélange (ou d'obscurcissement si on veut) est donné par la formule suivante :
f = (backDistance - Z) / (backDistance - frontDistance)

f varie de 1 (scène totalement visible) à 0 (scène complètement obscurcie par le brouillard). f varie linérairement avec Z, d'où le nom de bouillard linéaire.

En général, lorsqu'on veut ajouter du brouillard à une scène, il faut lui donner une couleur, le constructeur suivant permet de le faire :

public LinearFog(float r, float g, float b)
r : composante rouge de la couleur du brouillard
g : composante verte de la couleur du brouillard
b : composante bleue de la couleur du brouillard

Le réglage des distances frontDistance et backDistance se fait simplement par les méthodes d'accès :

public void setFrontDistance(double frontDistance)
frontDistance : distance à partir de laquelle le brouillard commence à abscurcir la scène

public void setBackDistance(double backDistance)
backDistance : distance à partir de laquelle la scène est complètement obscurcie par le brouillard

L'exemple de ce paragraphe montre une sphère animée qui s'enfonce dans le brouillard.

Le fichier LinearFogTest.java présente le code source complet de cet exemple.
Exécutez l'applet LinearFogTest.html pour voir le résultat.

 

Brouillard exponentiel

C'est la classe ExponentialFog qui permet de créer un bouillard exponentiel. Elle étend également la classe abstraite Fog.
Le facteur de mélange (ou d'obscurcissement) d'un brouillard de type exponentiel se calcule par la formule suivante :

f = exp(-densite * Z)
densite est la densite du brouillard et Z la distance par rapport à l'origine du repère 3D.

Dans notre petit exemple sur ce paragraphe concernant le brouillard exponentiel, nous avons utilisé le constructeur suivant :

public ExponentialFog(float r, float g, float b, float density)
r : composante rouge de la couleur du brouillard
g : composante verte de la couleur du brouillard
b : composante bleue de la couleur du brouillard
density : densite du brouillard

L'exemple de ce paragraphe montre une sphère animée qui s'enfonce dans le brouillard dont la densité est réglable par un curseur.
Il ne faut pas oublier, si l'on veut changer la densité de brouillard après qu'il a été construit, d'utiliser la constante de capabilité :
ExponentialFog.ALLOW_DENSITY_WRITE

Le fichier ExponentialFogTest.java présente le code source complet de cet exemple.
Exécutez l'applet ExponentialFogTest.html pour voir le résultat.

 

5. Textures à niveaux multiples (mipmapping)

 

Dans cette section, nous allons compléter le paragraphe du chapitre 5 consacré aux textures par une notion appelée mipmapping, ou textures à niveaux multiples. Le mipmapping est couramment utilisé dans les jeux vidéo.
Le but du mipmapping est d'afficher avec plus ou moins de détails une texture d'un objet en fonction de la distance de celui-ci avec la caméra. Il est en effet inutile d'afficher une texture avec une résolution optimale si l'objet est très éloigné du point de vue. Il ne faut pas confondre le mimapping avec le multitexturing qui consiste à appliquer plusieurs textures à un objet 3D.
Le terme "MIPmap" est un acronyme de la phrase latine "multum in parvo" qui signifie "beaucoup de choses dans peu de place".

Notre exemple illustrant le mipmapping contient un simple carré 2D sur lequel lequel on a mappé une texture. Le fait de zoomer sur ce carré va ajuster la résolution de la texture en fonction de la distance du carré au point de vue.

Lorsque nous avions étudié les textures dans le chapitre 5, nous avions créé une texture grâce à la méthode getTexture() de la classe TextureLoader. Dans le cas qui nous préoccupe ici, nous allons explicitement créer un objet de type Texture2D. La classe Texture2D hérite de la classe Texture.
Voici le constructeur de cette classe que nous allons utiliser dans notre exemple :

public Texture2D(int mipMapMode, int format, int width, int height)
mipMapMode doit être égal à Texture.MULTI_LEVEL_MIPMAP pour indiquer que notre texture possède plusieurs niveaux.
format représente le format des données des textures de l'objet Texture2D
width représente la largeur de l'image de premier niveau de la texture en pixels (doit être une puissance de 2)
height représente la hauteur de l'image de premier niveau de la texture en pixels (doit être une puissance de 2)

Nous utilisons ensuite la méthode setImage() de la classe Texture pour construire tous les niveaux de notre texture :

public void setImage(int level, ImageComponent image)
level correspond au niveau de la texture
image correspond à l'image de la texture de niveau level

La classe TextureLoader possède deux méthodes permettant d'obtenir des images de type ImageComponent permettant de construire notre texture à niveaux multiples :

public ImageComponent2D getImage()
renvoie l'image associée à notre objet de type TextureLoader
Le type ImageComponent2D étend la classe ImageComponent qui est abstraite

public ImageComponent2D getScaledImage(int width, int height)
renvoie l'image mise à l'échelle (de largeur width et de hauteur height) associée à notre objet de type TextureLoader

La méthode getImage() nous permet de construire le premier niveau de notre texture à niveaux multiples tandis que la méthode getScaledImage() nous permet de construire les niveaux suivants.

Enfin, nous utilisons deux méthodes de la classe Texture nous permettant de régler le mode d'interpolation des images de notre texture multi niveaux :

public void setMinFilter(int minFilter)
Cette méthode est utilisée lorsque l'image de notre texture doit être réduite pour être affichée.
Normalement, ce filtre est utilisé lorsqu'on affiche une image nécessitant une texture correspondant aux niveaux 1 et plus.
minFilter est le type du filtre d'interpolation, on peut utiliser Texture.MULTI_LEVEL_POINT qui est une interpolation qui va prendre le texel (pixel texturé) le plus proche dans le niveau de texture le plus proche.

public void setMagFilter(int magFilter)
Cette méthode est utilisée lorsque l'image de notre texture doit être agrandie pour être affichée.
Normalement, ça se produit lorsque on zoome jusqu'à afficher une image nécessitant une texture plus grande que celle de niveau 0.
magFilter est le type du filtre d'interpolation, on peut utiliser Texture.BASE_LEVEL_LINEAR qui est une interpolation bilinéaire des 4 texels (pixels texturés) du niveau 0 les plus proche du texel à interpoler.

Attention :

Il semblerait qu'il y ait une confusion dans la documentation Javadoc de Sun à propos des méthodes setMinFilter() et setMagFilter() de la classe Texture. Les descriptions de ces méthodes ont été inversées ainsi que les choix possibles pour les champs minFilter et magFilter.

Le fichier Mipmapping.java présente le code source complet de cet exemple.
Exécutez l'applet Mipmapping.html pour voir le résultat.

 

6. Anticrénelage (antialiasing)

 

Nous allons étudier dans ce paragraphe les différentes méthodes permettant d'afficher des objets en utilisant l'anticrénelage ou l'antialiasing en anglais. Nous allons nous baser sur l'exemple que nous avions étudié au chapitre 5 à propos des différents modes d'affichage d'un objet 3D.

La première idée à exploiter pour forcer l'antialiasing consiste à utiliser les constructeurs des classes PointAttributes et LineAttributes avec les paramètres pointAntialising et lineAntialiasing à true. C'est ce que nous avions fait dans l'exemple du chapitre 5 pour la classe PointAttributes.
En revanche, il est important de noter que cette méthode ne marche que pour la version OpenGL de Java 3D, l'antialiasing ne se fait pas lorsqu'on utilise une version Direct3D de Java 3D, que ce soit avec une carte graphique de marque NVidia ou ATI.
Enfin, lorsqu'on utilise la classe PolygonAttributes avec un remplissage des facettes (PolygonAttributes.POLYGON_FILL), il n'existe pas de paramètres équivalents à pointAntialiasing et lineAntialiasing. Nous sommes donc obligés d'utiliser une autre méthode.

Une autre idée pour afficher une scène avec de l'antialiasing consiste à utiliser la méthode setSceneAntialiasingEnable() de la classe View avec en paramètre le booléen true. Cependant, nous avons remarqué qu'avec une carte ATI en Direct3D ou OpenGL, il y a aucun effet et qu'avec une carte NVidia toute l'accélération matérielle 3D est désactivée.

Nous vous proposons alors une dernière méthode pour activer l'antialiasing, il s'agit du réglage explicite des paramètres dans les drivers. Cette méthode est de loin la moins noble de toutes (car ces réglages peuvent interférer avec d'autres applications, notamment les jeux) mais c'est le seul recours si Java 3D ne nous offre pas d'autre possibilité.
Avec les drivers pour une carte ATI ou NVidia, il suffit (que ce soit pour DirectX ou OpenGL), de désactiver dans les paramètres de réglage des drivers les préférences d'application pour l'anticrénelage et d'activer un échantillonage 2X qui est amplement suffisant et qui ne grèvera pas trop les performances 3D.
Cette dernière méthode permet de forcer l'antialiasing même pour les objets utilisant l'attribut d'apparence PolygonAttributes.POLYGON_FILL.

 

7. Gestion du niveau de détails (LOD)

 

Dans la librairie Java 3D, le comportement de type DistanceLOD va permettre de choisir l'objet à afficher parmi un groupe contenu dans un Switch en fonction de la distance de cet objet avec la caméra.
La classe DistanceLOD est la seule classe qui étend la classe LOD (Level Of Details) qui est abstraite et qui étend elle même la classe Behavior.
Le constructeur de la classe DistanceLOD que nous allons utiliser dans l'exemple illustrant ce paragraphe se présente comme suit :

public DistanceLOD(float[] distances)
distances représente un tableau de valeurs qui vont déterminer quel est l'objet du switch à afficher.
Si d est la distance de l'objet à la caméra, alors l'indice de l'objet du switch que le comportement DistanceLOD va permettre d'afficher est le suivant :
0 si d <= distances[0]
i si distances[i-1] < d <= distances[i]
n si d > distances[n-1]
Ainsi, si le tableau distances comporte n valeurs, il faudra que le switch associé au comportement de type DistanceLOD possède n+1 objets.
Le tableau distances doit posséder des valeurs croissantes.

Ainsi, le groupe de type Switch que l'on va construire doit être constitué d'objets qui seront de moins en moins bien définis au fur et à mesure que leur indice augmente.

Il ne faudra pas oublier 2 choses :
- Le Switch doit posséder la capabilité de pouvoir être modifié : Switch.ALLOW_SWITCH_WRITE
- DistanceLOD est un comportement, il faut donc impérativement lui associer un rayon d'action afin qu'il soit actif, par exemple : distanceLOD.setSchedulingBounds(new BoundingSphere()).

Comme un petit exemple vaut mieux qu'un long discours, nous avons écrit un programme permettant d'illustrer un comportement de type DistanceLOD.
Un cylindre rouge bien détaillé sera affiché par défaut au démarrage de l'application, mais si on éloigne le cylindre de la caméra par un effet de zoom (inverse), alors un cylindre vert moins détaillé s'affichera et si on poursuit l'éloignement un cylindre bleu encore moins détaillé s'affichera.

Le fichier LODTest.java présente le code source complet de cet exemple.
Exécutez l'applet LODTest.html pour voir le résultat.

 

8. Comportement de type "Billboard"

 

Nous allons étudier dans ce paragraphe, un dernier type de comportement : Billboard.
Le comportement de type Billboard s'applique à un groupe de transformation et fait en sorte que l'axe Z local à ce groupe de transformation pointe toujours dans la direction de la caméra.
Ce type de comportement peut être utile par exemple dans un jeu vidéo lorsque quelque soit le mouvement de la caméra, on désire toujours qu'un ennemi soit vu de face, et exclusivement de face.
Un autre application très utile de ce comportement est lorsqu'on veut afficher un texte qui soit toujours visible quelque soit la position de la caméra.

Pour illustrer cet aspect, nous allons créer une scène 3D dans laquelle nous allons placer 2 cubes : celui de gauche est normal et celui de droite possède un comportement de type Billboard. Une navigation au clavier permet de déplacer la caméra autour des 2 cubes et l'on peut ainsi voir que le cube de droite présente toujours sa face rouge vers la caméra. Il tourne en fait avec la caméra alors que le cube de gauche reste bien immobile.

La classe Billboard hérite naturellement de la classe Behavior, voici la description de deux constructeurs :

public Billboard(TransformGroup tg, int mode, Point3f point)
tg est le groupe de transformation sur lequel on applique le comportement
mode représente le mode d'alignement. Pour ce constructeur, la valeur à utiliser est : Billboard.ROTATE_ABOUT_POINT (l'axe Z local à tg fera face à la caméra suite à une rotation dont le centre est point). Une rotation additionnelle sera utilisée dans ce cas pour que l'axe Y de la caméra et l'axe Y local à tg soient alignés.

public Billboard(TransformGroup tg, int mode, Vector3f axis)
tg est le groupe de transformation que lequel on applique le comportement
mode représente le mode d'alignement. Pour ce constructeur, la valeur à utiliser est : Billboard.ROTATE_ABOUT_AXIS (l'axe Z local à tg fera face à la caméra suite à une rotation dont l'axe est axis).
Il est évident que l'axe de rotation ne doit pas être parallèle à l'axe Z local à tg, en effet, on ne peut pas faire pointer l'axe Z local à tg sur la caméra en appliquant une rotation autour de lui même.

Attention :

Tout comme la classe DistanceLOD, la classe Billboard est un comportement, ainsi pour qu'il soit actif il est impératif de lui assigner une zone d'influence avec la méthode setSchedulingBounds().

Voici une illustration concernant notre exemple sur le comportement de type Billboard. Par défaut un objet de type ColorCube a sa face rouge qui est orientée vers la caméra. En jouant alternativement sur les touches ALT+fleche gauche puis fleche droite par exemple, on peut faire tourner la caméra autour des cubes et l'on peut observer que le cube de droite tourne avec la caméra selon l'axe Y afin de toujours montrer sa face rouge :

Le cube de droite est soumis à un comportement de type Billboard : la face rouge fait toujours face à la caméra

L'archive src.zip présente le code source complet de cet exemple.
Exécutez l'applet BillboardTest.html pour voir le résultat.

 

9. Importation d'objets 3D existants

 

Java 3D offre la possibilité de charger des objets 3D qui ont été créés par des applications tierces. Dans ce paragraphe, nous allons étudier la possibilité de charger des objets de 3 formats différents :
.LWS (format Lightwave)
.OBJ (format Wavefront)
.3DS (format 3D Studio)

 

L'importation de scènes au format .LWS est intégrée à Java 3D. Il n'est pas nécessaire d'installer de packages supplémentaires. Ces fichiers scènes ne contiennent pas que des objets géométriques, ils peuvent aussi faire référence à d'autres objets comme des lumières, des animations etc ... un peu à l'image d'une scène dans Java 3D.
Il faut utiliser un objet de type com.sun.j3d.loaders.lw3d.Lw3dLoader (classe qui implémente l'interface Loader) pour importer une scène au format Lightwave.
Les imports suivants sont nécessaires afin de pouvoir charger une scène Lightwave dans un programme Java 3D :

import com.sun.j3d.loaders.lw3d.*;
import com.sun.j3d.loaders.*;

Voici le constructeur que nous allons utiliser dans notre exemple :

public Lw3dLoader(int flags)
flags est un indicateur permettant de spécifier les parties de la scène à charger. Pour charger la totalité d'une scène Lightwave dans une application Java 3D, on utilisera le flag Lw3dLoader.LOAD_ALL.

Si une scène Lightwave (.lws) contient plusieurs objets géométriques (.lwo), il est bien sûr impératif que tous ces fichiers soient présents dans le PATH indiqué dans le fichier .lws (qui est un fichier ASCII).
Pour charger une scène proprement dite, on utilisera la méthode load de la classe Lw3dLoader qui est surchargée 3 fois :

Scene load(java.io.Reader reader)
Scene load(java.lang.String filename)
Scene load(java.net.URL url)

Cette méthode permet donc de charger une scène Lighwave à partir d'un flux, d'un nom de fichier ou d'une URL et renvoie l'objet de type Scene correspondant en Java 3D.

Enfin, on peut récupérer l'objet de type BranchGroup qui correspond à l'objet racine de la scène Java 3D grâce à la méthode getSceneGroup() de la classe Scene :

BranchGroup getSceneGroup()
Renvoie l'objet de type BranchGroup qui contient la scène entière chargée par le loader.

Un exemple de scène Lightwave éclairée et animée chargée dans une application/applet Java 3D

Le fichier src.zip présente le code source complet de cet exemple ainsi que les fichiers Lightwave utilisés.
Exécutez l'applet ImportLightwave.html pour voir le résultat.

 

L'importation d'objets au format .OBJ de Wavefront est également intégrée à Java 3D sans installation de packages supplémentaires. Un fichier Wavefront est un fichier ASCII qui comporte plusieurs types d'informations comme les coordonnées des sommets, les normales, les facettes, les coordonnées de textures, les matériaux. On se reportera à la documentation Javadoc de Java 3D pour une description détaillée des fichiers .OBJ Wavefront.
Contrairement aux scènes Lightwave (.LWS), il n'y a pas d'éclairage ou d'animation dans un fichier Wavefront. L'éclairage et/ou l'animation devront être fournis par la scène 3D de notre application Java 3D.
Il est obligatoire d'avoir un éclairage pour que l'objet soit visible car les matériaux éventuellement stockés dans l'objet de type Wavefront ne possèdent pas de couleurs émissives. On rappelle que la couleur d'émission d'un matériau n'a pas besoin d'être soumise à une lumière pour être visible.
Il faut utiliser un objet de type com.sun.j3d.loaders.objectfile.ObjectFile (classe qui implémente l'interface Loader) pour importer un objet au format Wavefront.
Les imports suivants sont nécessaires afin de charger un objet Wavefront dans un programme Java 3D :

import com.sun.j3d.loaders.objectfile.*;
import com.sun.j3d.loaders.*;

Voici le constructeur que nous allons utiliser dans notre exemple :

public ObjectFile(int flags)
flags peut prendre comme valeurs les champs suivants :
ObjectFile.RESIZE : les vertices (coordonnées des sommets de l'objet) vont être ajustés de façon à ce que l'objet soit centré en (0,0,0) et qu'ils soient compris entre (-1,-1,-1) et (1,1,1)
ObjectFile.TRIANGULATE : l'objet sera construit en utilisant une géométrie de type GeometryInfo.POLYGON_ARRAY, ceci est utile lorsque l'objet à charger possède une géométrie à trous.
ObjectFile.REVERSE : il faut utiliser ce flag si les facettes de l'objet chargé sont orientées dans le sens des aiguilles d'une montre. En effet, Java 3D oriente les facettes dans le sens inverse des aiguilles d'une montre.
ObjectFile.STRIPIFY : permet d'analyser l'objet chargé afin d'en optimiser l'affichage sous forme de bandes de triangles.
On peut utiliser plusieurs valeurs simultanément pour flag avec l'opérateur OU logique : |

Pour charger une scène proprement dite, on utilisera la méthode load de la classe ObjectFile qui est surchargée 3 fois (tout comme celle de la classe Lw3dLoader). En fait, ces 3 méthodes load implémentent celles de l'interface Loader :

Scene load(java.io.Reader reader)
Scene load(java.lang.String filename)
Scene load(java.net.URL url)

Cette méthode permet donc de charger un objet Wavefront à partir d'un flux, d'un nom de fichier ou d'une URL et renvoie l'objet de type Scene correspondant en Java 3D.

Enfin, on peut récupérer l'objet de type BranchGroup qui correspond à l'objet racine de la scène Java 3D grâce à la méthode getSceneGroup() de la classe Scene :

BranchGroup getSceneGroup()
Renvoie l'objet de type BranchGroup qui contient la scène entière chargée par le loader.

Avant de poursuivre davantage, on trouvera dans la documentation Javadoc de Sun sur la classe ObjectFile, des informations très interessantes quant à la structure d'un fichier .obj de type Wavefront.

Nous avons créé un objet 3D (une tête de singe) avec le logiciel gratuit Blender, et nous l'avons sauvegardé au format Wavefront dans le fichier singe.obj. Ce fichier contient toutes les positions des sommets de la forme 3D ainsi que la construction des facettes et les normales à ces facettes. Nous avons rajouté une librairie qui contient le matériau que l'on va appliquer au singe, cette librairie contient un seul matériau nommé perso et nous l'avons sauvegardée dans le fichier libmat.mtl.
Voici le contenu de ce fichier librairie :

newmtl perso
Ka 1 0 0
Ks 0 1 0
Kd 0 0 1
Ns 8
illum 2

newmtl décrit le nom du matériau (ici perso)
Ka représente les 3 composantes [R,G,B] de la couleur ambiante du matériau
Ks représente les 3 composantes [R,G,B] de la couleur spéculaire du matériau
Kd représente les 3 composantes [R,G,B] de la couleur diffuse du matériau
Ns représente la brillance (shininess)
illum est un flag dont les 3 valeurs possibles sont :
0 : éclairage désactivé
1 : couleurs ambiante et diffuse actives seulement (couleur spéculaire noire)
2 : toutes les couleurs sont actives

Dans le fichier singe.obj, on appelle la librairie libmat.mtl grâces aux lignes suivantes :

mtllib libmat.mtl
usemtl perso

usemtl perso signifie que l'on utilise le matériau perso qui est décrit dans la librairie libmat.mtl. Une librairie pouvant contenir plusieurs matériaux bien sûr.

Enfin, un fichier .obj peut également contenir plusieurs objets. Le nom des objets est désigné par la lettre g :
g singe

Attention :

Avec la version 2.36 de Blender que j'ai utilisée, le nom de l'objet est précédé de la lettre o plutôt que g, ce qui provoque un échec du chargement du fichier .obj. Il faut bien veiller à corriger cet erreur avant de charger le fichier .obj.

 

Un exemple d'objet Wavefront chargé dans une application/applet Java 3D

Le fichier src.zip présente le code source complet de cet exemple ainsi que les fichiers Wavefront utilisés.
Exécutez l'applet ImportWavefront.html pour voir le résultat.

La tête de singe peut être tournée, déplacée et zoomée à la souris.

 

Enfin, pour clore ce paragraphe sur l'importation d'objets 3D, nous allons voir un type d'objet très célèbre mais dont l'importation dans une application Java 3D n'est pas intégrée : il s'agit d'objets au format .3DS de 3D Studio.
Il faut en effet utiliser un programme externe à Java 3D pour pouvoir intégrer des objets 3D Studio à une scène Java 3D.
Nous avons décidé d'utiliser l'archive Loader3DS.jar développée par Josh DeFord et Eric Miller qui supporte entre autres les animations, les lumières et les textures. Le fichier peut être téléchargé grâve au lien suivant :
http://sourceforge.net/projects/java3dsloader
Il existe une multitude d'autres loaders pour 3D Studio et d'autres formats, dont une liste (sûrement non exhaustive) est exposée sur ce site : http://java3d.j3d.org/utilities/loaders.html

Pour installer la librairie Loader3DS.jar afin qu'elle soit prise en compte par notre JDK Java, rien de plus simple, il suffit de la copier à l'endroit suivant : C:\Program\Java\j2sdk1.4.2_06\jre\lib\ext si le JDK Java 2 a été installé dans le répertoire C:\Program\Java\j2sdk1.4.2_06

Cette librairie possède également une classe de démarrage qui permet de lancer une petite démo en tapant en ligne de commande :
java -jar Loader3DS.jar

Enfin, pour pouvoir utiliser cette librairie dans un programme Java, il faudra rajouter une importation :

import com.realvue.sim.ui.loader.java3d.max3ds.*;

En plus de l'importation classique que nous devons utiliser pour tous les imports d'objets ou de scène externes :

import com.sun.j3d.loaders.*;

La classe Loader3DS possède un unique constructeur que nous allons utiliser pour notre exemple :

public Loader3DS()

Comme la classe Loader3DS implémente l'interface Loader, elle surcharge toutes les méthodes de cette interface et en particulier les 3 méthodes load :

Scene load(java.io.Reader reader)
Scene load(java.lang.String filename)
Scene load(java.net.URL url)

Cette méthode permet donc de charger un objet 3D Studio à partir d'un flux, d'un nom de fichier ou d'une URL et renvoie l'objet de type Scene correspondant en Java 3D.

Enfin, comme pour les 2 loaders précédents que nous venons d'étudier, on peut récupérer l'objet de type BranchGroup qui correspond à l'objet racine de la scène Java 3D grâce à la méthode getSceneGroup() de la classe Scene :

BranchGroup getSceneGroup()
Renvoie l'objet de type BranchGroup qui contient la scène entière chargée par le loader.

Nous aurions pu en rester là tout comme les précédents loaders que nous venons d'étudier. Cependant, nous allons aller un peu plus loin et voir qu'il est tout à fait possible de modifier l'objet 3D importé comme s'il avait été créé de toutes pièces en Java 3D. Cette modification d'objet importé n'est pas uniquement applicable au cas 3D Studio, on peut le faire pour n'importe quel objet importé dans Java 3D puisque nous allons jouer sur l'objet de type BranchGroup que nous avons récupéré après l'importation grâce à la méthode : getSceneGroup().
On peut également utiliser cette algorithme pour modifier n'importe quel objet 3D non importé et créé au sein même de notre programme Java 3D.

Nous allons étudier un exemple dans lequel nous importons un objet 3D Studio dont nous allons modifier l'apparence afin de l'afficher en mode "fil de fer".
Pour cela, il est nécessaire de récupérer tous les objets constituant la scène importée au sein d'une énumération :

BranchGroup bg = scene.getSceneGroup();
Enumeration children = bg.getAllChildren();

Mais nous savons bien que les objets constituant une scène 3D importée héritent tous du type Node et qu'ils peuvent être du type : Shape3D, Light, Group etc ...
Si nous voulons modifier l'apparence de l'objet 3D importé, il ne faudra le faire que sur le type Shape3D :

private void modifieApparence(Enumeration children) {

    // Construction d'une apparence fil de fer
    PolygonAttributes filDeFer = new PolygonAttributes(PolygonAttributes.POLYGON_LINE,
                                                       PolygonAttributes.CULL_NONE,
                                                       0);

    // On boucle sur tous les noeuds contenus dans l'enumeration
    while(children.hasMoreElements()){

        Object node = children.nextElement();
        System.out.println("Classe : " + node.getClass());

        // Changement de l'apparence des noeuds de type Shape3D
        if (node instanceof Shape3D){
            Shape3D shape = (Shape3D)node;
            Appearance appearance = shape.getAppearance();
            appearance.setPolygonAttributes(filDeFer);
        }
    } // fin while
}

Ceci serait presque parfait mais il y a un mais. En effet, parmi les nodes de notre énumération il y a des objets de type Group qui contiennent également des "fils" de type Shape3D, il faudra alors modifier notre boucle while() et faire un appel récursif sur modifieApparence() :

// On boucle sur tous les noeuds contenus dans l'enumeration
while(children.hasMoreElements()){

    Object node = children.nextElement();
    System.out.println("Classe : " + node.getClass());

    // Changement de l'apparence des noeuds de type Shape3D
    if (node instanceof Shape3D){
        Shape3D shape = (Shape3D)node;
        Appearance appearance = shape.getAppearance();
        appearance.setPolygonAttributes(filDeFer);
    }

    // Si un noeud est de type Group, il faut alors appeler de nouveau la
    // methode changeApparence() sur tous les noeuds fils de ce groupe
    // (recursivite)
    if (node instanceof Group) {
        Group group = (Group)node;
        modifieApparence(group.getAllChildren());
    }
} // fin while

Dans ce cas, nous sommes absolument sûrs que TOUS les noeuds de type Shape3D auront leur apparence modifiée en mode fil de fer.

Dans notre exemple, nous avons également introduit un comportement permettant de faire touner, de déplacer et de zoomer avec la souris l'objet 3D Studio importé.
Nous avons également introduit une transformation d'échelle afin que notre objet 3D soit entièrement visible :

// Creation de groupe de transformation permettant de reduire le facteur
// d'echelle de l'objet 3D Studio afin qu'il soit entierement visible
TransformGroup scaleTG = new TransformGroup();
Transform3D scale = new Transform3D();
scale.setScale(0.008);
scaleTG.setTransform(scale);

Importation d'un objet 3D Studio pour lequel nous avons modifié l'apparence : affichage en mode fil de fer

Le fichier src.zip présente le code source complet de cet exemple ainsi que le fichier 3D Studio utilisé.
Exécutez l'applet Import3DStudio.html pour voir le résultat.

Remarques :

Si vous lancez l'applet ci-dessus, vous remarquerez sans peine que le maillage de l'objet 3D est beaucoup plus grossier que celui de la capture d'écran présentée dans ce paragraphe. Cela est dû au fait que l'objet 3D studio utilisé pour exécuter l'applet est beaucoup plus petit (8 ko) que celui ayant permis de faire la capture d'écran (plus de 6 Mo) afin de limiter au manimum le temps de téléchargement.
Nous avons également remarqué que l'importation était quelque peu bugguée avec une version OpenGL de Java 3D (vertices parfois aléatoirement reliés entre eux) alors que le phénomène ne se produit pas avec une version Direct3D de Java 3D. Il faudrait voir si ce bug disparaît avec un autre loader pour les objets 3D Studio.

 

Retour haut de page

Chapitre 08 : Animations << >> Chapitre 10 : Utilisation de Java 3D avec AWT et Swing