PGE (Plegat Graphic Engine) - Définition et affichage des objets

Jean-Michel BORLOT

Troisième article de la série de tutoriels sur la création de mon moteur graphique PGE (Plegat Graphic Engine).
Nous aborderons ici la définition des objets, ainsi que leur intégration et leur positionnement dans la fenêtre 3D.

Commentez ♪

- Titre : PGE (Plegat Graphic Engine) - Définition et affichage des objets

- Auteur : Jean-Michel BORLOT

- Parution : 3 décembre 2011

- Mise à jour : 15 septembre 2013

- 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 ® 2011 JM BORLOT. 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.

Sommaire

- 1. Remplir l'espace

- 2. L'objet de base

- 2-A. Caractéristiques

- 2-B. L'interface IPGEObject

- 2-C. L'objet générique PGEObject

- 3. Pour quelques objets de plus…

- 3-A. … vous reprendrez bien un peu de thé ?

- 3-B. Un petit donut

- 3-B-1. Les ingrédients et la recette

- 3-B-2. Définir les nœuds : la classe PGENode

- 3-B-3. Définir les faces : les classes PGEFace et PGEQuad

- 3-B-4. Définir le maillage : la classe PGEMesh

- 3-B-5. Définir le tore : la classe PGETorus

- 3-C. Dupliquer et localiser les objets

- 3-D. Assembler plusieurs objets

- 4. La gestion de nos objets

- 4-A. Une bibliothèque pour ranger nos objets

- 4-B. Un gestionnaire graphique de bibliothèque

- 5. Améliorations des classes Main et PGEPanel

- 5-A. Interaction au clavier avec la caméra

- 5-B. Modifications de la classe PGEPanel

- 5-C. Modifications de la main class

- 6. Conclusion

- 7. Remerciements

1 - Remplir l'espace

Maintenant que nous avons créé une fenêtre dans laquelle nous pouvons effectuer notre rendu OpenGL, nous allons remplir l'espace avec les objets que l'on désire afficher. Pour cela, il va nous falloir définir nos objets de manière à ce qu'OpenGL soit capable de les dessiner à l'écran. Dans le cadre de PGE, les objets que je vais être amené à manipuler sont ce que l'on appelle communément des « meshes » (pluriel de « mesh », maillage en français), composés d'un assemblage de volumes, de faces, de segments et de points.

Une certaine hiérarchie peut être établie à partir de ces entités géométriques :

- un volume est délimité par un ensemble de faces ;

- une face est délimitée par un ensemble de segments ;

- un segment relie deux points.

Pour compléter cette hiérarchie, les faces peuvent être classifiées suivant le nombre d'entités les définissant :

