Retour table des matières Java 3D
1. Quelques astuces pour faire cohabiter correctement Java 3D et Swing
Problèmes d'affichage lorsqu'on modifie la taille de la fenêtre Java 3D
Lorqu'on veut modifier la taille ou maximiser une fenêtre, il se peut que le canvas 3D ne se redessine pas. Cela est apparremment dû à un bug dans le JDK version 1.4
Il existe un moyen de contourner ce bug en redéfinissant la classe Canvas3D de façon à synchroniser l'affichage :
L'astuce consiste à surcharger la méthode paint() de la classe Canvas3D en rajoutant l'instruction :
Toolkit.getDefaultToolkit().sync();
Voici un exemple de code où l'on crée une classe RefreshCanvas3D qui étend Canvas3D et dont on a surchargé la méthode paint() :
/**
* On surcharge la classe Canvas3D pour eviter les problemes d'affichage
* lorsqu'on modifie la taille de la fenetre de l'application
*/
class RefreshCanvas3D extends Canvas3D {
public RefreshCanvas3D(GraphicsConfiguration config) {
super(config);
}
public void paint(Graphics g) {
super.paint(g);
Toolkit.getDefaultToolkit().sync();
}
}
Nous avons également écrit un exemple d'application Java 3D complète utilisant cette classe RefreshCanvas3D :
Le fichier SyncCanvas3D.java présente le code source complet de cet exemple.
Problèmes d'affichage lorsqu'on utilise des menus contextuels
Lorsque l'on veut afficher des composants Swing (comme par exemple un menu contextuel) par dessus une fenêtre Java 3D, il se peut que le menu ne soit pas visible. Cela est dû au fait que les composants Swing sont des composants dits "lightweight", c'est à dire qu'ils font appel à des routines qui ne sont pas natives, contrairement aux composants AWT qui sont dit "heavyweight" car ils font appel à des routines natives.
Or les composants Java 3D (comme les canvas 3D) sont des composants "heavyweight", il faut donc ajouter une instruction spéciale si on veut afficher une menu contextuel Swing par dessus un canvas 3D :
JPopupMenu.setDefaultLightWeightPopupEnabled(false);
Cette instruction doit être placée de préférence en début de programme lorsque les composants graphiques ne sont pas encore créés. Elle permet d'assimiler les menus contextuels à des composants lourds "heavyweight".
Nous avons créé une petite application 3D utilisant un menu contextuel permettant de modifier le rendu d'une scène 3D :
Le menu contextuel s'affiche par dessus le canvas 3D
Il faut noter que cette instruction supplémentaire n'est pas obligatoire pour que le menu apparaîsse, mais dans certaines configurations, cela devient nécessaire.
Le fichier MenuContextuel.java présente le code source complet de cet exemple.
Exécutez l'applet MenuContextuel.html pour voir le résultat.
Problèmes d'affichage lorsqu'on utilise le composant JTabbedPane
Nous allons voir un dernier problème d'affichage concernant l'interaction composants Swing / Java 3D.
Il s'agit du composant JTabbedPane (panneau contenant plusieurs autres panneaux accessibles par des onglets). Le problème est que si l'on met un canvas 3D dans chaque panneau du composant JTabbedPane, ils risquent de ne pas s'afficher correctement. C'est notamment le dernier panneau qui a été ajouté lors de la construction du composant JTabbedPane qui s'affichera par défaut au lancement de l'application même si l'on a demandé l'affichage d'un autre panneau. Pour corriger ce problème, il faut créer une classe qui étende JPanel et qui contienne un objet de type Canvas3D. Il faut ensuite surcharger la méthode setVisible() de JPanel afin d'appeler cette même méthode setVisible() sur le canvas 3D lui même. Cela permet de synchroniser l'affichage du canvas 3D avec celui du JPanel :
/**
* Methode surchargee de JPanel afin de synchroniser l'affichage du
* canvas 3D avec celui du JPanel
* @param v boolean
*/
public void setVisible(boolean v) {
super.setVisible(v);
canvas3D.setVisible(v);
}
Nous avons créé un exemple présentant un objet de type JTabbedPane qui contient 3 canvas 3D, chaque canvas contient une sphère de couleur différente. On passe de l'affichage d'un canvas 3D à un autre en cliquant sur un onglet du JTabbedPane.
JTabbedPane contenant 3 canvas 3D
Le fichier JTabbedPaneCanvas3D.java présente le code source complet de cet exemple.
Exécutez l'applet JTabbedPaneCanvas3D.html pour voir le résultat.
2. Capture et sauvegarde s'une image 3D
Nous allons voir dans ce paragraphe comment capturer une image d'une application 3D et la transférer dans le presse-papiers ou la sauvegarder dans un fichier.
Capture d'une image 3D et transfert dans le presse-papiers
Dans ce paragraphe, nous allons voir comment capturer une image d'une application 3D afin de la transférer dans le presse-papiers. Ensuite, cette image pourra être utilisée dans d'autres applications par un simple coller à partir du presse-papiers.
Une telle classe existe déjà pour les chaînes de caractères, elle s'appelle StringSelection. Curieusement, celle pour les images n'a pas été créée, nous allons donc le faire nous mêmes, la chose étant relativement aisée.
Nous allons appeler cette classe englobante ImageSelection. Elle doit impérativement implémenter l'interface Transferable (située dans le package java.awt.datatransfert). Tous les objets qui peuvent être transférés via un presse-papiers doivent implémenter cette interface Transferable. Nous allons également utiliser la classe DataFlavor qui décrit le type de données pouvant être placées dans un presse-papiers.
Le SDK Java fournit la constante DataFlavor.imageFlavor qui effectue le lourd travail permettant de convertir des images Java et des images natives du presse-papiers.
Comme la classe ImageSelection implémente l'interface Transferable, elle doit également implémenter toutes les méthodes de cette interface, à savoir :
public Object getTransferData(DataFlavor flavor)
Renvoie la donnée à transférer selon le type spécifié. Ici le type spécifié, dans le cas d'une image est : DataFlavor.imageFlavor
public DataFlavor[] getTransferDataFlavors()
Renvoie un tableau des types qui peuvent être transférés. Ici, un seul type pour les images : DataFlavor.imageFlavor
public boolean isDataFlavorSupported(DataFlavor flavor)
Renvoie true si le type de donnée flavor est transférable. Dans le cadre d'une image cette méthode doit renvoyer true si flavor = DataFlavor.imageFlavor
Le canvas 3D offscreen va pouvoir être créé à partir d'un canvas 3D normal (onscreen) en utilisant le constructeur suivant de la classe Canvas3D :
public Canvas3D(GraphicsConfiguration graphicsConfiguration, boolean offscreen)
graphicsConfiguration représente la configuration graphique
offscreen est un booléan décrivant le type de canvas 3D, s'il est à true, le canvas 3D sera alors offscreen.
Ensuite, après avoir créé ce canvas 3D offscreen, il va falloir ajuster l'objet de type Screen3D associé au canvas offscreen à la taille de l'objet Screen3D associé au canvas onscreen :
// On regle le screen 3D (offscreen) a la taille du screen 3D (onscreen)
Screen3D onScreen = onScreenCanvas3D.getScreen3D();
Screen offScreen = this.getScreen3D();
offScreen.setSize(onScreen.getSize());
offScreen.setPhysicalScreenWidth(onScreen.getPhysicalScreenWidth());
offScreen.setPhysicalScreenHeight(onScreen.getPhysicalScreenHeight());
Le constructeur de notre classe OffScreenCanvas3D se présente donc de la façon suivante :
public OffScreenCanvas3D(Canvas3D onScreenCanvas3D) {
super(onScreenCanvas3D.getGraphicsConfiguration(), true);
// On regle le screen 3D (offscreen) a la taille du screen 3D (onscreen)
onScreen = onScreenCanvas3D.getScreen3D();
offScreen = this.getScreen3D();
offScreen.setSize(onScreen.getSize());
offScreen.setPhysicalScreenWidth(onScreen.getPhysicalScreenWidth());
offScreen.setPhysicalScreenHeight(onScreen.getPhysicalScreenHeight());
}
Pour terminer, nous allons faire un rendu de l'image 3D affichée à l'écran dans notre canvas 3D offscreen que nous venons juste de créer. Pour cela nous allons créer une méthode getOffScreenImage() de notre classe OffScreenCanvas3D que nous allons décomposer de la façon suivante :
BufferedImage bImage = new BufferedImage(dim.width, dim.height,
BufferedImage.TYPE_INT_ARGB);
ImageComponent2D buffer = new ImageComponent2D(ImageComponent.FORMAT_RGBA, bImage);
buffer.setCapability(ImageComponent2D.ALLOW_IMAGE_READ);
- On utilise la méthode setOffScreenBuffer() de la classe Canvas3D (dont hérite notre classe OffScreenCanvas3D) pour signifier au moteur de rendu que l'image 3D sera stockée en mémoire dans l'objet buffer :
setOffScreenBuffer(buffer);
- On effectue le rendu et on attend qu'il soit terminé :
renderOffScreenBuffer();
waitForOffScreenRendering();
- On récupère l'image rendue dans le canvas 3D offscreen :
bImage = getOffScreenBuffer().getImage();
Désormais, nous avons tous les éléments à notre disposition pour pouvoir stocker une image 3D affichée à l'écran dans le presse-papiers :
// Dimension (en pixels) de l'image a mettre dans le presse-papiers
Dimension dim = new Dimension(512, 512);
// On recupere l'image (pixmap) rendue par le canvas 3D offscreen
BufferedImage image = offScreenCanvas.getOffScreenImage(dim);
// Recuperation du presse-papiers systeme
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
// Copie de l'image dans le presse-papiers
ImageSelection selection = new ImageSelection(image);
clipboard.setContents(selection, null);
Nous avons créé un petit exemple qui représente un cône affiché dans un fenêtre Java 3D avec un bouton "Capturer". Le fait de presser ce bouton placera l'image dans le presse-papiers système, et ensuite, on pourra afficher cette même image dans une autre application comme Word par un simple coller.
L'archive src.zip contient tous les fichiers sources de cet exemple.
Exécutez l'applet CaptureImage3D.html pour voir le résultat.
Capture d'une image 3D et sauvegarde dans un fichier
Dans la continuité du paragraphe précédent, nous allons encore parler de capture d'image 3D mais suivie cette fois ci d'une sauvegarde dans un fichier image au lieu d'un transfert dans le presse-papiers système.
Toutes les étapes que nous avons expliquées pour la création et l'utilisation d'un canvas 3D offscreen dans notre classe OffScreenCanvas3D sont encore valables ici. En effet, nous rappelons qu'il est impossible de capturer une image 3D affichée dans une canvas 3D si celui ci correspond au canvas onscreen entrain d'être affiché.
En revanche, comme nous n'utilisons pas le presse-papiers, nous n'avons plus besoin de la classe enveloppe ImageSelection. Ici, il faudra simplement sauvegarder dans un fichier l'image préalablement capturée grâce à notre classe OffScreenCanvas3D.
Voici les différentes étapes de l'algorithme :
File imageFile = new File("image.png");
La taille de l'image sauvegardée n'est pas obligatoirement la même que la taille de l'image affichée
Dimension dim = new Dimension(512, 512);
BufferedImage image = offScreenCanvas.getOffScreenImage(dim);
Graphics2D gc = image.createGraphics();
gc.drawImage(image, 0, 0, null);
try {
ImageIO.write(image, "png", imageFile);
}
catch (IOException ex) {
System.out.println("Impossible de sauvegarder l'image");
}
Nous avons créé un petit exemple qui illustre la petite explication que nous venons de faire sur la sauvegarde d'images 3D. Mais cet exemple ne pourra pas être exécuté sous forme d'applet car pour des raisons de sécurité, il est impossible pour une applet d'écrire sur le disque dur afin de sauvegarder une image.
Cet exemple ne pourra se lancer que sous forme d'une application.
L'archive src.zip contient tous les fichiers sources de cet exemple.
L'archive SaveImage3D.jar est un jar exécutable permettant de lancer l'application
Il suffit de cliquer sur le lien de l'archive exécutable et de cliquer sur le bouton "Ouvrir" pour lancer l'application. Lorsqu'on clique sur le bouton Sauvegarder, l'image sera stockée sur le bureau Windows.
On peut aussi télécharger l'archive sur le disque dur et exécuter l'application à partir du disque dur en double cliquant sur l'archive, l'image sera sauvegardée à l'endroit où l'application à été téléchargée.
On peut encore exécuter l'application après l'avoir téléchargée sur le disque dur en ouvrant une invite de commande là où l'archive a été téléchargée et en tapant :
java -jar SaveImage3D.jar
L'image sera sauvegardée à l'endroit où l'application a été téléchargée.
Remarque :
Un jar exécutable n'est autre qu'une archive zip dans laquelle on a inclus toutes les classes (fichiers .class) nécessaires au fonctionnement de notre application. A la racine de l'archive zip se trouve un répertoire nommé META-INF dans lequel se trouve un fichier manifeste nommé MANIFEST.MF, ce fichier contient le nom de la classe de démarrage de notre application. Voici le contenu du fichier manifeste pour l'application de notre exemple :
Manifest-Version: 1.0
Created-By: 1.5.0_02 (Sun Microsystems Inc.)
Main-Class: SaveImage3D
3. Impression d'une image 3D
Nous allons voir dans ce paragraphe comment imprimer une image affichée dans un canvas 3D. En fait, la procédure est similaire à celle de l'exemple précédent lorsqu'on voulait sauvegarder l'image 3D dans un fichier. Nous allons simplement remplacer la portion de code qui consistait à sauvegarder le fichier par une portion consistant à imprimer l'image 3D.
Voici les étapes à suivre afin de pouvoir imprimer une image 3D :
import java.awt.print.*;
PrinterJob job = PrinterJob.getPrinterJob();
Nous allons utiliser la méthode setPrintable() qui attend un objet de type Printable en paramètre. Or Printable est une interface, il faut donc passer en argument de setPrintable() un objet qui implémente l'interface Printable. Une manière élégante de le faire consiste à utiliser une classe anonyme qui étend l'interface Printable. Cette classe anonyme devra obligatoirement implémenter la méthode print() déclarée dans l'interface Printable :
job.setPrintable(new Printable() {
// Classe anonyme (pas de nom de constructeur) qui etend l'interface
// Printable, on doit surcharger la methode print()
public int print(Graphics g, PageFormat pf, int page) {
// Portion de code permettant d'envoyer les données à imprimer
// .....
}
La méthode print() possède 3 arguments :
Graphics g est le contexte graphique dans lequel la page que nous allons imprimer est rendue
PageFormat pf représente la taille est l'orientation de la ou des pages à imprimer
int pageIndex est le numéro (commençant par 0) de la page à imprimer
Cette méthode doit toujours retourner la valeur Printable.PAGE_EXISTS si le rendu de la page à imprimer s'est bien déroulé et Printable.NO_SUCH_PAGE si la page d'index pageIndex n'existe pas.
Enfin, il reste à faire le plus dur, c'est à dire envoyer notre page dans le contexte graphique g fourni par la méthode print() :
public int print(Graphics g, PageFormat pf, int pageIndex) {
// Il n'y a qu'une seule page a imprimer (page d'index 0)
if (pageIndex >= 1)
return Printable.NO_SUCH_PAGE;
Graphics2D g2D = (Graphics2D)g;
// On positionne l'origine du contexte graphique a l'origine de la zone imprimable
g2D.translate(pf.getImageableX(), pf.getImageableY());
// Dimension (en pixels) de l'image a imprimer
Dimension dim = new Dimension(256, 256);
// On recupere l'image (pixmap) rendue par le canvas 3D offscreen
BufferedImage image = offScreenCanvas.getOffScreenImage(dim);
// Image finale qui va etre imprimee
g2D.drawImage(image, 0, 0, null);
return Printable.PAGE_EXISTS;
} // fin methode print()
Bon, au final on voit bien que ce n'était pas si dur que ça !
Les quelques lignes de code suivantes permettent de terminer le travail en lançant effectivement le travail d'impression :
// printDialog() sans parametre appelle une BDD d'impression native
if (job.printDialog()) {
try {
// Lance l'impression (lorsqu'on a appuye sur le bouton OK de la BDD d'impression)
job.print();
}
catch (PrinterException ex) {
System.out.println("Impossible d'imprimer l'image");
}
Nous avons repris le même exemple que dans le paragraphe précédent mais cette fois-ci avec un bouton "Imprimer" au lieu de "Sauvegarder".
L'archive src.zip contient tous les fichiers sources de cet exemple.
L'archive PrintImage3D.jar est un jar exécutable permettant de lancer l'application
4. Création d'un fichier vidéo à partir d'une animation Java 3D
Enfin, pour terminer (en beauté) ce chapitre, nous allons voir comment créer une vidéo à partir d'une animation Java 3D. En fait, nous allons un peu tricher car nous n'allons pas créer explicitement une vidéo mais plutôt une succession d'images qui pourront par la suite être facilement transformées en vidéo grâces à des outils tiers comme VirtualDub par exemple.
Nous utiliserons le même algorithme que celui que nous avons étudié dans la section consacrée à la capture et sauvegarde d'une image 3D.
L'idée consiste ici à sauvegarder régulièrement des images pendant que l'animation se déroule. La difficulté majeure consiste à obtenir une vidéo fluide qui ait la même cadence que l'animation Java 3D. Il est bien sûr évident qu'un système relativement puissant est nécessaire pour obtenir un bon résultat mais nous étudierons une méthode qui permet d'obtenir une vidéo à la bonne cadence même sur des ordinateurs un peu faiblards.
Pour obtenir une vidéo fluide, il est nécessaire d'avoir une vidéo cadencée à 25 images par seconde. Nous allons donc procéder à 25 captures pour 1 seconde d'animation, mais cette cadence impose qu'une capture d'image se déroule en 40 millisecondes au maximum. Or, sur mon PC personnel à base de Petium 4 3,4 Ghs, la durée décessaire pour faire une capture d'image 3D est plustôt de l'ordre de 500 ms. On voit donc là qu'il est impossible (du moins sur les machines actuelles) de réaliser une capture en temps réel. La solution que je propose donc pour résoudre ce problème consiste à réduire la cadence des captures d'images d'un certain facteur et de ralentir d'autant la vitesse de l'animation afin que notre vidéo sauvegardée (25 captures par seconde) soit conforme à l'animation originale non ralentie.
Nous avons écrit un petit exemple qui consiste à créer une vidéo à partir d'une animation représentant un cône entrain de tourner autour de son axe. Lorsque le programme se lance, l'animation se déroule en temps réel, la capture des images qui vont constituer la vidéo commence dès qu'on appuie sur le bouton "Enregistrer" et cesse dès qu'on appuie sur le bouton "Arrêter". Dès que l'enregistrement commence, la vitesse de l'animation est réduite d'une facteur 15 (facteur que j'ai choisi sur mon PC mais qui dépend de la puissance : plus le PC est lent, plus il faudra augmenter de facteur), ce qui permet de prendre des captures d'images toutes les 40*15 = 600 ms. Ensuite, dès qu'on arrête l'enregistrement, l'animation reprend sa vitesse normale initiale.
En fait, l'animation Java 3D se déroule dans un thread et nsou allons mettre en oeuvre la capture dans un autre thread. Comme la capture doit se produire à intervalles de temps réguliers, la meilleure façon de procéder est de créer un Timer pour déclencher la capture :
// Configure le Timer (une image produite toutes les
// 40*realTimeSpeedFactor ms pour avoir une video temps reel de
// l'animation a 25 images/s)
threadMonitor = new Timer(40*realTimeSpeedFactor, new ActionListener() {
public void actionPerformed(ActionEvent event) {
saveImageCB();
}
});
realTimeSpeedFactor est le facteur de ralentissement de l'animation (15 dans l'exemple).
La méthode saveImageCB() est exactement la même que celle que nous avions écrite dans le paragraphe consacré à la sauvegarde d'image 3D.
Enfin, la réelle difficulté se situe au niveau des méthodes callback des boutons "Enregistrer" et "Arreter" lorsqu'on va modifier la vitesse de l'animation 3D.
Dès que l'on clique sur "Enregister", la vitesse de l'animation va être ralentie d'un facteur realTimeSpeedFactor grâce à la méthode setIncreasingAlphaDuration() de la classe Alpha. Cependant, le startTime du timer Alpha doit également être mis à jour car le fait de changer le paramètre increasingAlphaDuration affecte le timer dès le début de son action. Il faut donc recalculer la valeur du startTime comme si le timer avait été construit dès le début avec la nouvelle valeur de increasingAlphaDuration qu'on vient de lui affecter :
startTime = System.currentTimeMillis()-
((System.currentTimeMillis()-startTime)*realTimeSpeedFactor);
rotationAlpha.setStartTime(startTime);
rotationAlpha.setIncreasingAlphaDuration(duree * realTimeSpeedFactor);
duree représente la valeur initiale du champ increasingAlphaDuration du timer, on la remplace par la nouvelle valeur qui va ralentir l'animation : duree*realTimeSpeedFactor
System.currentTimeMillis() représente le temps au moment où on clique sur "Enregistrer"
System.currentTimeMillis()-startTime représente le temps écoulé entre le moment où l'application a démarré et le moment où l'on a cliqué sur "Enregistrer" lorsque increasingAlphaDuration valait duree
(System.currentTimeMillis()-startTime)*realTimeSpeedFactor représente le temps virtuellement écoulé entre le moment où l'application a démarré et le moment où l'on a cliqué sur "Enregistrer" si increasingAlphaDuration avait valu duree*realTimeSpeedFactor à la création du timer.
On en déduit donc la nouvelle valeur du startTime.
Le callback du bouton "Enregistrer" lance alors le thread qui va procéder à la capture des images.
Il faut reprendre le même raisonnement lorsqu'on va cliquer sur le bouton "Arreter" afin de stopper la capture des images et de ramener l'animation à sa vitesse initiale.
La valeur du paramètre increasingAlphaDuration doit reprendre sa valeur initiale, c'est à dire duree. Ensuite, le startTime doit également reprendre sa valeur initiale :
startTime = System.currentTimeMillis()-
((System.currentTimeMillis()-startTime)/realTimeSpeedFactor);
L'archive src.zip contient tous les fichiers sources de cet exemple.
L'archive CreationVideo.jar est un jar exécutable permettant de lancer l'application
Attention :
Pour exécuter cet exemple, je conseille plutôt d'enregistrer l'archive exécutable dans un répertoire de votre choix plutôt que de l'exécuter directement après avoir cliqué sur le lien car lorsqu'on clique sur "Enregistrer" un grand nombre d'images est stocké sur le disque, et il vaut mieux que ce soit dans un répertoire choisi plutôt que sur le bureau Windows !
Les images sont enregistrées à l'endroit où l'application a été lancée et sont stockées sous la forme : imageXXX.png où XXX est le numéro de l'image. Ensuite, pour créé la vidéo, on peut utiliser le logiciel libre et gratuit VirtualDubMod qui peut être téléchargé à l'asresse suivante : http://virtualdubmod.sourceforge.net
Après avoir installé VirtualDubMod, aller dans File -> Open video file, sélectionner la première image sauvegardée (image0.png) puis cliquer sur Ouvrir.
VirtualDubMod charge alors automatiquement toutes les images
Sélectionner ensuite l'option Video -> Full processing mode puis Video -> Compression...
Choisir le codec de compression vidéo (les codecs XviD ou DivX sont un bon choix)
Enfin, après avoir réglé les paramètres de la compression (un débit (bitrate en anglais) de 1000 kbps est un bon choix), il ne faut pas oublier de régler le frame rate à 25 images par seconde grâce à l'option Video -> Frame Rate...
Cliquer ensuite sur OK puis sauvegarder la vidéo en sélectionnant l'option Fichier -> Save As...
Entrer un nom pour le fichier vidéo puis cliquer sur Enregistrer.
Le fichier testvideo.avi qui représente l'animation Java 3D est alors automatiquement créé.
Chapitre 09 : Outils et aspects avancés de Java 3D << >> Chapitre 11 : Programmation d'un jeu