Initiation à DirectX (DirectDraw)


Introduction :

Le but de DirectX est de faire de Windows une plate-forme intéressante pour la conception de jeux (oauis, bof pas terrible pour faire des jeux si ca plante toutes les 5 minutes, mais bon, c'est toujours mieux que Dos). Comme on le voit maintenant, tous les jeux sont sous Windows et utilisent DirectX. DirectX est un ensemble de bibliotheques (des DLL) qui sont utilises pour :
- des affichages rapides en utilisant un maximum les possibilites materielles des cartes video, realise par la partie DirectDraw
- des affichages 3D avec Direct3D (mais y'a OpenGl pour ça comme dirait Antoche)
- des effets sonores elabores avec DirectSound
- controler les peripheriques d'entree comme le clavier, la souris, les joystick en accedant directement aux peripheriques et en attendant pas que Windows bouge son cul pour nous envoyer les messages avec DirectInput
- des fonctionnalites multijoueurs sans avoir a se soucier des details relatifs au support utilise pour le transport des donnees grace a DirectPlay (l'avenir c'est les jeux en reseaux)

Dans ce superbe article (sans commentaires) y'a que DirectDraw qui est traite.
Pour le reste y'a le livre "Atelier DirectX" de chez Microsoft Press (bah ouais, y'a qu'eux qui peuvent sortir un livre dessus puisque c'est eux qui l'on fait pour essayer de ratraper Windows). Mais il est pas mal mais n'est pas tres "pedagogique" (un peut genre "voila les fonctions, demerde toi avec"). Ya aussi Borland C++ Builder 3 de chez Eyrolles qu'en cause un chapitre.
Pour utiliser DirectX, bah faut etre sous Windows et utiliser un compilateur C++ genre Visual C++ ou C++ Builder.
Ca marche aussi en C mais c'est plus chiant car DirectX est bâti autour du concept COM (Common Objet Model), c'est a dire qu'il utilise plein d'objet. Faut aussi avoir installe DirectX...

Sommaire :
I DirectDraw

1 L'objet DirectDraw
2 Les surfaces
3 La liberation
4 Les images a afficher
5 L'utilisation avec le GDI

I !!! DirectDraw !!!

DirectDraw c'est en fait un gestionnaire de memoire qui permet d'acceder directement a la memoire video et aux fonctions de la carte. DirectDraw, comme tous les composants de DirectX, se veut independants vis a vis du matos. Alors si on lui demande de faire une fonction genre etirement d'une image puis l'afficher apres eh bas DirectDraw il a 2 choix.
Soit la carte graphique sait le faire et le fait (niveau HAL)
Soit DirecDraw l'emule (niveau HEL).
On peut programmer alors sans penser au matos des autres.

1 L'objet DirectDraw

La premiere etape c'est l'initialisation de DirectDraw. Faut creer un objet DirecDraw. Bon d'abord faut inclure le fichier ddraw.h. Il est fourni par C++ Builder et il est dans le SDK de directX. Bon, le code doit ressembler a ça :

#include <ddraw.h>
LPDIRECTDRAW pDD;
...
if (DirectDrawCreate(NULL, &pDD, NULL) != DD_OK)
exit(1);

Les methodes de DirectX retournent DD_OK (de valeur 0) quand elles se sont bien deroulees.
LPDIRECTDRAW est synonyme de DIRECTDRAW *.

Le premier argument est soit l'addresse de l'identificateur unique du peripherique DirectDraw à utiliser ou bien l'une des 3 valeurs suivantes :
NULL utilise le peripherique principal
DDCREATE_EMULATIONONLY utilise uniquement HEL
DDCREATE_HARDWAREONLY utilse HAL avec le peripherique par defaut
En generale on met NULL, les 2 autres valeurs sont la pour les tests et le debogage.

Le troisieme argument n'est pas encore utilise et doit etre mis a NULL.

Voila, on vient de creer l'objet DirectDraw. Maintenant il faut specifier le mode d'utilisation et la resolution.
En general dans les jeux on travaille generalement en mode pleine ecran, avec acces exclusif :

pDD->SetCooperativeLevel(Handle, DDSCL_EXCLUSIVE | DDSCL_FULLSCREEN);

Le premier argument est le handle de la fenetre dans laquelle on utiliserat DirectDraw.
Le second argument peut avoir un ou plusieurs des indicateurs suivants :
DDSCL_EXCLUSIVE : acces exclusif a l'ecran (utilise avec DDSCL_FULLSCREEN)
DDSCL_FULLSCREEN : mode pleine ecran
DDSCL_ALLOWREBOOT : autorise le redemarage avec ctrl+Alt+Suppr quand on utilise les 2 indicateurs au dessus
DDSCL_NORMAL : affichages dans une fenetre Windows (moins bonnes performances)

Maintenant faut specifier la resolution :

pDD->SetDisplayMode(640, 480, 16);

Le troisieme argument c'est le nombre de bits de couleur.
SetDisplayMode retourne DDERR_INVALIDMODE s'il est impossible de placer la carte graphique dans le mode reclame.
Voila a quoi peut ressembler alors le code sous C++ Builder:

#include <ddraw.h>
...
LPDIRECTDRAW pDD;
...
// initialisation de l'object Direct Draw
if (DirectDrawCreate(NULL,&pDD,NULL) != DD_OK)
throw Exception ("Erreur sur DD Create");
if (pDD->SetCooperativeLevel(HandleF,DDSCL_EXCLUSIVE|DDSCL_FULLSCREEN) != DD_OK)
throw Exception ("Erreur de specification du mode d'utilisation");
if (pDD->SetDisplayMode(640,480,16) == DDERR_INVALIDMODE)
throw Exception ("Erreur lors de l'initialisation de la résolution");

2 Les surfaces

Une surface n'est qu'un tampon memoire gere comme un rectangle.
Une surface n'est pas oblige de s'afficher.
En generale on cree une surface principale (primary buffer) qui correspond a la surface d'affichage (ce qui est visible a l'ecran) ainsi qu'une surface secondaire (back buffer).
Cela permet de preparer le back buffer, ce qui ne provoque aucun affichage a l'ecran de d'echanger ensuite les surfaces primaires et secondaire. Comme il
s'agit la d'un simple echange de pointeurs, cette operation est particulierement rapide.
On peut utiliser la technique du tripple buffering (2 surfaces secondaires).

Pour creer une surface primaire ainsi qu'une surface secondaire, il faut d'abord initialiser une structure appelee DDSURFACEDESC (pour DirectDraw Surface
Description). On indique quelles informations doivent etre prises en compte par la fonction CreateSurface :

DDSURFACEDESC ddsd;
LPDIRECTDRAWSURFACE pDDSPrim;
...
memset(&ddsd, 0, sizeof ddsd);
ddsd.dwSize = sizeof(ddsd);
ddsd.dwFlags = DDSD_CAPS | DDSD_BACKBUFFERCOUNT;
ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE | DDSCAPS_FLIP | DDSCAPS_COMPLEX;
ddsd.dwBackBufferCount = 1;
pDD->CreateSurface(&ddsd, &pDDSPrim, NULL);

Voila, on a cree une surface primaire et une surface secondaire. Faut maintenant obtenir un pointeur sur la surface secondaire pour pouvoir y effectuer les operations de dessin :

LPDIRECTDRAWSURFACE pDDSSec;
DDSCAPS ddscaps;
...
ddscaps.dwcaps = DDSCAPS_BACKBUFFER;
pDDSPrim->GetAttachedSurface(&ddscaps, &pDDSSec);

Pour travailler avec 2 backs buffers il faut donner la valeur 2 a ddsd.dwBackBufferCount et appeler une deuxieme fois pDDSPrim->GetAttachedSurface, le troisieme argument contenant alors l'addresse d'un autre pointeur.
En generale on place toutes ses instructions dans la methode qui traite l'evenement OnCreate de la fenetre pour C++ Builder.
Pour Visual C++ on le place lors du traitement du message WM_CREATE.
Dans mes progs C++ Builder, ca ressemble a ceci :

#include <ddraw.h>
...
LPDIRECTDRAW pDD;
DDSURFACEDESC ddsd;
LPDIRECTDRAWSURFACE pDDSPrim;
LPDIRECTDRAWSURFACE pDDSSec;
DDSCAPS ddscaps;
...
// a utiliser au debut
// initialisation de l'object Direct Draw
if (DirectDrawCreate(NULL,&pDD,NULL) != DD_OK)
throw Exception ("Erreur sur DD Create");
if (pDD->SetCooperativeLevel(Handle,DDSCL_EXCLUSIVE|DDSCL_FULLSCREEN) != DD_OK)
throw Exception ("Erreur de specification du mode d'utilisation");
if (pDD->SetDisplayMode(640,480,16) == DDERR_INVALIDMODE)
throw Exception ("Erreur lors de l'initialisation de la résolution");
// preparation des surfaces de travail
memset(&ddsd, 0, sizeof ddsd);
ddsd.dwSize = sizeof(ddsd);
ddsd.dwFlags = DDSD_CAPS | DDSD_BACKBUFFERCOUNT;
ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE | DDSCAPS_FLIP | DDSCAPS_COMPLEX;
ddsd.dwBackBufferCount = 1;
pDD->CreateSurface(&ddsd, &pDDSPrim, NULL);
// enregistrement de la surface secondaire
ddscaps.dwCaps = DDSCAPS_BACKBUFFER;
pDDSPrim->GetAttachedSurface(&ddscaps, &pDDSSec);

En fait vous avez qu'a recopier

3 La liberation

On vient de creer 3 objets, il faut donc liberer l'espace memoire a la fin du programme.
Pour cela on execute les fonctions :

pDDSImage->Release(); // voir apres pour les images
pDDSPrim->Release();
pDD->Release();

4 Les images a afficher (enfin)

Bon, on passe au plus important. L'initialisation etant finie, on va pouvoir copier des images tres rapidement a l'ecran.
Il faut d'abord que l'image a afficher ne soit pas plus grandes que la surface de travail. Par exemple si on est en 800x600, l'image ne doit pas faire plus (c'est important).
Comme on peut copier une seule partie de l'image, les differentes images d'une animation sont generalement mises a la suite dans le meme fichier image. Pour eviter de distribuer un fichier image distinct de l'application, celui-ci est generalemtn place en ressource (c'est a dire greffe dans le programme).
Pour C++ Builder (ca doit aussi marcher pour Visual) on cree un fichier RC genre AppRes.rc que l'on inclut dans le projet et qui contient :

