Tutoriel de création d'un mini-moteur de rendu 3D en Java avec LWJGL

Le graphe de scène

Gérard Bourriaud

Suite du moteur 3D cette fois-ci nous allons aller plus loin en créant ce qu’on appelle un graphe de scène. Cet article vous expliquera ce que c’est, son rôle et donnera également une implémentation possible.

Nous supposons ici que vous ayez lu et suivit l’introduction qui se trouve : Introduction sur la création d’un mini moteur 3D en Java grâce à LWJGL

- Titre : Tutoriel de création d'un mini-moteur de rendu 3D en Java avec LWJGL

- Auteur : Gérard Bourriaud

- Parution : 1 décembre 2020

- Mise à jour : 1 décembre 2020

- Public visé :

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2020 Gérard Bourriaud. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Voir également :

- Introduction sur la création d'un moteur 3D en Java grâce à LWJGL

- Intéraction avec l'utilisateur

Sommaire

- I. Le graphe, une façon de représenter un objet complexe

- II. Les nœuds

- III. Le graphe de scène

- IV. Mise en place du graphe de scène

- IV-A. Les nœuds et leur description de position

- IV-B. « Dessiner » le nœud avec OpenGL

- IV-C. Les objets 3D et les mailles

- IV-D. Un peu de couleurs

- IV-E. Dessiner la maille avec OpenGL

- IV-F. Un exemple d’objet 3D : la boîte

- IV-G. Le graphe de scène

- IV-H. Lier le graphe de scène avec la boucle de rendue

- IV-I. Exemple de scène

- V. Optimiser le dessin de la maille

- V-A. Optimisation OpenGL : La notion de liste

- V-B. Optimisation Java : La notion de scellé

- VI. Les clones

- VII. Conclusion

I - Le graphe, une façon de représenter un objet complexe

Code source du tutoriel

Code source

Téléchargez le code source

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 :

Node3D

1. package fr.developez.tutorial.java.dimension3.render;

3. import fr.developez.tutorial.java.dimension3.tool.NonNulltool.NonNull;

4. import java.util.ArrayList;

5. import java.util.List;

6. import java.util.Objects;

8. /**

9. * Nœud virtuel de la scènescène

10. */

11. public class Node3D

12. {

13. /**

14. * Coordonne X de la position dans la 3D

15. */

16. public float x = 0f;

17. /**

18. * Coordonne Y de la position dans la 3D

19. */

20. public float y = 0f;

21. /**

22. * Coordonne Z de la position dans la 3D

23. */

24. public float z = 0f;

26. /**

27. * Angle de la rotation autour de l'axe X

28. */

29. public float angleX = 0f;

30. /**

31. * Angle de la rotation autour de l'axe Y

32. */

33. public float angleY = 0f;

34. /**

35. * Angle de la rotation autour de l'axe Z

36. */

37. public float angleZ = 0f;

39. /**

40. * Facteur d'échelle le long de l'axe X <br/>

41. * La valeur doit toujours être strictement positive

42. */

43. public float scaleX = 1f;

44. /**

45. * Facteur d'échelle le long de l'axe Y <br/>

46. * La valeur doit toujours être strictement positive

47. */

48. public float scaleY = 1f;

49. /**

50. * Facteur d'échelle le long de l'axe Z <br/>

51. * La valeur doit toujours être strictement positive

52. */

53. public float scaleZ = 1f;

55. /**

56. * Liste des nœuds placés relativement à celui-ci

57. */

58. private final List<Node3D> children = new ArrayList<>();

60. public Node3D()

61. {

62. }

64. /**

65. * Ajoute un nœud enfant

66. */

67. public final void addChild(@NonNull final Node3D node)

68. {

69. Objects.requireNonNull(node, "node must not be null");

71. // On synchronize pour éviter l'accès concurrent

72. synchronized (this.children)

73. {

74. this.children.add(node);

75. }

76. }

77. }

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 à :

Node3D

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

1. Position (X,Y,Z)

2. Rotation autour de X

3. Rotation autour de Y

4. Rotation autour de Z

5. Facteurs d'échelles

Node3D

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

Node3D

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

Node3D

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.

Vertex

1. /**

2. * Vertex décrivant un sommet d'une face d'une maille.

3. * <br/>

4. * Le vertex contient non seulement les coordonnées du point,

5. * mais également d'autre informations utiles pour les textures et la lumiere

6. */

