DirectX 8.1 - Tutoriel n°1

Introduction

Après de nombreuses recherches sur Internet pour m'initier à la programmation DirectX, je me suis rendu compte qu'il existait très peu de ressources en français sur ce sujet. Les seuls livres disponibles en français ne sont en fait que de simples traductions de la sdk et n'apportent pas réellement de valeur ajoutée aux informations fournies par microsoft, si ce n'est une version imprimée en français. J'ai donc décidé de mettre au point une série de tutoriels en français qui, je l'espère, pourront aider les débutants à dépasser le cap difficile des premiers mois de la découverte de l'api.

Je considère cependant que l'utilisateur possède déjà des notions suffisantes en programmation c/c++ et en algorithmique. Je n'ai pas l'intention d'écrire des tutoriels sur ces sujets, il existe déjà de nombreux livres les traitant très bien et de façon beaucoup plus approfondies que mes simples connaissances.

Je m'inspirerai dans un premier temps des tutoriels présents sur certains sites anglophones (NeHe Productions, andypike.com principalement) mais également des tutoriels de la sdk. Les premiers tutoriels sont en effet nécessaires pour poser des bases solides pour évoluer par la suite et il n'existe pas un grand nombre de méthodes différentes pour les réaliser.

Je ne rentrerai pas ici dans le détail du code, je préfère présenter les points importants et commenter abondamment le code pour le lecteur intéressé. A cet effet, le code présent sur cette page diffère quelque peu du code présent dans le projet fourni. J'ai trouvé préférable de faciliter le code de cette page pour améliorer la compréhension des bases mais de concevoir un code un peu plus complexe mais fournissant une base plus solide pour la suite des tutoriels.

La création d'un projet Visual Studio

Dans un souci de simplicité, j'utiliseris la programmation classique, c'est à dire les fonctions de l'api windows directement. Je pense que les MFC sont intéressantes pour créer facilement des interfaces utilisateurs conviviales mais leur utilisation rend le code plus difficile à comprendre et à maintenir. La connaissance de l'api "bas niveau" permet ainsi de garder un contrôle total sur l'application (c'est en tout cas mon avis personnel).

