Voir le flux RSS

bouye

Faire tourner les cartes - partie 6

Noter ce billet
par , 26/02/2015 à 21h00 (1202 Affichages)
La dernière étape de cette série de posts est arrivée. Nous avons vu la partie théorique concernant la 3D, maintenant voyons la partie pratique en JavaFX !

Nom : Card3D.png
Affichages : 753
Taille : 19,4 Ko

Nous allons tout d'abord charger notre image et en extraire les deux faces de la carte pour créer notre texture source :

Code Java : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
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 Canvas canvas = new Canvas(98 * 2, 143);
final GraphicsContext g2d = canvas.getGraphicsContext2D();
g2d.drawImage(sourceImage, 0, 286, 98, 143, 0, 0, 98, 143); // Face as.
g2d.drawImage(sourceImage, 197, 572, 98, 143, 98, 0, 98, 143); // Face dos.
final Image texture = canvas.snapshot(null, null);

Ici, nous avons chargé l'image contenant l’intégralité du jeu de carte directement sans effectuer un chargement en arrière-plan (le paramètre backgroundLoading dans un autre constructeur de la classe Image), cet appel est donc bloquant tant que l'image n'est pas chargée (ou qu'il n'y a pas un timeout). Nous extrayons ensuite les deux parties de la carte dans un même Canvas dans lequel nous dessinons grâce à son GraphicsContext et ensuite nous faisons une capture d’écran de ce canevas en appelant la méthode snapshot() pour créer la texture source de notre carte.

Maintenant, nous allons définir notre nuage de points P qui sont les points 3D de la géométrie de la carte dans l'espace :

Code Java : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
final float[] points = {
    0, 0, 0, // 0 - Upper left corner.
    98, 0, 0, // 1 - Upper right corner.
    98, 143, 0, // 2 - Lower right corner.
    0, 143, 0, // 3 - Lower left corner.
};

Ici, j'ai donné à la carte des dimensions (en pixels) similaires a ceux des faces dans l'image source, mais c’était plus une question d'habitude histoire que la carte ait les mêmes dimensions visuelles à l’écran.

Ensuite, nous définissions le nuage de points T qui sont les points de coordonnées 2D de texture sur la texture source :

Code Java : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
final float[] textCoords = {
    0, 0, // 0 - Upper left texture corner.
    0.5f, 0, // 1 - Upper middle texture.
    1, 0, // 2 - Upper right texture corner.
    1, 1, // 3 - Lower right texture corner.
    0.5f, 1, // 4 - Lower middle texture.
    0, 1, // 5 - lower left texture.
};

Vous noterez que ces points-ci représentent des valeurs de positionnement proportionnelles par rapport aux dimensions de l'image :
  • 0 = bord gauche ou haut ;
  • 0.5 = milieu de la largeur ou de la hauteur ;
  • 1 = bord droit ou bas.

Ce qui peut rendre évidement non-trivial l'utilisation ou le placement précis d'une texture sur une géométrie complexe. Encore une raison de plus pour utiliser un logiciel de modélisation externe quand on utilise un modèle 3D "statique" (non-généré au vol)

Mais du coup, comme vous utilisez des positionnements relatifs, vous pouvez utiliser une texture ayant une résolution, des dimensions ou même un ratio entre les dimensions complètement différents de ceux de la carte. Cela permet, par exemple, d'utiliser un pack optionnel de textures haute résolution, simplement en modifiant l'image de la texture, sans pour autant modifier le modèle 3D.

Maintenant, nous définissons les facettes, les triangles (P0, T0, P1, T1, P2, T2) qui sont composés à la fois de points T issus de la géométrie et de points P issus de la texture. Chaque facette contient donc bien 6 points :

Code Java : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
final int[] faces = {
    1, 1, 0, 0, 3, 5, // Front top left face.
    2, 4, 1, 1, 3, 5, // Front bottom right face.
    1, 1, 3, 3, 0, 2, // Back top left face.
    2, 4, 3, 3, 1, 1, // Back right left face.
};

Ici, le tableau faces, contient des indices de valeurs contenues dans les tableaux points et textCoords.

Et enfin, nous groupons le tout dans un objet de type TriangularMesh que nous rendons visible grâce à un nœud de type MeshView :

Code Java : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
final TriangleMesh mesh = new TriangleMesh();
mesh.getPoints().addAll(points);
mesh.getTexCoords().addAll(textCoords);
mesh.getFaces().addAll(faces);
final MeshView card = new MeshView(mesh);

Étant donné que pour le moment la texture n'est pas liée à la carte, si nous affichons cet objet tel quel, il apparaitra vide ou blanc. Il nous faut donc ajouter notre texture dans le matériau qui recouvre notre maillage :

Code Java : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
final PhongMaterial material = new PhongMaterial();
material.setDiffuseMap(texture);
card.setMaterial(material);

Nous avons fini la modélisation de notre carte en 3D, il ne nous reste plus que les parties création de l'IU et animation qui sont triviales et similaires à ce que nous avons vu il y a quelques jours :

Code Java : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
final StackPane stackPane = new StackPane();
stackPane.getChildren().addAll(card);
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: Mesh");
primaryStage.setScene(scene);
primaryStage.show();

Étant donné que nous manipulons un seul objet 3D, nous n'avons plus à nous soucier de savoir quelle face doit être visible ou pas. L'animation se résume donc à une simple rotation de la carte sur elle-même :

