1. Introduction▲
1-A. Présentation▲
Cet article inaugure toute une série sur la création d'un moteur graphique pour mon application de manipulation de modèles éléments finis.
Le but n'est pas de créer un moteur de jeu multiplateforme, aux capacités graphiques avancées utilisant les dernières technologies, mais de mettre en place un moteur graphique simple.
Ceci permettra d'aborder la programmation OpenGL en Java au travers de l'utilisation de l'API JOGL. Je n'aborderai donc pas explicitement la théorie concernant OpenGL, étant donné que le net regorge d'informations et de tutoriels, sauf ponctuellement si nécessaire. Les articles seront donc essentiellement orientés programmation Java et JOGL.
1-B. But▲
Plegat (en plus d'être mon pseudo) est le nom du logiciel que je développe. Plegat est un pré/postprocesseur pour logiciels éléments finis. Il est codé en Java, pour des raisons de facilité de portabilité, et il utilise OpenGL pour la partie affichage graphique.
Cela fait maintenant quelques années que je travaille sur son développement, et le moteur graphique n'avait jamais été repensé depuis les premières versions. La version actuelle n'est pas assez évolutive pour correspondre à mes besoins, et devait donc être recodée. Cette réécriture du moteur servira donc de support à cette série de tutoriels.
Le but final est donc d'obtenir :
- un moteur graphique 3D évolutif ;
- permettant trois types d'affichage : fil de fer, solide et texturé ;
- contrôlable via le clavier et la souris ;
- avec une gestion précise des éléments graphiques à afficher ;
- possédant plusieurs modes de sélection à l'écran (picking, par zone...) ;
- éventuellement extensible par un système de plugins.
Afin d'avoir une idée du rendu graphique souhaité, on pourra jeter un œil aux copies d'écran des logiciels suivants :
- Gmsh: http://www.geuz.org/gmsh/#Screenshots ;
- Salome: http://www.salome-platform.org/about/screenshots.
ainsi qu'aux vidéos de démonstration suivantes :
- Démo Gmsh (viméo) http://vimeo.com/3287369 ;
- Démo Plegat (viméo, v0.23, ancien moteur) http://vimeo.com/3082069.
1-C. Technologies employées▲
Pour suivre ces articles, vous aurez besoin :
- d'un JDK Java SE. J'utilise personnellement le JDK Java 6, pour des soucis de portabilité personnels, mais toute version supérieure du JDK pourra être utilisée sans problème. Vous pouvez télécharger le JDK sur le site d'OracleTéléchargement Java SE Development Kit (JDK) ;
- de l'API OpenGL JOGL. Celle-ci est disponible sur le site JogAmpSite Jogamp, qui maintient et développe le binding OpenGL JOGL. Vous trouverez également sur ce site les API JOCL (binding OpenCL) et JOAL (binding OpenAL). La version actuelle de JOGL est la 2.0, mais encore en RC (Release Candidate) ;
- de votre EDI préféré. Pour ma part, ce sera NetBeansSite Netbeans.
1-D. Installation de JogAmp▲
Vous pouvez télécharger une archive incluant tous les fichiers composant l'API JogAmp sur cette pagetéléchargement archive Jogamp. Choisissez l'archive correspondant à votre système d'exploitation (Win, Mac, Linux) et à votre architecture (32/64 bits). Les archives sont au format 7-zip.
Après avoir décompressé l'archive, les deux répertoires nécessaires pour la suite sont les répertoires "jar" et "lib". Le répertoire "jar" contient toutes les bibliothèques jar qui seront utilisées pour le développement de nos programmes. Le répertoire "lib" contient quant à lui les bibliothèques natives (*.dll sous Windows, *.so sous Linux) qui seront utilisées lors de l'exécution des programmes.
Il vous faut ensuite configurer votre EDI afin de pointer correctement sur ces deux répertoires. Pour cela, je vous renvoie vers la page du Wiki JogAmpwiki Jogamp - Configuration IDE qui présente le processus de configuration pour les EDI suivants : Eclipse, IntelliJ IDEA, et NetBeans.
2. Mon premier cube▲
2-A. Tout est relatif...▲
... il faut donc bien définir l'espace dans lequel nous allons travailler.
Il y a quatre choses très importantes à déterminer pour voir quelque chose à l'écran :
- l'endroit où sont nos éléments à dessiner ;
- l'endroit où l'on est ;
- l'endroit où l'on regarde ;
- où se trouve le haut.
Les éléments à dessiner seront pour le moment très simples. Nous commencerons par un cube unitaire (c'est-à-dire dont les côtés mesurent une unité), je vous fais grâce du triangle, un peu trop... basique ! Nous le placerons à l'origine de notre "univers" pour simplifier.
L'endroit où l'on est, celui où l'on regarde et le haut de la scène seront fixés dans ce premier article (ultérieurement, ils seront gérés par une caméra).
On définit tout d'abord une vision en perspective de notre scène grâce à l'instruction gluPerspective :
public
void
gluPerspective
(
double
fovy,
double
aspect,
double
zNear,
double
zFar)
fovy sera pris égal à 45°. aspect sera défini par le ratio entre la largeur de la fenêtre JOGL et sa hauteur. zNear sera égal à 0.1, et zFar à 100 (ces valeurs seront optimisées pour les prochains articles).
L'instruction JOGL permettant de définir la caméra (où l'on est, où l'on regarde, où est le haut) est gluLookAt :
public
void
gluLookAt
(
double
eyeX,
double
eyeY,
double
eyeZ,
double
centerX,
double
centerY,
double
centerZ,
double
upX,
double
upY,
double
upZ)
Dans ce premier article, la caméra sera fixe. Les points de définition sont donnés par les paramètres suivants :
- où l'on est : (eyeX,eyeY,eyeZ)=(3,5,2) ;
- où l'on regarde : (centerX,centerY,centerZ)=(0,0,0) ;
- où est le haut : (upX,upY,upZ)=(0,0,1).
Voilà. Notre environnement est défini, il ne reste plus qu'à y dessiner...
2-B. Le repère principal▲
Une des choses que j'apprécie, c'est de savoir où je suis... nous allons donc dessiner un repère à l'origine de notre univers. Ce repère sera composé de trois lignes de couleurs différentes, afin de repérer quels sont les différents axes :
- l'axe X sera en rouge ;
- l'axe Y sera en vert ;
- l'axe Z sera en bleu.
Rien de compliqué pour le dessin, nous utiliserons les instructions glColor3d pour définir la couleur du trait, les traits étant tracés en mode GL_LINES.
Le dessin du repère principal sera affecté à une classe dédiée :
package
pge_article_1;
import
javax.media.opengl.GL2;
public
class
PGEGlobalCoord {
public
void
draw
(
GL2 gl) {
gl.glBegin
(
GL2.GL_LINES);
// rouge : axe X
gl.glColor3d
(
1
, 0
, 0
);
gl.glVertex3d
(
0
, 0
, 0
);
gl.glVertex3d
(
1
, 0
, 0
);
// vert : axe Y
gl.glColor3d
(
0
, 1
, 0
);
gl.glVertex3d
(
0
, 0
, 0
);
gl.glVertex3d
(
0
, 1
, 0
);
// bleu : axe Z
gl.glColor3d
(
0
, 0
, 255
);
gl.glVertex3d
(
0
, 0
, 0
);
gl.glVertex3d
(
0
, 0
, 1
);
gl.glEnd
(
);
}
}
2-C. Un cube à six faces ▲
Pour le dessin du cube unitaire, une classe dédiée est également écrite. Le constructeur prend en paramètre la position dans l'espace du centre du cube, et une méthode draw permet de gérer l'affichage du cube. On y dessine les faces inférieures et supérieures du cube en mode GL_LINE_LOOP, puis les quatre arêtes joignant les deux faces en GL_LINES.
Pour la couleur, vous aurez droit à un joli cyan...
package
pge_article_1;
import
javax.media.opengl.GL2;
public
class
PGEUnitCube {
public
PGEUnitCube
(
double
x,double
y, double
z) {
this
.x=
x;
this
.y=
y;
this
.z=
z;
}
public
void
draw
(
GL2 gl) {
// dessin face inférieure
gl.glBegin
(
GL2.GL_LINE_LOOP);
gl.glColor3d
(
0
, 1
, 1
);
gl.glVertex3d
(
this
.x -
0.5
, this
.y -
0.5
, this
.z -
0.5
);
gl.glVertex3d
(
this
.x +
0.5
, this
.y -
0.5
, this
.z -
0.5
);
gl.glVertex3d
(
this
.x +
0.5
, this
.y +
0.5
, this
.z -
0.5
);
gl.glVertex3d
(
this
.x -
0.5
, this
.y +
0.5
, this
.z -
0.5
);
gl.glEnd
(
);
// dessin face supérieure
gl.glBegin
(
GL2.GL_LINE_LOOP);
gl.glVertex3d
(
this
.x -
0.5
, this
.y -
0.5
, this
.z +
0.5
);
gl.glVertex3d
(
this
.x +
0.5
, this
.y -
0.5
, this
.z +
0.5
);
gl.glVertex3d
(
this
.x +
0.5
, this
.y +
0.5
, this
.z +
0.5
);
gl.glVertex3d
(
this
.x -
0.5
, this
.y +
0.5
, this
.z +
0.5
);
gl.glEnd
(
);
// dessin des quatre arêtes manquantes
gl.glBegin
(
GL2.GL_LINES);
gl.glVertex3d
(
this
.x -
0.5
, this
.y -
0.5
, this
.z -
0.5
);
gl.glVertex3d
(
this
.x -
0.5
, this
.y -
0.5
, this
.z +
0.5
);
gl.glEnd
(
);
gl.glBegin
(
GL2.GL_LINES);
gl.glVertex3d
(
this
.x +
0.5
, this
.y -
0.5
, this
.z -
0.5
);
gl.glVertex3d
(
this
.x +
0.5
, this
.y -
0.5
, this
.z +
0.5
);
gl.glEnd
(
);
gl.glBegin
(
GL2.GL_LINES);
gl.glVertex3d
(
this
.x -
0.5
, this
.y +
0.5
, this
.z -
0.5
);
gl.glVertex3d
(
this
.x -
0.5
, this
.y +
0.5
, this
.z +
0.5
);
gl.glEnd
(
);
gl.glBegin
(
GL2.GL_LINES);
gl.glVertex3d
(
this
.x +
0.5
, this
.y +
0.5
, this
.z -
0.5
);
gl.glVertex3d
(
this
.x +
0.5
, this
.y +
0.5
, this
.z +
0.5
);
gl.glEnd
(
);
}
private
double
x,y,z; // position du centre du cube
}
2-D. La fenêtre JOGL▲
Maintenant, nous avons tout pour dessiner notre cube... sauf une fenêtre JOGL. Nous allons créer une classe, que je nommerai PgePanel, dérivant de GLJPanel. Cette classe mère est choisie plutôt que GLCanvas car elle est plus adaptée à une utilisation en environnement Swing (GLCanvas étant plus adaptée pour AWT).
La trame de la classe PgePanel est la suivante :
package
pge_article_1;
import
javax.media.opengl.GL;
import
javax.media.opengl.GLAutoDrawable;
import
javax.media.opengl.GLEventListener;
import
javax.media.opengl.GLJPanel;
import
javax.media.opengl.glu.GLU;
public
class
PGEPanel_article_1 extends
GLJPanel implements
GLEventListener {
public
PGEPanel_article_1
(
) {
super
(
);
this
.initComponents
(
);
}
private
void
initComponents
(
) {
// initialisation des composants de PgePanel
}
public
void
display
(
GLAutoDrawable arg0) {
// affichage de la scène
}
public
void
reshape
(
GLAutoDrawable arg0, int
arg1, int
arg2, int
arg3, int
arg4) {
// mise à jour suite à redimensionnement de la fenêtre
}
public
void
dispose
(
GLAutoDrawable glad) {
// libération des ressources OpenGL
}
public
void
init
(
GLAutoDrawable glad) {
// initialisation OpenGL
}
private
int
winAW; // largeur de la zone d'affichage
private
int
winAH; // hauteur de la zone d'affichage
}
On se contentera d'ajouter un écouteur JOGL dans la méthode initComponents(), ainsi que la création des deux objets PGEGlobalCoord et PGEUnitCube :
private
void
initComponents
(
) {
this
.addGLEventListener
(
this
);
this
.cube=
new
PGEUnitCube
(
0
, 0
, 0
);
this
.coord=
new
PGEGlobalCoord
(
);
}
La méthode reshape() redéfinit les valeurs de la largeur et de la hauteur de la fenêtre JOGL :
public
void
reshape
(
GLAutoDrawable arg0, int
arg1, int
arg2, int
arg3, int
arg4) {
System.out.println
(
"panel reshape"
);
// réinitialisation des dimensions de la zone d'affichage
this
.winAW =
this
.getWidth
(
);
this
.winAH =
this
.getHeight
(
);
}
Les méthodes dispose() et init() ne seront pas implémentées ici :
public
void
dispose
(
GLAutoDrawable glad) {
System.out.println
(
"running "
+
this
.getClass
(
).getName
(
)+
" dispose method"
);
}
public
void
init
(
GLAutoDrawable glad) {
System.out.println
(
"running "
+
this
.getClass
(
).getName
(
)+
" init method"
);
}
La méthode display() se chargera de l'affichage de la scène JOGL :
public
void
display
(
GLAutoDrawable arg0) {
GL2 gl =
arg0.getGL
(
).getGL2
(
);
GLU glu =
new
GLU
(
);
gl.glViewport
(
0
, 0
, this
.winAW, this
.winAH);
// initialisation de la matrice de projection
gl.glMatrixMode
(
GL2.GL_PROJECTION);
gl.glLoadIdentity
(
);
// définition de la matrice de projection
// vue en perspective, angle 45°, ratio largeur/hauteur, plan de clipping "near" et "far"
glu.gluPerspective
(
45
, (
double
) this
.winAW /
(
double
) this
.winAH, 0.1
, 100.0
);
// position de la caméra : point (3,5,2)
// point de visée: point (0,0,0)
// vecteur "up": point (0,0,1)
glu.gluLookAt
(
3
, 5
, 2
, 0
, 0
, 0
, 0
, 0
, 1
);
// initialisation de la matrice modèle
// couleur de fond: noir
gl.glMatrixMode
(
GL2.GL_MODELVIEW);
gl.glLoadIdentity
(
);
gl.glClearColor
(
0.0
f, 0.0
f, 0.0
f, 0.0
f);
gl.glClear
(
GL2.GL_COLOR_BUFFER_BIT);
// dessin des axes du repère global
this
.cube.draw
(
gl);
// dessine un cube unitaire à l'origine du repère global
this
.coord.draw
(
gl);
}
2-E. Premier affichage de PGE▲
Il ne nous manque que la "main class" pour pouvoir tester notre fenêtre JOGL :
package
pge_article_1;
import
java.awt.event.WindowAdapter;
import
java.awt.event.WindowEvent;
import
java.util.EventListener;
import
javax.media.opengl.GLCapabilities;
import
javax.media.opengl.GLProfile;
import
javax.swing.JFrame;
/**
* Article1.java
*
* Code source du premier article.
*
*/
public
class
Main_article_1 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);
final
JFrame frame =
new
JFrame
(
"Article 1"
);
PGEPanel_article_1 pane =
new
PGEPanel_article_1
(
caps);
frame.getContentPane
(
).add
(
pane);
frame.addWindowListener
(
new
WindowAdapter
(
) {
public
void
windowClosing
(
WindowEvent e) {
System.out.println
(
"closing main window..."
);
System.exit
(
0
);
}
}
);
frame.setSize
(
300
, 300
);
java.awt.EventQueue.invokeLater
(
new
Runnable
(
) {
public
void
run
(
) {
frame.setVisible
(
true
);
}
}
);
}
}
Il ne reste plus qu'à lancer l'exécution du programme pour obtenir notre premier cube sous PGE !
3. Conclusion▲
Ce premier article a permis de présenter PGE, et de commencer l'implémentation du moteur graphique par l'affichage d'une scène simple.
Dans le prochain article, nous mettrons en place un système de caméra mobile, ainsi qu'une gestion du clavier et de la souris.
Les sources de cet article sont disponibles en suivant ce lien : src_PGE_article_1.zipTBD.
4. Remerciements▲
Merci à Ricky81 et raptor70 pour leurs commentaires, à ClaudeLELOUP et _Max_ pour leur relecture orthographique, ainsi qu'à djibril pour son aide sur le KitOOoDVP.