Introduction à DirectX 9 en Delphi

Pierre Castelain

Introduction à l'utilisation des interfaces COM de DirectX 9 en Delphi par la programmation d'un exemple d'animation 2D simple.

- Titre : Introduction à DirectX 9 en Delphi

- Auteur : Pierre Castelain

- Parution : 7 décembre 2003

- Mise à jour : 30 décembre 2019

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2003 Pierre Castelain. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Remerciements

- II. Qu'est-ce que DirectX ?

- III. Comment se présente DirectX 9 ?

- IV. Comment utiliser DirectX avec Delphi ?

- V. Description du projet MiniDemo

- VI. Mise en œuvre

- VII. Énumération des adaptateurs, formats et modes

- VIII. Animation

- IX. Et maintenant ?

- X. Liens

I - Remerciements

Je tiens à remercier tout particulièrement les personnes suivantes pour leur aide précieuse lors de la rédaction et de la relecture de cet article :

- Alacazam,

- Nono40 : https://nono40.developpez.com,

- hachesse : https://hachesse.developpez.com,

- Olivier Constans : http://www.developpez.org/club/g3dbuilder.

II - Qu'est-ce que DirectX ?

DirectX est une API permettant de développer des applications multimédias rapides sans avoir à manipuler le matériel directement (ce qui est d'ailleurs interdit sous Windows NT). DirectX 9 (la dernière version disponible au moment où cet article est écrit) comprend un certain nombre de sous-systèmes que nous allons lister :

- DirectX Graphics est la réunion des anciens composants DirectDraw et Direct3D. Ce composant permet de créer des applications graphiques 2D et 3D performantes ;

- DirectInput est le composant chargé de la gestion des périphériques d'entrée comme les joysticks, gamepads, souris, etc. ;

- DirectPlay vous permet de créer des applications en réseau comme des jeux multijoueurs ;

- DirectSetup vous permet d'installer tout l'ensemble DirectX sans vous soucier du déploiement des divers fichiers nécessaires ;

- DirectMusic est le composant utilisé pour jouer de la musique dans vos applications ;

- DirectSound est lui orienté vers l'enregistrement ou la diffusion de sons ;

- DirectShow permet d'enregistrer ou de jouer des vidéos.

Dans cet article nous nous limiterons à l'utilisation de DirectX Graphics pour créer une petite application de démonstration utilisant les fonctions de l'API graphique.

III - Comment se présente DirectX 9 ?

DirectX 9 est basé sur l'architecture COM et se présente sous la forme d'interfaces.

Pour ceux qui n'auraient aucune notion de COM, disons qu'une interface COM est l'équivalent d'une classe Delphi. L'avantage de ces interfaces est d'être disponibles dans n'importe quel langage capable d'utiliser COM, voire dans certaines applications possédant un système de script ou un interpréteur embarqué (la gamme Microsoft Office par exemple). Les puristes me pardonneront cette simplification, mais cet article n'a pas pour but d'expliquer les arcanes de COM.

Globalement, nous disposons de fonctions permettant de créer une des interfaces de DirectX. Celles-ci nous permettent ensuite de créer d'autres objets par l'intermédiaire de leurs méthodes (une fonction ou procédure de classe est appelée méthode).

Si vous jetez un coup d'œil aux documents traitant de DirectX 9, vous verrez que ces interfaces contiennent un numéro indiquant la version de DirectX concernée. Par exemple l'interface Direct3D principale de DirectX 9 s'appelle IDirect3D9. Cette règle est guidée par un principe simple édicté par COM : il est strictement interdit de modifier une interface après que celle-ci a été rendue publique. L'ajout de fonctionnalités ne peut donc se faire qu'en créant de nouvelles interfaces. DirectX n'échappe pas à cette règle et propose donc des interfaces différentes pour chaque version de DirectX.

IV - Comment utiliser DirectX avec Delphi ?

Microsoft propose un SDK (Source Development Kit) très complet que je vous invite à télécharger si vous avez une bonne connexion (le dernier en date pèse environ 190 Mo). Vous y trouverez de nombreux exemples, documents et surtout l'ensemble des fichiers headers. Malheureusement pour nous, ceux-ci sont dédiés à Visual Studio et C++, et ne sont donc pas d'un grand secours pour les utilisateurs de Delphi. Il existe tout de même quelques conversions (ou portages) de ces fichiers sous Delphi. C'est l'une de celles-ci que nous allons utiliser pour cet article, celle d'Alexey Barkovoy. Cette conversion présente l'avantage d'être la plus complète et d'avoir été adoptée par le projet Jedi.

Ce SDK est disponible à partir du site http://www.delphi-jedi.org. Cliquez sur le lien « Delphi Graphics » et cliquez enfin sur le lien « Delphi DirectX header translations ». Attention, le site est généralement très long à s'afficher et le téléchargement des fichiers nécessaires peut prendre un certain temps. Voici la liste des fichiers indispensables à cet article :

- Clootie_DirectX90_Small.zip : cette archive contient uniquement les unités Delphi compatibles avec les standards Jedi. Ce sont celles que nous utiliserons ;

- Clootie_DX90_dlls.zip : cette archive contient deux dll utiles pour notre exemple. Nous en reparlerons plus loin.

Pour votre confort, ces fichiers ont été mis en miroir sur ce site afin de vous permettre de les télécharger si jamais vous aviez des difficultés avec le site originel. Je vous conseille toutefois de ne les utiliser qu'en dernier recours, car ils ne seront plus mis à jour après la rédaction de cet article. Pour obtenir les dernières versions, vous devrez donc toujours passer par le site du projet Jedi. Vous trouverez les liens vers ces fichiers en fin d'article.

L'installation de ce SDK est simple. Décompressez tout d'abord le fichier Clootie_DirectX90_Small.zip dans un dossier de votre choix et ajoutez le chemin du dossier JEDI qu'il contient dans la liste des chemins de bibliothèques de Delphi (Menu Outils-Options d'environnement, onglet Bibliothèque).