- une face à 3 côtés est un triangle (que l'on abrège « tri ») ;

- une face à 4 côtés est un quadrilatère (que l'on abrège « quad »),

ainsi que les volumes :

- un volume à 4 faces est un tétraèdre (4 tris) ;

- un volume à 5 faces est un pentaèdre (2 tris et 3 quads) ;

- un volume à 6 faces est un hexaèdre (6 quads).

On se limitera ici uniquement à ces entités géométriques, qui sont celles que l'on rencontrera le plus souvent dans le milieu de la 3D.

Tout objet à afficher peut être défini comme étant un assemblage de toutes ces différentes entités, que ce soit un volume, une surface, ou un simple trait. Suivant la complexité de l'objet, ces entités reproduiront fidèlement sa géométrie, ou l'approcheront au mieux. Étant donné qu'il est bien évidemment impossible de découper une surface courbe avec des entités planes, certains artifices algorithmiques sont utilisés afin de rendre l'affichage le plus fidèle à la réalité.

2 - L'objet de base

2-A - Caractéristiques

L'objet de base que nous utiliserons est l'objet « passe-partout », celui qui n'a aucune réelle caractéristique géométrique, mais qui possède toutes les caractéristiques intrinsèques dont vont hériter les objets enfants.

Il est caractérisé par :

- un nom, permettant de l'identifier ;

- sa visibilité, afin de savoir s'il est affiché ou pas à l'écran ;

- l'identité de son objet parent, qu'il permet de définir ;

- l'identité des ses objets enfants, qui permettent de le définir ;

- les façons dont on va le dessiner, en mode « fil de fer », et en mode « solide ».

Ces quelques caractéristiques permettent de décrire notre objet géométrique générique, et ainsi de définir une interface.

2-B - L'interface IPGEObject

Cette interface code en langage Java les caractéristiques qui ont été définies dans le paragraphe précédent. Tous les objets devant être affichés implémenteront cette interface.

Interface IPGEObject

/**
* Interface de définition de l'objet géométrique générique
*/
public interface IPGEObject {

/**
* Méthode de dessin de l'objet en mode "fil de fer"
* @param gl
*/
public void drawWire(GL2 gl);

/**
* Méthode de dessin de l'objet en mode "solide"
* @param gl
*/
public void drawSolid(GL2 gl);

/**
* Renvoie la visibilité de l'objet
* @return true si l'objet est visible, false sinon
*/
public boolean isVisible();

/**
* Fixe la visibilité de l'objet
* @param flag true si l'objet est visible, false sinon
*/
public void setVisible(boolean flag);

/**
* Renvoie les objets enfants de l'objet
* @return les objets enfants
*/
public IPGEObject[] getChildren();

/**
* Renvoie l'objet parent de l'objet
* @return l'objet parent
*/
public IPGEObject getParent();

/**
* Renvoie le nom de l'objet
* @return
*/
public String getId();

}

2-C - L'objet générique PGEObject

L'interface que nous avons définie précédemment est implémentée par notre objet générique dans la classe PGEObject. Celle-ci possède les propriétés suivantes :

- une liste d'objets enfants ;

- un objet parent ;

- un nom ;

- une couleur d'affichage en mode « fil de fer » ;

- une couleur d'affichage en mode « solide » ;

- une visibilité.

Vous remarquerez que ces propriétés sont toutes déclarées protected, afin que les classes filles puissent y accéder directement.

Parmi cette liste, seule la couleur d'affichage n'est pas intégrée comme caractéristique dans l'interface IPGEObject.

Les méthodes de l'interface IPGEObject sont définies dans la classe PGEObject :

Classe PGEObject

public class PGEObject implements IPGEObject {

protected ArrayList<IPGEObject> children;
protected IPGEObject parent;
protected String id;
protected float[] color = new float[3];
protected float[] colorDiffuse = new float[3];
protected boolean flagVisible = true;

/**
* Constructeur
* @param parent
*/
public PGEObject(IPGEObject parent) {
this.parent = parent;
this.initChildren();
}

/**
* Constructeur
*/
public PGEObject() {
this.parent = null;
this.initChildren();
}

@Override
public boolean isVisible() {
return this.flagVisible;
}

@Override
public void setVisible(boolean flag) {
this.flagVisible = flag;
}

@Override
public IPGEObject[] getChildren() {

int nbChildren = this.getChildrenNumber();
IPGEObject[] result = new IPGEObject[nbChildren];
result = this.children.toArray(result);

return result;
}

@Override
public IPGEObject getParent() {
return this.parent;
}

public void setParent(IPGEObject parent) {
this.parent = parent;
}

/**
* Initialise les objets enfants de l'objet
*/
protected final void initChildren() {
this.children = new ArrayList();
}

/**
* Ajoute un objet enfant à l'objet
* @param child l'objet enfant à ajouter
*/
public void addChildren(IPGEObject child) {
this.children.add(child);
}

@Override
public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}
}

La méthode toString permet de récupérer l'identité de l'objet (équivalent à la méthode getId) :

Classe PGEObject

@Override
public String toString() {
return this.getId();
}

Deux méthodes permettent de définir la couleur d'affichage de l'objet.
La première prend en paramètres les trois composantes RGB de la couleur, prises dans l'intervalle [0,1].
La seconde prend directement un objet Color en paramètre.

Classe PGEObject

/**
* Définit la couleur d'affichage de l'objet
* @param red composante rouge (entre 0 et 1)
* @param green composante verte (entre 0 et 1)
* @param blue composante bleue (entre 0 et 1)
*/
public void setColor(float red, float green, float blue) {
this.color[0] = red;
this.color[1] = green;
this.color[2] = blue;
}

/**
* Définit la couleur d'affichage de l'objet
* @param rgb la couleur
*/
public void setColor(Color rgb) {
this.color[0] = rgb.getRed() / 255.0f;
this.color[1] = rgb.getGreen() / 255.0f;
this.color[2] = rgb.getBlue() / 255.0f;
}

/**
* Renvoie la couleur de l'objet
* @return la couleur de l'objet
*/
public float[] getColor() {
return this.color;
}

/**
* Définit la couleur d'affichage de l'objet en mode "solide"
* @param red composante rouge (entre 0 et 1)
* @param green composante verte (entre 0 et 1)
* @param blue composante bleue (entre 0 et 1)
*/
public void setColorDiffuse(float red, float green, float blue) {
this.colorDiffuse[0] = red;
this.colorDiffuse[1] = green;
this.colorDiffuse[2] = blue;
}

/**
* Définit la couleur d'affichage de l'objet en mode "solide"
* @param rgb la couleur
*/
public void setColorDiffuse(Color rgb) {
this.colorDiffuse[0] = rgb.getRed() / 255.0f;
this.colorDiffuse[1] = rgb.getGreen() / 255.0f;
this.colorDiffuse[2] = rgb.getBlue() / 255.0f;
}

/**
* Renvoie la couleur de l'objet en mode "solide"
* @return la couleur de l'objet
*/
public float[] getColorDiffuse() {
return this.colorDiffuse;
}

Les méthodes d'affichage implémentées de l'interface n'ont rien à dessiner de particulier concernant l'objet générique, celui-ci n'ayant… rien à dessiner ! Elles se contentent donc d'appeler, pour chaque objet enfant, la méthode de dessin de celui-ci.

Classe PGEObject

@Override
public void drawWire(GL2 gl) {
this.drawChildrenWire(gl);
}

@Override
public void drawSolid(GL2 gl) {
this.drawChildrenSolid(gl);
}

/**
* Méthode pour dessiner les objets enfants de l'objet en mode "fil de fer"
* @param gl
*/
public void drawChildrenWire(GL2 gl) {
Iterator<IPGEObject> it = this.children.iterator();

while (it.hasNext()) {
it.next().drawWire(gl);
}
}

/**
* Méthode pour dessiner les objets enfants de l'objet en mode "plein"
* @param gl
*/
public void drawChildrenSolid(GL2 gl) {
Iterator<IPGEObject> it = this.children.iterator();

while (it.hasNext()) {
it.next().drawSolid(gl);
}
}

Une méthode supplémentaire permet de récupérer le nombre d'objets enfants de l'objet :

Classe PGEObject

/**
* Renvoie le nombre d'objets enfants de l'objet
* @return le nombre d'objets enfants
*/
public int getChildrenNumber() {
this.children.trimToSize();
return this.children.size();
}

Si vous avez bien suivi jusqu'ici, nous avons défini une classe pour dessiner un objet générique, mais celle-ci ne dessine rien ! Passons donc maintenant à nos premiers objets réels…

3 - Pour quelques objets de plus…

3-A - … vous reprendrez bien un peu de thé ?

Ne dérogeons pas à la tradition, le premier objet que nous afficherons est la fameuse théière OpenGL de Glut. Cela nous permet d'avoir un objet complexe très facilement.

Nous allons créer une classe PGETeapot, qui étend PGEObject, et pour laquelle nous allons simplement définir les méthodes drawWire et drawSolid. Une propriété size permet de régler la taille à l'affichage de notre théière.

Le constructeur prend en paramètres l'objet parent et la taille de la théière :

Classe PGETeapot

public class PGETeapot extends PGEObject {

private double size;

/**
* Constructeur
* @param parent objet parent
* @param size taille de la théière à l'affichage
*/
public PGETeapot(IPGEObject parent, double size) {
super(parent);
this.size = size;
this.initColor();
}
}

La méthode d'initialisation des couleurs fixe les couleurs à l'affichage en mode « fil de fer » et en mode « solide ».
Ici, respectivement en blanc et en orange :

Classe PGETeapot

/**
* Initialisation des couleurs d'affichage en mode "fil de fer" et "solide"
*/
private void initColor() {
this.setColor(Color.WHITE);
this.setColorDiffuse(Color.ORANGE);
}

La méthode drawWire définit la procédure d'affichage de notre objet en mode « fil de fer ».
La couleur de dessin est initialisée par un appel à glColor3f.
La méthode glutWireTeapot dessine ensuite directement la théière :

Classe PGETeapot

@Override
public void drawWire(GL2 gl) {

gl.glColor3f(this.color[0], this.color[1], this.color[2]);

GLUT glut = new GLUT();
glut.glutWireTeapot(size);
}

La méthode drawSolid définit la procédure d'affichage de notre objet en mode « solide ».
Les matériaux sont initialisés par appel à la méthode glMaterialfv, pour les composantes GL_DIFFUSE et GL_AMBIANT.
La théière est cette fois dessinée par un appel à glutSolidTeapot :

Classe PGETeapot

@Override
public void drawSolid(GL2 gl) {

float[] mat_diffuse = {this.colorDiffuse[0], this.colorDiffuse[1], this.colorDiffuse[2], 1.0f};
gl.glMaterialfv(GL2.GL_FRONT, GL2.GL_DIFFUSE, FloatBuffer.wrap(mat_diffuse));
gl.glMaterialfv(GL2.GL_FRONT, GL2.GL_AMBIENT, FloatBuffer.wrap(mat_diffuse));

GLUT glut = new GLUT();
glut.glutSolidTeapot(size);
}

Voilà, la classe définissant notre théière est terminée. Elle nous permettra d'obtenir le résultat suivant :

Rendu « fil de fer »

Rendu « solide »

3-B - Un petit donut

3-B-1 - Les ingrédients et la recette

Certes, ce n'est pas exactement un donut que nous allons maintenant dessiner, mais un tore.
Glut possède déjà un objet tore, dont nous pourrions nous servir de la même façon que nous avons utilisé la théière précédemment. Cependant, cet objet relativement simple à définir va nous permettre d'illustrer la procédure que nous allons suivre pour représenter des objets complexes sous forme de « mesh » en définissant notre propre objet tore.

Un tore est un solide de révolution ayant pour base un disque. Si l'on se place au niveau surfacique, sa surface est donc définie par l'extrusion circulaire sur 360 ° d'un cercle de rayon R. Cette définition nous sert de base pour la discrétisation de la surface du tore.

Le cercle, ne pouvant pas être représenté exactement point par point en OpenGL, est discrétisé en une suite de plusieurs segments, reliant des points répartis uniformément sur le cercle. Plus le nombre de points est élevé, plus la courbe discrétisée par les segments est visuellement proche du cercle. L'extrusion circulaire sur 360 ° est elle aussi discrétisée en plusieurs pas, afin d'approcher la forme circulaire que l'on souhaite obtenir. Chaque pas d'extrusion permet à chaque segment du cercle de recouvrir une petite surface de forme quadrangulaire, et donc de créer une face. L'ensemble de toutes les faces créées à la fin de l'extrusion circulaire définit la surface du tore.

Cet ensemble d'informations va être regroupé dans un objet « mesh », constitué de points (également appelés « nœuds ») et de quads. Nous allons pour cela définir les classes suivantes :

- la classe PGENode pour les nœuds (ou les points) ;

- la classe PGEFace pour les faces ;

- la classe PGEQuad pour les quads, qui étend la classe PGEFace (un quad étant une face à 4 côtés) ;

- la classe PGEMesh pour le mesh ;

- la classe PGETorus pour le tore.

3-B-2 - Définir les nœuds : la classe PGENode

L'objet nœud, ou point, hérite simplement de la classe PGEObject, à laquelle on rajoute trois propriétés donnant les coordonnées du nœud dans l'espace. Les getters et setters sont ajoutés, ainsi que deux méthodes supplémentaires :

- offset, qui permet de créer un nœud en passant ses coordonnées relatives par rapport à un autre nœud ;

- toArray, qui permet de convertir une ArrayList de nœuds en un tableau.

Classe PGENode

public class PGENode extends PGEObject {

private double x;
private double y;
private double z;

/**
* Constructeur
* @param id identité du noeud
* @param parent l'objet parent
* @param x coordonnée x
* @param y coordonnée y
* @param z coordonnée z
*/
public PGENode(String id, IPGEObject parent, double x, double y, double z) {
super(parent);
this.id = id;
this.x = x;
this.y = y;
this.z = z;
}

/**
* Retourne la coordonnée x du noeud
* @return la coordonnée x du noeud
*/
public double getX() {
return x;
}

/**
* Définit la coordonnée x du noeud
* @param x la coordonnée x du noeud
*/
public void setX(double x) {
this.x = x;
}

/**
* Retourne la coordonnée y du noeud
* @return la coordonnée y du noeud
*/
public double getY() {
return y;
}

/**
* Définit la coordonnée y du noeud
* @param y la coordonnée y du noeud
*/
public void setY(double y) {
this.y = y;
}

/**
* Retourne la coordonnée z du noeud
* @return la coordonnée z du noeud
*/
public double getZ() {
return z;
}

/**
* Définit la coordonnée z du noeud
* @param z la coordonnée z du noeud
*/
public void setZ(double z) {
this.z = z;
}

/**
* Crée un noeud relativement à un autre
* @param newId identité du noeud créé
* @param dx coordonnée x relative
* @param dy coordonnée y relative
* @param dz coordonnée z relative
* @return
*/
public PGENode offset(String newId, double dx, double dy, double dz) {
return new PGENode(newId, this.parent, this.x + dx, this.y + dy, this.z + dz);
}

/**
* Convertit une ArrayList de noeuds en un tableau
* @param list l'ArrayList de noeuds
* @return le tableau de noeuds
*/
public static PGENode[] toArray(ArrayList<PGENode> list) {

list.trimToSize();
PGENode[] temp = new PGENode[list.size()];
temp = list.toArray(temp);

return temp;
}
}

Les méthodes drawWire et drawSolid ne sont pas définies pour la classe PGENode. Étant donné que les méthodes héritées ne dessinent rien (voir §2.C), il n'y a donc pas d'affichage des nœuds pour le moment…

3-B-3 - Définir les faces : les classes PGEFace et PGEQuad

Nous allons créer deux classes pour la définition des faces, PGEFace, classe générique pour tous les objets de type « face » (surface plane délimitée par un ensemble de segments), et PGEQuad, classe étendant PGEFace et spécifique aux faces à quatre côtés.

La classe PGEFace possède les propriétés suivantes :

- un tableau des indices des nœuds. L'ordre du tableau donne l'ordre dans lequel les nœuds sont reliés, pas besoin de stocker les segments ;

- le nombre de nœuds définissant la face ;

- un tableau des vecteurs normaux aux nœuds, dans le même ordre que le tableau des nœuds.

Le tableau des nœuds ne contient pas directement les nœuds, mais les indices de ceux-ci, qui seront ensuite récupérés via l'objet PGEMesh parent. Le paramètre parent du constructeur doit donc obligatoirement être renseigné.

La classe PGEFace définit tout d'abord le constructeur et les méthodes héritées de PGEObject :

Classe PGEFace

public class PGEFace extends PGEObject {

private int[] nodes;
private int nbNodes;
private Vector3d[] nodeNormals;

public PGEFace(String id, IPGEObject parent, int[] nodes) {
super(parent);
this.setId(id);
this.setNodes(nodes);
this.initColor();
}

private void initColor() {
this.setColor(Color.RED);
this.setColorDiffuse(Color.WHITE);
}

@Override
public void drawWire(GL2 gl) {
}

@Override
public void drawSolid(GL2 gl) {
}
}

puis les méthodes liées aux nœuds. On notera les deux méthodes getNodesIndex, qui renvoie le tableau des indices des nœuds, et getNodes, qui renvoie un tableau des nœuds :

Classe PGEFace

public final void setNodes(int[] nodes) {
this.nodes = nodes;
this.nbNodes = this.nodes.length;
this.nodeNormals = new Vector3d[nbNodes];
}

public int[] getNodesIndex() {
return nodes;
}

public PGENode[] getNodes() {

PGENode[] temp = new PGENode[nbNodes];

for (int i = 0; i < nbNodes; i++) {
try {
temp[i] = ((PGEMesh) this.getParent()).getNode(this.nodes[i]);
} catch (Exception e) {
System.out.println("rang noeud: " + i);
e.printStackTrace();
}
}

return temp;
}

public int getNbNodes() {
return nbNodes;
}

et enfin les méthodes liées aux normales aux nœuds :

Classe PGEFace

public void setNodeNormal(int nodeRank, Vector3d normal) {
this.nodeNormals[nodeRank] = normal;
}

public void setDefaultNodeNormal(Vector3d normal) {
for (int i = 0; i < this.nbNodes; i++) {
this.nodeNormals[i] = normal;
}
}

public Vector3d getNodeNormal(int nodeRank) {
return this.nodeNormals[nodeRank];
}

Dans les méthodes setNodeNormal et getNodeNormal, le paramètre nodeRank est le rang du nœud que l'on vise au sein du tableau des indices de l'objet PGEFace, et non l'indice du nœud au sein de l'objet PGEMesh. Pour un quad, face à quatre nœuds, nodeRank peut donc prendre les valeurs 0, 1, 2 ou 3.

Revenons maintenant sur les méthodes d'affichage de l'objet PGEFace.
La méthode drawWire récupère les nœuds du contour de la face, et dessine le contour en mode GL_POLYGON.

Classe PGEFace

public void drawWire(GL2 gl) {

PGENode[] nd = this.getNodes();

gl.glBegin(GL2.GL_POLYGON);
gl.glColor3d(this.color[0], this.color[1], this.color[2]);

for (int i = 0; i < nd.length; i++) {
gl.glVertex3d(nd[i].getX(), nd[i].getY(), nd[i].getZ());
}

gl.glEnd();
}

La méthode drawSolid procède de même que drawWire. Les propriétés du matériau sont définies via glMaterialfv afin d'avoir le rendu souhaité.
Ici, le matériau est défini par :

- une couleur ambiante très sombre, quasiment noire. Elle permet de « voir » par contraste sur fond noir les faces non éclairées  ;

- une couleur diffuse définie par l'utilisateur (propriété de PGEObject, et donc de PGEFace) ;

- une couleur spéculaire blanche ;

- une brillance faible, pour rajouter un petit effet « plastique ».

Classe PGEFace

public void drawSolid(GL2 gl) {

PGENode[] nd = this.getNodes();

float[] mat_ambient = {0.05f, 0.05f, 0.05f, 1.0f};
float[] mat_diffuse = {this.colorDiffuse[0], this.colorDiffuse[1], this.colorDiffuse[2], 1.0f};
float[] mat_spec = {1.0f, 1.0f, 1.0f, 1f};
float[] mat_shini = {128f};

gl.glMaterialfv(GL2.GL_FRONT_AND_BACK, GL2.GL_AMBIENT, FloatBuffer.wrap(mat_ambient));
gl.glMaterialfv(GL2.GL_FRONT_AND_BACK, GL2.GL_DIFFUSE, FloatBuffer.wrap(mat_diffuse));
gl.glMaterialfv(GL2.GL_FRONT_AND_BACK, GL2.GL_SHININESS, FloatBuffer.wrap(mat_shini));
gl.glMaterialfv(GL2.GL_FRONT_AND_BACK, GL2.GL_SPECULAR, FloatBuffer.wrap(mat_spec));

gl.glBegin(GL2.GL_POLYGON);

for (int i = 0; i < nd.length; i++) {

gl.glNormal3d(this.nodeNormals[i].getX(), this.nodeNormals[i].getY(), this.nodeNormals[i].getZ());
gl.glVertex3d(nd[i].getX(), nd[i].getY(), nd[i].getZ());

}

gl.glEnd();
}

La couleur spéculaire et la brillance sont optionnelles. Nous les avons ajoutées ici par pur esthétisme.

La classe PGEQuad hérite de PGEFace sans modification autre que les couleurs d'affichage :

Classe PGEQuad

import object.IPGEObject;

public class PGEQuad extends PGEFace {

public PGEQuad(String id, IPGEObject parent, int[] nodes) {
super(id, parent, nodes);
this.setColor(0, 0, 1);
this.setColorDiffuse(0, 0, 1);
}
}

3-B-4 - Définir le maillage : la classe PGEMesh

Maintenant que nous avons nos nœuds et nos faces, il ne reste plus qu'à arranger l'ensemble pour former quelque chose de visuel. La classe PGEMesh définit le maillage en mettant en relation les nœuds et les faces.
PGEMesh étend PGEObject, et définit deux propriétés :

- une ArrayList objects, qui contiendra les faces composant le maillage ;

- une ArrayList nodes, qui contiendra les nœuds .

Les méthodes drawWire et drawSolid parcourent la liste des faces, et appellent leur méthode de dessin.

Les constructeurs de PGEMesh ne prennent pas d'objet parent en paramètre. On appellera donc explicitement la méthode setParent pour le définir.

Classe PGEMesh

public class PGEMesh extends PGEObject {

ArrayList<IPGEObject> objects = new ArrayList<IPGEObject>();
ArrayList<PGENode> nodes = new ArrayList<PGENode>();

public PGEMesh(IPGEObject[] obj, PGENode[] nd) {
super();
this.init();
this.objects.addAll(Arrays.asList(obj));
this.nodes.addAll(Arrays.asList(nd));
}

public PGEMesh(ArrayList<IPGEObject> obj, ArrayList<PGENode> nd) {
super();
this.init();
this.objects = obj;
this.nodes = nd;
}

public PGEMesh() {
super();
this.init();
}

@Override
public void drawWire(GL2 gl) {
int nbElements = this.objects.size();

for (int i = 0; i < nbElements; i++) {
this.objects.get(i).drawWire(gl);
}
}

@Override
public void drawSolid(GL2 gl) {
int nbElements = this.objects.size();

for (int i = 0; i < nbElements; i++) {
this.objects.get(i).drawSolid(gl);
}
}
}

La méthode init initialise les listes des faces et des nœuds.
Les méthodes addNode et setNode permettent soit d'ajouter des nœuds à la liste (unitaire ou par lot), soit d'affecter la liste complète. La méthode getNode permet quant à elle de récupérer le nœud d'indice souhaité.

Classe PGEMesh

public final void init() {
this.objects.clear();
this.nodes.clear();
}

public void addNode(PGENode nd) {
this.nodes.add(nd);
}

public void addNode(PGENode[] nd) {
this.nodes.addAll(Arrays.asList(nd));
}

public void setNode(ArrayList<PGENode> nd) {
this.nodes = nd;
}

public PGENode getNode(int rank) {
return this.nodes.get(rank);
}

Les mêmes méthodes existent pour les faces.
La méthode listObjects affiche la liste de toutes les faces sur la sortie standard.

Classe PGEMesh

public void addObject(IPGEObject obj) {
this.objects.add(obj);
}

public void addObject(IPGEObject[] obj) {
this.objects.addAll(Arrays.asList(obj));
}

public void setObject(ArrayList<IPGEObject> obj) {
this.objects = obj;
}

public IPGEObject getObject(int rank) {
return this.objects.get(rank);
}

public void listObjects() {
for (int i = 0; i < this.objects.size(); i++) {
System.out.println("   object " + i + ":" + this.objects.get(i).getClass());

}
}

On notera que la liste des faces de PGEMesh est en fait définie comme pouvant contenir des IPGEObject. Rien n'interdit donc d'y placer autre chose que des PGEFace.

3-B-5 - Définir le tore : la classe PGETorus

Maintenant que nous avons toutes les briques, passons à la définition de notre tore.

Pour le définir, nous allons créer la classe PGETorus, étendant PGEMesh.
Notre tore est caractérisé géométriquement par les paramètres suivants, que nous retrouvons en propriétés de la classe :

- le rayon externe du tore, externalRadius ;

- le rayon du cercle de base, radius ;

- le nombre de sections de l'extrusion circulaire, nbCircunfPts ;

- le nombre de points discrétisant le cercle de base, nbSectionPts.

Les couleurs sont initialisées au bleu.
Pour la définition du maillage, initialisée par la méthode initMesh, nous utilisons la méthodologie présentée au §3.B.1.

Les nœuds sont tout d'abord créés, consécutivement pour chaque cercle situé à chaque pas de l'extrusion.
Puis les quads sont créés en reliant les points correspondants.
Pour chaque nœud, le vecteur normal à la surface est calculé. Lors de la définition du quad, on affecte à chaque sommet le vecteur normal correspondant. Cette méthode permet d'avoir un effet « lissé » pour le rendu solide.

Classe PGETorus

public class PGETorus extends PGEMesh {

double externalRadius;
double radius;
int nbCircunfPts;
int nbSectionPts;

public PGETorus(IPGEObject parent, double externalRadius, double radius, int nbCircunfPts, int nbSectionPts) {

super();

this.parent = parent;
this.externalRadius = externalRadius;
this.radius = radius;
this.nbCircunfPts = nbCircunfPts;
this.nbSectionPts = nbSectionPts;

this.initColor();
this.initMesh();
}

private void initColor() {
this.setColor(Color.BLUE);
this.setColorDiffuse(Color.BLUE);
}

public final void initMesh() {

PGENode[] nodesLocal = new PGENode[nbCircunfPts * nbSectionPts];
PGEQuad[] elements = new PGEQuad[nbCircunfPts * nbSectionPts];

Vector3d[] normal = new Vector3d[nbCircunfPts * nbSectionPts];

// définition des noeuds

for (int i = 0; i < nbCircunfPts; i++) {

double angleCircunf = i * 2 * Math.PI / nbCircunfPts;

for (int j = 0; j < nbSectionPts; j++) {

double angleSection = j * 2 * Math.PI / nbSectionPts;
double rayon = (this.externalRadius - this.radius) + this.radius * Math.cos(angleSection);
double x = rayon * Math.cos(angleCircunf);
double y = rayon * Math.sin(angleCircunf);
double z = this.radius * Math.sin(angleSection);

nodesLocal[i * nbSectionPts + j] = new PGENode("N" + (i * nbSectionPts + j + 1), this, x, y, z);

double xNorm = Math.cos(angleSection) * Math.cos(angleCircunf);
double yNorm = Math.cos(angleSection) * Math.sin(angleCircunf);
double zNorm = Math.sin(angleSection);

normal[i * nbSectionPts + j] = new Vector3d(xNorm, yNorm, zNorm);
}
}

// définition des faces/quads

for (int i = 0; i < nbCircunfPts; i++) {

for (int j = 0; j < nbSectionPts; j++) {

int[] nd = new int[4];
nd[0] = i * nbSectionPts + j;
nd[1] = i * nbSectionPts + j + 1;

if (i < (nbCircunfPts - 1)) {
nd[2] = (i + 1) * nbSectionPts + j + 1;
nd[3] = (i + 1) * nbSectionPts + j;
} else {
nd[2] = j + 1;
nd[3] = j;
}

if (j == (nbSectionPts - 1)) {
nd[1] = nd[1] - nbSectionPts;
nd[2] = nd[2] - nbSectionPts;
}

elements[i * nbSectionPts + j] = new PGEQuad("Q" + (i * nbSectionPts + j + 1), this, nd);
elements[i * nbSectionPts + j].setParent(this);

for (int k = 0; k < 4; k++) {
elements[i * nbSectionPts + j].setNodeNormal(k, normal[nd[k]]);
}

elements[i * nbSectionPts + j].setColor(this.color[0], this.color[1], this.color[2]);
elements[i * nbSectionPts + j].setColorDiffuse(this.colorDiffuse[0], this.colorDiffuse[1], this.colorDiffuse[2]);
}
}

this.addObject(elements);
this.addNode(nodesLocal);
}
}

Le résultat obtenu est le suivant :

Rendu « fil de fer »

Rendu « solide »

3-C - Dupliquer et localiser les objets

Nous savons maintenant créer un objet, et l'afficher. Mais cela peut devenir laborieux si pour chaque objet, il nous faut définir une classe spécifique et définir les méthodes d'affichage pour qu'il apparaisse au bon endroit.

La classe PGELocalizedObject répond à ce besoin, en permettant de positionner et d'orienter un IPGEObject sans avoir à retoucher la classe définissant cet objet.

Comme tout objet graphique, elle étend PGEObject.
Elle a pour propriétés :

- l'objet à positionner, de type IPGEObject ;

- la position de l'objet, de type Point3d ;

- l'orientation de l'objet, donné par un Quaternion.

Les méthodes drawWire et drawSolid appellent la méthode d'affichage de l'objet, après avoir translaté l'origine de la scène 3D à la position souhaitée de l'objet par un appel à glTranslated, et orienté la scène 3D comme défini par le quaternion par un appel à glRotated (après avoir converti le quaternion en un axe et un angle de rotation). La matrice MODELVIEW est auparavant sauvegardée via un appel à glPushMatrix, et est réinitialisée finalement via un appel à glPopMatrix.

glRotated nécessite un angle en degrés, alors que notre classe Quaternion utilise des angles en radians.

Les getters et setters permettent d'affecter et de récupérer la localisation et l'orientation de l'objet.

Classe PGELocalizedObject

public class PGELocalizedObject extends PGEObject {

private IPGEObject object;
private Point3d loc;
private Quaternion quat;

public PGELocalizedObject(IPGEObject object, Point3d loc, Quaternion quat) {
super();
this.object = object;
this.loc = loc;
this.quat = quat;

this.setId(this.object.getId());
}

public PGELocalizedObject(IPGEObject object, Point3d loc) {
this(object, loc, null);
this.setId(this.object.getId());
}

@Override
public void drawWire(GL2 gl) {
gl.glPushMatrix();
gl.glTranslated(this.loc.getX(), this.loc.getY(), this.loc.getZ());
if (this.quat != null) {
Vector3d vect = this.quat.getAxisAngle();
gl.glRotated(vect.getAngle() / Math.PI * 180., vect.getX(), vect.getY(), vect.getZ());
}
this.object.drawWire(gl);
gl.glPopMatrix();
}

@Override
public void drawSolid(GL2 gl) {
gl.glPushMatrix();
gl.glTranslated(this.loc.getX(), this.loc.getY(), this.loc.getZ());
if (this.quat != null) {
Vector3d vect = this.quat.getAxisAngle();
gl.glRotated(vect.getAngle() / Math.PI * 180., vect.getX(), vect.getY(), vect.getZ());
}
this.object.drawSolid(gl);
gl.glPopMatrix();
}

public Point3d getLoc() {
return loc;
}

public void setLoc(Point3d loc) {
this.loc = loc;
}

public Quaternion getQuat() {
return quat;
}

public void setQuat(Quaternion quat) {
this.quat = quat;
}
}

La théière dupliquée, déplacée et orientée

3-D - Assembler plusieurs objets

La dernière classe graphique que nous allons définir dans cet article est la classe PGEAssembly.
Elle va nous permettre d'assembler plusieurs des objets précédemment définis au sein de la scène, au sein d'un groupe que nous appellerons un « assemblage ».

Les principales méthodes de la classe sont les différentes versions de addPart, permettant d'ajouter un objet à l'assemblage en passant en paramètre(s) :

- un objet localisé ;

- un objet, une localisation et une orientation ;

- un objet, et une localisation ;

- un objet, et ses coordonnées de localisation.

Les méthodes drawWire et drawSolid appellent les méthodes d'affichage des objets contenus dans l'assemblage.

Classe PGEAssembly

public class PGEAssembly extends PGEObject {

public PGEAssembly(IPGEObject parent) {
super(parent);
}

public void addPart(PGELocalizedObject object) {
this.addChildren(object);
}

public void addPart(IPGEObject object, Point3d loc, Quaternion quat) {
this.addChildren(new PGELocalizedObject(object, loc, quat));
}

public void addPart(IPGEObject object, Point3d loc) {
this.addChildren(new PGELocalizedObject(object, loc));
}

public void addPart(IPGEObject object, double x, double y, double z) {
this.addChildren(new PGELocalizedObject(object, new Point3d(x, y, z)));
}

@Override
public void drawWire(GL2 gl) {

IPGEObject[] objects = this.getChildren();
int nbObj = objects.length;

for (int i = 0; i < nbObj; i++) {
((PGELocalizedObject) objects[i]).drawWire(gl);
}
}

@Override
public void drawSolid(GL2 gl) {

IPGEObject[] objects = this.getChildren();
int nbObj = objects.length;

for (int i = 0; i < nbObj; i++) {
((PGELocalizedObject) objects[i]).drawSolid(gl);
}
}
}

PGEAssembly n'assemble que des objets PGELocalizedObject, soit définis directement, soit convertis par une des méthodes addPart.

Un assemblage de la théière et du tore

4 - La gestion de nos objets

4-A - Une bibliothèque pour ranger nos objets

La fenêtre PGE que nous avons créée dans le précédent article ne permet l'affichage que d'un objet à la fois. Afin de profiter de tous les objets que nous avons créés dans cet article sans avoir à compiler le code à chaque changement, nous allons programmer un objet qui regroupera et gèrera notre collection d'objets.

La classe PGERoot va gérer notre collection d'objets grâce à un ArrayList qui stocke les objets. Un seul parmi ces objets sera l'objet courant, et sera affiché dans la fenêtre PGE. Elle va également gérer le style d'affichage de la scène (« fil de fer » ou « solide »). Enfin, elle va aussi gérer le mode de fonctionnement de l'affichage, qui sera ici en pipeline direct avec display list (nous utiliserons les VBO dans un article ultérieur).

On définit tout d'abord les différentes propriétés de la classe ainsi que son constructeur :

Classe PGERoot

public class PGERoot {

private ArrayList<IPGEObject> children;     // liste d'objets
private IPGEObject activeChild;             // objet actif
private PgePanel activePanel;               // PGEPanel actif
private String projectName;                 // nom du projet
private int drawingMode;                    // mode d'affichage des objets, "fil de fer" ou "solide"
private boolean updateDisplayList;          // flag sur la nécessité de mettre à jour la display list
private int displayListIndex = 0;           // index de la display list utilisée
private int GLdrawingMode;                  // mode d'affichage OpenGL: pipeline direct, VBO...

static public int WIRE = 1;                 // constante à utiliser avec drawingMode: affichage "fil de fer"
static public int SOLID = 2;                // constante à utiliser avec drawingMode: affichage "solide"

static public int DIRECT_DRAW = 10;         // constante à utiliser avec GLdrawingMode: pipeline direct + display list

public PGERoot() {
this.children=new ArrayList<IPGEObject>();
this.activeChild = null;
this.drawingMode=WIRE;
this.projectName="New Project";

this.updateDisplayList = true;
}
}

Les méthodes liées aux objets permettent :

- d'ajouter un objet ou un tableau d'objets ;

- de récupérer la liste des objets sous forme de tableau ;

- de récupérer un objet par son nom, ou par son rang dans la liste ;

- de récupérer le nombre d'objets dans la liste.

Méthodes liées aux objets

public void addChild(IPGEObject child) {
this.children.add(child);
this.listChildren();
this.setDefaultActiveChild();
}

public void addChildren(IPGEObject[] child) {
this.children.addAll(Arrays.asList(child));
this.setDefaultActiveChild();
}

public IPGEObject[] getChildren() {
this.children.trimToSize();
int nbChildren = this.children.size();

IPGEObject[] result = new IPGEObject[nbChildren];

for (int i = 0; i < nbChildren; i++) {
result[i] = this.children.get(i);
}

return result;
}

public IPGEObject getChild(String idObject) {

int nbObject = this.children.size();

for (int i = 0; i < nbObject; i++) {
IPGEObject temp = this.children.get(i);
if (idObject.equals(temp.getId())) {
return temp;
}
}
return null;
}

public IPGEObject getChild(int rank) {

int nbObject = this.children.size();
if ((rank >= 0) && (rank < nbObject)) {
return this.children.get(rank);
} else {
return null;
}
}

public int getNbChildren() {
return this.children.size();
}

Les méthodes gérant l'objet actif sont les suivantes :

- définir l'objet actif, soit par l'objet lui-même, soit par son rang dans la liste des objets ;

- récupérer l'objet actif ;

- définir un objet actif par défaut. La méthode active le premier objet de la liste.

Méthodes liées à l'objet actif

public void setActiveChild(IPGEObject obj) {
this.activeChild = obj;
this.updateDisplayList = true;

if (this.activePanel != null) {
this.activePanel.display();
}
}

public void setActiveChild(int rank) {
IPGEObject child = this.getChild(rank);
if (child != null) {
this.setActiveChild(child);
}
}

public IPGEObject getActiveChild() {
return this.activeChild;
}

public void setDefaultActiveChild() {
if ((this.activeChild == null) && (this.getNbChildren() > 0)) {
this.setActiveChild(0);
}
}

La méthode listChildren permet d'afficher sur la sortie console la liste des objets, l'objet actif étant indiqué par les caractères « *** » :

Liste des objets

public void listChildren() {
for (int i = 0; i < this.getNbChildren(); i++) {
IPGEObject temp = this.getChild(i);
System.out.print("rank " + i + ": " + temp.getId());

if (temp == this.activeChild) {
System.out.println(" ***");
} else {
System.out.println("");
}
}
}

Les méthodes gérant le PGEPanel actif permettent de le définir et de le récupérer :

Méthodes liées au PGEPanel actif

public PgePanel getActivePanel() {
return activePanel;
}

public void setActivePanel(PgePanel activePanel) {
this.activePanel = activePanel;
}

Les getter et setter liés à la propriété projectName sont définis :

getter/setter projectName

public String getProjectName() {
return projectName;
}

public void setProjectName(String projectName) {
this.projectName = projectName;
}

Ainsi que pour les propriétés drawingMode et GldrawingMode :

getters/setters modes d'affichage

public int getDrawingMode() {
return drawingMode;
}

public void setDrawingMode(int drawingMode) {

this.drawingMode = drawingMode;

if (this.glDrawingMode==DIRECT_DRAW) {
this.forceDisplayListUpdate();
}
}

public int getGLdrawingMode() {
return glDrawingMode;
}

public void setGLdrawingMode(int GLdrawingMode) {
this.glDrawingMode = GLdrawingMode;
}

public void forceDisplayListUpdate() {
this.updateDisplayList=true;
}

Le setter setDrawingMode appelle la méthode forceDisplayListUpdate en cas de mode d'affichage OpenGL en pipeline direct, afin de remettre à jour la display list d'affichage de la scène. Cette méthode change uniquement l'état de la propriété updateDisplayList en la passant à true. La mise à jour de la display list est ensuite faite lors de l'appel à la méthode draw.

Celle-ci ne gère pour le moment que le mode d'affichage OpenGL en pipeline direct. Elle vérifie si une mise à jour de la display list a été demandée. Si oui, elle efface la display list courante, si elle existe, puis en crée une nouvelle, en mode GL_COMPILE. Elle appelle ensuite la méthode d'affichage de l'objet courant suivant le mode d'affichage souhaité (« fil de fer » ou « solide »). La display list est ensuite appelée pour mettre à jour l'affichage.

Méthode draw

public void draw(GL2 gl) {

if (this.glDrawingMode == PGERoot.DIRECT_DRAW) {

if (this.updateDisplayList) {

if (this.displayListIndex > 0) {
gl.glDeleteLists(this.displayListIndex, 1);
}

this.displayListIndex = gl.glGenLists(1);
gl.glNewList(this.displayListIndex, GL2.GL_COMPILE);

if (this.drawingMode == WIRE) {
this.activeChild.drawWire(gl);
} else if (this.drawingMode == SOLID) {
this.activeChild.drawSolid(gl);
}

gl.glEndList();
this.updateDisplayList = false;
}

gl.glCallList(this.displayListIndex);

} else {
System.out.println("GL drawing mode is not defined !!!");
}
}

Nous avons maintenant un « super objet » nous permettant de gérer nos différents objets, et de naviguer parmi eux afin d'afficher celui que l'on souhaite.

4-B - Un gestionnaire graphique de bibliothèque

L'objet PGERoot peut être géré par du code, mais nous allons intégrer à notre fenêtre un composant graphique afin de naviguer visuellement parmi notre bibliothèque d'éléments. Ce composant sera simplement composé d'un JPanel intégrant un JTree, chaque feuille du JTree correspondant à un des objets présents dans la bibliothèque.

Ce composant est défini dans la classe RootTree, qui étend JPanel et implémente TreeSelectionListener afin de gérer les évènements liés à la sélection du l'arbre. Elle possède deux propriétés :

- le JTree contenu dans le JPanel ;

- une référence vers le PGERoot de l'application.

Le constructeur définit un arbre, contenu dans un JScrollPane placé dans notre composant. Il crée la racine de l'arbre et lui affecte le nom du projet. Il attache ensuite à la racine les feuilles correspondant à la liste des objets du PGERoot.

Classe RootTree

public class RootTree extends JPanel implements TreeSelectionListener {

private JTree tree;
private PGERoot root;

public RootTree(PGERoot root) {
super();
DefaultMutableTreeNode dmtn = new DefaultMutableTreeNode(root.getProjectName());
this.root = root;

this.setLayout(new BorderLayout());

createNodes(dmtn);
tree = new JTree(dmtn);
tree.addTreeSelectionListener(this);

JScrollPane treeView = new JScrollPane();
treeView.setViewportView(tree);
this.add(treeView,BorderLayout.CENTER);
}
}

Les feuilles de l'arbre sont générées par l'appel à la méthode createNodes. Pour chaque objet du PGERoot, elle ajoute à la racine de l'arbre une feuille correspondant à l'objet.

Création des feuilles

private void createNodes(DefaultMutableTreeNode dmtn) {
IPGEObject[] children = this.root.getChildren();

for (int i = 0; i < children.length; i++) {
dmtn.add(this.getLeaf(children[i]));
}
}

La feuille correspondant à chaque objet est définie par l'appel à la méthode getLeaf :

Création de la feuille d'un objet

private DefaultMutableTreeNode getLeaf(IPGEObject obj) {
DefaultMutableTreeNode leaf=new DefaultMutableTreeNode(obj);

IPGEObject[] children=obj.getChildren();

for (int i = 0; i < children.length; i++) {
leaf.add(this.getLeaf(children[i]));
}

return leaf;
}

Celle-ci crée une feuille, portant le nom de l'objet, l'appel du constructeur DefaultMutableTreeNode avec un objet en paramètre appelant la méthode toString de l'objet pour nommer la feuille (ce qui explique la redéfinition de la méthode toString de l'objet PGEObject dont héritent tous nos composants graphiques, et qui renvoie l'identité de l'objet).