7. public class Vertex

8. {

9. public float x = 0f;

10. public float y = 0f;

11. public float z = 0f;

13. public Vertex()

14. {

15. }

17. public Vertex(float x, float y, float z)

18. {

19. this.x = x;

20. this.y = y;

21. this.z = z;

22. }

23. }

Maintenant que nous avons notre vertex, on peut décrire une face

Face

1. package fr.developez.tutorial.java.dimension3.render.mesh;

3. import fr.developez.tutorial.java.dimension3.tool.NonNull;

4. import java.util.ArrayList;

5. import java.util.Collections;

6. import java.util.List;

7. import java.util.Objects;

8. import java.util.function.Consumer;

10. /**

11. * Décrit une face d'une maille

12. */

13. public final class Face

14. {

15. private final List<Vertex> vertexs = new ArrayList<>();

17. public Face(Vertex... vertexs)

18. {

19. if (vertexs != null)

20. {

21. Collections.addAll(this.vertexs, vertexs);

22. }

23. }

25. public void addVertex(float x, float y, float z)

26. {

27. this.addVertex(new Vertex(x, y, z));

28. }

30. public void addVertex(@NonNull final Vertex vertex)

31. {

32. Objects.requireNonNull(vertex, "vertex must not be null");

34. synchronized (this.vertexs)

35. {

36. this.vertexs.add(vertex);

37. }

38. }

40. public void forEachVertex(@NonNull final Consumer<Vertex> action)

41. {

42. Objects.requireNonNull(action, "action must not be null");

44. synchronized (this.vertexs)

45. {

46. for (final Vertex vertex : this.vertexs)

47. {

48. action.accept(vertex);

49. }

50. }

51. }

52. }

Quant à la maille

Mesh

1. package fr.developez.tutorial.java.dimension3.render;

3. import fr.developez.tutorial.java.dimension3.render.mesh.Face;

4. import fr.developez.tutorial.java.dimension3.tool.NonNull;

5. import java.util.ArrayList;

6. import java.util.List;

7. import java.util.Objects;

9. /**

10. * Maille d'un objet

11. */

12. public class Mesh

13. {

14. private final List<Face> faces = new ArrayList<>();

16. public Mesh()

17. {

18. }

20. public void addFace(@NonNull final Face face)

21. {

22. Objects.requireNonNull(face, "face must not be null");

24. synchronized (this.faces)

25. {

26. this.faces.add(face);

27. }

28. }

29. }

Et l'objet 3D

Object3D

1. package fr.developez.tutorial.java.dimension3.render;

3. import fr.developez.tutorial.java.dimension3.tool.NonNull;

4. import java.util.Objects;

6. /**

7. * Objet 3D

8. */

9. public class Object3D

10. extends Node3D

11. {

12. @NonNull

13. public final Mesh    mesh  = new Mesh();

15. public Object3D()

16. {

17. }

18. }

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.

Color4f

1. package fr.developez.tutorial.java.dimension3.render;

3. import fr.developez.tutorial.java.dimension3.tool.MathTools;

4. import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;

5. import org.lwjgl.opengl.GL11;

7. /**

8. * Représente une couleur

9. */

10. public class Color4f