Le fichier Clootie_DX90_dlls.zip contient deux dll que vous pourrez copier dans le dossier de votre projet Delphi ou dans le dossier système de Windows. Je vous conseille toutefois d'utiliser la première option. Même si elle est plus fastidieuse, car elle vous oblige à copier ces fichiers à chaque nouveau projet, elle vous permet de ne pas mélanger d'éventuelles versions différentes de ces dll. Vous saurez ainsi toujours quelle version est utilisée par votre programme.

Enfin, il vous faudra bien évidemment avoir installé le runtime DirectX 9 sur votre machine. Celui-ci est disponible en téléchargement sur le site de Microsoft : http://www.microsoft.com/directx.

Si vous ne téléchargez pas le SDK complet de Microsoft, vous pourrez trouver toutes les documentations sur le site de MSDN : http://msdn.microsoft.com/directx.

V - Description du projet MiniDemo

Notre projet est divisé en deux étapes : la première nous permettra de choisir le type d'affichage que nous allons utiliser pour notre programme, la seconde contiendra le code de tracé proprement dit.

Le but est d'afficher en plein écran une image (un sprite) se déplaçant sur un fond uni et rebondissant contre les bords de celui-ci. Je vous l'accorde, ce n'est pas encore du niveau des jeux disponibles sur le marché, mais c'est un bon début. Une version améliorée du programme sera également présentée. Celle-ci sera enrichie d'un fond changeant, d'un joli logo statique ainsi que d'une indication de la vitesse de notre programme : les FPS chers aux gamers.

L'enchaînement des instructions sera donc le suivant :

- création des objets DirectX nécessaires ;

- énumération des modes d'affichage disponibles en fonction de la carte vidéo et du moniteur ;

- sélection d'un de ces modes ;

- création de la zone d'affichage DirectX (device) ;

- création d'une texture servant à dessiner le sprite en utilisant un fichier image au format PNG (qui permet la transparence) ;

- création du sprite ;

- boucle de tracé pouvant être interrompue par l'appui sur la touche Echap ;

- et enfin libération des objets instanciés.

Remarque importante : il est quasiment impossible d'utiliser le système d'exceptions de Delphi dans une application DirectX en mode plein écran, car le déclenchement d'une telle exception bloque l'application alors que le débogueur se trouve à l'arrière-plan. Si une exception survient, vous serez presque certainement obligé de « tuer » votre programme et également l'IDE Delphi. Il vous faudra donc être extrêmement attentif à ne pas vous tromper dans le code que vous écrirez. Il existe bien une solution technique, mais celle-ci dépasse largement le cadre de cette initiation.

VI - Mise en œuvre

Commencez par créer un nouveau projet et appelez-le MiniDemo. Nommez ensuite la fenêtre MainForm et enregistrez-la sous le nom MainFrm.pas. Cela vous permettra d'utiliser les mêmes noms que moi.

Nous allons avoir besoin d'un objet de type IDirect3D9 que nous appellerons FD3D et que nous placerons dans la section private de notre fenêtre. L'interface IDirect3D9 étant déclarée dans l'unité Direct3D9.pas, nous ajouterons donc cette unité dans la clause uses de la partie interface de notre unité.

Cet objet représente l'objet principal nous permettant de manipuler DirectX. C'est à partir de lui que nous pourrons créer tous les autres objets DirectX de notre exemple.

Le code source de votre unité devrait donc ressembler à ceci :

unit MainFrm;

interface

uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, Direct3D9;

type
TMainForm = class(TForm)
private
{ Private declarations }
FD3D: IDirect3D9;
public
{ Public declarations }
end;

var
MainForm: TMainForm;

implementation

{$R *.dfm}

end.

Pour sélectionner un mode d'affichage parmi ceux disponibles, il nous faut bien comprendre comment DirectX gère ces modes.