Au niveau du TreeSelectionListener, la méthode valueChanged est renseignée afin de gérer le changement de sélection dans le JTree :

Changement de sélection JTree

@Override
public void valueChanged(TreeSelectionEvent e) {
DefaultMutableTreeNode node = (DefaultMutableTreeNode) tree.getLastSelectedPathComponent();

if (node == null) {
return;
}

Object nodeObject = node.getUserObject();

if (nodeObject instanceof IPGEObject) {
IPGEObject obj = (IPGEObject) nodeObject;

this.root.setActiveChild(obj);
}
}

La méthode récupère l'objet ayant déclenché l'évènement et contrôle si c'est un IPGEObject. Si tel est le cas, elle le définit en tant qu'objet actif dans le PGERoot.

Il ne nous reste plus qu'à intégrer ce composant dans notre fenêtre.

5 - Améliorations des classes Main et PGEPanel

5-A - Interaction au clavier avec la caméra

Nous allons ajouter un petit gestionnaire d'interaction par le clavier avec notre fenêtre PGE. Cela nous permettra d'interagir facilement avec le gestionnaire de bibliothèque et avec la fenêtre PGE, et également de lancer quelques procédures utiles, comme une méthode effectuant une impression écran de la fenêtre graphique.

Dans l'article précédent, nous avons défini un gestionnaire d'interaction avec la souris, avec la classe MouseCameraManager. Pour le clavier, nous allons créer la classe KeyCameraManager. Celle-ci implémente l'interface KeyListener afin de gérer les interactions clavier, et possède comme propriétés le PGEPanel auquel elle se rattache, et la Camera qu'elle va gérer :