11. {

12. public static final Color4f BLACK      = new Color4f(0f);

13. public static final Color4f DARK_GREY  = new Color4f(0.25f);

14. public static final Color4f GREY       = new Color4f(0.5f);

15. public static final Color4f LIGHT_GREY = new Color4f(0.75f);

16. public static final Color4f WHITE      = new Color4f(1f);

18. public static final Color4f DARK_RED  = new Color4f(0.5f, 0f, 0f);

19. public static final Color4f RED       = new Color4f(1f, 0f, 0f);

20. public static final Color4f LIGHT_RED = new Color4f(1f, 0.5f, 0.5f);

22. public static final Color4f DARK_GREEN  = new Color4f(0f, 0.5f, 0f);

23. public static final Color4f GREEN       = new Color4f(0f, 1f, 0f);

24. public static final Color4f LIGHT_GREEN = new Color4f(0.5f, 1f, 0.5f);

26. public static final Color4f DARK_BLUE  = new Color4f(0f, 0f, 0.5f);

27. public static final Color4f BLUE       = new Color4f(0f, 0f, 1f);

28. public static final Color4f LIGHT_BLUE = new Color4f(0.5f, 0.5f, 1f);

30. /**

31. * Intensité de rouge

32. */

33. public final float red;

34. /**

35. * Intensité de vert

36. */

37. public final float green;

38. /**

39. * Intensité de bleu

40. */

41. public final float blue;

42. /**

43. * Niveau d'opacité

44. */

45. public final float alpha;

47. /**

48. * Construit la couleur à partir d'un description ARGB

49. *

50. * @param argb Description ARGB

51. */

52. public Color4f(int argb)

53. {

54. this((float) ((argb >> 16) & 0xFF) / 255f,

55. (float) ((argb >> 8) & 0xFF) / 255f,

56. (float) (argb & 0xFF) / 255f,

57. (float) ((argb >> 24) & 0xFF) / 255f);

58. }

60. /**

61. * Construit une couleur avec niveau de gris opaque

62. *

63. * @param grey Niveau de gris

64. */

65. public Color4f(float grey)

66. {

67. this(grey, grey, grey, 1f);

68. }

70. /**

71. * Construit une couleur avec niveau de gris

72. *

73. * @param grey  Niveau de gris

74. * @param alpha Niveau d'opacité

75. */

76. public Color4f(float grey, float alpha)

77. {

78. this(grey, grey, grey, alpha);

79. }

81. /**

82. * Construit une couleur opaque

83. *

84. * @param red   Intensité de rouge

85. * @param green Intensité de vert

86. * @param blue  Intensité de bleu

87. */

88. public Color4f(float red, float green, float blue)

89. {

90. this(red, green, blue, 1f);

91. }

93. /**

94. * Construit une couleur

95. *

96. * @param red   Intensité de rouge

97. * @param green Intensité de vert

98. * @param blue  Intensité de bleu

99. * @param alpha Niveau d'opacité

100. */

101. public Color4f(float red, float green, float blue, float alpha)

102. {

103. this.red   = MathTools.bounds(red, 0f, 1f);

104. this.green = MathTools.bounds(green, 0f, 1f);

105. this.blue  = MathTools.bounds(blue, 0f, 1f);

106. this.alpha = MathTools.bounds(alpha, 0f, 1f);

107. }

109. /**

110. * Demande à OpenGL d'utilisé cette couleur

111. */

112. @ThreadOpenGL

113. void apply()

114. {

115. GL11.glColor4f(this.red, this.green, this.blue, this.alpha);

116. }

117. }

La méthode `apply` permettra de demander à l'OpenGL d'utiliser la couleur.

On ajoute l'information de couleur à notre objet 3D

Object3D

1. package fr.developez.tutorial.java.dimension3.render;

3. import fr.developez.tutorial.java.dimension3.tool.NonNull;

4. import java.util.Objects;

6. /**

7. * Objet 3D

8. */

9. public class Object3D

10. extends Node3D

11. {

12. private      Color4f color = Color4f.GREY;

13. @NonNull

14. public final Mesh    mesh  = new Mesh();

16. public Object3D()

17. {

18. }

20. @NonNull

21. public Color4f getColor()

22. {

23. return this.color;

24. }

26. public void setColor(@NonNull final Color4f color)

27. {

28. this.color = Objects.requireNonNull(color, "color must not be null");

29. }

30. }

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

Mesh

1. package fr.developez.tutorial.java.dimension3.render;

3. import fr.developez.tutorial.java.dimension3.render.mesh.Face;

4. import fr.developez.tutorial.java.dimension3.tool.NonNull;

5. import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;

6. import java.util.ArrayList;

7. import java.util.List;

8. import java.util.Objects;

9. import org.lwjgl.opengl.GL11;

11. /**

12. * Maille d'un objet

13. */

14. public class Mesh

15. {

16. private final List<Face> faces = new ArrayList<>();

18. public Mesh()

19. {

20. }

22. public void addFace(@NonNull final Face face)

23. {

24. Objects.requireNonNull(face, "face must not be null");

26. synchronized (this.faces)

27. {

28. this.faces.add(face);

29. }

30. }

32. @ThreadOpenGL

33. void drawMesh()

34. {

35. synchronized (this.faces)

36. {

37. for (final Face face : this.faces)

38. {

39. // Annonce à OpenGL le début du polygone

40. GL11.glBegin(GL11.GL_POLYGON);

42. face.forEachVertex(vertex -> GL11.glVertex3f(vertex.x, vertex.y, vertex.z));

44. // Annonce à OpenGL la fin du polygone

45. GL11.glEnd();

46. }

47. }

48. }

49. }

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