DirectX propose un adaptateur par carte vidéo disponible. Dans la majorité des cas, vous n'en aurez donc qu'un seul.

Chaque adaptateur propose un certain nombre de formats d'écran. Ces formats sont distingués par le nombre de couleurs affichables (ou profondeur d'écran), par exemple la majorité des cartes vidéo proposent des modes « couleurs vraies » (24 bits) ou « couleurs » (16 bits).

Le nombre de bits global correspond à la somme des bits utilisés par chaque composante de couleur. Par exemple dans le mode 24 bits classique, 8 bits sont utilisés pour chaque composante rouge, vert et bleu. La couleur d'un pixel sera donc déterminée par la valeur de chaque composante. Nous avons donc bien 3*8=24 bits par couleur (ou pixel).

Il nous sera enfin possible de déterminer les modes d'affichage disponibles pour chacun de ces formats. Ce mode d'affichage correspond à la résolution associée à la fréquence de rafraîchissement. Sans entrer dans les détails, cette fréquence nous indique combien de fois l'image est redessinée par seconde. Une fréquence de 75 Hz (75 Hertz) nous donnera donc 75 images par seconde.

L'énumération de ces modes se fera donc sous la forme :

Adaptateur 1
- Format 1
- Mode 1
- Mode 2
...
-Format 2
- Mode 1
- Mode 2
...
Adaptateur 2
...

Nous allons donc avoir besoin de variables pour stocker ces valeurs. Pour simplifier notre code au maximum, nous n'allons pas créer de variables supplémentaires, mais utiliser la propriété Objects de chaque liste d'items pour stocker un indice. Ces indices devront être transtypés en TObject pour pouvoir être affecté. Ces indices représenteront le numéro d'ordre de chaque adaptateur, format ou mode d'affichage dans leurs listes respectives. La liste des formats sera fournie par un tableau de notre création. Ce tableau contiendra les formats autorisés, car tous les formats disponibles ne peuvent être utilisés pour créer un « écran » DirectX. Il sera appelé D3DFormats et inséré sous forme de constante après la déclaration de notre fenêtre. Nous y ajouterons un autre tableau contenant les noms en clair de ces formats.

const
D3DFormats : array [0..5] of TD3DFormat = (
D3DFMT_X8R8G8B8, D3DFMT_A8R8G8B8, D3DFMT_A2R10G10B10, D3DFMT_X1R5G5B5,
D3DFMT_A1R5G5B5, D3DFMT_R5G6B5);

D3DFormatDescriptions : array [0..5] of string = (
'32 bits RVB (8 bits par couleur)',
'32 bits ARVB avec canal alpha (8 bits par canal)',
'32 bits avec canal alpha (10 bits par couleur et 2 bits pour le canal alpha)',
'16 bits (5 bits par couleur)',
'16 bits avec canal alpha (5 bits par couleur et 1 bit pour le canal alpha)',
'16 bits RVB (5 bits pour le rouge, 6 bits pour le vert et 5 bits pour le bleu)');

Le choix se fera par l'intermédiaire de trois TComboBox que nous appellerons cbxAdapters, cbxFormats et cbxModes. La valeur de la propriété Style de ces trois TComboBox devra être fixée à csDropDownList pour éviter que l'on puisse taper du texte dans ceux-ci.

VII - Énumération des adaptateurs, formats et modes

Maintenant que nous avons des variables et quelques composants, il nous faut utiliser tout ceci. Nous allons utiliser l'événement OnCreate de la fenêtre pour créer notre instance de IDirect3D9 et remplir le TComboBox des adaptateurs.

Pour obtenir la liste des adaptateurs, nous utiliserons les méthodes GetAdapterCount et GetAdapterIdentifier de l'instance de IDirect3D9. Cette dernière méthode nous permet d'obtenir une description plus parlante qu'un simple numéro d'adaptateur.

