IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Vous êtes nouveau sur Developpez.com ? Créez votre compte ou connectez-vous afin de pouvoir participer !

Vous devez avoir un compte Developpez.com et être connecté pour pouvoir participer aux discussions.

Vous n'avez pas encore de compte Developpez.com ? Créez-en un en quelques instants, c'est entièrement gratuit !

Si vous disposez déjà d'un compte et qu'il est bien activé, connectez-vous à l'aide du formulaire ci-dessous.

Identifiez-vous
Identifiant
Mot de passe
Mot de passe oublié ?
Créer un compte

L'inscription est gratuite et ne vous prendra que quelques instants !

Je m'inscris !

JavaFX : faire tourner les cartes - partie 5
Un billet de blog de bouye

Le , par bouye

0PARTAGES

Voici enfin venu le temps de notre première incursion dans le monde de la vraie 3D (c'est à dire pas avec des nœuds 2D, ce que nous avons vus lors de précédentes tentatives). Je vais commencer par aborder une approche théorique du problème histoire de bien poser les choses avant de montrer le code final en action lors de mon intervention suivante. Alors, autant vous prévenir tout se suite que je ne suis pas un expert dans ce domaine-là, donc si je dis un truc qui n'est pas correct, pensez à me le signaler.

Nous allons donc utiliser ici un nœud 3D. Nous n'allons pas créer une carte à partir de briques (Box), sphères (Sphere) ou cylindre (Cylinder), nous allons utiliser la quatrième primitive 3D, celle qui permet de créer des objets de forme arbitraire et de les texturer : les maillages (Mesh et plus spécifiquement TriangularMesh). Généralement, on crée ce genre d'objets dans un outils de modélisation externe spécialisé (et vous allez vite comprendre pourquoi) et ensuite on l'importe grâce à un filtre dans le logiciel ou le moteur de rendu.

Commençons par les bases : on a tendance à préférer utiliser des agencement de petites surfaces planes pour simuler des surfaces courbes ou des géométries de surfaces complexes. Ces surfaces sont stockées sous forme d'un maillage de points qui forment un agencement de petites surfaces qui sont généralement triangulaires. Pourquoi un triangle ? Car 3 points définissent un plan. Si on prend un polygone défini par 4 points, il est possible que le dernier point ne soit pas dans le même plan (coplanaire) que les 3 premiers et donc l'ensemble peut ne pas former un plan ; donc on utilise des triangles par soucis de simplicité ou de performance : on est sur d'avoir des surfaces planaires. Bien qu'il soit, en théorie, possible de rendre des surfaces courbes en 3D, des NURBS (Non-uniform rational B-spline - B-splines rationnelles non uniformes), dans la pratique on le fait rarement hors usage mathématique ou scientifique : outre le fait que les NURBS ne sont pas encore supportés en JavaFX (à ma connaissance), leur rendu demande également une plus grande puissance de calcul. Tout comme les splines en 2D peuvent être approximés par une succession de petits segments droits, on peut approximer des surface courbes en 3D avec des agencements de surfaces triangulaires. On peut ainsi par exemple produire des modèles 3D composes de maillage en "base résolution" à partir de modèles 3D en "haute résolution" comportant des NURBS. Cela permet de les rendre plus rapidement.

Ainsi, la primitive sphère, bien que définie par un centre et un rayon, est rendue sous la forme non pas d'une sphère, mais d'un volume dont la surface extérieure est constituée de triangles. Plus il y aura de triangles, plus l’apparence de l'objet sera "sphérique" (lissée - hors application de shaders et autres modificateurs d’éclairage), c'est ce qu'on appelle la tesselation. Si on prend un cube ou une brique, il dispose de 6 faces carrées ou rectangulaires mais en fait chacune de ces faces sont constituées de 2 triangles chacune. Une brique est donc constituée de 12 triangles. Il en est de même pour un cylindre. On peut s'en rendre compte en activant le rendu fil-de-fer sur des objets primitifs, par exemple en utilisant le code suivant :

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
package test; 
  
import javafx.application.Application; 
import javafx.scene.AmbientLight; 
import javafx.scene.PerspectiveCamera; 
import javafx.scene.Scene; 
import javafx.scene.layout.Pane; 
import javafx.scene.paint.Color; 
import javafx.scene.paint.PhongMaterial; 
import javafx.scene.shape.Box; 
import javafx.scene.shape.Cylinder; 
import javafx.scene.shape.DrawMode; 
import javafx.scene.shape.Sphere; 
import javafx.stage.Stage; 
  
public class Test_Triangle extends Application { 
  
    @Override 
    public void start(Stage primaryStage) throws Exception { 
        final PhongMaterial red = new PhongMaterial(Color.RED); 
        final PhongMaterial green = new PhongMaterial(Color.GREEN); 
        final PhongMaterial blue = new PhongMaterial(Color.BLUE); 
        final Box cube = new Box(200, 200, 200); 
        cube.setLayoutX(150); 
        cube.setLayoutY(800); 
        cube.setDrawMode(DrawMode.LINE); 
        cube.setMaterial(red); 
        final Cylinder cylinder = new Cylinder(150, 50); 
        cylinder.setLayoutX(500); 
        cylinder.setLayoutY(800); 
        cylinder.setDrawMode(DrawMode.LINE); 
        cylinder.setMaterial(green); 
        final Sphere sphere = new Sphere(100); 
        sphere.setLayoutX(850); 
        sphere.setLayoutY(800); 
        sphere.setDrawMode(DrawMode.LINE); 
        sphere.setMaterial(blue); 
        final AmbientLight light = new AmbientLight(Color.WHITE); 
        final Pane root = new Pane(); 
        root.setStyle("-fx-background-color: transparent;"); 
        root.getChildren().addAll(cube, cylinder, sphere, light); 
        final int[] tesselations = {1, 5, 10, 50, 100}; 
        for (int index = 0; index < tesselations.length; index++) { 
            final int dx = 1000 / tesselations.length; 
            final int tesselation = tesselations[index]; 
            final Sphere tesselatedSphere = new Sphere(75, tesselation); 
            tesselatedSphere.setDrawMode(DrawMode.LINE); 
            root.getChildren().add(tesselatedSphere); 
            tesselatedSphere.setTranslateX(100 + dx * index); 
            tesselatedSphere.setTranslateY(400); 
            final Cylinder tesselatedCylinder = new Cylinder(75, 50, tesselation); 
            tesselatedCylinder.setDrawMode(DrawMode.LINE); 
            root.getChildren().add(tesselatedCylinder); 
            tesselatedCylinder.setTranslateX(100 + dx * index); 
            tesselatedCylinder.setTranslateY(100); 
        } 
        final Scene scene = new Scene(root, 1000, 1000); 
        scene.setFill(Color.BLACK); 
        scene.setCamera(new PerspectiveCamera()); 
        primaryStage.setScene(scene); 
        primaryStage.setTitle("Test_Triangle"); 
        primaryStage.show(); 
    } 
  
    public static void main(String... args) { 
        Application.launch(args); 
    } 
}

Ce qui donne le résultat suivant :


Le monde est en fait constitué de petits triangles !

La quatrième primitive 3D, le maillage (ou mesh) permet de créer des volumes de forme arbitraire en spécifiant un nuage de points (p) dans l'espace 3D et une liste de surfaces triangulaires utilisant ces mêmes points. Cela permet de définir la géométrie d'une surface.

Mais ce type d'objet supporte également une fonctionnalité qui n'est pas présente dans les autres primitives : les coordonnées de texture. Nous disposons d'un second nuage de points (t) qui sont des coordonnées 2D sur une image servant de texture. Nous pouvons alors définir un ensemble de triangles sur cette texture grâce à ce second ensemble de coordonnées. Ces triangles vont définir l'apparence de notre surface : lors du rendu, un triangle issue de l'image, et qui contient donc un bout de cette texture, va être déformé et positionné dans l'espace pour correspondre à un triangle défini dans la géométrie.

Ainsi, la surface triangulaire dans notre maillage va être définie non pas avec 3 mais avec 6 points : p0, t0, p1, t1, p2, t2 où px sont les points issues du nuage de points de la géométrie et tx sont les points issues du nuage de points de la texture. De plus, le triangle issue de la texture n'a pas forcement ni le même ratio de dimensions, ni les mêmes angles, ni même la même orientation que celui de la géométrie, ce qui peut mener au fait que vos texture peuvent apparaitre déformées, rétrécies, agrandies, étirées, orientée dans un sens différent ou même inversées (effet miroir) lors du rendu final.

À ce niveau, il faut faire attention à une nouvelle notion que nous n'avons pas encore abordée : la notion de l'orientation de la surface. Chaque triangle qui forme la géométrie de l'objet est un petit bout de plan et donc possède deux faces qui sont déterminée par l'orientation d'un vecteur directeur (aussi plus couramment appelé la normale) qui est perpendiculaire à la surface du polygone. L'ordre dans lesquels les points sont spécifiés lors de la création du triangle permet de définir quelle face est la face avant et laquelle est la face arrière. Dans JavaFX, c'est le sens antihoraire (sens inverse des aiguilles d'une montre ou encore règle de la main droite) qui détermine la face avant du triangle. Avec les options par défaut (voir la propriété cullFace de l'objet), seule cette face est rendue lors de l'affichage. Par défaut, notre triangle issue de la texture seront donc affiché sur cette face du triangle définissant la géométrie.

Note : Si on avait choisit d'afficher la face arrière, la texture y aurait été affichée en miroir (par rapport à son affichage sur la face avant), puisque vue "dans le mauvais sens".

Comment allons-nous définir notre carte désormais ? Ici, la carte est une surface plane rectangulaire à deux faces positionnées dos à dos et sans épaisseur entre les deux faces (histoire de ne pas compliquer les choses inutilement). Il nous faut donc 4 triangles : deux triangles forment le rectangle de la face avant tandis que deux autres triangles forment le rectangle la face arrière. De plus, la face arrière étant orientée dans l'autre sens, les triangles formant le dos de la carte doivent avoir une orientation différente de ceux de la face avant.

Il nous faut donc un nuage de 4 points pour définir la face avant : nous avons deux triangles adjacents qui se partagent 2 points et ont autres deux points distincts sont nécessaires pour compléter les triangles. Nous pouvons également réutiliser ces 4 points pour la face arrière en spécifiant des triangles qui ont une orientation de leur normale différente de ceux de la face avant.


Définition des triangles de géométrie.

Nous avons désormais 4 points P0, P1, P2 et P3 qui font nous permettre de définir deux faces constituées chacune de 2 triangles :
  • La face avant, constituée de triangles dont les normales sont dirigées vers l’extérieur de l’écran :
    • Le triangle F0 : P1 -> P0 -> P3
    • Le triangle F1 : P2 -> P1 -> P3
  • La face arrière, constituée de triangles dont les normales sont dirigées vers l’intérieur de l’écran :
    • Le triangle F2 : P1 -> P3 -> P0
    • Le triangle F3 : P2 -> P3 -> P1



De manière similaire, il nous faut définir 4 points 2D pour chaque texture de chaque face (4 pour la face avant et 4 pour la face arrière).


Nous allons cependant simplifier les choses en extrayant les deux faces de leur image source et en les collant côte à côte dans une nouvelle image qui deviendra la texture source de notre objet. Comme si on avait passé une fine lame de rasoir dans l’épaisseur de la tranche de la carte pour ensuite déplier cette dernière de manière à ce que les deux faces nous soient visibles en même temps. Cela permettra également dans cet exemple d'utiliser des coordonnées plus simples qui reprennent les dimensions de l'image. Donc, du coup, seuls 6 points sont nécessaires pour le nuage de points des coordonnées de texture.


Définition des triangles de texture.

Nous avons désormais 4 triangles :
  • La face avant est constituée de :
    • Le triangle G0 : T1 -> T0 -> T3
    • Le triangle G1 : T4 -> T1 -> T5
  • La face arrière est constituée de :
    • Le triangle G2 : T1 -> T4 -> T2
    • Le triangle G3 : T4 -> T3 -> T1



Sans surprise, nous allons directement mapper les triangles de la texture sur la face avant sur les triangles de la géométrie de la face avant :
  • G0 est mappé sur F0 avec :
    • T1 -> P1
    • T0 -> P0
    • T3 -> P3

  • G1 est mappé sur F1 avec :
    • T4 -> P2
    • T1 -> P1
    • T5 -> P3



Dans notre contexte, les triangles de texture sont appliqués sur les triangles de géométrie sans modification aucune.

Les choses sont un poil plus complexes pour les texture du dos. Comme vous pouvez le voir nos triangles de texture ne correspondent pas à nos triangles de géométrie. Cela est du au fait que nos triangles de géométrie ne sont pas orientés vers nous, ils sont orientés vers l’intérieur de l’écran. Si on mappait directement des triangles de texture identiques à ceux de géométrie on aurait un soucis : la texture serait affichée en miroir. En effet, le dos n'est visible que lorsque la carte a pivoté de 180°, ce qui impliquerait que la texture serait affichée en miroir. Évidement, avec ce dos de carte-ci qui est symétrique suivant l'axe Y on ne verrait pas la différence... mais avec un dos asymétrique ou contenant du texte, ce problème sauterait aux yeux.


Image fourre-tout car j'ai atteins mon quota de 5 images.

On peut donc corriger ce problème de deux manières (au moins ; il en existe probablement d'autres) :
  • Définir des rectangles de texture du dos qui sont les miroirs suivant l'axe Y de ceux de la géométrie du dos. C'est la solution que j'ai choisie pour cet exemple.
  • Faire le miroir de l'image du dos de la carte quand on crée la texture source.


Si on "déplie" les polygones qui forment la géométrie de la carte, tout comme nous l'avons fait avec ceux de la texture, de manière à ce que les normales nous fassent face, nous nous retrouvons alors avec des triangles en tout point similaires à ceux de la texture.

Donc, pour le dos :

  • G2 est mappé sur F2 avec :
    • T1 -> P1
    • T3 -> P3
    • T2 -> P0

  • G3 est mappé sur F3 avec :
    • T4 -> P2
    • T3 -> P3
    • T1 -> P1



Nous voyons donc que, bien que notre objet 3D soit très simple et ne soit composé que de 4 triangles (chacun composé de 6 points), nous devons manipuler un grand nombre de points : 4 pour la géométrie dans l'espace, 6 pour les textures (et encore nous avons simplifié l'usage) sans parler du fait qu'il faille faire attention à l'orientation des triangles. Bref, on s'y perdra rapidement si la forme à modéliser est trop complexe. Donc généralement, on aura tendance à utiliser un modélisateur visuel pour sculpter l'objet 3D doublé d'un outil visuel qui permet de plaquer les textures sur les faces à la souris, stylet ou stylet 3D. Vous importerez ensuite cet objet grâce à un filtre dans votre logiciel, moteur de jeu, etc.

Par contre, bien sûr, il n'est pas toujours possible de créer un modèle à l'avance : si, par exemple, vous devez générer du terrain à partir de relevés topographiques et ensuite y plaquer une image satellite, il vous faudra passer par du code pour générer les coordonnées de géométrie de votre surface ainsi que les coordonnées de textures que vous voulez mapper dessus.

Voilà qui termine l'approche théorique ; nous aborderons l'aspect pratique lors de ma prochaine intervention sur ce sujet (qui devrait être également la dernière).

Une erreur dans cette actualité ? Signalez-nous-la !