Nous allons donc commencer par créer un projet "Win32 Application" vide ("empty project"). Une fois ce projet créé, il est possible soit d'inclure au projet un fichier externe contenant le programme principal ou bien de créer un nouveau fichier source ("c++ source file") que nous appelerons "main.cpp" par convention. Il est important également de remplir correctement les chemins des headers et des librairies de la sdk pour que le compilateur s'y retrouve (Tools/Options/Directories). Il faut mettre les chemins de la sdk (include et libraries) en tête de la liste, car les headers de visual studio comprennent des définitions qu'il ne faut pas utiliser, il faut utiliser en priorité celles de la sdk de directX. De plus, pour chaque projet, il est nécessaire d'inclure les librairies que nous allons utiliser (d3d8.dll pour l'instant). Il existe deux solutions, la première consiste à la rajouter à la main dans les propriétés du projet en le rajoutant en version debug et en version release. La deuxième consiste à utiliser un pragma qui va indiquer au programme de charger la bonne librairie. C'est cette deuxième méthode que j'utilise, elle est plus simple à utiliser et elle évite les oublis malheureux lors de la création d'un nouveau projet. La ligne concernée est située au tout début du fichier.

Les bases d'un programme windows

Le point d'entrée d'un programme windows est la fonction WinMain, c'est une fonction normalisée ce qui signifie que c'est le système qui se charge d'appeler cette fonction quand l'application est lancée, en initialisant les différents paramètres. Le programme principal est donc de la forme suivante :

INT WINAPI WinMain (HINSTANCE hInst, HINSTANCE hPrevInst, LPSTR lpCmdLine, INT nCmdShow) { return 0; }

Le programme peut éventuellement retourner des erreurs gràce au return de la fin. Je rappelle ici que par convention, une fonction qui renvoie 0 indique une sortie correcte alors qu'une fonction qui renvoie autre chose (-1 le plus souvent) indique un problème lors de l'exécution.

Pour les besoins d'une application 3D, il est nécessaire d'avoir au moins une fenètre windows dans laquelle nous allons pouvoir effectuer le rendu DirectX. Pour créer une fenètre, il faut tout d'abord définir une classe de fenètre, c'est à dire préciser les caractéristiques génériques des fenètres de la classe. Sous windows, tous les composants de l'interface sont des fenètres, il existe donc des classes prédéfinies pour simplifier l'utilisation de l'api, c'est par exemple le cas des boutons, des listbox, ...

// Une structure qui sauvegarde les informations de la classe WNDCLASS wc; wc.style = 0; // Pointeur vers la "windows procedure" wc.lpfnWndProc = WndProc; wc.cbClsExtra = 0; wc.cbWndExtra = 0; // Instance de l'application wc.hInstance = hInstance; // Icone DirectX (ressource) wc.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_ICON)); // Curseur par défaut wc.hCursor = LoadCursor(NULL, IDC_ARROW); // Couleur de fond (géré par dx) wc.hbrBackground = NULL; // Pas de menu wc.lpszMenuName = NULL; // Nom de la classe wc.lpszClassName = "DX_Tut"; // Enregistrement de la classe de la fenètre RegisterClass(&wc);

Une fois la classe enregistrée, il est possible de créer la fenètre que nous allons utiliser dans notre programme

// Création de la fenètre HWND hwnd = CreateWindow( "DX_Tut", // Nom de la classe de la fenètre "DX - Tutoriel n°1", // Titre de la fenètre WS_OVERLAPPEDWINDOW, // Style de la fenètre 100, 100, // Position de la fenètre 800, 600, // Taille de la fenètre GetDesktopWindow(), //Fenètre mère NULL, // Menu wc.hInstance, // Instance de l'application NULL); // Pour les fenètres MDI // Affichage de la fenètre ShowWindow(hwnd, SW_SHOW);

Windows est un système d'exploitation qui utilise des messages pour dialoguer entre les applications mais également pour chaque éléments d'une application. Par exemple, lorsque l'utilisateur déplace sa souris au dessus d'une fenètre, le système envoit à l'application qui possède cette fenètre un message standardisé que le concepteur de l'application aura, ou n'aura pas s'il désire conserver un fonctionnement standard, pris en compte pour effectuer une opération spécifique. Lorsque nous désirons mettre en place une application windows, il faut donc écrire une boucle de message qui va permettre à l'application de récupérer les messages qui lui sont destinés et, par la suite, de les traiter de la façon adéquate. La boucle de message standard est de cette forme :

MSG msg; while (msg.message != WM_QUIT) { if (PeekMessage(&msg, NULL, 0U, 0U, PM_REMOVE)) { TranslateMessage(&msg); DispatchMessage(&msg); } }

C'est la boucle principale du programme qui pourrait se traduire par : "Tant que tu n'as pas le message quitter, tu analyses les messages et tu les transmets aux bonne personnes". Cette boucle de message va être modifiée par la suite pour indiquée à DirectX qu'il peut dessiner. Nous allons, lorsqu'il n'y a pas de messages à traiter (le else du PeekMessage() ), effectuer un rendu de la scène.

Nous avons donc mis au point une structure qui permet d'envoyer les messages aux bons endroits, c'est à dire à la fenètre que nous avons créé, qui est le seul composant dont nous disposons pour l'instant. Il est donc nécessaire d'écrire la fonction qui va gérer les messages reçus, c'est une fonction CALLBACK, c'est à dire une fonction qui va être appelée directement par le système avec les paramètres nécessaires initialisés correctement lors de la réception d'un message windows.

LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch (msg) { case WM_DESTROY: PostQuitMessage(0); break; } return DefWindowProc(hWnd, msg, wParam, lParam); }

Lorsque windows appelle cette fonction, il indique la fenètre qui est concernée, le message reçu et les paramètres du message. Pour chaque type de messages que nous voulons traiter (switch) nous effectuons le code correspondant. Dans notre exemple, lorsque la fenètre reçoit un message de destruction (quand on clique sur la croix par exemple), on envoie un message de sortie qui va permettre à l'application de se terminer. La dernière ligne permet de gérer les messages non traités de façon standard.

