I. Le graphe, une façon de représenter un objet complexe▲
Code source du tutoriel
Imaginons que nous voulions modéliser une trottinette.
Nous aimerions que le guidon puisse tourner par rapport à la base.
Que les deux roues soient indépendantes.
Modélisé la trottinette avec une seule maille, ne rendrait pas les choses faciles pour animer le guidon et les roues.
En effet on serait obligé de la recalculée à chaque rotation d’une roue ou du guidon.
C’est pour cela que nous allons utiliser, quatre mailles différentes et indépendantes.
Nous allons aussi vouloir stocker d’autres informations que les mailles, par exemple leur couleur.
Pour cela, on crée un objet 3D, qui contiendra la maille ainsi que d’autres informations, comme la couleur.
Notre trottinette sera donc constituée de quatre objets 3D :
- Un objet 3D avec la maille modélisant la base.
- Un objet 3D avec la maille modélisant le guidon.
- Un objet 3D avec la maille modélisant la roue avant.
- Un objet 3D avec la maille modélisant la roue arrière.
Ainsi, il suffira de bouger l’un de ces objets sans modifier les mailles pour créer une animation.
À chaque fois que la base bouge, on veut que les trois autres objets la suivent du même mouvement.
Sinon, la trottinette se « casserait ».
Le plus simple est de placer relativement ces trois objets par rapport à la base.
Ainsi, à chaque mouvement de la base, toutes les parties de la trottinette bougent de manière cohérente.
Quand on tourne le guidon, on souhaite que la roue avant fasse de même.
En suivant le même raisonnement, la roue avant sera plutôt placée relativement au guidon.
On a donc la base. Le guidon et la roue arrière placés relativement à la base.
Et la roue avant placée relativement au guidon.
On peut voir les choses ainsi, la base est le parent.
Elle a pour enfant direct le guidon et la roue arrière.
Le guidon quant à lui a pour enfant direct la roue avant.
Ce qui peut se représenter en graphe

Ce raisonnement peut se reproduire pour tout un tas d’objets complexes
À chaque fois nous pouvons modéliser les parties indépendantes avec un graphe formé d’un ou plusieurs objets 3D indépendants.
Le fait de pour bouger les uns par rapport aux autres n’est pas le seul cas où l’on va découper en plusieurs objets.
Par exemples si le rendu est différent d’une partie à l’autre.
Par exemple une porte en bois vitrée.
On aura un objet pour la partie bois, et au moins un pour la partie vitre.
Ainsi la partie boiss pourra être opaque et la ou les parties vitre semi-transparentes.
II. Les nœuds▲
Il est parfois utile de pouvoir placer des objets 3D relativement à un point virtuel.
Ce point virtuel est un peu comme un objet 3D qui n’aurait pas de maille.
Ce point virtuelle s’appelle un nœud.
En général on dit qu’un objet 3D est un nœud possédant une maille. Qui donne, d'un point de vue conception objet :