Object3D

1. package fr.developez.tutorial.java.dimension3.render;

3. import fr.developez.tutorial.java.dimension3.tool.NonNull;

4. import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;

5. import java.util.Objects;

7. /**

8. * Objet 3D

9. */

10. public class Object3D

11. extends Node3D

12. {

13. private      Color4f color = Color4f.GREY;

14. @NonNull

15. public final Mesh    mesh  = new Mesh();

17. public Object3D()

18. {

19. }

21. @NonNull

22. public Color4f getColor()

23. {

24. return this.color;

25. }

27. public void setColor(@NonNull final Color4f color)

28. {

29. this.color = Objects.requireNonNull(color, "color must not be null");

30. }

32. @ThreadOpenGL

33. @Override

34. void drawSpecific()

35. {

36. this.color.apply();

37. this.mesh.drawMesh();

38. }

39. }

IV-F - Un exemple d’objet 3D : la boîte

Afin d'avoir un objet de base à représenter créons une boîte

Box

1. package fr.developez.tutorial.java.dimension3.geometry;

3. import fr.developez.tutorial.java.dimension3.render.Object3D;

4. import fr.developez.tutorial.java.dimension3.render.mesh.Face;

6. /**

7. * Une boîte

8. */

9. public class Box

10. extends Object3D

11. {

12. public Box()

13. {

14. // Face de la boîte

15. Face face = new Face();

16. face.addVertex(-0.5f, 0.5f, 0.5f);

17. face.addVertex(0.5f, 0.5f, 0.5f);

18. face.addVertex(0.5f, -0.5f, 0.5f);

19. face.addVertex(-0.5f, -0.5f, 0.5f);

20. this.mesh.addFace(face);

22. // Dos de la boîte

23. face = new Face();

24. face.addVertex(-0.5f, -0.5f, -0.5f);

25. face.addVertex(0.5f, -0.5f, -0.5f);

26. face.addVertex(0.5f, 0.5f, -0.5f);

27. face.addVertex(-0.5f, 0.5f, -0.5f);

28. this.mesh.addFace(face);

30. // Haut de la boîte

31. face = new Face();

32. face.addVertex(-0.5f, 0.5f, -0.5f);

33. face.addVertex(0.5f, 0.5f, -0.5f);

34. face.addVertex(0.5f, 0.5f, 0.5f);

35. face.addVertex(-0.5f, 0.5f, 0.5f);

36. this.mesh.addFace(face);

38. // Bas de la boîte

39. face = new Face();

40. face.addVertex(-0.5f, -0.5f, 0.5f);

41. face.addVertex(0.5f, -0.5f, 0.5f);

42. face.addVertex(0.5f, -0.5f, -0.5f);

43. face.addVertex(-0.5f, -0.5f, -0.5f);

44. this.mesh.addFace(face);

46. // Coté droit de la boîte

47. face = new Face();

48. face.addVertex(0.5f, -0.5f, 0.5f);

49. face.addVertex(0.5f, 0.5f, 0.5f);

50. face.addVertex(0.5f, 0.5f, -0.5f);

51. face.addVertex(0.5f, -0.5f, -0.5f);

52. this.mesh.addFace(face);

54. // Coté gauche de la boîte

55. face = new Face();

56. face.addVertex(-0.5f, -0.5f, -0.5f);

57. face.addVertex(-0.5f, 0.5f, -0.5f);

58. face.addVertex(-0.5f, 0.5f, 0.5f);

59. face.addVertex(-0.5f, -0.5f, 0.5f);

60. this.mesh.addFace(face);

61. }

62. }

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.

SceneGraph

1. package fr.developez.tutorial.java.dimension3.render;

3. import fr.developez.tutorial.java.dimension3.tool.NonNull;

4. import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;

5. import java.util.Objects;

6. import org.lwjgl.opengl.GL11;

8. /**

9. * Le graphe de scène

10. */

11. public class SceneGraph

12. {

13. @NonNull

14. public final Node3D  root            = new Node3D();

15. private      Color4f backgroundColor = Color4f.WHITE;

17. public SceneGraph()

18. {

19. }

21. @NonNull

22. public Color4f getBackground()

23. {

24. return this.backgroundColor;

25. }

27. public void setBackground(@NonNull final Color4f backgroundColor)

28. {

29. this.backgroundColor = Objects.requireNonNull(backgroundColor, "backgroundColor must not be null");

30. }

32. @ThreadOpenGL

33. void drawScene()

34. {

35. GL11.glClearColor(this.backgroundColor.red, this.backgroundColor.green, this.backgroundColor.blue, 1f);

36. this.root.drawNode();

37. }

38. }

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