image BITMAP image.bmp

image etant le nom que l'on utilisera dans l'application, BITMAP specifiant que c'est une image et image.bmp etant le nom du fichier a inclure dans l'executable.

D'abord faut charger l'image en memoire. On utilise une fonction define dans les fichiers ddutil.h et ddutil.cpp du DirectX SDK.
Borland C++ Builder fournit ces deux fichiers dans le sous-repertoire des exemples. Faut donc include ddutil.h et ajouter ddutil.cpp au projet.

#include "ddutil.h"
...
LPDIRECTDRAWSURFACE pDDSImage;
...
pDDSImage = DDLoadBitmap(pDD, "image", 0, 0); // dans le cas du fichier greffe
pDDSImage = DDLoadBitmap(pDD, "image.bmp", 0, 0); // dans le cas d'un fichier a part

Les deux derniers arguments doivent etre mis a 0. La fonction retourne NULL en cas d'erreur. En cas de reussite, l'image est chargee en memoire dans la surface pointee par pDDSImage.

Dans le cas d'une utilisation de DirectX en 256 couleurs (totalement depasse) il faut extraire la palette du fichier image et forcer le pilote de la carte video a utiliser cette palette (mais je vous conseille de vous mettre au moins en 16 bits de couleurs) :

LPDIRECTDRAWPALETTE pDDPal;
...
pDDPal = DDloadPalette(pDD, "image");
if (pDDPal) pDDSprim->SetPalette(pDDPal);