procedure TMainForm.FormCreate(Sender: TObject);
var
i: Integer;
id: TD3DAdapter_Identifier9;
begin
{ Création de l'instance IDirect3D9 }
FD3D:= Direct3DCreate9(D3D_SDK_VERSION);

{ Énumération des adaptateurs }
cbxAdapters.Items.Clear;
for i:= 0 to FD3D.GetAdapterCount - 1 do
begin
{ Récupération de l'identificateur }
FD3D.GetAdapterIdentifier(i, 0, id);
cbxAdapters.Items.Add(Format('%s [ %s ]', [id.Description, id.Driver]));
end;
end;

Vous pouvez dès à présent compiler le programme et l'exécuter. Vous devriez obtenir dans le premier TComboBox la liste des adaptateurs disponibles. Voici une copie d'écran du programme exécuté sur mon ordinateur.

Nous allons utiliser l'événement OnChange de cbxAdapters pour remplir le TComboBox cbxFormats. Nous ferons de même pour remplir le TComboBox cbxModes en utilisant l'événement OnChange de cbxFormats. Nous aurons ainsi un remplissage automatique de chaque TComboBox en fonction du précédent.

L'énumération des formats se fera en parcourant notre tableau (en passant les formats non supportés) alors que l'énumération des modes fera appel aux méthodes GetAdapterModeCount et EnumAdapterModes de notre instance de IDirect3D9.

procedure TMainForm.cbxAdaptersChange(Sender: TObject);
var
adapter: Integer;
i: Integer;
begin
cbxFormats.Clear;
{ Récupération de l'adaptateur sélectionné }
adapter:= cbxAdapters.ItemIndex;
if adapter <> -1 then
begin
{ Parcours des formats autorisés }
for i:= Low(D3DFormats) to High(D3DFormats) do
{ Nous sautons les formats non supportés par la carte. C'est-à-dire }
{ ceux qui ne possèdent aucun mode d'affichage. }
if FD3D.GetAdapterModeCount(adapter, D3DFormats[i]) <> 0 then
{ L'indice du format est stocké comme un objet dans la liste des }
{ éléments du TComboBox. Nous devons faire ceci, car nous avons }
{ peut-être (sûrement) sauté certains formats et les indices ne }
{ sont donc plus synchronisés. }
cbxFormats.Items.AddObject(D3DFormatDescriptions[i], TObject(i));
end;
end;

procedure TMainForm.cbxFormatsChange(Sender: TObject);
var
adapter: Integer;
formatIndex: Integer;
i: Integer;
displayMode: TD3DDisplayMode;
begin
cbxModes.Clear;
{ Récupération de l'adaptateur sélectionné }
adapter:= cbxAdapters.ItemIndex;
if (adapter <> -1) and (cbxFormats.ItemIndex <> -1) then
begin
{ Récupération de l'indice du format sélectionné }
formatIndex:= Integer(cbxFormats.Items.Objects[cbxFormats.ItemIndex]);
{ Énumération des modes supportés }
for i:= 0 to FD3D.GetAdapterModeCount(adapter, D3DFormats[formatIndex]) - 1 do
begin
FD3D.EnumAdapterModes(adapter, D3DFormats[formatIndex], i, displayMode);
cbxModes.Items.Add(Format('%d x %d - %d Hz', [displayMode.Width,
displayMode.Height, displayMode.RefreshRate]));
end;
end;
end;

VIII - Animation

Avant de démarrer l'animation, il nous faut trouver un moyen permettant de l'arrêter lorsqu'elle aura démarré. Nous allons simplement scruter le clavier et fermer l'application lors de l'appui sur la touche Echap. Pour ce faire, nous utiliserons l'événement OnKeyDown de la fenêtre avec le code suivant. N'oubliez pas de changer la valeur de la propriété KeyPreview de la fenêtre à True pour intercepter tous les événements clavier même lorsqu'un contrôle possède la focalisation.

procedure TMainForm.FormKeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
begin
if Key = VK_ESCAPE then
Close;
end;

Il nous faut maintenant ajouter un bouton nous permettant de démarrer l'animation une fois le mode d'affichage choisi. Nous l'appellerons btnStart. Lors du clic sur ce bouton, nous allons récupérer les trois indices précédents et créer un dispositif servant à l'affichage ainsi que le sprite et sa texture. Ces objets devront être stockés pour pouvoir être utilisés dans le code réalisant le tracé de l'animation. Nous allons donc ajouter quatre nouvelles variables à la suite de FD3D : FDevice de type IDirect3DDevice9 qui contiendra le dispositif, FBackBuffer de type IDirect3DSurface9 qui contiendra le buffer de tracé, FSpriteTexture de type IDirect3DTexture9 et FSprite de type ID3DXSprite qui contiendront respectivement la texture et le sprite. Le type de cette dernière variable est déclaré dans l'unité D3DX9.pas que nous ajoutons à la clause uses de la partie interface (comme Direct3D9.pas).

Il va nous falloir également stocker la position et la vitesse de déplacement de notre sprite. Nous pouvons utiliser quatre variables, mais je trouve plus élégant de définir un nouveau type record qui contiendra tout ceci. Cela nous permettra plus tard de facilement multiplier le nombre de sprites si nous le désirons. Nous pourrions même aller jusqu'à créer une classe contenant ces paramètres et les méthodes permettant de calculer les déplacements du sprite. Amusez-vous à le faire, c'est un très bon entraînement à la programmation orientée objet.

Note type record sera défini comme suit et inséré juste avant la déclaration de la fenêtre :

TSpritePos = record
coords: TPoint; // Coordonnées courantes du sprite
speed: TPoint; // Vitesse de déplacement horizontale et verticale
end;

La variable correspondante FSpritePos sera ajoutée comme les précédentes.

Voici enfin le moment où les choses deviennent intéressantes. Nous allons écrire le code de l'événement OnClick du bouton btnStart.

La création du dispositif utilisera la méthode CreateDevice de l'interface IDirect3D. Mais si vous regardez les arguments de celle-ci, vous verrez qu'il nous faut lui fournir une structure de type TD3DPresent_Parameters. Cette structure permet de passer un grand nombre de paramètres à la méthode en évitant une succession d'arguments qui serait fastidieuse. Nous n'allons pas étudier ici l'intégralité de cette structure, mais seulement les valeurs importantes pour notre projet. Voici la définition de cette structure :

BackBufferWidth:                    LongWord;
BackBufferHeight:                   LongWord;
BackBufferFormat:                   TD3DFormat;
BackBufferCount:                    LongWord;

MultiSampleType:                    TD3DMultiSampleType;
MultiSampleQuality:                 DWORD;

SwapEffect:                         TD3DSwapEffect;
hDeviceWindow:                      HWND;
Windowed:                           Bool;
EnableAutoDepthStencil:             Bool;
AutoDepthStencilFormat:             TD3DFormat;
Flags: LongInt;

{ FullScreen_RefreshRateInHz must be zero for Windowed mode }
FullScreen_RefreshRateInHz:         LongWord;
PresentationInterval:               LongWord;

Les quatre premières valeurs ont trait au back buffer. Il s'agit d'une zone de mémoire tampon dans laquelle nous allons tracer notre animation. Lorsque nous aurons terminé nos opérations de dessin, nous demanderons à DirectX de transférer cette zone vers l'écran. Cela nous permet de n'afficher que des images « complètes » et de ne pas voir l'image se tracer au fur et à mesure. La vitesse d'affichage s'en trouve de plus améliorée.

Les valeurs MultiSampleType, MultiSampleQuality et SwapEffect ne nous seront pas utiles et dépassent le cadre de cette initiation.

La valeur hDeviceWindow servira à indiquer à DirectX la fenêtre qui va servir de support au tracé, nous fournirons donc le handle de notre propre fenêtre.

Windowed nous permet d'indiquer si l'animation doit s'exécuter en mode fenêtré ou plein écran.

Les valeurs EnableAutoDepthStencil et AutoDepthStencilFormat ne nous intéressent pas dans cet article.

La variable Flags peut recevoir un certain nombre de paramètres dont un en particulier : si vous désirez utiliser les fonctions du GDI pour afficher du texte ou dessiner, vous devrez y inclure la constante D3DPRESENTFLAG_LOCKABLE_BACKBUFFER. Celle-ci indique à DirectX que le back buffer peut être verrouillé par GDI pour y dessiner. Il faut cependant avoir en tête que cela peut ralentir l'affichage de l'animation. Nous n'en avons pas besoin pour le moment, mais la version améliorée du programme utilisera le GDI pour afficher le nombre d'images par seconde ainsi que pour tracer un logo.

FullScreen_RefreshRateInHz doit recevoir la fréquence de rafraîchissement du mode d'affichage que nous avons choisi. Notez que cette valeur doit être mise à zéro si nous voulons utiliser le mode fenêtré.

La valeur PresentationInterval indique si DirectX doit attendre la fin du rafraîchissement de l'écran pour envoyer le buffer à l'affichage. Cette fonctionnalité fait chuter le nombre de FPS, mais permet d'avoir un affichage propre. Vous pourrez vous amuser à modifier cette valeur pour tester la vitesse maximale de votre programme. Les valeurs que vous pourrez utiliser sont D3DPRESENT_INTERVAL_DEFAULT pour le premier cas (synchronisation avec le balayage de l'écran) et D3DPRESENT_INTERVAL_IMMEDIATE pour un affichage immédiat.

Ouf ! Il est temps de commencer le codage de notre méthode :

procedure TMainForm.btnStartClick(Sender: TObject);
var
adapter: Integer;
formatIndex: Integer;
displayModeIndex: Integer;
displayMode: TD3DDisplayMode;
params: TD3DPresentParameters;
begin
{ Récupération de tous les indices }
adapter:= cbxAdapters.ItemIndex;
if cbxFormats.ItemIndex <> -1 then
formatIndex:= Integer(cbxFormats.Items.Objects[cbxFormats.ItemIndex])
else
formatIndex:= -1;
displayModeIndex:= cbxModes.ItemIndex;

{ Si un des indices est incorrect, nous sortons directement }
if (adapter = -1) or (formatIndex = -1) or (displayModeIndex = -1) then
exit
else
begin
{ Mise à zéro de toutes les valeurs de la structure }
FillChar(params, SizeOf(params), 0);

{ Récupération du mode d'affichage }
FD3D.EnumAdapterModes(adapter, D3DFormats[formatIndex], displayModeIndex,
displayMode);

{ Remplissage des valeurs de la structure }
with params do
begin
BackBufferWidth:= displayMode.Width;
BackBufferHeight:= displayMode.height;
BackBufferFormat:= D3DFormats[formatIndex];
SwapEffect:= D3DSWAPEFFECT_DISCARD; // Obligatoire
hDeviceWindow:= self.Handle;
Windowed:= false;
Flags:= D3DPRESENTFLAG_LOCKABLE_BACKBUFFER;
FullScreen_RefreshRateInHz:= displayMode.RefreshRate;
PresentationInterval:= D3DPRESENT_INTERVAL_DEFAULT;
end;

{ Nous allons redimensionner la fenêtre, nous pourrons ainsi utiliser ces }
{ dimensions pour détecter les bords }
Width:= displayMode.Width;
Height:= displayMode.height;

{ Création du dispositif }
FD3D.CreateDevice(adapter, D3DDEVTYPE_HAL, self.handle,
D3DCREATE_SOFTWARE_VERTEXPROCESSING, @params, FDevice);
end;
end;

La première partie de la méthode devrait vous être familière puisqu'il s'agit de récupérer les indices, opération devenue habituelle. Nous remplissons ensuite la variable params avec les valeurs adéquates. Et enfin, nous appelons la méthode de création du dispositif. Une remarque à ce sujet : jusqu'à présent nous ne nous sommes pas préoccupés des éventuelles erreurs pouvant être déclenchées par DirectX lors des appels des diverses méthodes. Ce n'est pas joli, joli, mais je tenais à conserver un maximum de simplicité à notre code pour que vous puissiez bien distinguer les appels. Pour être un peu plus rigoureux, nous devrions au moins tester la valeur de retour de ces méthodes. Un moyen simple de le faire est d'utiliser la fonction Failed disponible dans l'unité ActiveX.pas. Celle-ci nous indique si le code de retour correspond à un code d'erreur ou non. Le dernier appel devrait donc être codé comme ceci (au minimum) :

{ Création du dispositif }
if Failed(FD3D.CreateDevice(adapter, D3DDEVTYPE_HAL, self.handle,
D3DCREATE_SOFTWARE_VERTEXPROCESSING, @params, FDevice)) then
exit;

En ce qui concerne les arguments de la méthode CreateDevice nous voyons l'indice de l'adaptateur, une constante indiquant que nous voulons utiliser les primitives câblées dans la carte vidéo, le handle de notre fenêtre qui indique qu'elle aura la focalisation, une constante informant DirectX d'utiliser son propre code pour traiter les vertex au cas où la carte ne proposerait pas cette fonctionnalité, nos paramètres et la variable recevant le dispositif.

Il nous faut maintenant récupérer le buffer de tracé dans la variable FBackBuffer. Nous utiliserons l'appel suivant :

{ Récupération du back buffer }
FDevice.GetBackBuffer(0, 0, D3DBACKBUFFER_TYPE_MONO, FBackBuffer);

Nous passerons les deux premiers arguments que vous pouvez laisser à zéro. Le troisième argument est plus intéressant, même s'il n'est pas possible d'en changer dans cette version de DirectX. Il permettrait, dans le cas où le dispositif serait capable de gérer des images stéréoscopiques (en vraie 3D, avec les lunettes adaptées), de récupérer le buffer associé à l'œil gauche ou droit. Pour l'instant nous n'avons pas le droit de demander autre chose qu'un buffer mono. Le dernier argument est évidemment notre variable recevant le back buffer.

Nous allons à présent créer notre sprite. Pour ce faire, nous commencerons par charger une image qui servira de texture au sprite. L'utilisation de la fonction 3DXCreateTextureFromFile nous permet de créer la texture et de la charger en un seul appel. La fonction D3DXCreateSprite nous fournira un sprite aussi simplement. Ces fonctions, appelées des helpers, sont utilisables lorsque vous ne voulez pas vous « fatiguer » à tout coder vous-même. Elles réalisent des opérations courantes qu'il serait fastidieux de réécrire à chaque fois. Il ne faut donc pas hésiter à s'en servir. Elles ont cependant un petit inconvénient (on n'a jamais rien sans rien), elles nécessitent le déploiement de D3DX9ab.dll avec votre projet, car c'est cette dll qui contient ces fonctions. Celle-ci n'étant pas distribuée avec le reste des fichiers de DirectX 9, ce sera à vous de l'installer.

{ Création de la texture du sprite }
D3DXCreateTextureFromFile(FDevice, 'Ball.png', FSpriteTexture);
{ Création du sprite }
D3DXCreateSprite(FDevice, FSprite);

Le fichier Ball.png utilisé est disponible ici, mais vous pouvez le remplacer par n'importe quel autre fichier au format bmp, dib, jpg, png, tga ou dds de votre choix.

Il ne nous reste plus qu'à initialiser la position et la vitesse de notre sprite avant de lancer l'animation.

{ Initialisation des coordonnées du sprite }
Randomize; // Pour ne pas tomber à chaque fois sur les mêmes valeurs
with FSpritePos do
begin
{ Le sprite démarrera dans le coin haut-gauche de l'écran }
Coords.X:= 0;
Coords.Y:= 0;
Speed.X:= Random(10) + 1; // il se déplacera de 1 à 10 pixels par image
Speed.Y:= Random(10) + 1;
end;

{ Nous branchons un gestionnaire d'événement sur l'événement OnIdle de }
{ l'application }
Application.OnIdle:= DrawImage;

L'initialisation des coordonnées du sprite ne présente pas de difficulté. En revanche, vous êtes peut-être surpris par l'utilisation de l'événement OnIdle de l'application. Celui-ci est déclenché à chaque fois que l'application passe au « repos », c'est-à-dire qu'aucun événement clavier ou souris n'est reçu. Alors, comment pouvons-nous l'utiliser pour dessiner nos images ? Grâce à une petite subtilité : la procédure associée reçoit un argument appelé Done. Celui-ci déclaré en var peut donc être modifié par le code de la procédure. Il indique au code de l'application que la procédure n'a pas terminé ses traitements. L'application va donc rappeler celle-ci jusqu'à ce que Done soit égal à True. Nous allons utiliser cette astuce pour créer une « pompe à image ». À chaque appel, nous tracerons l'image et demanderons à DirectX de l'afficher. Nous fixerons donc Done à False afin que l'application appelle notre procédure indéfiniment. Les messages seront tout de même traités par le code appelant ce qui nous permet d'éviter de coder une boucle de messages ou d'appeler Application.ProcessMessages à tour de bras, ce qui serait néfaste à la vitesse de notre animation.

Il nous faut donc ajouter la déclaration de notre procédure dans la section private de la fenêtre de cette façon :

procedure DrawImage(Sender: TObject; var Done: Boolean);

Il ne reste plus qu'à écrire le corps de la procédure. Celle-ci va effacer l'écran, tracer le sprite, et modifier ces coordonnées pour la prochaine image. Un appel à la méthode Present du dispositif permettra d'afficher notre image à l'écran. Et on recommence. Et ainsi de suite jusqu'à ce que l'utilisateur appuie sur la touche Echap pour fermer l'application. Je vous donne directement le code complet de la procédure pour ne pas vous faire attendre plus longtemps, j'ai déjà été trop bavard.

procedure TMainForm.DrawImage(Sender: TObject; var Done: Boolean);
var
dxcolor: Cardinal;
dxVector: TD3DXVector2;
desc: D3DSURFACE_DESC;
begin
{ Tracé du fond, un simple effacement dans notre exemple }
dxColor:= D3DCOLOR_XRGB(255, 255, 255); // blanc pour le fond
FDevice.Clear(0, nil, D3DCLEAR_TARGET, dxColor, 1, 0);

{ Le tracé du sprite nécessite un vecteur de points réels }
dxVector.x:= FSpritePos.Coords.X;
dxVector.y:= FSpritePos.Coords.Y;

FDevice.BeginScene; // Il faut indiquer au dispositif que nous allons tracer
FSprite._Begin; // Idem pour le sprite
FSprite.Draw(FSpriteTexture, nil, nil, nil, 0, @dxVector, $FFFFFFFF);
FSprite._End; // Fin du tracé
FDevice.EndScene; // Idem

{ Modification de la position du sprite et détection des collisions avec }
{ les bords de l'écran }
{ Le premier appel nous permet de récupérer une structure qui nous donne, }
{ entre autres, les dimensions de l'image utilisée pour le sprite. Cela }
{ nous évite de coder "en dur" les dimensions de celle-ci. }
FSpriteTexture.GetLevelDesc(0, desc);
with FSpritePos do
begin
Inc(Coords.X, Speed.X);
if (Coords.X < 0) or (Coords.X > Width - desc.Width) then
begin
Speed.X:= - Speed.X;
Inc(Coords.X, Speed.X);
end;
Inc(Coords.Y, Speed.Y);
if (Coords.Y < 0) or (Coords.Y > Height - desc.Height) then
begin
Speed.Y:= - Speed.Y;
Inc(Coords.Y, Speed.Y);
end;
end;

{ DirectX doit maintenant transférer le contenu du buffer vers l'affichage }
FDevice.Present(nil, nil, 0, nil);

{ Ne pas oublier de mettre Done à false pour que la méthode soit }
{ appelée à nouveau }
Done:= false;
end;

Quelques commentaires sur les méthodes utilisées.

Nous commençons par effacer l'écran à l'aide de la méthode Clear du dispositif en utilisant une couleur unie. Cette méthode est beaucoup plus rapide que le tracé d'un rectangle plein. La valeur de la couleur est obtenue par la fonction D3DCOLOR_XRGB qui prend la valeur des trois composantes en argument (ici le blanc, soit $FFFFFF). Il serait un peu long de détailler les arguments passés à Clear, mais sachez qu'il est possible de demander au dispositif d'effacer certaines portions de l'écran en passant un vecteur de rectangles comme second argument. La valeur D3DCLEAR_TARGET indique à DirectX de se contenter d'effacer la zone de tracé en utilisant la couleur spécifiée.

Nous remplissons ensuite un vecteur de réels avec les coordonnées du sprite pour pouvoir passer celui-ci à la méthode Draw du sprite. Les différents arguments correspondent à la texture ainsi qu'à la portion de texture à utiliser, les coordonnées du centre du sprite ainsi que sa position (que nous avons spécifié), et enfin la couleur à utiliser pour la gestion du canal alpha.

L'appel à la méthode GetLevelDesc de FSpriteTexture nous permet de récupérer les dimensions de l'image utilisée. C'est beaucoup plus élégant que de spécifier des valeurs en dur. Il serait par contre plus judicieux de ne faire cet appel qu'une seule fois en conservant les dimensions de l'image dans des variables de la fenêtre.

Le code gérant les collisions est évident. On teste si le sprite dépasse du bord de l'écran et si c'est le cas, on inverse sa vitesse et on recalcule sa position.

L'explication des arguments de la méthode Present du dispositif dépasse un peu le cadre de cette initiation, je laisse donc le soin aux plus curieux de regarder eux-mêmes leur signification. Je vous rappelle d'ailleurs qu'il est très instructif d'étudier la documentation de ces méthodes, afin de mieux comprendre le fonctionnement de DirectX 9. Je vous invite donc à consulter cette documentation, soit sur le site de Microsoft http://msdn.microsoft.com/directx, soit directement dans le fichier d'aide DirectX9_c.chm si vous avez téléchargé le SDK complet.

IX - Et maintenant ?

Voila, notre petite application de test est terminée. Il n'y a pas de quoi rivaliser avec les productions des éditeurs de jeux, mais la base est là.

Maintenant que nous connaissons les bases de ce type de programmation, nous pouvons améliorer notre programme. Nous ne passerons pas en revue l'ensemble de ces améliorations, mais voici une liste des nouveautés que vous trouverez dans la version enrichie du programme.

- Le code de sélection du mode graphique a été amélioré et déplacé dans une fenêtre qui nous servira de dialogue. Nous pourrons ainsi la réutiliser à loisir. Pour faciliter son écriture, de nouvelles classes ont été créées afin de gérer l'énumération des adaptateurs et modes d'affichage. Ce n'était pas vraiment nécessaire, mais cela montre que l'on peut encapsuler des fonctionnalités de DirectX dans des classes Delphi. Vous trouverez ces classes dans l'unité DXHelpers.pas.

- Le fond uni a été remplacé par un dégradé de couleurs se déplaçant à chaque image. Les couleurs utilisées sont stockées dans un tableau simple à modifier. Plusieurs versions de ce tableau sont présentes afin de vous permettre de tester divers effets.

- Un sprite c'est bien, mais ce n'est pas suffisant. La nouvelle version du programme est donc capable d'en gérer un nombre quelconque. Il a suffi de transformer notre variable FSpritePos en vecteur de TSpritePos et de rajouter quelques boucles aux endroits où l'ancienne variable était utilisée. Le nombre de sprites peut être modifié en changeant simplement la valeur de la constante MAXSPRITES située au début de l'unité principale.

- Pour améliorer l'aspect visuel du programme, un logo a été ajouté en bas de l'écran. Celui-ci est tracé en utilisant GDI. Vous pourrez constater ainsi qu'il est très simple de mélanger GDI et DirectX. L'image est fournie par un composant TImage posé sur la fenêtre principale. L'image est tracée après les sprites pour être toujours au premier plan.

- Enfin, un compteur de FPS est inséré en haut de l'écran pour visualiser les performances de notre programme. Quelques variables entières servent à compter le nombre d'images (frames) totales et par seconde. La fonction GetTickCount est utilisée pour mesurer le temps écoulé entre l'affichage de chaque image. Le texte est ensuite tracé en utilisant encore GDI, car c'est le moyen le plus simple et c'est largement suffisant pour notre programme.

Il ne me reste plus qu'à vous souhaiter de prendre autant de plaisir à utiliser vos nouvelles compétences que j'en ai pris à écrire cette initiation.

Bon DirectX à tous !

X - Liens

Sources Microsoft

- Le SDK 9.0 : http://msdn.microsoft.com/library/default.asp?url=/downloads/list/directx.asp

- Toutes les informations sur DirectX : http://msdn.microsoft.com/directx

- Les runtimes DirectX : http://www.microsoft.com/directx

Miroir des fichiers de la conversion DirectX d'Alexey Barkovoy (distribution du 7/09/2003)

- Clootie_DirectX90_Small.zip

- Clootie_DX90_dlls.zip

Projets

- Sources Delphi 7 et programme compilé de la minidémo : MiniDemo.zip

- Sources Delphi 7 et programme compilé de la démo enrichie : DemoDirectX9.zip

- Le fichier image utilisé pour les sprites : Ball.png