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 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 :
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) :
@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.
/**
* 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.0
f;
this
.color[1
] =
rgb.getGreen
(
) /
255.0
f;
this
.color[2
] =
rgb.getBlue
(
) /
255.0
f;
}
/**
* 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.0
f;
this
.colorDiffuse[1
] =
rgb.getGreen
(
) /
255.0
f;
this
.colorDiffuse[2
] =
rgb.getBlue
(
) /
255.0
f;
}
/**
* 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.
@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 :
/**
* 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 :
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 :
/**
* 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 :
@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 :
@Override
public
void
drawSolid
(
GL2 gl) {
float
[] mat_diffuse =
{
this
.colorDiffuse[0
], this
.colorDiffuse[1
], this
.colorDiffuse[2
], 1.0
f}
;
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 :
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.
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 :
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 :
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 :
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.
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 ».
public
void
drawSolid
(
GL2 gl) {
PGENode[] nd =
this
.getNodes
(
);
float
[] mat_ambient =
{
0.05
f, 0.05
f, 0.05
f, 1.0
f}
;
float
[] mat_diffuse =
{
this
.colorDiffuse[0
], this
.colorDiffuse[1
], this
.colorDiffuse[2
], 1.0
f}
;
float
[] mat_spec =
{
1.0
f, 1.0
f, 1.0
f, 1
f}
;
float
[] mat_shini =
{
128
f}
;
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 :
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.
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é.
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.
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.
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 :
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.
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;
}
}
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.
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.
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 :
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.
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.
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 « *** » :
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 :
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 :
public
String getProjectName
(
) {
return
projectName;
}
public
void
setProjectName
(
String projectName) {
this
.projectName =
projectName;
}
Ainsi que pour les propriétés drawingMode et GldrawingMode :
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.
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.
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.
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 :
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 :
@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 :
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 :
@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 :
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 :
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 :
@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.0
f, 0.0
f, 0.0
f, 0.0
f);
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.0
f, 5.0
f, 5.0
f, 0.0
f}
;
float
[] blancAmbient =
{
1.0
f, 1.0
f, 1.0
f, 0.2
f}
;
float
[] blanc =
{
1.0
f, 1.0
f, 1.0
f, 1.0
f}
;
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 :
@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) :
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 !
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.
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.