Réaliser un effet graphique avec DirectX sous DELPHI par :
Plouf
(plouf@win.be ou theplouf@yahoo.com)

PARTIE II (2/3)

Chapitre 2 : Le DirectDraw
.
a) Notion de surface
Une surface est un objet dans lequel on peut dessiner. Jusque là, rien de bien compliqué. Il existe deux types de surfaces : les surfaces primaires "Primary Surface" et les surfaces secondaires "Seconday Surface".
Les surfaces primaires correspondent à l'écran. C'est à dire que tout ce qui y sera dessiné sera directement affiché à l'écran physique. On peut comparer cette surface avec un pointeur dont l'adresse correspond à A000:0000 en mode 13h sous Dos.
A l'inverse, les surfaces secondaires sont des surfaces qui se trouvent "ailleurs", soit dans la RAM, soit dans la mémoire vidéo dans une autre page d'écran.
Une surface est dite complexe (Complex Surface) si elle se décompose en plusieurs sous-surfaces, appelées BackBuffer Surface. On pourra passer d'une sous-surface à une autre en disant "au suivant". Quand on est passé par la dernière sous-surface, on repasse à la première.
Un exemple pour bien saisir le principe et l'utilité :
Nous créons une surface primaire complexe constituée de deux sous-surface. Seulement une de ces deux sous-surface est affichée à l'écran, pendant que nous pouvons dessiner sur la seconde. A un moment, on passe à la suivante, ce qui a pour conséquence de littéralement inverse les deux surfaces, permettant ainsi un page flip en douceur.
On peut imposer à DirectDraw de placer une surface en mémoire RAM plutôt qu'en mémoire vidéo. Cela sert en général quand il faut pouvoir à la fois LIRE et ECRIRE sur l'écran (pour un effet feu par exemple), car en général une seule de ces deux opérations est possible à la fois.
En général, il y a trois façons d'utiliser les surfaces :
- On utilise la mémoire vidéo, on déclare une surface primaire complexe composée de deux sous-surfaces et on utilise l'écran avec un page flip classique.
- On utilise la mémoire vidéo, on déclare une surface primaire simple, et on passe son temps à y copier le contenu d'un buffer depuis la mémoire de l'application où l'image aura été dessinée (C'est en général comme cela que ça se passait avec le mode 13h sous Dos)
- On utilise RAM, on y déclare une surface primaire complexe composée de deux sous-surfaces, on trace dans la seconde sous-surface tandis que la demande de flip ("au suivant") provoque en réalité la COPIE de cette seconde sous-surface vers l'écran, symbolisé par la première sous-surface.
On ne dessine jamais directement sur l'écran quand il est en RAM, on passe toujours par la sous-surface non affichée, car sinon cela est bien trop lent. A chaque octet envoyé dans la RAM, DirectDraw doit effectuer l'opération similaire vers l'écran, je vous laisse imaginer le faste de ce genres d'opérations.
Voyons à présent les avantages de chacune de ces méthodes :
- Le page flipping : marche assez bien pour les routines 3D où l'écran est de toutes façons totalement effacé d'une image à une autre. On prend la sous-surface non affichée, on efface tout son contenu, on y trace l'image suivante, on flipe et on boucle.
- La mémoire vidéo avec buffer en RAM : pratique pour les effets où il faut utiliser l'image précédente pour générer l'image suivante, ou s'il faut pouvoir lire le contenu de l'écran au fur et à mesure de son traçage (On en aura besoin ici pour les MetaBalls). Il y a cependant un problème ennuyant : puisque notre buffer se trouve chez nous, DirectDraw n'en a pas connaissance et on ne pourra pas se servir des primitives Windows pour y tracer quoi que ce soit.
- La sous-surface en RAM : quand on y réfléchit, cela revient exactement à ce que l'on fait dans le point précédant, à la différence que cette fois, DirectDraw connaît le buffer en ram vu que c'est lui qui l'a créé. Cela fait que nous pourrons y utiliser les primitives Windows, et c'est cette solution que nous adopterons pour notre programme.
.
ATTENTION : il faut bien se souvenir qu'avec cette troisème méthode, DirectDraw COPIE entièrement le buffer vers l'écran, même si pratiquement rien n'a changé. Ca peut être un gaspillage incroyable, et cela devient invivable avec des résolutions de l'ordre de 800x600 en 32bits. Mais pour notre effet qui sera en 640x480 en 8bit, cela fait parfaitement l'affaire. (Et puis, on peut toujours espérer que DirectDraw trouvera un support Hardware pour la copie, ce que l'on ne peut pas envisager si c'est nous qui faisons directement la copie).
.
b) Un peu de pratique
Parce que bon, c'est un peu vague tout cela... On est toujours incapables d'afficher un pixel à l'écran...
Primo et avant toutes choses, il va vous falloir les headers du DirectX que vous pouvez aller charger à l'adresse suivante :
Normalement il vous suffit de déziper les fichiers dans le répertoire Lib de Delphi et le problème est réglé.
Vous devez utiliser l'unité DDraw.
Donc il vous faut d'abord un OBJET DIRECTX. Ben waip, nos amis de chez Microsoft ont décidé qu'on se la jouerait à la POO, donc tant pis, c'est comme ça que ça marche. Créons une variable de type IDirectDraw. Il va nous falloir également une surface pour l'affichage, une surface complexe qui plus est, accompagnée d'une BackBuffer Surface.
Au moment de créer les surfaces, il faudra passer en paramètre à une des fonctions de DirectX une structure contenant les instructions qu'on lui donne. Cette structure sera de type TDDSURFACEDESC et nous créons donc également une variable de ce type.
Enfin, il nous faudra une palette des couleurs comme pour tout mode qui se respecte. Nos couleurs devront être stockées dans un tableau de 256 éléments de type PaletteEntry. Il faudra également un objet palette en lui-même, de type IDirectDrawPalette. PaletteEntry se trouve dans l'Unit Windows il faudra donc l'utiliser aussi.
Ca a l'air beaucoup d'un coup, mais rassurez-vous, après c'est tout :)
Tout cela nous donne ceci :
.
Var (..)
lpDD:IDirectdraw;
Surface,Buffer:IDirectDrawSurface;
ddSD:TDDSURFACEDESC;
Palette:array[0..255] of PaletteEntry;
ddPalette:IDirectDrawPalette;
.
Bien, alors la première chose, c'est de créer cet objet de type IDirectDraw : très simple, nous appelons une des Api du DirectX :
If DirectDrawCreate(Nil,lpDD,Nil)<>DD_OK Then Exit;
.
Suite à cet appel, lpDD contient un objet de type IDirectDraw. C'est à partir de lui que nous allons créer nos surfaces. Mais avant cela, pensons à lui expliquer ce que nous attendons de lui : nous voulons tout l'écran pour nous, en fullscreen, tout en autorisant les appels Ctrl+Alt+Del on ne sait jamais si ça plante :
If lpDD.SetCooperativeLevel(MyForm.Handle,DDSCL_EXCLUSIVE OR DDSCL_FULLSCREEN OR DDSCL_ALLOWMODEX OR DDSCL_ALLOWREBOOT)<>DD_OK Then Exit;
.
Remarquez qu'il doit connaître le handle de notre fenêtre. Ha bon...
Ensuite donnons-lui notre résolution :
If lpDD.SetDisplayMode(640,480,8)<>DD_OK Then Exit;
.
Bien, c'est déjà une bonne chose, il nous faut à présent créer nos surfaces : pour cela nous allons une nouvelle fois appeler une méthode lpDD, mais il nous faudra lui donner une structure. C'est notre fameuse structure TDDSURFACEDESC.
.
Nous commençons par tout mettre à zéro, par prudence :
Fillchar(ddSD,SizeOf(ddSD),0);
.
Ensuite, expliquons ce qu'on veut comme surface. Tout d'abord il lui faut la taille de la structure, pour vérifier que tout va bien :
ddSD.dwSize:=SizeOf(ddSD);
.
Ensuite, nous devons lui dire quels sont les champs de la structure qu'il doit effectivement prendre en compte. Nous lui expliquons qu'il faudra qu'il regarde le contenu des caractéristiques ainsi que le nombre des sous-buffers. ddSD.dwflags:=ddSD_caps or
ddSD_backbuffercount;
.
Et puisqu'on lui a dit qu'il fallait qu'il regarde les caractéristiques, il s'agit de les entrer : une surface primaire en ram, de type complexe qui supporte le flip.
ddSD.ddscaps.dwcaps:=ddscaps_primarysurface or ddscaps_systemmemory or ddscaps_complex or ddscaps_flip;
.
Et puis également pour le nombre de sous-buffers de cette surface complexe : ATTENTION : même si ici nous avons une surface composée de deux sous-buffers, nous devons lui dire "1", car il y a "une surface en plus que celle de base".
ddSD.dwbackbuffercount:=1;
.
Nous pouvons donc créer la surface :
If lpDD.CreateSurface(ddSD,Surface,Nil)<>DD_OK Then Exit;
.
A partir de cet endroit, Surface contient un objet de type IDirectDrawSurface. A ce moment, il nous semble interessant d'avoir également un pointeur vers la buffer secondaire, car ce que nous donne l'objet Surface, c'est en fait le pointeur vers la première surface, celle qui est affichée à l'écran, et c'est justement l'autre qui nous interesse :
ddSD.ddscaps.dwCaps:=DDSCAPS_BACKBUFFER;
If Surface.GetAttachedSurface(ddSD.ddscaps,Buffer)<>DD_OK Then Exit;
.
A partir d'ici, Buffer contient un objet de type IDirectDrawSurface, mais cette surface représente la surface non affichée.
Bon, il est temps de songer à notre palette des couleurs, remplissons notre tableau de couleurs, on va se taper un beau dégradé de gris :
.
For I:=0 To 255 Do Begin
Palette[I].peRed:=I;
Palette[I].peGreen:=I;
Palette[I].peBlue:=I;
End;
If lpDD.CreatePalette(DDPCAPS_8BIT,@Palette,ddPalette,Nil)<>DD_OK Then Exit;
Surface.SetPalette(ddPalette);
.
Et enfin, un truc qu'on oublie toujours : marquer le curseur de la souris :
ShowCursor(False);
.
Pfou...
Ben c'est fini, l'écran est en 640x480x8 et nous avons tout contrôle dessus. Avant d'y afficher quoi que ce soit, pensons déjà à la désinitialisation :
lpDD.RestoreDisplayMode;
ShowCursor(True);
ddPalette:=nil;
Buffer:=nil;
Surface:=nil;
lpDD:=nil;
.
Dans l'état actuel de notre progression, notre programme se présente comme suit :
program Effect;
.
uses
Forms,Windows,DDraw;
.
var
IsTerminated:Boolean;
MyForm:TForm;
lpDD:IDirectdraw;
Surface,Buffer:IDirectDrawSurface;
ddSD:TDDSURFACEDESC;
Palette:array[0..255] of PaletteEntry;
ddPalette:IDirectDrawPalette;
I:Integer;
.
Procedure Key(Sender: TObject; Var Key: Char);
Begin
IsTerminated:=True;
End;
.
begin
IsTerminated:=False;
MyForm:=TForm.Create(Nil);
MyForm.BorderStyle:=bsNone;
@MyForm.OnKeyPress:=@Key;
MyForm.Show
.
If DirectDrawCreate(Nil,lpDD,Nil)<>DD_OK Then Exit;
If lpDD.SetCooperativeLevel(MyForm.Handle,DDSCL_EXCLUSIVE OR DDSCL_FULLSCREEN OR DDSCL_ALLOWMODEX OR DDSCL_ALLOWREBOOT)<>DD_OK Then Exit;
If lpDD.SetDisplayMode(640,480,8)<>DD_OK Then Exit;
Fillchar(ddSD,SizeOf(ddSD),0);
ddSD.dwSize:=SizeOf(ddSD);
ddSD.dwflags:=ddSD_caps or ddSD_backbuffercount;
ddSD.ddscaps.dwcaps:=ddscaps_primarysurface or ddscaps_systemmemory or ddscaps_complex or ddscaps_flip;
ddSD.dwbackbuffercount:=1; If lpDD.CreateSurface(ddSD,Surface,Nil)<>DD_OK Then Exit;
ddSD.ddscaps.dwCaps:=DDSCAPS_BACKBUFFER;
If Surface.GetAttachedSurface(ddSD.ddscaps,Buffer)<>DD_OK Then Exit;
For I:=0 To 255 Do Begin
Palette[I].peRed:=I;
Palette[I].peGreen:=I;
Palette[I].peBlue:=I;
End;
If lpDD.CreatePalette(DDPCAPS_8BIT,@Palette,ddPalette,Nil)<>DD_OK Then Exit;
Surface.SetPalette(ddPalette);
ShowCursor(False);
MyForm.WindowState:=wsMaximized;
.
Repeat
Application.ProcessMessages;
Until IsTerminated;
MyForm.Hide;
lpDD.RestoreDisplayMode;
ShowCursor(True);
ddPalette:=nil;
Buffer:=nil;
Surface:=nil;
lpDD:=nil;
MyForm.Release;
end.
.
Il ne fait rien de plus que de changer la résolution, d'attendre une touche et revenir sous Windows.
.
c) Le premier dessin
Bien, nous avons des surfaces et tout le tralala dont on a besoin pour dessiner : il ne nous reste plus qu'à dessiner vraiment à l'écran.
Nous allons écrire une nouvelle procédure que l'on appelera à chaque tour de boucle, appelons là Show. Dans cette procédure, il faudra demander un pointeur vers notre surface secondaire, dessiner dans ce pointeur, libérer le pointeur et demander le "flipping" afin que s'opère la copie de notre image vers l'écran.
Pour demander ce pointeur, il faut utiliser la méthode Lock sur l'objet Buffer. Nous devons lui passer un pointeur vers une structure de type tddsurfacedesc qu'il remplira alors des informations qui nous seront utiles. Une fois que nous aurons terminé notre image, il nous faudra libérer le pointeur en appelant UnLock. Cela est impératif, on ne peut pas locker une bonne fois pour toute la surface, il faut la libérer à chaque tour de boucle. Cela se présente comme ceci :
Procedure Show;
Var SurfaceDesc:TDDSurfaceDesc;
Begin
SurfaceDesc.dwSize:=SizeOf(SurfaceDesc);
If Buffer.Lock(Nil,SurfaceDesc,0,0)<>DD_OK Then Begin
IsTerminated:=True;
Exit;
End;
(...)
Buffer.UnLock(SurfaceDesc.LPSurface);
lpDD.WaitForVerticalBlank(DDWAITVB_BLOCKBEGIN,0);
Surface.Flip(Nil,0);
End;
.
Une fois l'appel Lock effectué, la structure SurfaceDesc contient une multitude d'informations, mais seulement deux champs vont réelement nous servir : SurfaceDesc.LPSurface : il s'agit d'un pointeur vers la zone mémoire de la surface. On y dessine comme on y dessinerait en mode 13H sous Dos.
SurfaceDesc.LPitch : contient une valeur qui est le Pitch de l'écran. Cette valeur peut parraître curieuse si l'on en connaît pas son origine. En mode 13H, vous êtes bien sûr habitués à avoir une longueur de ligne de 320pixels, et par conséquent une longueur équivalant à 320octets. Il semble à nouveau évident que si on était en 32bit, la longueur de cette même ligne serait de 320x4 octets. Mais tout n'est pas si simple, car parfois la longueur de la ligne en mémoire EST PLUS GRANDE que la longueur réelement affichée, afin de garantir un alignement plus facile dans la mémoire. Il n'est pas rare en effet dans les modes 640x480x8 d'avoir une longueur de ligne en mémoire de 1024 octets. La coordonée d'un pixel se calcule alors selon la formule X+Y*Pitch et non plus X+Y*640.
Il y a bien sur neuf chances sur dix que Pitch valle 640, mais on ne sait jamais...
.
Créons une nouvelle routine : DrawPixel. Cette routine se présente comme ceci :
Procedure DrawPixel(X,Y:Integer;C:Byte;Desc:TDDSurfaceDesc);
Type TScreen=Array Of Byte;
Begin
If (X>=0) And (X<=639) And (Y>=0) And (Y<=479) Then Begin
TScreen(Desc.LPSurface)[X+Y*Desc.LPitch]:=C;
End;
End;
.
Ce qu'elle fait semble clair : elle affiche le plus simplement du monde un pixel dans ce buffer. Elle est loin d'être optimisée mais tel n'est pas encore le but ici...
.
Ensuite nous pouvous créer par exemple une routine DrawRectangle :
Procedure DrawRectangle(X,Y,W,H:Integer;C:Byte;Desc:TDDSurfaceDesc);
Var I,J:Integer;
Begin
For J:=Y To Y+H-1 Do Begin
For I:=X To X+W-1 Do Begin
DrawPixel(I,J,C,Desc);
End;
End;
End;
.
Bien sûr cette routine est d'une lenteur appocalyptique mais on s'en fiche pour le moment...
Nous pouvons remplacer le (...) de tout à l'heure dans Show par
DrawRectangle(Random(640),Random(480),Random(50)+10,Random(50)+10,Random(256),SurfaceDesc);
.
Normalement, si vous n'oubliez pas d'appeler Show dans la routine principale avant d'appeler Application.ProcessMessages, vous devriez voir apparaître des rectangles à l'écran.
Voici le programme à peine modifié qui se contente d'afficher un rectangle se déplaçant à l'écran pour montrer que l'on peut tout aussi bien des des choses animées si l'on oublie pas d'effacer le buffer avant chaque mise à jour : remarquez le FillChar.
.
Program Effect;
.
Uses
Forms,Windows,DDraw;
.
Var
IsTerminated:Boolean;
MyForm:TForm;
lpDD:IDirectdraw;
Surface,Buffer:IDirectDrawSurface;
ddSD:TDDSURFACEDESC;
Palette:array[0..255] of PaletteEntry;
ddPalette:IDirectDrawPalette;
I:Integer;
X,Y,Dx,Dy:Integer;
.
Procedure Key(Sender: TObject; Var Key: Char);
Begin
IsTerminated:=True;
End;
.
Procedure DrawPixel(X,Y:Integer;C:Byte;Desc:TDDSurfaceDesc);
Type TScreen=Array Of Byte;
Begin
If (X>=0) And (X<=639) And (Y>=0) And (Y<=479) Then Begin
TScreen(Desc.LPSurface)[X+Y*Desc.LPitch]:=C;
End;
End;
.
Procedure DrawRectangle(X,Y,W,H:Integer;C:Byte;Desc:TDDSurfaceDesc);
Var I,J:Integer;
Begin
For J:=Y To Y+H-1 Do Begin
For I:=X To X+W-1 Do Begin
DrawPixel(I,J,C,Desc);
End;
End;
End;
.
Procedure Show;
Var
SurfaceDesc:TDDSurfaceDesc;
Begin
SurfaceDesc.dwSize:=SizeOf(SurfaceDesc);
If Buffer.Lock(Nil,SurfaceDesc,0,0)<>DD_OK Then Begin
IsTerminated:=True;
Exit;
End;
Inc(X,Dx);Inc(Y,Dy);
If (X<0) Or (X>639-50) Then Begin Dx:=-Dx;Inc(X,Dx);End;
If (Y<0) Or (Y>479-50) Then Begin Dy:=-Dy;Inc(Y,Dy);End;
FillChar(SurfaceDesc.LPSurface^,480*SurfaceDesc.LPitch,0);
DrawRectangle(X,Y,50,50,255,SurfaceDesc);
Buffer.UnLock(SurfaceDesc.LPSurface);
lpDD.WaitForVerticalBlank(DDWAITVB_BLOCKBEGIN,0);
Surface.Flip(Nil,0);
End;
.
Begin
IsTerminated:=False;
MyForm:=TForm.Create(Nil);
MyForm.BorderStyle:=bsNone;
@MyForm.OnKeyPress:=@Key;
MyForm.Show;
.
If DirectDrawCreate(Nil,lpDD,Nil)<>DD_OK Then Exit;
If lpDD.SetCooperativeLevel(MyForm.Handle,DDSCL_EXCLUSIVE OR DDSCL_FULLSCREEN OR DDSCL_ALLOWMODEX OR DDSCL_ALLOWREBOOT)<>DD_OK Then Exit;
If lpDD.SetDisplayMode(640,480,8)<>DD_OK Then Exit;
Fillchar(ddSD,SizeOf(ddSD),0);
ddSD.dwSize:=SizeOf(ddSD);
ddSD.dwflags:=ddSD_caps or ddSD_backbuffercount;
ddSD.ddscaps.dwcaps:=ddscaps_primarysurface or ddscaps_systemmemory or ddscaps_complex or ddscaps_flip;
ddSD.dwbackbuffercount:=1;
If lpDD.CreateSurface(ddSD,Surface,Nil)<>DD_OK Then Exit;
ddSD.ddscaps.dwCaps:=DDSCAPS_BACKBUFFER;
If Surface.GetAttachedSurface(ddSD.ddscaps,Buffer)<>DD_OK Then Exit;
For I:=0 To 255 Do Begin
Palette[I].peRed:=I;
Palette[I].peGreen:=I;
Palette[I].peBlue:=I;
End;
If lpDD.CreatePalette(DDPCAPS_8BIT,@Palette,ddPalette,Nil)<>DD_OK Then Exit;
Surface.SetPalette(ddPalette);
ShowCursor(False);
MyForm.WindowState:=wsMaximized;
.
X:=0;
Y:=0;
Dx:=1;
Dy:=1;
.
Repeat
Show;
Application.ProcessMessages;
Until IsTerminated;
MyForm.Hide;
lpDD.RestoreDisplayMode;
ShowCursor(True);
ddPalette:=nil;
Buffer:=nil;
Surface:=nil;
lpDD:=nil;
MyForm.Release;
End.
.
d) Approfondissements
Evidement ici je n'ai pas pu entrer dans les détails. Je ne me suis même pas pris la peine de détailler les paramètres des diverses méthodes de DirectX que j'ai utilisé. En général, le code ci-dessus suffira, mais je vous conseille de charger le SDK de DirectX (il ne fait que 8Mo après tout) depuis le link se trouvant normalement quelque part sur le site de Keskivoufo pour pouvoir utiliser à fond le DirectX.
Il faut tout de même faire quelques remarques :
Quand on locke une surface, il se peut que la réponse soit autre chose que DD_OK. Habituellement ça sera DDERR_SURFACELOST, ce qui signifie que quelque chose s'est passé entraînant le passage à l'avant plan d'autre chose que notre fenêtre. Si une telle chose arrive, il faudra attendre que notre fenêtre repasse à l'avant-plan et RECREER LES SURFACES avant de pouvoir continuer. En général cela se produit quand une application affiche un message ou bien quand vous appuiez sur alt+tab. La politique utilisée par notre programme est tout simplement de quitter quand une telle chose arrive.
Un oeil attentif aura peut-être repéré que j'ai déplacé MyForm.WindowState:=wsMaximized APRES avoir créé les surfaces et changé le mode d'écran. Bien qu'il faut que la fenêtre soit créée et visible pour correctement configurer l'écran, le changement de résolution entraîne des comportements anormaux en ce qui concerne l'état de maximisation de la fenêtre. Pour être tout à fait précis : si la résolution est effectivement changée (c'est à dire si elle est différente du bureau), la fenêtre est automatique maximisée. Mais par contre, si la résolution ne change pas, la fenêtre, même si elle était maximisée AVANT l'appel, se retrouvera en mode normal! Ne me demandez pas de vous expliquer pourquoi, je n'en sais rien. Afin d'éviter toutes surprises, il vaut mieux maximiser la fenêtre après avoir créé les surfaces.
Il est bien entendu possible de changer la palette des couleurs pendant l'exécution du programme, je n'ai montré ici que la configuration statique de la palette. Il faut utiliser la méthode SetEntries sur notre objet IDirectDrawPalette. Vous aurez tous les détails dans les références de DirectDraw.
Comme son nom l'indique, lpDD.WaitForVerticalBlank(DDWAITVB_BLOCKBEGIN,0) attends la fin du balayage vertical de l'écran. Cette ligne n'est pas obligatoire, bien entendu.
Evitez de trop faire les marioles une fois que vous être en mode FullScreen : si votre programme plante, vous aurez souvent un mal de chien à récupérer la machine. Ici cela peut se faire grâce à l'autorisation d'utiliser Ctrl+Alt+Del (AllowReboot) afin de killer l'application, mais si vous oubliez (ou interdisez) de la donner, soyez prudents. De même, évitez le pas à pas quand votre programme est en full-screen.

Retour | PARTIE I | PARTIE III

Hit-Parade