Initialiser OpenGL avec Windows

1. Créer une fenêtre Windows

Comme je l'ai dit dans le premier chapitre, il faut obtenir un Device Context (DC), puis un Rendering Context (RC) afin d'initialiser OpenGL. Et pour cela, nous avons besoin de maîtriser quelque peu la gestion de fenêtres dans Windows. Nous allons donc voir comment créer une fenêtre et récupérer les infos nécessaires à l'initialisation.
Il y a en fait deux moyens d'obtenir un DC : soit en se créant sa petite fenêtre en Win32, soit en utilisant les MFC. Ici, je ne vais parler que de Win32, car c'est beaucoup plus simple et pratique que les MFC, qui sont très lourdes et chiantes à utiliser. Mais si vous savez gérer les MFC et compris comment récupérer un DC et un RC, alors vous ne devriez pas avoir de problème à intégrer OpenGL dans vos programmes utilisant les MFC.
Autre chose : j'utilise Ms Visual C++, que je trouve plus pratique pour ce genre de choses. Les procédures spécifiques au compilateur, du genre inclure les .lib, seront donc expliquées pour ce logiciel, et pas pour le Borland C++ Builder. Mais le code restera bien sûr le même quelque soit le compilateur.
Enfin, sachez que la touche F1 est votre amie : à chaque fois que je vous présente une nouvelle fonction, étudiez-la plus en détail avec l'aide de votre compliateur, car je ne peux pas tout vous dire sur tout, ou je n'aurais jamais fini. Servez-vous en donc le plus possible, et si jamais vous avez encore des doutes, envoyez-moi vos questions.

Pour les newbies de VC++, créez d'abord un nouveau projet par File>New>Project>Win32 Application, puis un nouveau fichier C++ par File>New>File>C/C++ Source File.

Bon, commencont par le commencement, à savoir les #include. Il y en a 3 obligatoires, sans lesquels votre programme ne pourra pas marcher. Les voici :

#include <windows.h>
#include <gl/gl.h>
#include <gl/glu.h>

gl.h et glu.h servent bien évidemment à utiliser les fonctions OpenGL. Quand à windows.h, il est utilisé pour créer les fenêtres, et vous n'en avez normalement pas besoin si vous utilisez glut.h. Les header OpenGL sont généralement dans le répertoire \include\gl de votre compilateur.
Mais ce n'est pas tout : les headers se rapportent à des fonctions qui sont définies dans les opengl32.dll et glu32.dll : il faut donc les lier à votre projet. Pour ce faire, avec VC++, allez dans Project>Settings>Links, et incluez opengl32.lib et glu32.lib dans la liste Object/library modules.

Ensuite, il faut savoir que dans un programme Win32, la fonction de départ n'est plus main mais WinMain. Créez donc la première fonction de votre programme :