Code Java : Sélectionner tout - Visualiser dans une fenêtre à part
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
final RotateTransition animation = new RotateTransition(halfFlipDuration.multiply(4), card);
animation.setInterpolator(Interpolator.LINEAR);
animation.setAxis(Rotate.Y_AXIS);
animation.setFromAngle(0);
animation.setToAngle(360);
animation.setCycleCount(RotateTransition.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();
    }
});

Par défaut, une scène (ou sous-scène) 3D contient une source de lumière ponctuelle (une lampe, un spot, un objet de type PointLight) blanche placée en son centre, hors de l’écran et qui inonde tous les objets 3D placés dans la scène. Avec cette lampe par défaut, nous n'avons donc plus du tout besoin de nous soucier de l’éclairage de la carte, ses faces s'assombriront ou s’éclairciront automatiquement durant la rotation lorsqu'elle disparaissent ou apparaissent dans le champs de vision de l'utilisateur et le champ d’éclairage du spot (qui grosso-modo coïncident quand on utilise l’éclairage par défaut).

Nom : card rotate.jpg
Affichages : 150
Taille : 17,5 Ko

Nous pouvons voir, de plus, que l’éclairage n'est pas uniforme : durant la rotation, le bord de la carte qui est le plus proche de l’écran est plus clair que le bord qui en est le plus éloigné. Cela donne quand même une apparence plus réaliste.

Et voici le code complet du programme :

Code Java : Sélectionner tout - Visualiser dans une fenêtre à part
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
package test;

import javafx.animation.Interpolator;
import javafx.animation.RotateTransition;
import javafx.application.Application;
import javafx.beans.binding.DoubleBinding;
import javafx.geometry.Pos;
import javafx.scene.AmbientLight;
import javafx.scene.PerspectiveCamera;
import javafx.scene.Scene;
import javafx.scene.SceneAntialiasing;
import javafx.scene.SubScene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Slider;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.ToolBar;
import javafx.scene.image.Image;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.MeshView;
import javafx.scene.shape.TriangleMesh;
import javafx.scene.transform.Rotate;
import javafx.stage.Stage;
import javafx.util.Duration;

public class Test_D3Mesh1 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 Canvas canvas = new Canvas(98 * 2, 143);
        final GraphicsContext g2d = canvas.getGraphicsContext2D();
        g2d.drawImage(sourceImage, 0, 286, 98, 143, 0, 0, 98, 143); // Face as.
        g2d.drawImage(sourceImage, 197, 572, 98, 143, 98, 0, 98, 143); // Face dos.
        final Image texture = canvas.snapshot(null, null);
        //
        final float[] points = {
            0, 0, 0, // 0 - Upper left corner.
            98, 0, 0, // 1 - Upper right corner.
            98, 143, 0, // 2 - Lower right corner.
            0, 143, 0, // 3 - Lower left corner.
        };
        final float[] textCoords = {
            0, 0, // 0 - Upper left texture corner.
            0.5f, 0, // 1 - Upper middle texture.
            1, 0, // 2 - Upper right texture corner.
            1, 1, // 3 - Lower right texture corner.
            0.5f, 1, // 4 - Lower middle texture.
            0, 1, // 5 - lower left texture.
        };
        final int[] faces = {
            1, 1, 0, 0, 3, 5, // Front top left face.
            2, 4, 1, 1, 3, 5, // Front bottom right face.
            1, 1, 3, 3, 0, 2, // Back top left face.
            2, 4, 3, 3, 1, 1, // Back right left face.
        };
        final TriangleMesh mesh = new TriangleMesh();
        mesh.getPoints().addAll(points);
        mesh.getTexCoords().addAll(textCoords);
        mesh.getFaces().addAll(faces);
        final MeshView card = new MeshView(mesh);
        final PhongMaterial material = new PhongMaterial();
        material.setDiffuseMap(texture);
        card.setMaterial(material);
        card.setRotationAxis(Rotate.Y_AXIS);
        //
        final StackPane stackPane = new StackPane();
        stackPane.getChildren().addAll(card);
        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: Mesh");
        primaryStage.setScene(scene);
        primaryStage.show();
        //
        final RotateTransition animation = new RotateTransition(halfFlipDuration.multiply(4), card);
        animation.setInterpolator(Interpolator.LINEAR);
        animation.setAxis(Rotate.Y_AXIS);
        animation.setFromAngle(0);
        animation.setToAngle(360);
        animation.setCycleCount(RotateTransition.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();
            }
        });
    }

    public static void main(String[] args) {
        launch(args);
    }
}

Voilà, c'est fini ! Sur la fin nous nous somme largement éloigné du sujet d'origine qui était de faire tourner un composant sur lui-même pour afficher un panneau de configuration derrière. Ce problème se réglant facilement a grand coup d'effet de type PerspectiveTransform, une fois le pied mis dedans, autant pousser l'essai jusqu'au bout et s’en servir d'excuse pour s’essayer à la 3D. J’espère que le sujet vous aura plus et que je n'ai pas trop raconté de bêtises

Envoyer le billet « Faire tourner les cartes - partie 6 » dans le blog Viadeo Envoyer le billet « Faire tourner les cartes - partie 6 » dans le blog Twitter Envoyer le billet « Faire tourner les cartes - partie 6 » dans le blog Google Envoyer le billet « Faire tourner les cartes - partie 6 » dans le blog Facebook Envoyer le billet « Faire tourner les cartes - partie 6 » dans le blog Digg Envoyer le billet « Faire tourner les cartes - partie 6 » dans le blog Delicious Envoyer le billet « Faire tourner les cartes - partie 6 » dans le blog MySpace Envoyer le billet « Faire tourner les cartes - partie 6 » dans le blog Yahoo

Tags: carte
Catégories
Java , Java , JavaFX , 3D

Commentaires