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