Classe KeyCameraManager

public class KeyCameraManager implements KeyListener {

private PgePanel panel;
private Camera camera;

public KeyCameraManager(PgePanel pan) {
this.panel = pan;
this.updateData();
}

public final void updateData() {
this.camera = panel.getCamera();
}
}

Les évènements sont ensuite programmés :

Évènements KeyListener

@Override
public void keyPressed(KeyEvent e) {

if (e.getKeyChar() == 'x') {
this.camera.rotateX(1);
} else if (e.getKeyChar() == 'y') {
this.camera.rotateY(1);
} else if (e.getKeyChar() == 't') {
this.camera.translateX(0.1);
} else if (e.getKeyChar() == 'T') {
this.camera.translateX(-0.1);
} else if ((e.getKeyChar() >= '0') && (e.getKeyChar() <= '9')) {
this.panel.getRoot().setActiveChild(Integer.parseInt("" + e.getKeyChar()));
} else if (e.getKeyChar() == 'q') {
this.panel.getRoot().setDrawingMode(PGERoot.SOLID);
} else if (e.getKeyChar() == 'w') {
this.panel.getRoot().setDrawingMode(PGERoot.WIRE);
} else if (e.getKeyChar() == 'p') {
this.takeScreenshot();
} else {
System.out.println("touche "+e.getKeyChar()+" non définie");
}

this.panel.display();
}