Render3D

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.

Window3D

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 :

MianBlueAngGreenCubes

1. package fr.developez.tutorial.java.dimension3;

3. import fr.developez.tutorial.java.dimension3.geometry.Box;

4. import fr.developez.tutorial.java.dimension3.render.Color4f;

5. import fr.developez.tutorial.java.dimension3.render.SceneGraph;

6. import fr.developez.tutorial.java.dimension3.render.Window3D;

7. import fr.developez.tutorial.java.dimension3.tool.NonNull;

9. public class MainBlueAndGreenCubes

10. {

11. public static void main(String[] args)

12. {

13. final Window3D window3D = new Window3D(800, 600, "Tutoriel 3D - Une boite blue et une verte");

14. MainBlueAndGreenCubes.drawScene(window3D.getSceneGraph());

15. }

17. private static void drawScene(@NonNull final SceneGraph sceneGraph)

18. {

19. final Box blueBox  = new Box();

20. final Box greenBox = new Box();

21. sceneGraph.root.addChild(blueBox);

22. sceneGraph.root.addChild(greenBox);

24. blueBox.x      = -1f;

25. blueBox.z      = -2f;

26. blueBox.angleX = 22.5f;

27. blueBox.angleY = 22.5f;

28. blueBox.setColor(Color4f.BLUE);

30. greenBox.x      = 1f;

31. greenBox.z      = -2f;

32. greenBox.angleX = 22.5f;

33. greenBox.angleY = 22.5f;

34. greenBox.setColor(Color4f.GREEN);

35. }

36. }

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

Mesh

1. package fr.developez.tutorial.java.dimension3.render;

3. import fr.developez.tutorial.java.dimension3.render.mesh.Face;

4. import fr.developez.tutorial.java.dimension3.tool.NonNull;

5. import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;

6. import java.util.ArrayList;

7. import java.util.List;

8. import java.util.Objects;

9. import org.lwjgl.opengl.GL11;

11. /**

12. * Maille d'un objet

13. */

14. public class Mesh

15. {

16. private final List<Face> faces = new ArrayList<>();

18. /**

19. * Identifiant OpenGL de la liste des polygones décrivant la maille

20. */

21. private int     idList                 = -1;

22. /**

23. * Indique que la maille à été modifiée et donc a besoin d'être rafraîchie par OpenGL

24. */

25. private boolean needReconstructTheList = true;

27. public Mesh()

28. {

29. }

31. public void addFace(@NonNull final Face face)

32. {

33. Objects.requireNonNull(face, "face must not be null");

35. synchronized (this.faces)

36. {

37. this.faces.add(face);

38. }

40. // On a modifié la maille, il faut donc la rafraîchir

41. this.needReconstructTheList = true;

42. }

44. /**

45. * Indique qu'on a modifier l'une de faces ou l'un des vertex d'une des faces depuis l'extérieur.

46. *

47. * Dans ce cas on a besoin de rafraîchir la maille

48. */

49. public void forceRefresh()

50. {

51. this.needReconstructTheList = true;

52. }

54. @ThreadOpenGL

55. void drawMesh()

56. {

57. // On va créer la liste des polygones la première fois et chaque fois que nécessaire

58. if (this.idList < 0 || this.needReconstructTheList)

59. {

60. // On va construire la liste, donc on aura plus besoin de le faire au prochain tour

61. this.needReconstructTheList = false;

63. // On efface l'ancienne liste de la mémoire de la carte graphique, celle-ci étant devenu obsolète

64. if (this.idList >= 0)

65. {

66. GL11.glDeleteLists(this.idList, 1);

67. }

69. // On demande une nouvelle référence de liste à la carte graphique

70. this.idList = GL11.glGenLists(1);

71. // On indique le début de la liste

72. GL11.glNewList(this.idList, GL11.GL_COMPILE);

74. synchronized (this.faces)

75. {

76. for (final Face face : this.faces)

77. {

78. // Annonce à OpenGL le début du polygone

79. GL11.glBegin(GL11.GL_POLYGON);

81. face.forEachVertex(vertex -> GL11.glVertex3f(vertex.x, vertex.y, vertex.z));

83. // Annonce à OpenGL la fin du polygone

84. GL11.glEnd();

85. }

86. }

88. // On indique la fin de la liste

89. GL11.glEndList();

90. }

92. // On dessine la liste mémorisé

93. GL11.glCallList(this.idList);

94. }

95. }

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.