III. Le graphe de scène▲
L’idée est de généraliser ce que nous venons de voir à toute la scène.
La scène dans son ensemble peut être perçue comme un objet très complexe.
Du coup on peut utiliser la représentation sous forme de graphe en lui donnant un nœud principal nommé racine.
Tous les objets de la scène seront relativement par rapport à ce nœud.
Ainsi pour bouger toute la scène, il suffit de déplacer ce nœud racine.
En décrivant ainsi la scène on obtient le graphe de scène.
Par exemple
IV. Mise en place du graphe de scène▲
Comme nous l’avons vu, le graphe de scène est composé de nœuds (les objets 3D héritant de nœud)
Avant de créer un nœud, parlons de comment décrire sa position dans la 3D.
IV-A. Les nœuds et leur description de position▲
On veut non seulement pouvoir les positionner avec (X, Y, Z), mais aussi pouvoir faire des rotations et changer les facteurs d’échelle.
Pour la position (X, Y, Z) un triplet de 3 floats feront l’affaire.
Pour la rotation, on pourrait spécifier l’axe de rotation et un angle. Mais dans la pratique ce n’est pas très simple à utiliser.
Le plus simple est de définir un angle de rotation autour de chacun des trois axes X, Y et Z.
Donc trois floats, un pour chaque angle, feront l’affaire.
Pour les facteurs d’échelles on a à un sur chaque axe, donc également 3 floats.
Il faut bien garder en tête que cette description est relative au nœud parent.
Sauf bien sûr pour le nœud racine pour qui elles sont absolues
On ajoute au nœud ces enfants dans une liste de nœuds.
Ce qui donne pour le moment :
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.
package fr.developez.tutorial.java.dimension3.render;
import fr.developez.tutorial.java.dimension3.tool.NonNulltool.NonNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* Nœud virtuel de la scènescène
*/
public class Node3D
{
/**
* Coordonne X de la position dans la 3D
*/
public float x = 0f;
/**
* Coordonne Y de la position dans la 3D
*/
public float y = 0f;
/**
* Coordonne Z de la position dans la 3D
*/
public float z = 0f;
/**
* Angle de la rotation autour de l'axe X
*/
public float angleX = 0f;
/**
* Angle de la rotation autour de l'axe Y
*/
public float angleY = 0f;
/**
* Angle de la rotation autour de l'axe Z
*/
public float angleZ = 0f;
/**
* Facteur d'échelle le long de l'axe X <br/>
* La valeur doit toujours être strictement positive
*/
public float scaleX = 1f;
/**
* Facteur d'échelle le long de l'axe Y <br/>
* La valeur doit toujours être strictement positive
*/
public float scaleY = 1f;
/**
* Facteur d'échelle le long de l'axe Z <br/>
* La valeur doit toujours être strictement positive
*/
public float scaleZ = 1f;
/**
* Liste des nœuds placés relativement à celui-ci
*/
private final List<Node3D> children = new ArrayList<>();
public Node3D()
{
}
/**
* Ajoute un nœud enfant
*/
public final void addChild(@NonNull final Node3D node)
{
Objects.requireNonNull(node, "node must not be null");
// On synchronize pour éviter l'accès concurrent
synchronized (this.children)
{
this.children.add(node);
}
}
}
IV-B. « Dessiner » le nœud avec OpenGL▲
On a les propriétés du nœud, maintenant il faut dire à OpenGL de les utiliser afin de placer notre nœud.
Le principe va être de placer le nœud, puis chacun de ces enfants relativement à celui-ci de manière récursive.
On peut toujours considérer que lors du placement d'un nœud la matrice actuelle est celle du parent.
Cela marche aussi pour le nœud racine en voyant le parent comme le repère de départ.
Pour ne pas perturber les éventuels autres enfants due son parent, un nœud va devoir restaurer la matrice du parent comme à l'origine à la fin.
Comme nous l'avons vu dans l'introduction, OpenGL possède une mécanisme pour cela : la pile des matrices.
C'est pour cela que le code du placement va ressembler à :
package fr.developez.tutorial.java.dimension3.render;
// ...
import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;
import org.lwjgl.opengl.GL11;
// ...
/**
* Nœud virtuel de la scène
*/
public class Node3D
{
// ...
/**
* Dessine le noeud
*/
@ThreadOpenGL
final void drawNode()
{
// On sauvegarde la matrice actuelle
GL11.glPushMatrix();
// TODO placer le nœud et ces enfants
// On restaure la matrice sauvegardée
GL11.glPopMatrix();
}
}Comme dit dans l'introduction, l'ordre des instructions de placements est important.
On veut que les rotations et les facteurs d'échelles soit relatifs à la position du nœud.
On va donc d'abord placer le nœud en (X, Y, Z)
On veut que les facteurs d'échelles n'influencent pas les rotations, ils seront donc après.
L'ordre des rotations des rotations entre elles n'a pas d'importance.
Tourner sur soi-même vers la droite, puis vers le haut, revient à tourner vers le haut, puis vers la droite, par exemple.