@Override
public void keyReleased(KeyEvent e) {
// not used
}

@Override
public void keyTyped(KeyEvent e) {
// not used
}

Seul l'évènement keyPressed est utilisé ici, mais keyReleased et keyTyped peuvent être utilisés selon les besoins.

Le code définit quelques comportements en fonction de la touche pressée :

- touche « x » :

- touche « y » : rotation autour de l'axe Oy de un degré ;

- touche « t » et « T » : translation suivant Ox d'une unité respectivement dans le sens positif et dans le sens négatif ;

- touches numériques (0 à 9) : activation de l'objet au rang de la touche pressée ;

- touche « q » : passage en affichage « solide » ;

- touche « w » : passage en affichage « fil de fer » ;

- touche « p » : impression écran de la zone graphique, et enregistrement dans un fichier image.

Si la touche pressée n'est pas gérée, un message est affiché pour l'indiquer

La méthode getRoot utilisée dans ce code n'est pas définie dans la classe PGEPanel du précédent article. Elle fait partie des améliorations présentées dans le chapitre suivant.

L'impression écran est programmée dans la méthode takeScreenshot :

Impression écran

private void takeScreenshot() {

JFileChooser fc = new JFileChooser();

int returnVal = fc.showOpenDialog(this.panel);

if (returnVal == JFileChooser.APPROVE_OPTION) {
try {
File fichier = fc.getSelectedFile();
String nomFichier = fichier.getName().toLowerCase();

if ((nomFichier.endsWith("png")) || (nomFichier.endsWith("jpg"))) {

try {
this.panel.getContext().makeCurrent();

Screenshot.writeToFile(fichier, this.panel.getWidth(), this.panel.getHeight());

System.out.println("Enregistrement impression écran ok");
System.out.println("Fichier: " + fichier.getAbsolutePath());
} catch (GLException ex) {
ex.printStackTrace();
} finally {
this.panel.getContext().release();
}
} else {
JOptionPane.showMessageDialog(this.panel, "Veuillez préciser l'extension du fichier image (png ou jpg)",
"Extension fichier manquante", JOptionPane.ERROR_MESSAGE);
}
} catch (IOException ex) {
ex.printStackTrace();
}
}

}