Mesh

1. package fr.developez.tutorial.java.dimension3.render;

3. import fr.developez.tutorial.java.dimension3.render.mesh.Face;

4. import fr.developez.tutorial.java.dimension3.tool.NonNull;

5. import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;

6. import java.util.ArrayList;

7. import java.util.List;

8. import java.util.Objects;

9. import java.util.concurrent.atomic.AtomicBoolean;

10. import org.lwjgl.opengl.GL11;

12. /**

13. * Maille d'un objet

14. */

15. public final class Mesh

16. {

17. private final List<Face> faces = new ArrayList<>();

19. /**

20. * Identifiant OpenGL de la liste des polygones décrivant la maille

21. */

22. private int           idList                 = -1;

23. /**

24. * Indique que la maille à été modifiée et donc a besoin d'être rafraîchit par OpenGL

25. */

26. private boolean       needReconstructTheList = true;

27. /**

28. * Indique si la maille est scellée

29. */

30. private AtomicBoolean sealed                 = new AtomicBoolean(false);

32. public Mesh()

33. {

34. }

36. /**

37. * Indique si la maille est scellée

38. */

39. public boolean sealed()

40. {

41. return this.sealed.get();

42. }

44. /**

45. * Scelle la maille, elle ne pourra plus être modifiée

46. */

47. public void seal()

48. {

49. if (this.sealed.compareAndSet(false, true))

50. {

51. // On s'assure de prendre les éventuelles dernières modifications

52. this.needReconstructTheList = true;

53. }

54. }

56. public void addFace(@NonNull final Face face)

57. {

58. Objects.requireNonNull(face, "face must not be null");

60. // Si la maille est scellée, on n'ajoute pas la face

61. if (this.sealed.get())

62. {

63. return;

64. }

66. synchronized (this.faces)

67. {

68. this.faces.add(face);

69. }

71. // On a modifié la mail, il faut donc la rafraîchir

72. this.needReconstructTheList = true;

73. }

75. /**

76. * Indique qu'on a modifier l'une de faces ou l'un des vertex d'une des faces depuis l'extérieur.

77. * <p>

78. * Dans ce cas on a besoin de rafraîchir la maille

79. */

80. public void forceRefresh()

81. {

82. // La maille ne peut changée que si elle n'est pas scellée

83. if(!this.sealed.get())

84. {

85. this.needReconstructTheList = true;

86. }

87. }

89. @ThreadOpenGL

90. void drawMesh()

91. {

92. // On va créer la liste des polygones la première fois et chaque fois que nécessaire

93. if (this.idList < 0 || this.needReconstructTheList)

94. {

95. // On va construire la liste, donc on aura plus besoin de le faire au prochain tour

96. this.needReconstructTheList = false;

98. // On efface l'ancienne liste de la mémoire de la carte graphique, celle-ci étant devenu obsolète

99. if (this.idList >= 0)

100. {

101. GL11.glDeleteLists(this.idList, 1);

102. }

104. // On demande une nouvelle référence de liste à la carte graphique

105. this.idList = GL11.glGenLists(1);

106. // On indique le début de la liste

107. GL11.glNewList(this.idList, GL11.GL_COMPILE);

109. synchronized (this.faces)

110. {

111. for (final Face face : this.faces)

112. {

113. // Annonce à OpenGL le début du polygone

114. GL11.glBegin(GL11.GL_POLYGON);

116. face.forEachVertex(vertex -> GL11.glVertex3f(vertex.x, vertex.y, vertex.z));

118. // Annonce à OpenGL la fin du polygone

119. GL11.glEnd();

120. }

121. }

123. // On indique la fin de la liste

124. GL11.glEndList();

126. // 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

127. if (this.sealed.get())

128. {

129. this.faces.clear();

130. }

131. }

133. // On dessine la liste mémorisée

134. GL11.glCallList(this.idList);

135. }

136. }

Changeons un peu l'exemple de tout à l'heure pour illustrer notre propos

MainBlueAndGreenCubes