int WINAPI WinMain( HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{

La valeur de retour est la même que pour la fonction main() classique. Voyons maintenant ce que veulent bien dire ces paramètres bizarres :

  • hInstance est une sorte de handle pour votre programme : comme Windows est multitâche, il a besoin de savoir quel programme lui dit quoi. Et c'est à travers cette variable que Windows sait à qui il a affaire : c'est en quelque sorte la carte d'identité du programme. C'est pourquoi il est conseillé de la sauvegarder dans une variable globale, ce que nous allons faire :
HInst = hInstance; //HInst est un variable globale de type HINSTANCE
  • hPervInstance ne sert plus à rien, il est conservé pour des raisons de compaptibilité avec les programmes 16 bits
  • lpCmdLine est un pointeur sur la ligne de commande, pour récupérer les paramètres éventuels
  • nCmdShow est le style initial de la fenêtre : normale, réduite...etc. Il ne nous sert pas à grand chose pour le moment

Hé bien puisque nous avons notre fonction principale, pourquoi ne pas créer tout de suite notre fenêtre ? Allons-y :

HWND OpenGLWindow = CreateWindow
( "BUTTON", //Classe de la fenêtre
"Fenêtre OpenGL", //Nom de la fenêtre
WS_VISIBLE | WS_BORDER, //Caractéristiques
0, //Position x
0, //Position y
640, //Largeur
480, //Hauteur
0, //Handle de la fenêtre même
0, //Identifiant de la fenêtre fille
HInst, //HINSTANCE du programme
NULL /*Chaine de caractères envoyée
en paramètre lors de la création
de la fenêtre*/
);
if (!OpenGLWindow) exit(1);

Je sais que les puristes vont m'incendier, mais je trouve que c'est plus simple de commencer comme ca. Etudions maintenant cette fonction CreateWindow :

  • Elle renvoie une valeur de type HWND, c'est-à-dire un handle pour la fenêtre (tiens, on se rapproche du DC). Il s'apparente au HINSTANCE pour le programme, sauf que là c'est pour une fenêtre. Vous pouvez aussi le sauvegarder en tant que variable globale. S'il est nul, il y a eu erreur.
  • Le premier paramètre correspond à la classe de la fenêtre : c'est elle qui définit presque toutes ses propriétés. Ici, nous avons pris une classe préexistante, pour simplifer l'explication, mais plus tard nous allons devoir en créer une nous-mêmes. Je disais que les puristes allaient m'incendier car en fait on n'utilise jamais ce genre de classe prédéfinie, parce que ca sert à rien, puisqu'on ne peut même pas définir la fonction WinProc. En fait leur seul utilité est d'apprendre à utiliser CreateWindow.
  • Les caractéristiques de la fenêtre peuvent être multiples. Faites F1 pour obtenir toutes les options possibles
  • Les 4 paramètres suivant parlent d'eux-mêmes
  • Les 2 d'après ne servent que dans le cas d'applications à plusieurs fenêtres, donc on s'en fout
  • Et enfin le dernier paramètre permet de passer des arguments à la fonction WinProc avec le message WM_CREATE. Je sais, vous ne comprenez pas, mais bient vous saurez.

Ca y est, la fenêtre est créée. Le programme est fini ? Pas tout à fait. Car il faut gérer les interactions avec Windows. Et pour cela il faut savoir comment le programme communique avec Windows : en fait, ce n'est pas Windows qui transmet au programme les messages le concernant, mais plutt le programme qui doit demander à Windows s'il doit faire quelque chose, grâce à la fonction PeekMessage. En quelque sorte, le programme va voir dans son casier s'il n'y a pas des messages pour lui via PeekMessage, et s'il y en a, il les récupère avec GetMessage, puis les distribue aux membres de son équipe (les fenêtres), via DispatchMessage. C'est très imagé mais au moins c'est clair et je suis sūr de m'être fait comprendre. Voilà donc le code à écrire après la création de la fenêtre :

MSG msg;
do
{  
  while (PeekMessage(&msg,OpenGLWindow,0,0,PM_NOREMOVE))
{

if(!GetMessage(&msg,OpenGLWindow,0,0)) exit(0);
DispatchMessage(&msg);

}
}  
while(1);

Lorsque le message WM_QUIT est envoyé au programme, cela signifie qu'il doit fermer et que tout est fini. Si c'est ce message que récupère GetMessage, il renvoie 0 : cela explique le if(!GetMessage(&msg,OpenGLWindow,0,0)) exit(0); C'est la seule condition pour qu'un programme quitte. Lorsque le message est récupéré par GetMessage, il est supprimé de la pile des messages (le 'casier' de tout-à-l'heure), à une exception près : le message WM_PAINT n'est supprimé que lorsqu'il a été traité. Pour l'instant nous n'avons pas besoin de savoir ce que msg contient, puisque le seul message qui nous importe est WM_QUIT, et il est géré par GetMessage.
Peut-être ces histoires de messages vous semblent-elles un peu obscures, mais ne vous inquiétez pas, les rares taches d'ombres vont bientôt s'éclaircir.

Mais finissons d'abord notre programme par les lignes qui suivent :

return 0; //(pour la forme)
}

Et voilà ! Le premier programme d'exemple est terminé et vous pouvez le lancer : vous verrez un zoli bouton avec "Fenêtre OpenGL" marqué dessus, et vous pourrez même cliquer dessus, et alors là : RIEN ! Hé oui ! Il ne se passe rien quand on clique sur le bouton ! Ben oui, c'est là le problème : ca sert à rien d'utiliser la classe prédéfinie, puisqu'on peut même pas lui dire ce qu'on veut qu'il se passe quand on clique sur le bouton.

Il va donc falloir qu'on définisse notre propre classe de fenêtre. C'est d'ailleurs toujours comme ce qu'on fait. Stoppez le programme et recopiez donc le code suivant juste avant l'appel à CreateWindow :

WNDCLASS WindowClass =
{ 0, //Style
WinProc, //Procédure pour la gestion des messages
0, //octets supplémentaires à allouer à la classe
0, //octets supplémentaires à allouer à la fenêtre
HInst, //Handle du programme
0, //Icne
0, //Curseur
0, //Couleur d'arrière-plan
NULL, //Pointeur sur le menu associé à la classe
"La classe!" //Nom de la classe fenêtre
};
if (!RegisterClass(&WindowClass)) exit(1);

Ca y est : nous avons défini notre classe de fenêtre, qui s'appelle "La classe!". On peut donc remplacer "BUTTON" par "La classe!" dans la procédure CreateWindow. Mais si vous essayez de lancer le programme tel quel, le compilateur va vous renvoyer une erreur : en effet, dans la déclaration de la classe, nous lui passons comme fonction de gestion des messages une fonction que nous avons nommé WinProc (nous aurions aussi bien pu l'appeler Salutcavaouimoicavaettoicavaouicavaettoiohmoitusaiscava, mais je trouve que WinProc est plus approprié), mais cette fonction n'existe pas. Il faut donc la créer :

LRESULT CALLBACK WinProc(
HWND hwnd, //Handle de la fenêtre
UINT uMsg, //Message
WPARAM wParam, //Paramètre word
LPARAM lParam ) //Paramètre long
 
{
return DefWindowProc(
hwnd,
uMsg,
wParam,
lParam
);
//Lance la procédure par défaut
}

Voici donc la procédure si mystérieuse : un truc tout con en fait, qui ne fait que passer le relais à une autre procédure déjà définie. Et puis si vous êtes perspicace, vous auriez pu me dire "Mais alors, on aurait pu tout connement mettre DefWindowProc à la place de WinProc !". Hé ben en fait...oui. Mais maintenant, on va personnaliser un peu cette fonction, histoire de s'amuser.

D'abord, vous aurez srement remarqué que le fait d'appuyer sur Alt+F4 ferme la fenêtre, mais pas le programme : il faut l'arrêter manuellement avec le compilateur ou Ctrl+Alt+Del. En effet, le fait d'envoyer le message "Ferme la fenêtre" ne provoque pas chez DefWindowProc l'envoi du message "Quitte le programme" : nous allons donc remplacer le contenu de WinProc par :

if (uMsg==WM_CLOSE) {PostQuitMessage(0);return 0;}
else return DefWindowProc(hwnd,uMsg,wParam,lParam);

Ca y est ! nous avons enfin notre propre fonction de gestion de messages à nous, qui sait fermer le programme. Et vous remarquerez que les fameux messages évoqués plus haut (WM_PAINT,WM_QUIT,etc...) sont contenus dans la variable uMsg, transmise à WinProc, et que PostQuitMessage dit au programme de se fermer sans autre forme de procès.

Bon et bien maintenant, si nous intégrions OpenGL à notre fenêtre, pour voir ? C'est vrai, depuis le début du tutorial, on a toujours pas vu de DC ou de RC, juste un HWND, qui ma foi faisait penser à un DC de par sa définition. Je ne vous mentirais pas plus longtemps : le DC est en effet directement tiré du HWND. En fait, il suffit de le récupérer grâce à la fonction GetDC. "Hé bien on n'a qu'à le faire alors !" Ok, ok on va le faire...mais o ? après la création de la fenêtre, dans le WinMain ? Oui, pourquoi pas, mais ca ferait un petit peu bidouille, non ? Alors que lorsque l'on appelle CreateWindow, celle-ci exécute justement WinProc une seule fois, avec comme message WM_CREATE... C'est trop beau : tant qu'à faire, on va en profiter pour récupérer notre DC ! Etoffons donc notre chère WinProc :

switch(uMsg)
{
case WM_CLOSE:

ReleaseDC(hwnd,DC);
PostQuitMessage(0);
break;

//Libère le DC et ferme le programme
case WM_CREATE:

DC=GetDC(hwnd);
break;

//Récupère le DC
default:

return DefWindowProc(
hwnd,
uMsg,
wParam,
lParam);
break;

//Sinon, fait le truc habituel
}
return 0;

Sans oublier de rajouter

HDC DC;

dans les déclaration de variables globales. Et voilà, nous avons notre DC ! Lancez le programme et vous verrez ce que ca change : ... rien. Car l'initialisation d'OpenGL n'est même pas commencée. Il nous faut encore récupérer le Rendering Context, et le définir comme sortie OpenGL courante, en faisant

RC = wglCreateContext(DC); //RC est une variable globale de type HGLRC
if (!RC) SendMessage(hwnd,WM_CLOSE,0,0);
wglMakeCurrent(DC, RC);

après le GetDC du WM_CREATE, et en rajoutant

wglMakeCurrent(NULL, NULL);
if (RC) wglDeleteContext(RC);

avant le ReleaseDC du WM_CLOSE.
Voilà, normalement ca devrait marcher. Mais ca marche pas. Pourquoi ? Et bien tout simplement parce qu'on a pas paramétré le Device Context. En effet, il faudrait d'abord lui dire en quelle résolution on veut être, et puis si on utilise le double buffering, et puis qu'on veut lui associer un RC. Bref, il faut paramétrer ce qu'on appelle le PixelFormat (cad en gros le format d'affichage). Créons donc une petite fonction qui va nous faire tout ca :

void SetupPixelFormat(HDC hDC)
{  
  PIXELFORMATDESCRIPTOR pfd =
{  
sizeof(PIXELFORMATDESCRIPTOR), //taille du descripteur de format
1, //version
PFD_SUPPORT_OPENGL |
PFD_DRAW_TO_WINDOW |
PFD_DOUBLEBUFFER,
//Propriété
PFD_TYPE_RGBA, //Mode de couleurs
16, //Bits de couleur
0, 0, 0, 0, 0, 0, //Paramètres des couleurs
0,0, //Paramètres alpha
0,0, 0, 0, 0, //Paramètres du buffer d'accumulation
32, //Bits de profondeur
0, //Bits du buffer stencil
0, //Nombre de buffers auxiliaires
0, //ignoré (obsolète)
0, //réservé/code>
0, //ignoré (obsolète)
0, //Couleur de transparence
0 //Ignoré (obsolète)
  };  
 
  int pixelFormat;
pixelFormat = ChoosePixelFormat(hDC, &pfd);
if (!pixelFormat)
{

 

MessageBox
(
WindowFromDC(hDC),
"Mode graphique non supporté",
"Problème",
MB_ICONERROR | MB_OK
);
exit(1);

/*Vérifie si un PixelFormat du type demandé existe*/
}
if (!SetPixelFormat(hDC, pixelFormat, &pfd))
{    
 

MessageBox
(
WindowFromDC(hDC),
"Mode graphique non supporté",
"Problème",
MB_ICONERROR | MB_OK
);
exit(1);

/*Applique le PixelFormat. Arrête si erreur*/
}
}  

Je me fais chier à décrire cette fonction, mais en fait il n'y a pas à en dire beaucoup plus. Les paramètres sont toujours les mêmes, sauf peut-être le nombre de bits de couleur et de profondeur. A part ca, le reste ne sert pas à grand chose, mais c'est nécessaire. Contentez-vous de recopier la fonction, et interrogez votre compilateur ou moi si quelque chose vous gêne. Mais je vous rassure, vous n'avez pas grand chose à savoir sur ces paramètres.

Tout ce qu'il reste à faire, c'est d'inclure un appel à cette fonction après la récupération du DC et avant celle du RC (sans cela wglCreateContext vous renverra une erreur). Et voilà ! Là c'est réellement fini, et votre fonction WinProc doit ressembler à ceci :

LRESULT CALLBACK WinProc(HWND hwnd,UINT uMsg,WPARAM wParam,LPARAM lParam )
{  
  switch(uMsg)
{  
  case WM_CLOSE:

wglMakeCurrent(NULL, NULL);
if (RC) wglDeleteContext(RC);
ReleaseDC(hwnd,DC);
PostQuitMessage(0);
break;

//Libère RC et DC et ferme le programme
case WM_CREATE:

DC=GetDC(hwnd);
SetupPixelFormat(DC);
RC = wglCreateContext(DC);
if (!RC) SendMessage(hwnd,WM_CLOSE,0,0);
wglMakeCurrent(DC, RC);
break;

//Récupère le DC et le RC
default:

return DefWindowProc(hwnd,
uMsg,
wParam,
lParam);
break;

//Sinon, fait le truc habituel
  }  
return 0;
}  

Si vous lancez le programme maintenant, vous ne verrez cependant pas de changement par rapport à avant, tout simplement parce que le message WM_PAINT est encore géré par DefWindowProc. Mais sachez qu'OpenGL est bien lié à cette fenêtre, et que le pas entre la fenêtre vide et un rendu OpenGL est très mince. Pour le découvrir, passez à la deuxiême partie de ce tutorial !

 

2. Attacher OpenGL à cette fenêtre

Dans la premiè partie, je vous ai appris à créer votre fenêtre par CreateWindow(), à associer la sortie OpenGL à cette fenêtre par GetDC(), SetupPixelFormat(), wglCreateContext() et wglMakeCurrent(), et à gérer les messages envoyés à cette fenêtre par une fonction que nous avons appelé WinProc(). Je vais maintenant finir de vous expliquer comment initialiser OpenGL lui-même, maintenant que la fenêtre est bien créée.
Reprenez donc votre programme de la dernière fois, et modifiez votre fonction WinProc() de façon à ce qu'elle ressemble à ça :

LRESULT CALLBACK WinProc(HWND hwnd,UINT uMsg,WPARAM wParam,LPARAM lParam )
{  
  switch(uMsg)
{  
  case WM_CLOSE:

wglMakeCurrent(NULL, NULL);
if (RC) wglDeleteContext(RC);
ReleaseDC(hwnd,DC);
PostQuitMessage(0);
break;

//Libère RC et DC et ferme le programme
case WM_CREATE:

DC=GetDC(hwnd);
SetupPixelFormat(DC);
RC = wglCreateContext(DC);
if (!RC) SendMessage(hwnd,WM_CLOSE,0,0);
wglMakeCurrent(DC, RC);
InitGL();
break;

//Récupère le DC et le RC
case WM_SIZE:

Reshape(
LOWORD(lParam),
HIWORD(lParam)
);
break;

//Met à jour les paramètres OGL
case WM_PAINT:

Draw();
break;

//Exécute le rendu
default:

return DefWindowProc(
hwnd,
uMsg,
wParam,
lParam
);
break;

//Sinon, fait le truc habituel
  }  
return 0;
}  

Nous avons rajouté un appel à 3 fonctions que nous allons écrire un peu plus loin : Draw(), Reshape() et InitGL(). Nous reparlerons beaucoup de Draw() et InitGL() dans les prochains tutoriaux. Mais parlons d'abord de Reshape() : voici ce qu'il faut écrire à l'intérieur :

void Reshape(int width, int height)
{
glViewport(0,0,width,height);
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  gluPerspective(45,float(width)/float(height),0.1,100);
}
  • glViewport() informe d'abord OpenGL sur la taille de la zone de sortie
  • Ensuite glMatrixMode() charge la matrice de projection. Une matrice ? quésako ? En fait, c'est un tableau sur lequel on peut effectuer des opérations mathématiques spécifiques. La matrice de projection sert à définir comment les coordonnées 3D sont transformées en coordonnées 2D sur l'écran. La matrice modèle et vue (GL_MODELVIEW) définit la position du repère de coordonnées dans le monde. C'est sur cette matrice que l'on effectuent des opérations qui se traduisent par des rotations, des translations ou des homotéties. La matrice texture permet de définir l'aspect des textures plaquées sur les faces. On peut aussi lui faire subir des rotations, etc, qui se répercutent sur l'affichage. Les matrices sont donc des outils très puissant de modélisation.
  • glLoadIdentity() fait un reset de la matrice (il la réinitialise) pour pouvoir effectuer des modifications sur celle-ci
  • Et enfin gluPerspective modifie la matrice courante (c'est-à-dire ici la matrice de projection) pour qu'OpenGL transforme les coordonnées 3D au moment du rendu en coordonnées 2D par rapport à l'écran, de façon à ce qu'on ait 'impression de regarder à travers un objectif de 45 de focale, de largeur width et de hauteur height, et dont le clipping va de 0.1 à 100 (attention la valeur du clipping near - ici 0.1 - doit être strictement supérieure à 0).

Cette fonction Reshape() est appelée à chaque fois que la fenêtre est redimensionnée, et notemment à sa création. Grâce à cette fonction, OpenGL est totalement initialisé et est parfaitement près à dessiner. Mais alors qu'est-ce que vient faire InitGL() ? Et bien en fait on va mettre dans cette fonction toutes les fonctions optionnelles destinées à paramétrer le rendu : activation du test de profondeur, définition des lumières, textures, transparence... Donc pour l'instant, comme on n'a rien à y mettre, on va quand même la créer, mais en la laissant vide :

void InitGL()
{
}

Et maintenant, la fontion Draw() : c'est la fonction qui sera appelée à chaque fois que la fenêtre sera rafraîchie, et donc c'est là-dedans qu'on va placer toute la sauce qui va afficher à l'écran de magnifiques images. La fonction Draw() commencera toujours par glClear() (sauf effet visuel particulier) et finira toujours par SwapBuffers(DC). Mais trêve de bavardages, la voici :

void Draw()
{  
} glClear
(
GL_COLOR_BUFFER_BIT |
GL_DEPTH_BUFFER_BIT
);
//Efface le frame buffer et le Z-buffer
glMatrixMode(GL_MODELVIEW); //Choisit la matrice MODELVIEW
glLoadIdentity(); //Réinitialise la matrice
 
//Placer ici tout le code de transformation et de dessin
 

SwapBuffers(DC);

//Echange les 2 frame buffers
  • glClear() efface les buffers passés en paramètre. Ici, nous n'utilisons que le frame buffer (c'est-à-dire le buffer correspondant à l'image affiché à l'écran) et le z-buffer (utilisé pour les tests de profondeur). Pour l'instant, nous n'utiliseront pas le z-buffer, mais comme plus tard on l'utilisera toujours, autant vous montrer tout de suite comment on s'en sert.
  • Ensuite on charge la matrice modèle/vue et on la réinitialise pour pouvoir par la suite effectuer toutes les transformations 3D nécessaires (rotations, translations...).
  • Après vient tout le code de dessin et de transformation, que l'on remplira au fur et à mesure des tutorials
  • Et enfin la fonction SwapBuffers(DC) échange les deux frame buffers : celui sur lequel on travaillait est affiché à l'écran tandis que celui qui était à l'écran est caché et servira pour le prochain rendu.

Vous pouvez dès maintenant essayer ce programme : vous y verrez un magnifique écran noir, car pour l'instant rien n'est dessiné dans notre écran
Mais pour ne pas vous laisser sur votre fin, voici quelques lignes à insérer dans Draw() entre glLoadIdentity() et SwapBuffers() pour vous montrer que ca marche vraiment :

gluLookAt(0,0,-10,0,0,0,0,1,0);
glBegin(GL_TRIANGLES);

glVertex2i(0,1);
glVertex2i(-1,0);
glVertex2i(1,0);

glEnd();

Je vous laisse découvrir ce que ca donne (pour les impatients, allez voir dans le tut glut).

Et voila ! Vous avez réellement dessiné en OpenGL, rien qu'avec un petit tutorial de rien du tout ! C'est-y pas génial ? Avouez que c'est pas compliqué. Tout ce que vous avez besoin de savoir pour programmer en OpenGL sous Windows, c'est créer une fenêtre, savoir récupérer et gérer les messages qui lui sont transmis, et récupérer un Device Context et un Rendering Context pour initialiser le PixelFormat. Et en plus, vous n'avez même pas besoin de connaître par coeur comment le faire ! Et si vous trouvez ca déjà trop dur, alors je ne vous laisse même pas imaginer ce que ce serait si vous vous mettiez à Direct3D.



Antoche
 


← Introduction↑ Tutoriaux OpenGL ↑Initialiser OpenGL avec GLUT →