Cette méthode affiche un JFileChooser afin de sélectionner le fichier où enregistrer l'image. Celle-ci doit être de type png ou jpg, sous peine de voir s'afficher une boite de dialogue d'erreur. La méthode appelle ensuite la méthode writeToFile de la classe Screenshot.

Un contexte OpenGL doit être activé avant l'appel à writeToFile sous peine de déclencher une GLException.

L'activation et la désactivation du contexte OpenGL sont assurées par les appels :

this.panel.getContext().makeCurrent();

et :

this.panel.getContext().release();

La méthode release est placée dans le bloc finally afin de débloquer le contexte OpenGL et ne pas bloquer l'affichage en cas d'exception.

Le format png est conseillé pour l'affichage en mode « fil de fer », et le format jpg pour le mode « solide ».

5-B - Modifications de la classe PGEPanel

De nouvelles propriétés sont ajoutées à la classe définie dans le précédent article :

private KeyCameraManager kcm;       // manager interaction clavier
private PGERoot root;               // PGERoot associé
private double fovy;                // "field of view" perspective suivant y
private double ratio;               // ratio perspective, W/H

Le constructeur et les méthodes d'initialisation sont modifiés afin d'intégrer ces nouvelles propriétés :

Classe PGEPanel

public PgePanel(GLCapabilitiesImmutable caps, PGERoot root) {
super(caps);
this.initComponents();

this.root = root;
this.root.setActivePanel(this);

this.fovy=45.0;
}