Par habitude, on mettra la rotation autour de X, puis Y, enfin Z.
Donc l'ordre sera
- Position (X,Y,Z)
- Rotation autour de X
- Rotation autour de Y
- Rotation autour de Z
- Facteurs d'échelles
package fr.developez.tutorial.java.dimension3.render;
// ...
import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;
import org.lwjgl.opengl.GL11;
// ...
/**
* Noeud virtuel de la scène
*/
public class Node3D
{
// ...
/**
* Dessine le noeud
*/
@ThreadOpenGL
final void drawNode()
{
// On sauvegarde la matrice actuelle
GL11.glPushMatrix();
// *** Position du noeud ***
// Position aux coordonnées X, Y, Z
GL11.glTranslatef(this.x, this.y, this.z);
// Rotation autour de l'axe X
GL11.glRotatef(this.angleX, 1f, 0f, 0f);
// Rotation autour de l'axe Y
GL11.glRotatef(this.angleY, 0f, 1f, 0f);
// Rotation autour de l'axe Z
GL11.glRotatef(this.angleZ, 0f, 0f, 1f);
// Application des facteurs d'échelle
GL11.glScalef(this.scaleX, this.scaleY, this.scaleZ);
// TODO placer les enfants
// On restaure la matrice sauvegardée
GL11.glPopMatrix();
}
}Avant de placer les enfants, nous allons préparer l'affichage spécifique. Pour un nœud générique, il n'y aura rien à y faire.
Mais, pour les objets 3D nous permettra de dessiner leur maille
package fr.developez.tutorial.java.dimension3.render;
// ...
import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;
import org.lwjgl.opengl.GL11;
// ...
/**
* Nœud virtuel de la scène
*/
public class Node3D
{
// ...
/**
* Dessine le noeud
*/
@ThreadOpenGL
final void drawNode()
{
// On sauvegarde la matrice actuelle
GL11.glPushMatrix();
// *** Position du nœud ***
// Position aux coordonnées X, Y, Z
GL11.glTranslatef(this.x, this.y, this.z);
// Rotation autour de l'axe X
GL11.glRotatef(this.angleX, 1f, 0f, 0f);
// Rotation autour de l'axe Y
GL11.glRotatef(this.angleY, 0f, 1f, 0f);
// Rotation autour de l'axe Z
GL11.glRotatef(this.angleZ, 0f, 0f, 1f);
// Application des facteurs d'échelle
GL11.glScalef(this.scaleX, this.scaleY, this.scaleZ);
// Dessin spécifique
this.drawSpecific();
// TODO placer les enfants
// On restaure la matrice sauvegardée
GL11.glPopMatrix();
}
/**
* Dessin supplémentaire pour le nœud
* <p>
* A surcharger par les classes enfants
* <p>
* Ne fait rien par défaut
*/
@ThreadOpenGL
void drawSpecific()
{
// Rien à faire d'autres. Les classes enfants y mettront leur code spécifique
}
}Et maintenant, par récursion ajoutons les enfants
package fr.developez.tutorial.java.dimension3.render;
// ...
import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;
import org.lwjgl.opengl.GL11;
// ...
/**
* Noeud virtuel de la scène
*/
public class Node3D
{
// ...
/**
* Dessine le noeud
*/
@ThreadOpenGL
final void drawNode()
{
// On sauvegarde la matrice actuelle
GL11.glPushMatrix();
// *** Position du noeud ***
// Position aux coordonnées X, Y, Z
GL11.glTranslatef(this.x, this.y, this.z);
// Rotation autour de l'axe X
GL11.glRotatef(this.angleX, 1f, 0f, 0f);
// Rotation autour de l'axe Y
GL11.glRotatef(this.angleY, 0f, 1f, 0f);
// Rotation autour de l'axe Z
GL11.glRotatef(this.angleZ, 0f, 0f, 1f);
// Application des facteurs d'échelle
GL11.glScalef(this.scaleX, this.scaleY, this.scaleZ);
// Dessin specific
this.drawSpecific();
// Placement des noeuds enfants relativement à la position de ce noeud
// On synchronize pour éviter l'accès concurrent
synchronized (this.children)
{
for (final Node3D child : this.children)
{
child.drawNode();
}
}
// On restaure la matrice sauvegardée
GL11.glPopMatrix();
}
// …
}Cet algorithme nous posera des soucis quand nous parleront de transparence dans un tutoriel dédié aux matériaux. Nous vous indiquerons à ce moment-là, la solution. Pour le moment, il nous suffit pour ce tutoriel.
IV-C. Les objets 3D et les mailles▲
Comme nous l'avons vu dans l'introduction, pour se dessiner, un objet 3D doit être constitué d'une maille.
Une maille est un ensemble de faces
Chaque face décrit un polygone de 3 ou 4 cotés en général.
On va définir les faces par les sommets du polygone.
Comme nous le verrons quand nous parlerons des textures ou des effets de lumière, le sommet possédera d'autre informations que ses coordonnées (X, Y, Z).
En infographie cela s'appelle un vertex.
Pour le moment il sera simplement, ce qui suit, il sera enrichit dans des prochains tutoriels.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
/**
* Vertex décrivant un sommet d'une face d'une maille.
* <br/>
* Le vertex contient non seulement les coordonnées du point,
* mais également d'autre informations utiles pour les textures et la lumiere
*/
public class Vertex
{
public float x = 0f;
public float y = 0f;
public float z = 0f;
public Vertex()
{
}
public Vertex(float x, float y, float z)
{
this.x = x;
this.y = y;
this.z = z;
}
}
Maintenant que nous avons notre vertex, on peut décrire une face
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.
package fr.developez.tutorial.java.dimension3.render.mesh;
import fr.developez.tutorial.java.dimension3.tool.NonNull;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
/**
* Décrit une face d'une maille
*/
public final class Face
{
private final List<Vertex> vertexs = new ArrayList<>();
public Face(Vertex... vertexs)
{
if (vertexs != null)
{
Collections.addAll(this.vertexs, vertexs);
}
}
public void addVertex(float x, float y, float z)
{
this.addVertex(new Vertex(x, y, z));
}
public void addVertex(@NonNull final Vertex vertex)
{
Objects.requireNonNull(vertex, "vertex must not be null");
synchronized (this.vertexs)
{
this.vertexs.add(vertex);
}
}
public void forEachVertex(@NonNull final Consumer<Vertex> action)
{
Objects.requireNonNull(action, "action must not be null");
synchronized (this.vertexs)
{
for (final Vertex vertex : this.vertexs)
{
action.accept(vertex);
}
}
}
}
Quant à la maille
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.
package fr.developez.tutorial.java.dimension3.render;
import fr.developez.tutorial.java.dimension3.render.mesh.Face;
import fr.developez.tutorial.java.dimension3.tool.NonNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* Maille d'un objet
*/
public class Mesh
{
private final List<Face> faces = new ArrayList<>();
public Mesh()
{
}
public void addFace(@NonNull final Face face)
{
Objects.requireNonNull(face, "face must not be null");
synchronized (this.faces)
{
this.faces.add(face);
}
}
}
Et l'objet 3D
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
package fr.developez.tutorial.java.dimension3.render;
import fr.developez.tutorial.java.dimension3.tool.NonNull;
import java.util.Objects;
/**
* Objet 3D
*/
public class Object3D
extends Node3D
{
@NonNull
public final Mesh mesh = new Mesh();
public Object3D()
{
}
}
IV-D. Un peu de couleurs▲
On voudrait pouvoir appliquer une couleur à notre objet.
Au lieu de stocker les quatre informations d'une couleur, l'intensité de rouge, vert, bleu ainsi que le niveau d'opacité.
Nous allons créer un objet qui représente une couleur, ce qui nous permettra de créer quelques couleurs de bases.
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.
package fr.developez.tutorial.java.dimension3.render;
import fr.developez.tutorial.java.dimension3.tool.MathTools;
import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;
import org.lwjgl.opengl.GL11;
/**
* Représente une couleur
*/
public class Color4f
{
public static final Color4f BLACK = new Color4f(0f);
public static final Color4f DARK_GREY = new Color4f(0.25f);
public static final Color4f GREY = new Color4f(0.5f);
public static final Color4f LIGHT_GREY = new Color4f(0.75f);
public static final Color4f WHITE = new Color4f(1f);
public static final Color4f DARK_RED = new Color4f(0.5f, 0f, 0f);
public static final Color4f RED = new Color4f(1f, 0f, 0f);
public static final Color4f LIGHT_RED = new Color4f(1f, 0.5f, 0.5f);
public static final Color4f DARK_GREEN = new Color4f(0f, 0.5f, 0f);
public static final Color4f GREEN = new Color4f(0f, 1f, 0f);
public static final Color4f LIGHT_GREEN = new Color4f(0.5f, 1f, 0.5f);
public static final Color4f DARK_BLUE = new Color4f(0f, 0f, 0.5f);
public static final Color4f BLUE = new Color4f(0f, 0f, 1f);
public static final Color4f LIGHT_BLUE = new Color4f(0.5f, 0.5f, 1f);
/**
* Intensité de rouge
*/
public final float red;
/**
* Intensité de vert
*/
public final float green;
/**
* Intensité de bleu
*/
public final float blue;
/**
* Niveau d'opacité
*/
public final float alpha;
/**
* Construit la couleur à partir d'un description ARGB
*
* @param argb Description ARGB
*/
public Color4f(int argb)
{
this((float) ((argb >> 16) & 0xFF) / 255f,
(float) ((argb >> 8) & 0xFF) / 255f,
(float) (argb & 0xFF) / 255f,
(float) ((argb >> 24) & 0xFF) / 255f);
}
/**
* Construit une couleur avec niveau de gris opaque
*
* @param grey Niveau de gris
*/
public Color4f(float grey)
{
this(grey, grey, grey, 1f);
}
/**
* Construit une couleur avec niveau de gris
*
* @param grey Niveau de gris
* @param alpha Niveau d'opacité
*/
public Color4f(float grey, float alpha)
{
this(grey, grey, grey, alpha);
}
/**
* Construit une couleur opaque
*
* @param red Intensité de rouge
* @param green Intensité de vert
* @param blue Intensité de bleu
*/
public Color4f(float red, float green, float blue)
{
this(red, green, blue, 1f);
}
/**
* Construit une couleur
*
* @param red Intensité de rouge
* @param green Intensité de vert
* @param blue Intensité de bleu
* @param alpha Niveau d'opacité
*/
public Color4f(float red, float green, float blue, float alpha)
{
this.red = MathTools.bounds(red, 0f, 1f);
this.green = MathTools.bounds(green, 0f, 1f);
this.blue = MathTools.bounds(blue, 0f, 1f);
this.alpha = MathTools.bounds(alpha, 0f, 1f);
}
/**
* Demande à OpenGL d'utilisé cette couleur
*/
@ThreadOpenGL
void apply()
{
GL11.glColor4f(this.red, this.green, this.blue, this.alpha);
}
}
La méthode `apply` permettra de demander à l'OpenGL d'utiliser la couleur.
On ajoute l'information de couleur à notre objet 3D
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.
package fr.developez.tutorial.java.dimension3.render;
import fr.developez.tutorial.java.dimension3.tool.NonNull;
import java.util.Objects;
/**
* Objet 3D
*/
public class Object3D
extends Node3D
{
private Color4f color = Color4f.GREY;
@NonNull
public final Mesh mesh = new Mesh();
public Object3D()
{
}
@NonNull
public Color4f getColor()
{
return this.color;
}
public void setColor(@NonNull final Color4f color)
{
this.color = Objects.requireNonNull(color, "color must not be null");
}
}
IV-E. Dessiner la maille avec OpenGL▲
Pour chaque face de la maille, on la dessine grâce à ses sommets.
On doit dire à OpenGL où commence et où finit le polygone.
Le nouveau code de la maille
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.
package fr.developez.tutorial.java.dimension3.render;
import fr.developez.tutorial.java.dimension3.render.mesh.Face;
import fr.developez.tutorial.java.dimension3.tool.NonNull;
import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import org.lwjgl.opengl.GL11;
/**
* Maille d'un objet
*/
public class Mesh
{
private final List<Face> faces = new ArrayList<>();
public Mesh()
{
}
public void addFace(@NonNull final Face face)
{
Objects.requireNonNull(face, "face must not be null");
synchronized (this.faces)
{
this.faces.add(face);
}
}
@ThreadOpenGL
void drawMesh()
{
synchronized (this.faces)
{
for (final Face face : this.faces)
{
// Annonce à OpenGL le début du polygone
GL11.glBegin(GL11.GL_POLYGON);
face.forEachVertex(vertex -> GL11.glVertex3f(vertex.x, vertex.y, vertex.z));
// Annonce à OpenGL la fin du polygone
GL11.glEnd();
}
}
}
}
Comme pour le moment un vertex n'est composé que des coordonnées, on les précise à OpenGL. Quand nous auront plus d'informations, on mettra les instructions supplémentaires au sein de la lambda
Remarque
Devoir dessiner toutes les faces à chaque boucle de rendu n'est pas très optimisé.
Heureusement, comme nous le verrons un peu plus loin, OpenGL à un mécanisme pour nous aider.
Puis il nous reste plus qu'a complété l'objet pour que celui-ci puisse être dessiné par l'OpenGL
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.
package fr.developez.tutorial.java.dimension3.render;
import fr.developez.tutorial.java.dimension3.tool.NonNull;
import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;
import java.util.Objects;
/**
* Objet 3D
*/
public class Object3D
extends Node3D
{
private Color4f color = Color4f.GREY;
@NonNull
public final Mesh mesh = new Mesh();
public Object3D()
{
}
@NonNull
public Color4f getColor()
{
return this.color;
}
public void setColor(@NonNull final Color4f color)
{
this.color = Objects.requireNonNull(color, "color must not be null");
}
@ThreadOpenGL
@Override
void drawSpecific()
{
this.color.apply();
this.mesh.drawMesh();
}
}
IV-F. Un exemple d’objet 3D : la boîte▲
Afin d'avoir un objet de base à représenter créons une boîte
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.
package fr.developez.tutorial.java.dimension3.geometry;
import fr.developez.tutorial.java.dimension3.render.Object3D;
import fr.developez.tutorial.java.dimension3.render.mesh.Face;
/**
* Une boîte
*/
public class Box
extends Object3D
{
public Box()
{
// Face de la boîte
Face face = new Face();
face.addVertex(-0.5f, 0.5f, 0.5f);
face.addVertex(0.5f, 0.5f, 0.5f);
face.addVertex(0.5f, -0.5f, 0.5f);
face.addVertex(-0.5f, -0.5f, 0.5f);
this.mesh.addFace(face);
// Dos de la boîte
face = new Face();
face.addVertex(-0.5f, -0.5f, -0.5f);
face.addVertex(0.5f, -0.5f, -0.5f);
face.addVertex(0.5f, 0.5f, -0.5f);
face.addVertex(-0.5f, 0.5f, -0.5f);
this.mesh.addFace(face);
// Haut de la boîte
face = new Face();
face.addVertex(-0.5f, 0.5f, -0.5f);
face.addVertex(0.5f, 0.5f, -0.5f);
face.addVertex(0.5f, 0.5f, 0.5f);
face.addVertex(-0.5f, 0.5f, 0.5f);
this.mesh.addFace(face);
// Bas de la boîte
face = new Face();
face.addVertex(-0.5f, -0.5f, 0.5f);
face.addVertex(0.5f, -0.5f, 0.5f);
face.addVertex(0.5f, -0.5f, -0.5f);
face.addVertex(-0.5f, -0.5f, -0.5f);
this.mesh.addFace(face);
// Coté droit de la boîte
face = new Face();
face.addVertex(0.5f, -0.5f, 0.5f);
face.addVertex(0.5f, 0.5f, 0.5f);
face.addVertex(0.5f, 0.5f, -0.5f);
face.addVertex(0.5f, -0.5f, -0.5f);
this.mesh.addFace(face);
// Coté gauche de la boîte
face = new Face();
face.addVertex(-0.5f, -0.5f, -0.5f);
face.addVertex(-0.5f, 0.5f, -0.5f);
face.addVertex(-0.5f, 0.5f, 0.5f);
face.addVertex(-0.5f, -0.5f, 0.5f);
this.mesh.addFace(face);
}
}
Comme on le voit, il suffit de créer chaque face de la boîte en faisant bien attention au sens des sommets pour que la partie extérieure soit visible.
IV-G. Le graphe de scène▲
Maintenant que tout est prêt, on peut créer le graphe de scène.
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.
package fr.developez.tutorial.java.dimension3.render;
import fr.developez.tutorial.java.dimension3.tool.NonNull;
import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;
import java.util.Objects;
import org.lwjgl.opengl.GL11;
/**
* Le graphe de scène
*/
public class SceneGraph
{
@NonNull
public final Node3D root = new Node3D();
private Color4f backgroundColor = Color4f.WHITE;
public SceneGraph()
{
}
@NonNull
public Color4f getBackground()
{
return this.backgroundColor;
}
public void setBackground(@NonNull final Color4f backgroundColor)
{
this.backgroundColor = Objects.requireNonNull(backgroundColor, "backgroundColor must not be null");
}
@ThreadOpenGL
void drawScene()
{
GL11.glClearColor(this.backgroundColor.red, this.backgroundColor.green, this.backgroundColor.blue, 1f);
this.root.drawNode();
}
}
On remarque, que l'on a ajouté l'information de couleur de fond afin de pouvoir facilement la changer.
IV-H. Lier le graphe de scène avec la boucle de rendue▲
Maintenant, on peut ajouter notre graphe de scène à la boucle de rendu
package fr.developez.tutorial.java.dimension3.render;
import fr.developez.tutorial.java.dimension3.tool.GLU;
import fr.developez.tutorial.java.dimension3.tool.NonNull;
import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;
import java.util.Objects;
import org.lwjgl.glfw.GLFW;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
/**
* Boucle de redu
*/
class Render3D
{
/**
* Graphe de scène associé
*/
@NonNull
final SceneGraph sceneGraph = new SceneGraph();
Render3D()
{
}
// ...
/**
* Dessine la scène 3D
*/
@ThreadOpenGL
private void drawScene()
{
this.sceneGraph.drawScene();
}
}Exposons le graphe de scène afin de pouvoir décrire une scène.
package fr.developez.tutorial.java.dimension3.render;
import fr.developez.tutorial.java.dimension3.thread.ThreadManager;
import fr.developez.tutorial.java.dimension3.tool.NonNull;
import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;
import java.nio.IntBuffer;
import java.util.Objects;
import org.lwjgl.glfw.Callbacks;
import org.lwjgl.glfw.GLFW;
import org.lwjgl.glfw.GLFWErrorCallback;
import org.lwjgl.glfw.GLFWVidMode;
import org.lwjgl.opengl.GL;
import org.lwjgl.system.MemoryStack;
import org.lwjgl.system.MemoryUtil;
public class Window3D
{
// ...
@NonNull
public SceneGraph getSceneGraph()
{
return this.render3D.sceneGraph;
}
// ...
}IV-I. Exemple de scène▲
Un petit exemple pour utiliser ce qu’on nous venons d’écrire :
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.
package fr.developez.tutorial.java.dimension3;
import fr.developez.tutorial.java.dimension3.geometry.Box;
import fr.developez.tutorial.java.dimension3.render.Color4f;
import fr.developez.tutorial.java.dimension3.render.SceneGraph;
import fr.developez.tutorial.java.dimension3.render.Window3D;
import fr.developez.tutorial.java.dimension3.tool.NonNull;
public class MainBlueAndGreenCubes
{
public static void main(String[] args)
{
final Window3D window3D = new Window3D(800, 600, "Tutoriel 3D - Une boite blue et une verte");
MainBlueAndGreenCubes.drawScene(window3D.getSceneGraph());
}
private static void drawScene(@NonNull final SceneGraph sceneGraph)
{
final Box blueBox = new Box();
final Box greenBox = new Box();
sceneGraph.root.addChild(blueBox);
sceneGraph.root.addChild(greenBox);
blueBox.x = -1f;
blueBox.z = -2f;
blueBox.angleX = 22.5f;
blueBox.angleY = 22.5f;
blueBox.setColor(Color4f.BLUE);
greenBox.x = 1f;
greenBox.z = -2f;
greenBox.angleX = 22.5f;
greenBox.angleY = 22.5f;
greenBox.setColor(Color4f.GREEN);
}
}
Ce qui donne
On remarque au passage que la construction et la modification de la scène ne sont pas faite dans le thread OpenGL.
Ainsi l'utilisateur de la fenêtre n'a pas à s'en occuper
V. Optimiser le dessin de la maille▲
La maille ne va pas changer souvent.
Même, la plupart du temps, une fois définit, elle ne va pas changer du tout.
Pour cela nous allons voir deux optimisations au sein de la maille elle même.
L'une OpenGL, l'autre Java.
V-A. Optimisation OpenGL : La notion de liste▲
L'idée de cette optimisation, est de dire à la carte 3D de se rappelée l'ensemble des faces de notre maille.
Pour cela OpenGL introduit la notion de liste d'instructions.
Une liste d'instructions va permettre de sauvegarder dans la carte 3D les différentes instructions OpenGL pour construire notre maille.
Cela va même plus loin, en fait cela va permettre de lui dire qu'elles font parties d'un même bloc et donc pouvoir les pré-compiler et les optimiser.
Bref en plus de gagner du temps en n'envoyant plus systématiquement toutes les instructions OpenGL.
On va gagner du temps car la carte 3D va utiliser directement un format à elle et optimiser pour elle.
Nous allons avoir besoin de garder une référence sur la liste. Cette référence sera un entier positif ou nul. C'est la seule chose que l'on peut être sûr.
Comme le pointeur sur la fenêtre 3D, c'est une valeur opaque, c'est-à-dire que seule la carte 3D connaît sa vraie signification.
Mais tout ce que nous avons besoin de savoir c'est qu'elle est unique par liste et positive ou nulle.
Si la maille change, il faudra dire à la carte 3D d'oublier notre liste devenue obsolète et en créer une autre
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.
package fr.developez.tutorial.java.dimension3.render;
import fr.developez.tutorial.java.dimension3.render.mesh.Face;
import fr.developez.tutorial.java.dimension3.tool.NonNull;
import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import org.lwjgl.opengl.GL11;
/**
* Maille d'un objet
*/
public class Mesh
{
private final List<Face> faces = new ArrayList<>();
/**
* Identifiant OpenGL de la liste des polygones décrivant la maille
*/
private int idList = -1;
/**
* Indique que la maille à été modifiée et donc a besoin d'être rafraîchie par OpenGL
*/
private boolean needReconstructTheList = true;
public Mesh()
{
}
public void addFace(@NonNull final Face face)
{
Objects.requireNonNull(face, "face must not be null");
synchronized (this.faces)
{
this.faces.add(face);
}
// On a modifié la maille, il faut donc la rafraîchir
this.needReconstructTheList = true;
}
/**
* Indique qu'on a modifier l'une de faces ou l'un des vertex d'une des faces depuis l'extérieur.
*
* Dans ce cas on a besoin de rafraîchir la maille
*/
public void forceRefresh()
{
this.needReconstructTheList = true;
}
@ThreadOpenGL
void drawMesh()
{
// On va créer la liste des polygones la première fois et chaque fois que nécessaire
if (this.idList < 0 || this.needReconstructTheList)
{
// On va construire la liste, donc on aura plus besoin de le faire au prochain tour
this.needReconstructTheList = false;
// On efface l'ancienne liste de la mémoire de la carte graphique, celle-ci étant devenu obsolète
if (this.idList >= 0)
{
GL11.glDeleteLists(this.idList, 1);
}
// On demande une nouvelle référence de liste à la carte graphique
this.idList = GL11.glGenLists(1);
// On indique le début de la liste
GL11.glNewList(this.idList, GL11.GL_COMPILE);
synchronized (this.faces)
{
for (final Face face : this.faces)
{
// Annonce à OpenGL le début du polygone
GL11.glBegin(GL11.GL_POLYGON);
face.forEachVertex(vertex -> GL11.glVertex3f(vertex.x, vertex.y, vertex.z));
// Annonce à OpenGL la fin du polygone
GL11.glEnd();
}
}
// On indique la fin de la liste
GL11.glEndList();
}
// On dessine la liste mémorisé
GL11.glCallList(this.idList);
}
}
Plus de détails :
- `GL11.glDeleteLists(this.idList, 1)` libère de la carte 3D la liste. Étant devenue obsolete, autant libéré la mémoire qui lui correspond.
- `GL11.glGenLists(1)` demande et réserve une nouvelle référence de liste. Cette référence ne sera plus attribuée tant qu'elle n'est pas détruite.
- `GL11.glNewList(this.idList, GL11.GL_COMPILE)` Indique le début de la description de la liste. On a choisi l'option `COMPILE` afin de demander de la compiler à la fin afin de profiter au maximum des optimisations coté carte 3D.
- `GL11.glEndList()` Indique la fin de la liste, et lance sa compilation. À noter que rien n'est dessiner à l'écran entre le `glNewList` et cette instruction.
- `GL11.glCallList(this.idList)` demande d'afficher le contenu de la liste. Ici notre maille.
V-B. Optimisation Java : La notion de scellé▲
Nous avons maintenant la description de la maille en mémoire sur la carte 3D.
Si on est sûr que la maille ne va plus changer, on peut effacer de la mémoire de la JVM la description de celle-ci.
Nous allons ajouter la possibilité de sceller la maille. C'est-à-dire, la figée pour de bon.
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.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
package fr.developez.tutorial.java.dimension3.render;
import fr.developez.tutorial.java.dimension3.render.mesh.Face;
import fr.developez.tutorial.java.dimension3.tool.NonNull;
import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import org.lwjgl.opengl.GL11;
/**
* Maille d'un objet
*/
public final class Mesh
{
private final List<Face> faces = new ArrayList<>();
/**
* Identifiant OpenGL de la liste des polygones décrivant la maille
*/
private int idList = -1;
/**
* Indique que la maille à été modifiée et donc a besoin d'être rafraîchit par OpenGL
*/
private boolean needReconstructTheList = true;
/**
* Indique si la maille est scellée
*/
private AtomicBoolean sealed = new AtomicBoolean(false);
public Mesh()
{
}
/**
* Indique si la maille est scellée
*/
public boolean sealed()
{
return this.sealed.get();
}
/**
* Scelle la maille, elle ne pourra plus être modifiée
*/
public void seal()
{
if (this.sealed.compareAndSet(false, true))
{
// On s'assure de prendre les éventuelles dernières modifications
this.needReconstructTheList = true;
}
}
public void addFace(@NonNull final Face face)
{
Objects.requireNonNull(face, "face must not be null");
// Si la maille est scellée, on n'ajoute pas la face
if (this.sealed.get())
{
return;
}
synchronized (this.faces)
{
this.faces.add(face);
}
// On a modifié la mail, il faut donc la rafraîchir
this.needReconstructTheList = true;
}
/**
* Indique qu'on a modifier l'une de faces ou l'un des vertex d'une des faces depuis l'extérieur.
* <p>
* Dans ce cas on a besoin de rafraîchir la maille
*/
public void forceRefresh()
{
// La maille ne peut changée que si elle n'est pas scellée
if(!this.sealed.get())
{
this.needReconstructTheList = true;
}
}
@ThreadOpenGL
void drawMesh()
{
// On va créer la liste des polygones la première fois et chaque fois que nécessaire
if (this.idList < 0 || this.needReconstructTheList)
{
// On va construire la liste, donc on aura plus besoin de le faire au prochain tour
this.needReconstructTheList = false;
// On efface l'ancienne liste de la mémoire de la carte graphique, celle-ci étant devenu obsolète
if (this.idList >= 0)
{
GL11.glDeleteLists(this.idList, 1);
}
// On demande une nouvelle référence de liste à la carte graphique
this.idList = GL11.glGenLists(1);
// On indique le début de la liste
GL11.glNewList(this.idList, GL11.GL_COMPILE);
synchronized (this.faces)
{
for (final Face face : this.faces)
{
// Annonce à OpenGL le début du polygone
GL11.glBegin(GL11.GL_POLYGON);
face.forEachVertex(vertex -> GL11.glVertex3f(vertex.x, vertex.y, vertex.z));
// Annonce à OpenGL la fin du polygone
GL11.glEnd();
}
}
// On indique la fin de la liste
GL11.glEndList();
// Si la maille est scellée, il s'agit de la dernière construction de la liste, on peut libérer la description de la maille de la mémoire
if (this.sealed.get())
{
this.faces.clear();
}
}
// On dessine la liste mémorisée
GL11.glCallList(this.idList);
}
}
Changeons un peu l'exemple de tout à l'heure pour illustrer notre propos
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.
package fr.developez.tutorial.java.dimension3;
import fr.developez.tutorial.java.dimension3.geometry.Box;
import fr.developez.tutorial.java.dimension3.render.Color4f;
import fr.developez.tutorial.java.dimension3.render.SceneGraph;
import fr.developez.tutorial.java.dimension3.render.Window3D;
import fr.developez.tutorial.java.dimension3.tool.NonNull;
public class MainBlueAndGreenCubes
{
public static void main(String[] args)
{
final Window3D window3D = new Window3D(800, 600, "Tutoriel 3D - Une boite bleue et une verte");
MainBlueAndGreenCubes.drawScene(window3D.getSceneGraph());
}
private static void drawScene(@NonNull final SceneGraph sceneGraph)
{
final Box blueBox = new Box();
blueBox.mesh.seal();
final Box greenBox = new Box();
greenBox.mesh.seal();
sceneGraph.root.addChild(blueBox);
sceneGraph.root.addChild(greenBox);
blueBox.x = -1f;
blueBox.z = -2f;
blueBox.angleX = 22.5f;
blueBox.angleY = 22.5f;
blueBox.setColor(Color4f.BLUE);
greenBox.x = 1f;
greenBox.z = -2f;
greenBox.angleX = 22.5f;
greenBox.angleY = 22.5f;
greenBox.setColor(Color4f.GREEN);
}
}
Bien entendu, cela ne change rien visuellement.
VI. Les clones▲
Si deux objets 3D ont exactement la même description de maille, il est interéssant de pouvoir partagée celle-ci.
Cela évite d'avoir en mémoire JVM deux fois la même description et d'avoir coté carte 3D deux listes qui contiennent exactement la même chose.
C'est l'idée derrière les clones.
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.
package fr.developez.tutorial.java.dimension3.render;
import fr.developez.tutorial.java.dimension3.tool.NonNull;
import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;
import java.util.Objects;
/**
* Clone d'un objet 3D
*/
public class Clone3D
extends Node3D
{
private Color4f color = Color4f.GREY;
/**
* Objet cloné
*/
private final Object3D cloned;
public Clone3D(@NonNull final Object3D cloned)
{
this.cloned = Objects.requireNonNull(cloned, "cloned must not be null");
}
@NonNull
public Color4f getColor()
{
return this.color;
}
public void setColor(@NonNull final Color4f color)
{
this.color = Objects.requireNonNull(color, "color must not be null");
}
@ThreadOpenGL
@Override
void drawSpecific()
{
this.color.apply();
// Ici on utilise la maille de l'objet cloné
this.cloned.mesh.drawMesh();
}
}
Modifions notre exemple
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.
package fr.developez.tutorial.java.dimension3;
import fr.developez.tutorial.java.dimension3.geometry.Box;
import fr.developez.tutorial.java.dimension3.render.Clone3D;
import fr.developez.tutorial.java.dimension3.render.Color4f;
import fr.developez.tutorial.java.dimension3.render.SceneGraph;
import fr.developez.tutorial.java.dimension3.render.Window3D;
import fr.developez.tutorial.java.dimension3.tool.NonNull;
public class MainBlueAndGreenCubes
{
public static void main(String[] args)
{
final Window3D window3D = new Window3D(800, 600, "Tutoriel 3D - Une boite blue et une verte");
MainBlueAndGreenCubes.drawScene(window3D.getSceneGraph());
}
private static void drawScene(@NonNull final SceneGraph sceneGraph)
{
final Box blueBox = new Box();
blueBox.mesh.seal();
final Clone3D greenBox = new Clone3D(blueBox);
sceneGraph.root.addChild(blueBox);
sceneGraph.root.addChild(greenBox);
blueBox.x = -1f;
blueBox.z = -2f;
blueBox.angleX = 22.5f;
blueBox.angleY = 22.5f;
blueBox.setColor(Color4f.BLUE);
greenBox.x = 1f;
greenBox.z = -2f;
greenBox.angleX = 22.5f;
greenBox.angleY = 22.5f;
greenBox.setColor(Color4f.GREEN);
}
}
Bien sûr l'aspect graphique n'a pas changé.
VII. Conclusion▲
Nous avons vu ce qu’est le graphe de scène, un exemple d’implémentation d’un et comment optimiser nos objets pour qu’ils soit plus rapidement dessiné et qu’ils prennent la mémoire juste nécessaire.
Dans le prochain tutoriel nous allons pouvoir jouer avec notre 3D en ajoutant de l’interaction avec l’utilisateur avec clavier, souris et manette de jeux. Nous verrons aussi comment capturer les objets sous la souris. Interaction avec l'utilisateurIntéraction avec l'utilisateur