Enfin, il ne faut pas oublier, à la fin du programme de libérer la mémoire correctement en détruisant la fenètre que nous avons créé et de "désenregistrer" la classe de la fenètre.

// Destruction de la fenètre DestroyWindow(hwnd); // Désenregistrement de la classe de la fenètre UnregisterClass("DX_Tut",hInstance);

L'utilisation de direct3d

Pour pouvoir utiliser direct3d, il est nécessaire d'effectuer 2 opérations. La première consiste à créer un objet direct3d qui va permettre de gérer les capacités de direct3d, notamment d'énumérer les différentes cartes graphiques disponibles dans le système et la création de devices adaptés à la version de directx utilisée.

LPDIRECT3D8 d3d = Direct3DCreate8(D3D_SDK_VERSION);

Le paramètre de cette fonction permet de vérifier que les headers utilisés correspondent bien à la version de directx utilisée.

Une fois cet objet créé, il va nous permettre de créer un device qui servira à dialoguer avec une des cartes graphiques. Le device va permettre de modifier tous les paramètres d'affichages de direct3d (lumières, multitexturing, brouillard, ...), mais également de créer des textures, des formes, ... Pour l'initialiser, il faut définir les caractéristiques du backbuffer, rappelons que le backbuffer est une zone de mémoire tampon dans lequel direct3d va dessiner pour ensuite l'afficher "d'un coup". Cette façon de procéder évite les scintillements que l'on peut apercevoir en dessinant directement dans le front buffer, qui est la mémoire qui est affichée à l'écran. Ses caractéristiques sont principalement la taille (la résolution) et le format des couleurs utilisées. Le format est une partie délicate à gérer car certains formats ne sont pas pris en compte par certaines cartes graphiques, nous allons donc récupérer le format du bureau pour obtenir un format qui sera compatible.

// On récupère la résolution du bureau. D3DDISPLAYMODE d3ddm; d3d->GetAdapterDisplayMode(D3DADAPTER_DEFAULT, &d3ddm); // On définit une structure qui va permettre de paramètrer le device D3DPRESENT_PARAMETERS d3dpp; ZeroMemory(&d3dpp, sizeof(d3dpp)); d3dpp.Windowed = TRUE; d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD; d3dpp.BackBufferFormat = d3ddm.Format; // Création du device. d3d->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hwnd, D3DCREATE_SOFTWARE_VERTEXPROCESSING, &d3dpp, &device);

Nous pouvons remarquer que le device est paramétré via une structure (D3DPRESENT_PARAMETERS) qui va conserver les différentes caractéristiques du device. Le champ Windowed indique si l'application est en plein écran ou non, le champ SwapEffect précise comment DirectX doit gérer le passage du backbuffer au frontbuffer. Pour la création du device, nous demandons d'utiliser la première carte (celle par défaut), en mode hardware (D3DEVTYPE_HAL). nous fournissons également la fenètre que nous allons associer au device, cellle où le rendu se fera. Enfin, nous demandons une gestion logicielle des vertex étant donné que certaines cartes ne peuvent pas gérer les vertex de façon matérielle. Cette partie demandera bien évidemment des évolutions par la suite pour définir une configuration plus adaptée aux différentes cartes graphiques.

Le rendu de la scène

Comme je l'ai dit plus haut, le rendu de la scène est effectué lorsqu'il n'y a pas de messages à traiter. La base du rendu d'une scène est simple, on commence par effacer le backbuffer avec la couleur de fond.

device->Clear(0, NULL, D3DCLEAR_TARGET, 0xffffff00, 1.0f, 0);

Ensuite, on indique à direct3d que nous allons commencer à effectuer le rendu de la scène.

device->BeginScene();

Il faut alors effectuer le rendu de chaque objet de la scène.

Nous indiquons alors à direct3d que le rendu de la scène est terminé.

device->EndScene();

Enfin, nous effectuons la présentation du backbuffer contenant la représentation 2D de la scène 3D à l'écran.

device->Present(NULL, NULL, NULL, NULL);
Télécharger le projet Visual Studio 6 Visualiser les sources en ligne