private void initComponents() {

this.addGLEventListener(this);

this.mcm = new MouseCameraManager(this);
this.addMouseListener(this.mcm);
this.addMouseMotionListener(this.mcm);

this.kcm = new KeyCameraManager(this);
this.addKeyListener(this.kcm);

this.setFocusable(true);

}

@Override
public void init(GLAutoDrawable arg0) {

System.out.println("panel init");

GL gl = arg0.getGL();
gl.setSwapInterval(1);
gl.getContext().makeCurrent();

Point3d loc = new Point3d(-1, -1, 1);
Point3d targ = new Point3d(0, 0, 0);
Vector3d up = new Vector3d(0, 0, 1);

Point3d rotCenter = new Point3d(0, 0, 0);

if (this.camera == null) {
this.camera = new Camera(loc, targ, up, rotCenter);
}

this.mcm.updateData();

this.kcm.updateData();

this.root.setGLdrawingMode(PGERoot.DIRECT_DRAW);

}

La méthode display est retouchée afin de prendre en compte toutes les améliorations de cet article :

Méthode display

@Override
public void display(GLAutoDrawable arg0) {

GL2 gl = arg0.getGL().getGL2();
GLU glu = new GLU();

int winAW = this.getWidth();
int winAH = this.getHeight();

gl.glViewport(0, 0, winAW, winAH);
gl.glMatrixMode(GL2.GL_PROJECTION);
gl.glLoadIdentity();

this.ratio = (double) winAW / (double) winAH;
glu.gluPerspective(this.fovy, this.ratio, 0.1, 1000.0);

gl.glMatrixMode(GL2.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glLoadMatrixf(this.camera.getCameraMatrix());
gl.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
gl.glClear(GL2.GL_COLOR_BUFFER_BIT | GL2.GL_DEPTH_BUFFER_BIT);

gl.glDisable(GL2.GL_LIGHTING);
gl.glDisable(GL2.GL_LIGHT0);
gl.glDepthFunc(GL.GL_LEQUAL);
gl.glEnable(GL2.GL_DEPTH_TEST);
gl.glDisable(GL2.GL_TEXTURE_2D);
gl.glDisable(GL2.GL_COLOR_MATERIAL);

// dessin du repère global de la scène
this.coord.draw(gl);

// affichage de la scène
if (this.root.getDrawingMode() == PGERoot.SOLID) {

float[] light_position = {5.0f, 5.0f, 5.0f, 0.0f};
float[] blancAmbient = {1.0f, 1.0f, 1.0f, 0.2f};
float[] blanc = {1.0f, 1.0f, 1.0f, 1.0f};

gl.glEnable(GL2.GL_TEXTURE_2D);

// définition de la lumière
gl.glLightfv(GL2.GL_LIGHT0, GL2.GL_POSITION, FloatBuffer.wrap(light_position));
gl.glLightfv(GL2.GL_LIGHT0, GL2.GL_AMBIENT, FloatBuffer.wrap(blancAmbient));
gl.glLightfv(GL2.GL_LIGHT0, GL2.GL_DIFFUSE, FloatBuffer.wrap(blanc));

gl.glEnable(GL2.GL_LIGHTING);
gl.glEnable(GL2.GL_LIGHT0);
gl.glEnable(GL2.GL_AUTO_NORMAL);
gl.glEnable(GL2.GL_NORMALIZE);

// mode d'affichage "solide"
gl.glPolygonMode(GL2.GL_FRONT_AND_BACK, GL2.GL_FILL);

} else if (this.root.getDrawingMode() == PGERoot.WIRE) {

gl.glDisable(GL2.GL_LIGHTING);
gl.glDisable(GL2.GL_LIGHT0);
gl.glDisable(GL2.GL_AUTO_NORMAL);
gl.glDisable(GL2.GL_NORMALIZE);

// mode d'affichage "fil de fer"
gl.glPolygonMode(GL2.GL_FRONT_AND_BACK, GL2.GL_LINE);

}

this.root.draw(gl);

}

Celle-ci gère donc les deux méthodes d'affichage, en « solide » et en « fil de fer ». Suivant l'option choisie, elle initialise les paramètres OpenGL, puis appelle la méthode draw du PGERoot.

La méthode reshape est modifiée pour appeler une mise à jour de la display list du PGERoot en cas de redimensionnement de la fenêtre :

Méthode reshape

@Override
public void reshape(GLAutoDrawable arg0, int arg1, int arg2, int arg3, int arg4) {
this.root.forceDisplayListUpdate();
}

Le contexte OpenGL étant réinitialisé par le redimensionnement de la fenêtre, et donc la display list d'affichage, un simple appel à repaint ne redessine pas l'affichage. Il est nécessaire de recréer la display list.

Les getters/setters des nouvelles propriétés sont également définis (sauf pour le KeyCameraManager pour lequel ce n'est pas nécessaire) :

getters/setters

public PGERoot getRoot() {
return this.root;
}

public double getFovyAngle() {
return this.fovy;
}

public void setFovyAngle(double fovyAngle) {
this.fovy=fovyAngle;
}

public double getScreenRatio() {
return this.ratio;
}

5-C - Modifications de la main class

La main class de notre application est inspirée de celle de l'article précédent. On intègre notre nouveau PGEPanel, accompagné du RootPanel pour gérer nos objets. Celui-ci contient tous les objets présentés dans cet article… voire même un petit bonus disponible dans les sources de l'article !

Main class

public class Main implements EventListener {

public static void main(String[] args) {

//Initialisation JOGL
GLProfile glp=GLProfile.get(GLProfile.GL2); // profil Opengl Desktop 1.x à 3.0
GLProfile.initSingleton(true);
GLCapabilities caps=new GLCapabilities(glp);

String osName = System.getProperty("os.name");
String osVersion = System.getProperty("os.version");
String userName = System.getProperty("user.name");
String javaVersion = System.getProperty("java.version");

System.out.println("User: " + userName);
System.out.println("Operating_System: " + osName + ", version: " + osVersion);
System.out.println("Version_Java: " + javaVersion);
System.out.println("");

Frame frame = new Frame("PGE - Article 3");
PGERoot root = new PGERoot();
final PgePanel pane = new PgePanel(caps,root);

root.setGLdrawingMode(PGERoot.DIRECT_DRAW);

// objet tore

PGETorus torus = new PGETorus(null, 0.6, 0.1, 36, 36);
torus.setId("Torus");

// objets théière - jaune, rouge et bleue

PGETeapot teapot = new PGETeapot(null, 0.4);
teapot.setId("Teapot");

PGETeapot teapot2 = new PGETeapot(null, 0.4);
teapot2.setColorDiffuse(Color.RED);
teapot2.setId("Red teapot");

PGETeapot teapot3 = new PGETeapot(null, 0.4);
teapot3.setColorDiffuse(Color.BLUE);
teapot3.setId("Blue teapot");

// assemblage théières
PGEAssembly ass = new PGEAssembly(null);
ass.addPart(teapot, 0.5, 0.5, -0.5);
ass.addPart(teapot2, 0.5, -0.5, -0.5);
ass.addPart(teapot, -0.5, 0.5, -0.5);
ass.addPart(teapot, -0.5, -0.5, -0.5);
ass.addPart(teapot, 0.5, 0.5, 0.5);
ass.addPart(teapot3, 0.5, -0.5, 0.5);
ass.addPart(teapot, -0.5, 0.5, 0.5);
ass.addPart(teapot, -0.5, -0.5, 0.5);
ass.setId("Assy-Teapots");

// assemblage théières localisées

PGEAssembly assLoc = new PGEAssembly(null);
assLoc.addPart(teapot,0,0,0);

Quaternion quat1=new Quaternion();
quat1.FromAxis(new Vector3d(0, 1, 0), 60.0/180*Math.PI);

assLoc.addPart(new PGELocalizedObject(teapot, new Point3d(1, 0,0), quat1));

Quaternion quat2=new Quaternion();
quat2.FromAxis(new Vector3d(0, 1, 0), 120.0/180*Math.PI);
assLoc.addPart(new PGELocalizedObject(teapot, new Point3d(2, 0,0), quat2));

Quaternion quat3=new Quaternion();
quat3.FromAxis(new Vector3d(0, 1, 0), 180.0/180*Math.PI);
assLoc.addPart(new PGELocalizedObject(teapot, new Point3d(3, 0,0), quat3));

assLoc.setId("Assy-Localized");

// assemblage théière/tore

PGEAssembly assTest = new PGEAssembly(null);

assTest.addPart(teapot,0,0,0);

Point3d locTest=new Point3d(1.5,0,0);
Quaternion quatTest=new Quaternion();
quatTest.FromAxis(new Vector3d(1, 0, 0), -Math.PI/180.*20);

assTest.addPart(torus,locTest,quatTest);

assTest.setId("Assy-teapot-torus");

// objet texturé - /!\ Bonus sources /!\

PGEEarth earth=new PGEEarth();

// définition arbre des objets à afficher

root.addChild(torus);
root.addChild(teapot);
root.addChild(ass);
root.addChild(assLoc);
root.addChild(assTest);
root.addChild(earth);

RootTree tree = new RootTree(root);
tree.setPreferredSize(new Dimension(200,500));

// définition de la fenêtre

frame.add(tree, BorderLayout.WEST);

pane.setVisible(true);
pane.setFocusable(true);
pane.setPreferredSize(new Dimension(500, 500));
frame.add(pane, BorderLayout.CENTER);

frame.addWindowListener(new WindowAdapter() {

@Override
public void windowClosing(WindowEvent e) {
System.out.println("fermeture fenêtre");
System.exit(0);
}

});

frame.setSize(800, 1000);

frame.pack();

frame.setVisible(true);

}

}

Il ne reste plus qu'à lancer l'application, et à naviguer entre les différents objets, et interagir avec eux grâce à la souris et au clavier.

La fenêtre de notre application

Le bonus inclus dans les sources de l'article

6 - Conclusion

Nous avons maintenant la possibilité de créer et d'intégrer des objets dans notre monde 3D. Nous pouvons également naviguer parmi ces objets, et interagir avec la fenêtre 3D.

Les sources de cet article sont disponibles en suivant ce lien : src_PGE_article_3.zip.

Vous pouvez également regarder la vidéo de démonstration sur Vimeo pour voir le résultat de cet article.

7 - Remerciements

Merci à ClaudeLELOUP pour sa relecture orthographique.