DDloadPalette est une fonction egalement incluse dans le fichier DDutil.cpp.

Voila, maintenant on peut copier l'image a l'ecran. Mais ca copie que des rectangles.
Alors faut specifier une couleur de transparence, les pixels de la couleur de transparence ne seront alors pas affiche. Faut ecrire :

DDSetColorKey(pDDSImage, RGB(255,0,0));

Ca specifie le rouge comme couleur de transparence. Mais on peut mettre n'importe laquelle.
Maintenant, pour copier une partie de l'image on utilise BltFast.
Ca copie rapidement une partie de l'image sur la backbuffer de preference.

pDDSSec->BltFast(x, y, pDDSImage, &rc, DDBLTFAST_SRCCOLORKEY | DDBLTFAST_WAIT);

Les deux premiers arguments indiquent les coordonnees x et y dans la zone de destination (le backbuffer dans ce cas la).
Le troisieme argument designe la surface source.
Le quatrieme designe une structure RECT passee par adresse (les quatre champs de cette structure sont left,top,right et bottom). Cette structure RECT doit contenir les coordonnees dans la surface source du rectangle a copier.
Le cinquieme argument designe un type de transfert :
DDBLTFAST_DESTCOLORKEY : la couleur de tansparence est celle de la zone de destination
DDBLTFAST_NOCOLORKEY : pas de couleur de transparence
DDBLTFAST_SRCCOLORKEY : la couleur de transparence est celle de la zone source
DDBLTFAST_WAIT : la fonction est bloquante jusqu'a que ce termine le transfert

