Désormais, nous allons passer à l’échelon supérieur : l'utilisation de vraie 3D ! Évidement, il vous fait désormais une machine qui dispose d'assez de puissance, de mémoire et d'une carte vidéo capable de supporter de telles sorties. JavaFX supporte le positionnement des nœuds dans la 3eme dimension depuis la version 1.3 (si mes souvenirs sont bons). Mais ce n'est que depuis la version 8 que les primitives 3D sont enfin prises en charge. La prise en charge de la 3D est une fonctionnalité optionnelle, donc vous devez vérifier si votre plateforme la supporte.
Notre premier jet va donc encore utiliser des nœuds 2D (ImageView comme précédemment) mais avec un positionnement et une rotation 3D. Le but, ici, est de faire tourner la carte autour de l'axe Y, dans le plan (XZ). Dans un premier temps, nous allons continuer d'animer les faces séparément puis nous nous efforceront d'animer l'ensemble comme une vraie carte à jouer.
Commençons comme à notre habitude par charger nos cartes dans des instances d'ImageView pour ensuite les positionner dans un gestionnaire de mise en page pour les afficher à l'écran :
Code Java : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | final Image sourceImage = new Image("http://upload.wikimedia.org/wikipedia/commons/thumb/a/a1/Svg-cards-2.0.svg/1280px-Svg-cards-2.0.svg.png"); // final ImageView frontCard = new ImageView(sourceImage); frontCard.setViewport(new Rectangle2D(0, 286, 98, 143)); // final ImageView backCard = new ImageView(sourceImage); backCard.setViewport(new Rectangle2D(197, 572, 98, 143)); backCard.setRotationAxis(Rotate.Y_AXIS); backCard.setRotate(-90); // final StackPane stackPane = new StackPane(); stackPane.getChildren().addAll(frontCard, backCard); // final SubScene subScene = new SubScene(stackPane, 300, 250, false, SceneAntialiasing.BALANCED); |
Grace à la rotation que nous avons spécifié, nous avons positionné la face dos de la carte sur la tranche, donc elle est invisible pour nous et nous ne voyons que la face as d'affichée.
Nous avons ajouté notre groupe d'objet dans un noeud de type SubScene, qui permettra d'avoir de la 3D dans une sous-région de l'UI.
Code Java : | Sélectionner tout |
1 2 3 4 | final BorderPane root = new BorderPane(); root.setTop(toolBar); root.setCenter(subScene); final Scene scene = new Scene(root, 300, 250); |
Nous allons continuer sur le principe des demi-animations que nous avons utilisé précédemment :
- de 0 à 90° : la face avant disparait.
- de 90 à 180° : la face arrière apparait.
- de 180 à 360°, on effectue la même animation mais en inversant les faces arrières et les faces avant.
Nous n'avons donc besoin de coder que deux animations :
- de 0 à 90° - La face A tourne de 0 à 90° autour de l'axe Y. La face B est sur la tranche donc invisible.
- de 90 à 180° - La face B tourne de -90 à 0° autour de l'axe Y. La face A est sur la tranche donc invisible.
Notre fonction flip() devient donc :
Code Java : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | private Transition flip(Node front, Node back) { final RotateTransition rotateOutFront = new RotateTransition(halfFlipDuration, front); rotateOutFront.setInterpolator(Interpolator.LINEAR); rotateOutFront.setAxis(Rotate.Y_AXIS); rotateOutFront.setFromAngle(0); rotateOutFront.setToAngle(90); // final RotateTransition rotateInBack = new RotateTransition(halfFlipDuration, back); rotateInBack.setInterpolator(Interpolator.LINEAR); rotateInBack.setAxis(Rotate.Y_AXIS); rotateInBack.setFromAngle(-90); rotateInBack.setToAngle(0); // return new SequentialTransition(rotateOutFront, rotateInBack); } |
On peut voir que pour la première fois, nous effectuons de vraies rotations ! Celles-ci s'effectuent autour de l'axe Y, l'axe vertical des coordonnées de l’écran orienté du haut vers le bas. Il s'agit bien de rotations dans la 3ème dimension puis les nœuds verront leurs points bouger autour de l'axe Y, dans le plan (XZ) : X étant l'axe horizontal de l’écran orienté de la gauche vers la droite, tandis que Z est l'axe de la profondeur qui part vers l’intérieur de l’écran.
Rotation autour de l'axe Y.
Étant donné que nous ne faisons pas complètement tourner nos faces de 360° (chaque face subit en fait 1/4 d'animation à chaque cycle durant laquelle l'autre face est invisible car tournée de 90°), nous n'avons pas vraiment besoin de changer l'ordre d'affichage des nœuds à l’écran.
Pourtant un défaut surgit rapidement lorsqu'on tente d’exécuter ce code : on ne voit pas de perspective. En fait on se retrouve avec une animation en tout point similaire a celle que nous avions dans notre tout premier essai :
Quelque chose cloche !
En effet, par défaut la scène (ou, ici, la sous-scène) dispose d'une caméra parallèle qui ne permet pas de voir l'effet de perspective ; c'est à dire, en tout point du plan (XY) de l’écran, l'axe Z est perpendiculaire à ce plan. Il est totalement impossible de voir la profondeur. Il nous faut donc à la place une caméra de perspective : c'est à dire qu'en tout point du plan (XY) de l’écran, l'axe Z converge vers un même point de fuite. Une telle caméra permet de voir la profondeur.
Les différents types de caméra et leur rendu.
Nous pouvons faire cela très simplement grâce à la ligne de code suivante :
Code Java : | Sélectionner tout |
subScene.setCamera(new PerspectiveCamera());
Ce qui nous donne :
Le résultat correct !
Et voici le code complet associé :
Code Java : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 | package test; import javafx.animation.Interpolator; import javafx.animation.RotateTransition; import javafx.animation.SequentialTransition; import javafx.animation.Transition; import javafx.application.Application; import javafx.beans.binding.DoubleBinding; import javafx.geometry.Pos; import javafx.geometry.Rectangle2D; import javafx.scene.Node; import javafx.scene.PerspectiveCamera; import javafx.scene.Scene; import javafx.scene.SceneAntialiasing; import javafx.scene.SubScene; import javafx.scene.control.Slider; import javafx.scene.control.ToggleButton; import javafx.scene.control.ToolBar; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.BorderPane; import javafx.scene.layout.StackPane; import javafx.scene.transform.Rotate; import javafx.stage.Stage; import javafx.util.Duration; public class Test_D3ImageView1 extends Application { private final Duration halfFlipDuration = Duration.seconds(1); @Override public void start(Stage primaryStage) { final Image sourceImage = new Image("http://upload.wikimedia.org/wikipedia/commons/thumb/a/a1/Svg-cards-2.0.svg/1280px-Svg-cards-2.0.svg.png"); // final ImageView frontCard = new ImageView(sourceImage); frontCard.setViewport(new Rectangle2D(0, 286, 98, 143)); // final ImageView backCard = new ImageView(sourceImage); backCard.setViewport(new Rectangle2D(197, 572, 98, 143)); backCard.setRotationAxis(Rotate.Y_AXIS); backCard.setRotate(-90); // final StackPane stackPane = new StackPane(); stackPane.getChildren().addAll(frontCard, backCard); final SubScene subScene = new SubScene(stackPane, 300, 250, false, SceneAntialiasing.BALANCED); subScene.setCamera(new PerspectiveCamera()); final ToggleButton playButton = new ToggleButton("Play"); StackPane.setAlignment(playButton, Pos.TOP_LEFT); final Slider timeSlider = new Slider(0, 4 * halfFlipDuration.toMillis(), 0); timeSlider.setDisable(true); final ToolBar toolBar = new ToolBar(); toolBar.getItems().addAll(playButton, timeSlider); final BorderPane root = new BorderPane(); root.setTop(toolBar); root.setCenter(subScene); final Scene scene = new Scene(root, 300, 250); primaryStage.setTitle("3D: ImageView"); primaryStage.setScene(scene); primaryStage.show(); // final SequentialTransition animation = new SequentialTransition( flip(frontCard, backCard), flip(backCard, frontCard)); animation.setCycleCount(SequentialTransition.INDEFINITE); playButton.selectedProperty().addListener((observableValue, oldValue, newValue) -> { if (newValue) { animation.play(); } else { animation.pause(); } }); timeSlider.valueProperty().bind(new DoubleBinding() { { bind(animation.currentTimeProperty()); } @Override public void dispose() { super.dispose(); unbind(animation.currentTimeProperty()); } @Override protected double computeValue() { return animation.getCurrentTime().toMillis(); } }); } private Transition flip(Node front, Node back) { final RotateTransition rotateOutFront = new RotateTransition(halfFlipDuration, front); rotateOutFront.setInterpolator(Interpolator.LINEAR); rotateOutFront.setAxis(Rotate.Y_AXIS); rotateOutFront.setFromAngle(0); rotateOutFront.setToAngle(90); // final RotateTransition rotateInBack = new RotateTransition(halfFlipDuration, back); rotateInBack.setInterpolator(Interpolator.LINEAR); rotateInBack.setAxis(Rotate.Y_AXIS); rotateInBack.setFromAngle(-90); rotateInBack.setToAngle(0); // return new SequentialTransition(rotateOutFront, rotateInBack); } public static void main(String[] args) { launch(args); } } |
Il semble que lorsqu'on utilise des contrôles 2D dans un environnement 3D, l’éclairage de la scène ne s'applique pas à ces derniers (j'ai fait des tests en insérant une source de lumiere dans la sous-scène). Donc, pour simuler à nouveau un éclairage, nous utilisons la même astuce que d’habitude :
Code Java : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | package test; import javafx.animation.Interpolator; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.ParallelTransition; import javafx.animation.RotateTransition; import javafx.animation.SequentialTransition; import javafx.animation.Timeline; import javafx.animation.Transition; import javafx.application.Application; import javafx.beans.binding.DoubleBinding; import javafx.geometry.Pos; import javafx.geometry.Rectangle2D; import javafx.scene.Node; import javafx.scene.PerspectiveCamera; import javafx.scene.Scene; import javafx.scene.SceneAntialiasing; import javafx.scene.SubScene; import javafx.scene.control.Slider; import javafx.scene.control.ToggleButton; import javafx.scene.control.ToolBar; import javafx.scene.effect.ColorAdjust; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.BorderPane; import javafx.scene.layout.StackPane; import javafx.scene.transform.Rotate; import javafx.stage.Stage; import javafx.util.Duration; public class Test_D3ImageView2 extends Application { private final Duration halfFlipDuration = Duration.seconds(1); @Override public void start(Stage primaryStage) { final Image sourceImage = new Image("http://upload.wikimedia.org/wikipedia/commons/thumb/a/a1/Svg-cards-2.0.svg/1280px-Svg-cards-2.0.svg.png"); // final ImageView frontCard = new ImageView(sourceImage); frontCard.setViewport(new Rectangle2D(0, 286, 98, 143)); final ColorAdjust frontColorAdjust = new ColorAdjust(); frontCard.setEffect(frontColorAdjust); // final ImageView backCard = new ImageView(sourceImage); backCard.setViewport(new Rectangle2D(197, 572, 98, 143)); backCard.setRotationAxis(Rotate.Y_AXIS); backCard.setRotate(-90); final ColorAdjust backColorAdjust = new ColorAdjust(); backCard.setEffect(backColorAdjust); // final StackPane stackPane = new StackPane(); stackPane.getChildren().addAll(frontCard, backCard); final SubScene subScene = new SubScene(stackPane, 300, 250, false, SceneAntialiasing.BALANCED); subScene.setCamera(new PerspectiveCamera()); final ToggleButton playButton = new ToggleButton("Play"); StackPane.setAlignment(playButton, Pos.TOP_LEFT); final Slider timeSlider = new Slider(0, 4 * halfFlipDuration.toMillis(), 0); timeSlider.setDisable(true); final ToolBar toolBar = new ToolBar(); toolBar.getItems().addAll(playButton, timeSlider); final BorderPane root = new BorderPane(); root.setTop(toolBar); root.setCenter(subScene); final Scene scene = new Scene(root, 300, 250); primaryStage.setTitle("3D: ImageView + shadow"); primaryStage.setScene(scene); primaryStage.show(); // final SequentialTransition animation = new SequentialTransition( flip(frontCard, frontColorAdjust, backCard, backColorAdjust), flip(backCard, backColorAdjust, frontCard, frontColorAdjust)); animation.setCycleCount(SequentialTransition.INDEFINITE); playButton.selectedProperty().addListener((observableValue, oldValue, newValue) -> { if (newValue) { animation.play(); } else { animation.pause(); } }); timeSlider.valueProperty().bind(new DoubleBinding() { { bind(animation.currentTimeProperty()); } @Override public void dispose() { super.dispose(); unbind(animation.currentTimeProperty()); } @Override protected double computeValue() { return animation.getCurrentTime().toMillis(); } }); } private Transition flip(Node front, ColorAdjust frontColorAdjust, Node back, ColorAdjust backColorAdjust) { final RotateTransition rotateOutFront = new RotateTransition(halfFlipDuration, front); rotateOutFront.setInterpolator(Interpolator.LINEAR); rotateOutFront.setAxis(Rotate.Y_AXIS); rotateOutFront.setFromAngle(0); rotateOutFront.setToAngle(90); final Timeline changeBrightnessFront = new Timeline( new KeyFrame(Duration.ZERO, new KeyValue(frontColorAdjust.brightnessProperty(), 0)), new KeyFrame(halfFlipDuration, new KeyValue(frontColorAdjust.brightnessProperty(), -1))); final ParallelTransition flipOutFront = new ParallelTransition(rotateOutFront, changeBrightnessFront); // final RotateTransition rotateInBack = new RotateTransition(halfFlipDuration, back); rotateInBack.setInterpolator(Interpolator.LINEAR); rotateInBack.setAxis(Rotate.Y_AXIS); rotateInBack.setFromAngle(-90); rotateInBack.setToAngle(0); final Timeline changeBrightnessBack = new Timeline( new KeyFrame(Duration.ZERO, new KeyValue(backColorAdjust.brightnessProperty(), -1)), new KeyFrame(halfFlipDuration, new KeyValue(backColorAdjust.brightnessProperty(), 0))); final ParallelTransition flipInBack = new ParallelTransition(rotateInBack, changeBrightnessBack); // return new SequentialTransition(flipOutFront, flipInBack); } public static void main(String[] args) { launch(args); } } |
Point de capture ici, car on est limité à 5 images grand maximum par message. De toute manière, vous savez deja ce à quoi vous attendre au niveau de la sortie.
Je comptais également aborder le groupement des deux faces de la carte en un seul objet et la simplification de la rotation dans ce post-ci histoire d'en finir définitivement avec la 2D ; mais, compte tenu de cette limitation du nombre d'images, ça sera pour la prochaine fois. Rassurez-vous ce sera une entrée de blog assez courte et après nous enchainerons assez rapidement sur une vraie forme 3D.