1. package fr.developez.tutorial.java.dimension3;

3. import fr.developez.tutorial.java.dimension3.geometry.Box;

4. import fr.developez.tutorial.java.dimension3.render.Color4f;

5. import fr.developez.tutorial.java.dimension3.render.SceneGraph;

6. import fr.developez.tutorial.java.dimension3.render.Window3D;

7. import fr.developez.tutorial.java.dimension3.tool.NonNull;

9. public class MainBlueAndGreenCubes

10. {

11. public static void main(String[] args)

12. {

13. final Window3D window3D = new Window3D(800, 600, "Tutoriel 3D - Une boite bleue et une verte");

14. MainBlueAndGreenCubes.drawScene(window3D.getSceneGraph());

15. }

17. private static void drawScene(@NonNull final SceneGraph sceneGraph)

18. {

19. final Box blueBox = new Box();

20. blueBox.mesh.seal();

21. final Box greenBox = new Box();

22. greenBox.mesh.seal();

23. sceneGraph.root.addChild(blueBox);

24. sceneGraph.root.addChild(greenBox);

26. blueBox.x      = -1f;

27. blueBox.z      = -2f;

28. blueBox.angleX = 22.5f;

29. blueBox.angleY = 22.5f;

30. blueBox.setColor(Color4f.BLUE);

32. greenBox.x      = 1f;

33. greenBox.z      = -2f;

34. greenBox.angleX = 22.5f;

35. greenBox.angleY = 22.5f;

36. greenBox.setColor(Color4f.GREEN);

37. }

38. }

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.

Clone3D

1. package fr.developez.tutorial.java.dimension3.render;

3. import fr.developez.tutorial.java.dimension3.tool.NonNull;

4. import fr.developez.tutorial.java.dimension3.tool.ThreadOpenGL;

5. import java.util.Objects;

7. /**

8. * Clone d'un objet 3D

9. */

10. public class Clone3D

11. extends Node3D

12. {

13. private       Color4f  color = Color4f.GREY;

14. /**

15. * Objet cloné

16. */

17. private final Object3D cloned;

19. public Clone3D(@NonNull final Object3D cloned)

20. {

21. this.cloned = Objects.requireNonNull(cloned, "cloned must not be null");

22. }

24. @NonNull

25. public Color4f getColor()

26. {

27. return this.color;

28. }

30. public void setColor(@NonNull final Color4f color)

31. {

32. this.color = Objects.requireNonNull(color, "color must not be null");

33. }

35. @ThreadOpenGL

36. @Override

37. void drawSpecific()

38. {

39. this.color.apply();

40. // Ici on utilise la maille de l'objet cloné

41. this.cloned.mesh.drawMesh();

42. }

43. }

Modifions notre exemple

MainBlueAndGreenCubes

1. package fr.developez.tutorial.java.dimension3;

3. import fr.developez.tutorial.java.dimension3.geometry.Box;

4. import fr.developez.tutorial.java.dimension3.render.Clone3D;

5. import fr.developez.tutorial.java.dimension3.render.Color4f;

6. import fr.developez.tutorial.java.dimension3.render.SceneGraph;

7. import fr.developez.tutorial.java.dimension3.render.Window3D;

8. import fr.developez.tutorial.java.dimension3.tool.NonNull;

10. public class MainBlueAndGreenCubes

11. {

12. public static void main(String[] args)

13. {

14. final Window3D window3D = new Window3D(800, 600, "Tutoriel 3D - Une boite blue et une verte");

15. MainBlueAndGreenCubes.drawScene(window3D.getSceneGraph());

16. }

18. private static void drawScene(@NonNull final SceneGraph sceneGraph)

19. {

20. final Box blueBox = new Box();

21. blueBox.mesh.seal();

22. final Clone3D greenBox = new Clone3D(blueBox);

23. sceneGraph.root.addChild(blueBox);

24. sceneGraph.root.addChild(greenBox);

26. blueBox.x      = -1f;

27. blueBox.z      = -2f;

28. blueBox.angleX = 22.5f;

29. blueBox.angleY = 22.5f;

30. blueBox.setColor(Color4f.BLUE);

32. greenBox.x      = 1f;

33. greenBox.z      = -2f;

34. greenBox.angleX = 22.5f;

35. greenBox.angleY = 22.5f;

36. greenBox.setColor(Color4f.GREEN);

37. }

38. }

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'utilisateur