A moins de specifier DDBLTFAST_WAIT le transfert s'effectue de mainiere asynchrone, ce qui permet d'effectuer d'autres operations apres que DDBLTFAST_WAIT soir lance. Pour savoir l'etat du transfert, il faut executer (applique a la surface de destination) :

pDDSSec->GetBltStatus(flags);

Avec flags qui peut valoir l'un des deux indicateurs suivants :
DDGBS_CANBLT : GetBltStatus retourne DD_OK (de valeur 0) si une operation de transfert peut etre effectuee
DDGBS_ISBLTDONE : GetBltStatus retourne DD_OK si l'operation de transfert est terminee

Pour l'instant ya toujours rien a l'ecran. C'est normal, c'est tout sur la back-buffer. Pour l'afficher, faut faire :

pDDSPrim->Flip(NULL, 0);

Le second argument pourrait etre DDFLIP_WAIT pour que la fonction ne retourne qu'apres achevement de l'echange. Sinon faut executer GetFlipStatus(flags) où flags peut prendre l'une des deux valeurs suivantes : DDGFS_CANFLIP ou DDGFS_ISFLIPDONE. GetFlipStatus se comporte comme GetBltStatus.

Voila c'est tout. Maintenant on peut afficher tous ce qu'on veut. Sauf du texte. Pour ca, faut faire appel au GDI, c'est a dire a l'API Windows.

5 L'utilisation du GDI

Pour utiliser le GDI, il faut faire les etapes suivantes :
1 obtention d'un contexte de peripherique (DC)
2 utilisation du GDI
3 liberation du contexte de peripherique

Par exemple pour afficher Salut au point x,y on fait :

HDC hDC;
pDDSec->GetDC(&hDC);
char buf[] = "Salut";
TextOut(hDC,x,y,buf,lstrlen(buf));
pDDSec->Release(hDC);

i_jeune

PARMENTIER Jean-François
jf_parmentier@oreka.com