Utilisation de DirectX sous Delphi : Asphyre 4.1

Pierre RODRIGUEZ

Quand on parle développement de jeu, la grande majorité de personnes pensent que C++ est le seul langage adapté à ce type de programmation. Toutefois, nous allons voir que vous pouvez tout à fait créer un jeu ou du moins, utiliser DirectX avec Delphi. ♪

- Titre : Utilisation de DirectX sous Delphi : Asphyre 4.1

- Auteur : Pierre RODRIGUEZ

- Parution : 6 mai 2008

- Mise à jour : 12 février 2009

- 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 ® 2008 Pierre RODRIGUEZ. 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. Introduction

- II. Présentation de la bibliothèque Asphyre

- II-A. Introduction

- II-B. Les versions d'Asphyre

- II-B-1. Asphyre eXtreme (3.1)

- II-B-2. Asphyre 4.1

- II-B-3. Asphyre Casual

- II-B-4. Asphyre Sphinx

- II-C. Version utilisée

- II-C-1. Prérequis

- II-C-2. Installation d'Asphyre 4.1

- II-D. Remerciements

- III. La 2D

- III-A. Introduction

- III-B. Asphyre en 2D : Comment ça marche ?

- III-B-1. Initialiser

- III-B-2. Le Timer

- III-B-2-a. OnTimer

- III-B-2-b. OnProcess

- III-B-3. Finaliser

- III-C. Premier programme: « Hello World! »

- III-C-1. Bug de librairie d3dx9d_31.dll

- III-D. Méthodes de dessin

- III-D-1. Écrire du texte

- III-D-1-a. Écriture « simple » à partir d'une police système

- III-D-1-a-i. Écrire le nombre d'images par seconde (FPS)

- III-D-1-b. Écriture à partir d'une police « complexe »

- III-D-1-b-i. Description de l'outil FontTool.exe

- III-D-1-b-ii. Création d'une police

- III-D-1-b-iii. Créer le fichier XML de ressources

- III-D-1-b-iv. Charger la police dans le projet

- III-D-1-b-v. Écrire avec la police

- III-D-2. Dessiner des formes

- III-D-2-a. Paramétrer le dessin

- III-D-2-b. Dessiner un point

- III-D-2-c. Dessiner une ligne

- III-D-2-d. Dessiner un rectangle

- III-D-2-e. Dessiner un cercle et une ellipse

- III-D-3. Afficher une image

- III-D-3-a. Utilisation de TextureMaker

- III-D-3-a-i. Qu'est-ce qu'un pattern ?

- III-D-3-a-ii. Paramétrage de TextureMaker

- III-D-3-b. Déclarer l'image dans le XML

- III-D-3-c. Charger l'image dans le programme

- III-D-3-d. Dessiner l'image

- III-D-3-e. Les effets de déformations

- III-E. Téléchargement du programme d'exemple

- III-F. Conclusion

- IV. La 3D

- IV-A. Introduction

- IV-B. Les shaders

- IV-B-1. Qu'est-ce que c'est ?

- IV-B-2. Créer un shader

- IV-B-2-a. Bloc-notes

- IV-B-2-b. Delphi

- IV-B-2-c. FX Composer

- IV-C. La 3D dans Asphyre

- IV-C-1. Les matrices

- IV-C-2. Les maillages

- IV-C-2-a. La classe de maillage utilisée

- IV-C-3. L'éclairage

- IV-D. Utilisation des shaders dans Asphyre : La classe TAsphyreShaderEffect

- IV-D-1. Le programme

- IV-D-2. Shader simple

- IV-D-2-a. Les techniques

- IV-D-2-b. Utilisation des paramètres et des techniques

- IV-D-2-b-i. La méthode Describe

- IV-D-2-b-ii. La méthode UpdateParam

- IV-D-2-c. Le rendu

- IV-D-2-d. Débogage du ficher .fx

- IV-D-2-e. Téléchargement du projet

- IV-D-3. Shader avec paramètre

- IV-D-3-a. Le shader

- IV-D-3-b. Les paramètres et les techniques

- IV-D-3-c. Téléchargement du projet

- IV-D-4. Shader avec texture

- IV-D-4-a. Le shader

- IV-D-4-a-i. Les paramètres

- IV-D-4-a-ii. Les textures

- IV-D-4-a-iii. Le rendu

- IV-D-4-b. Le programme

- IV-D-4-c. Téléchargement du projet

- IV-E. Le post processing

- IV-F. Conclusion

- V. Les entrées utilisateurs

- V-A. Introduction

- V-B. Les entrées utilisateur dans Asphyre

- V-C. Le clavier

- V-D. Les joysticks

- V-E. La souris

- V-F. Téléchargement du programme d'exemple

- V-G. Conclusion

I - Introduction

Combien d'entre vous ont été frustrés de ne pas pouvoir créer le jeu que vous avez imaginé avec Delphi. Et pour cause, les librairies d'affichage les plus utilisées dans ce domaine (DirectX, OpenGL) sont écrites en C++.
Bien qu'il existe des traductions à la lettre des fichiers header de DirectX et de OpenGL (plus ou moins bien retranscrites), la création d'un moteur d'affichage reste tout de même assez difficile avec cette méthode.

DelphiX a été une des premières (si ce n'est la première) bibliothèques de composants encapsulant DirectX. Malheureusement, son développement a été abandonné et seul DirectX version 7 est pris en compte. Même si les versions supérieures n'ajoutent rien en affichage 2D, la 3D a énormément évolué depuis ce temps. Toutes les nouvelles possibilités de votre carte vidéo flambante neuve ne sont donc pas accessibles.
Pour couronner le tout, les fonctions fournies ne sont pas traduites correctement et interdisent des manipulations pourtant possibles.
Pour OpenGL, il existe GLScene qui fonctionne très bien. Malheureusement, son orientation non-objet ne correspond pas réellement aux standards actuels.

Entre DirectX et OpenGL, chacun ira de sa préférence et de ses arguments. Ce tutoriel n'a pas pour but de les comparer. Pour ma part, j'ai pris pour habitude d'utiliser DirectX ; nous verrons donc ici, les principes d'utilisation des composants Asphyre qui encapsulent DirectX.

Ce tutoriel est destiné à un public de niveau moyen ayant quelques notions de graphisme.

Avant de commencer toute chose, je me dois d'être clair sur un point : le développement d'un jeu demande énormément de temps. Les jeux actuels demandent des millions d'euros d'investissement ainsi que des centaines de personnes travaillant dessus et ayant chacune leur spécialité (graphisme, Intelligence Artificielle, moteurs du jeu, réseau, etc.).

Donc, comme dans tout apprentissage, n'ayez pas les yeux plus gros que le ventre et donnez-vous des objectifs progressifs. Il est inutile de commencer par le MMORPG qui va révolutionner le genre ou le FPS qui va enterrer la concurrence ! Dites-vous bien que le jeu vidéo est peut-être le domaine de développement le plus compliqué puisqu'il fait intervenir énormément de connaissances.

Les principes décrits dans ce tutoriel ont pour but de vous expliquer les rudiments de la programmation avec DirectX. Les méthodes utilisées ne sont pas forcément celles qui respectent les règles de l'art. L'objectif est simplement d'afficher quelque chose en utilisant DirectX.

Vous trouverez de plus amples explications et tutoriels en la matière dans l'excellente section 2D-3D-Jeux de developpez.com en particulier sa page de tutoriels qui est une vraie mine d'or pour qui veut développer des jeux.

Je ne peux que vous conseiller d'aller voir ces tutoriels mis à disposition !

II - Présentation de la bibliothèque Asphyre

II-A - Introduction

Les composants Asphyre ont été développés par Yuriy Kotsarenko depuis maintenant plusieurs années. Ils permettent une utilisation de DirectX assez aisée sous Delphi.
Vous pourrez trouver des informations sur Asphyre sur le site dédié : http://www.afterwarp.net/

II-B - Les versions d'Asphyre

II-B-1 - Asphyre eXtreme (3.1)

Cette ancienne release est toujours la plus utilisée. C'est la seule qui utilise des composants invisibles. Il existe une grande variété d'add-ons pour cette version (éditeur d'interface, support du réseau, etc.).
Téléchargez Asphyre 3.1

II-B-2 - Asphyre 4.1

Dès l'arrivée de Turbo Delphi, Yuriy Kotsarenko a entrepris de développer une version d'Asphyre qui soit compatible avec la particularité de Delphi Turbo Explorer pour Win32 : l'impossibilité d'installer des composants tiers.
La totalité du code a donc été reprise afin de proposer une approche non visuelle (sans composant), ce qui permet aux utilisateurs de Turbo Explorer de développer avec Asphyre.
Évidemment, ce n'est pas la seule modification. Des fonctionnalités ont été implémentées et ce package utilise Direct3D 9.0c.
Téléchargez Asphyre 4.1

II-B-3 - Asphyre Casual

Cette version d'Asphyre est un peu particulière puisqu'elle ne permet que d'afficher de la 2D. Grâce à cela, vous pouvez développer des jeux pour DirectX 7. La palette de personnes pouvant utiliser votre création en est donc forcément plus grande!
L'approche utilisée est la même que Asphyre 4.1 à savoir, non visuelle.
Cette version est également plus facile à utiliser.
Toutefois, cette version n'est plus distribuée et est remplacée dorénavant par Asphyre Sphinx dont vous trouverez un descriptif sommaire ci-dessous.

II-B-4 - Asphyre Sphinx

Cette version apparue en février 2009 remplace toutes les versions précédentes d'Asphyre.
Elle requiert au moins BDS 2006 et Lazarus (Free Pascal).
Elle encapsule DirectX7, Direct9 et OpenGL. Ainsi, pour de simples applications 2D, vous pouvez vous contenter de DirectX7 pour élargir le champ d'application de votre programme.
Téléchargez Asphyre Sphinx

II-C - Version utilisée

Dans ce tutoriel, nous utiliserons la version 4.1. qui est la version la plus aboutie actuellement et permet de tout faire (2D et 3D).
Nous verrons dans un premier temps l'affichage en 2D. Dans un second temps, nous nous essaierons à la programmation d'un tout petit moteur 3D. Pour finir, nous étudierons les entrées utilisateurs (joystick, clavier, etc.).

Asphyre 4.1 nécessite au moins BDS 2006 (Borland Delphi Studio) ou Turbo Delphi Explorer (gratuit). Vous pourrez télécharger Turbo Delphi Explorer ici :

Téléchargement de Turbo Delphi Explorer.

En effet, Asphyre 4.1 utilise les dernières avancées du langage par exemple, la surcharge des opérateurs sur des records.

Dans ce tutoriel, la version de Delphi utilisée est RAD Studio 2007. Si vous avez une version inférieure, dans chacune des sources téléchargeables, si vous compilez, vous aurez une erreur

Identificateur non déclaré : 'MainFormOnTaskbar'

Cette méthode a été introduite dans RAD Studio 2007. Vous pouvez donc sans souci, supprimer ou mettre en commentaire cette ligne dans le dpr :

Application.MainFormOnTaskbar := True;

II-C-1 - Prérequis

Avant toute chose, il est impératif de mettre DirectX à jour. Vous trouverez les mises à jour ici : Page DirectX Microsoft

II-C-2 - Installation d'Asphyre 4.1

Comme dit plus haut, cette bibliothèque ne déclare pas de composants. Il n'y a donc pas d'installation à proprement parler.
Il suffit de décompresser le fichier dans un répertoire en prenant bien soin de conserver l'arborescence des répertoires.
En revanche, il faut bien sûr déclarer le chemin d'accès aux sources. Pour cela, rendez-vous dans les options du projet (Projet\Options… puis section Répertoires/Conditions) et ajoutez les deux chemins suivants dans le Chemin de recherche :

[AsphyreInstallation]\Source
[AsphyreInstallation]\Source\Common

Où [AsphyreInstallation] est le chemin où vous avez décompressé Asphyre.

Il est tout à fait possible d'utiliser Asphyre 4.1 avec la version eXtreme déjà installée. Pour cela, il suffit d'enlever toutes les références à Asphyre eXtreme des chemins de recherche et n'ajouter que les chemins d'Asphyre 4.1.

II-D - Remerciements

Je tiens tout particulièrement à remercier Loka, raptor70 et Pascal Jankowski pour leurs encouragements, leurs relectures et leurs conseils avisés.

III - La 2D

III-A - Introduction

La 2D est l'aspect le plus accessible dans le développement de jeu. En revanche, cela ne veut pas dire que c'est facile !
Certains d'entre vous se souviendront d'une époque où le jeu de plateforme et le shoot-them-up étaient rois. Malgré leur âge, ces jeux utilisaient des effets parfois saisissants pour immerger encore plus le joueur. Afficher un personnage reste facile, l'intégrer dans un décor réactif et le faire bouger selon les actions du joueur est bien plus difficile ! Même si la méthode d'affichage reste la même.
C'est pour cette raison que nous verrons dans cette partie les différentes façons d'afficher des textes, des formes géométriques et des images (dit « sprites ») grâce à Asphyre.

Avant de commencer, il faut savoir que la version 4.1 (à la différence de la version Casual) nécessite une DLL pour fonctionner. Celle-ci est fournie dans le répertoire DLL et s'appelle « d3dx9_31.dll ».

Cette DLL est normalement fournie avec DirectX 9.0c (version 2006 donc incluse dans la dernière mise à jour mars 2008). Certains postes qui n'ont pas été mis à jour peuvent ne pas avoir cette DLL.

Comme pour chaque DLL indispensable à un programme, plusieurs choix s'offrent à vous :

- déplacer cette DLL une bonne fois pour toutes dans le répertoire System32 de Windows. Cela permettra à tous vos programmes de fonctionner sans vous en soucier, mais vous risquez de l'oublier lors du déploiement sur d'autres machines qui n'en sont pas forcément équipées ;

- placer cette DLL dans le répertoire de chacun de vos programmes. De cette façon, vous ne risquez pas de l'oublier. En revanche, il faut la placer dans le répertoire à chaque nouveau programme.

Les deux méthodes fonctionnent, choisissez celle qui vous convient le mieux. De toute façon, sans cette DLL, Asphyre refusera de démarrer dès l'initialisation.

III-B - Asphyre en 2D : Comment ça marche ?

Asphyre propose des méthodes à appeler pour initialiser, modifier toutes sortes de paramètres. En y regardant bien, le système n'est pas très différent d'Asphyre eXtreme sauf qu'aucun objet n'est visible, ce qui peut paraitre un peu déroutant au début.
En appelant la méthode Initialize, on initialise (oui le nom est bien choisi !) le moteur d'affichage et Asphyre se charge de créer tout ce dont on a besoin.
En fin de programme, il suffit d'appeler Finalize pour détruire tous les objets créés et libérer la surface utilisée.

III-B-1 - Initialiser

Cette initialisation est indispensable pour tout projet utilisant Asphyre et DirectX en général. Il ne faut pas l'oublier !

L'initialisation se fait de la plus simple des façons. Il suffit en effet d'appeler :

Devices.Initialize(CfgEvent: TScreenConfigEvent; Tag: TObject): Boolean;

Cette méthode Initialize accepte deux paramètres :

- CfgEvent : permet de spécifier une méthode qui sera exécutée au début de l'initialisation ;

- Tag : permet de conserver un lien vers le composant qui affiche (en général, la Form principale).

Le booléen renvoyé permet de vérifier que l'initialisation s'est correctement effectuée. Si ce n'est pas le cas, on peut refermer l'application puisque Asphyre n'affichera rien.

La méthode de type TScreenConfigEvent s'écrit de cette manière et devrait être déclarée en private puisque, théoriquement, seule la fiche d'affichage devrait y avoir accès :

type
TMainFrm = class(TForm)
...
private
procedure ConfigureDevice(Sender: TAsphyreDevice; Tag: TObject;
var Config: TScreenConfig);
end;

...

procedure TMainFrm.ConfigureDevice(Sender: TAsphyreDevice; Tag: TObject;
var Config: TScreenConfig);
begin
end;

Cette méthode ConfigureDevice aura pour but principal de paramétrer le composant qui affichera, son type d'affichage, sa taille, s'il est en mode fenêtré, etc.

Pour paramétrer votre affichage, il suffit, dans cette procédure, de modifier le paramètre Config passé en variable.

Typiquement, voici un exemple de ce que l'on peut trouver dans l'un des exemples fournis avec Asphyre (Extrait de BasicShdr) :

procedure TMainFrm.ConfigureDevice(Sender: TAsphyreDevice; Tag: TObject;
var Config: TScreenConfig);
begin
Config.WindowHandle:= Self.Handle; //Spécifie le handle de la Form pour l'affichage
Config.HardwareTL  := True; //Active le Transform and Lightning matériel

Config.Width   := ClientWidth; //Taille de la largeur de l'affichage
Config.Height  := ClientHeight; //Taille de la hauteur de l'affichage
Config.Windowed:= False; //Définit le mode fenêtré. Ici à faux
Config.BitDepth:= bd24bit; //Définit la profondeur des couleurs. ici 24 bits
end;

De cette façon, ces données seront utilisées lors de l'initialisation.

Il est également possible d'ajouter des méthodes lorsque certains événements inaccessibles autrement doivent être utilisés :

- EventDeviceCreate : se déclenche après que l'initialisation ait été effectuée.
Utilisé pour charger les ressources statiques ;

- EventDeviceDestroy : se déclenche juste avant la finalisation
De la même façon, les ressources statiques doivent être libérées ici ;

- EventDeviceReset : se déclenche quand le dispositif (Device) est placé en état fonctionnel ou qu'il est récupéré lors d'une perte.
Toutes les ressources non persistantes doivent être créées ici ;

- EventDeviceLost : se déclenche lorsque le dispositif perd son état fonctionnel ou bien après qu'il ait été perdu.
Toutes les ressources créées dans DeviceReset doivent être libérées ici ;

- EventDeviceFault : se déclenche lorsqu'une erreur fatale se produit et que l'application ne peut pas continuer. On peut l'utiliser par exemple, pour afficher un message d'erreur et clore l'application ;

- EventBeginScene : se déclenche après l'appel réussi de IDirect3DDevice9.BeginScene ;

- EventEndScene : se déclenche avant l'appel à IDirect3DDevice9.EndScene ;

- EventSymbolResolve : se déclenche avant quand une ressource (image par exemple) est en train d'être créée et chargée ;

- EventResolveFailed : se déclenche lorsque la création d'une ressource n'a pas fonctionné pour diverses raisons ;

Pour utiliser ces événements, il faut créer une procédure de type TAsphyreEventCallback dont voici sa syntaxe :

TAsphyreEventCallback = procedure(Sender: TObject; EventParam: Pointer;
var Success: Boolean) of object;

Une fois les procédures écrites, il faut appeler les méthodes Subscribe de chacun des événements que vous voulez utiliser.
Par exemple, si vous voulez modifier le Caption de la fiche principale lorsque le moteur est chargé, il faut faire comme ceci :

procedure TMainFrm.OnDeviceResetCallback(Sender: TObject;
EventParam: Pointer; var Success: Boolean);
begin
Caption := 'OK';
end;

Écrivez ensuite, dans la procédure ConfigureDevice :

procedure TMainFrm.ConfigureDevice(Sender: TAsphyreDevice; Tag: TObject;
var Config: TScreenConfig);
begin
Config.WindowHandle:= Self.Handle; //Cette spécification est nécessaire sinon, l'initialisation ne réussit pas!

EventDeviceReset.Subscribe(OnDeviceResetCallback, Sender);
end;

Ainsi, lorsque l'état du dispositif passera à l'état « En cours de fonctionnement » (fonctionnel), 'OK' sera affiché dans la barre de titre de la fiche.

Pour finir, il suffit de lancer la méthode Initalize de cette façon :

procedure MainFrm.Create(Sender: TObject);
begin
if (not Devices.Initialize(ConfigureDevice, Self)) then
begin
MessageDlg('Asphyre n''a pas pu s''initialiser!', mtError, [mbOk], 0);
Close();
Exit;
end;
end;

Ainsi, si Asphyre n'a pas réussi son activation pour une raison quelconque, la fiche principale se ferme.

III-B-2 - Le Timer

Le timer est une notion importante en programmation DirectX.
En effet, lorsque vous développez une application qui doit dessiner par exemple quelque chose sur la fiche à l'aide de son Canvas (surface qui permet de dessiner du texte et des formes géométriques de type TCanvas), il convient d'écrire le code du dessin dans le OnPaint de la fiche. Lorsque la fiche aura besoin de se redessiner, elle appellera cette méthode. Il est néanmoins parfois nécessaire de forcer le dessin avec un appel à Invalidate, Repaint, etc.
Ici, rien de tout ça : un timer déclenche à chaque cycle la procédure de dessin. L'impact direct est que chacune de vos modifications (par exemple, la position d'une image) sont visibles quasiment en temps réel (Parler de temps réel est un abus de langage : en informatique, rien ne se fait en temps réel. En revanche, le temps entre la modification et la visualisation est tellement court que l'on parle de temps réel).

Une fois l'initialisation effectuée et réussie, il faut déclencher le timer :

Timer.OnTimer  := TimerEvent;
Timer.OnProcess:= ProcessEvent;
Timer.MaxFPS   := 100;
Timer.Enabled  := True;

Comme vous pouvez le constater, ce Timer est déjà défini dans l'unité AsphyreTimer. Ceux qui ont déjà utilisé la version eXtreme d'Asphyre auront sans doute reconnu les événements OnTimer et OnProcess indispensables au fonctionnement d'Asphyre. En voici un descriptif :

- Enabled : permet exactement la même chose que le Enabled du TTimer de la VCL, c'est-à-dire, démarrer le Timer ;

- OnTimer : se déclenche à chaque cycle.
En règle générale, cet événement doit déclencher le rendu de la scène (2D ou 3D) et le Process du Timer ;

- OnProcess : est directement déclenché dans l'événement OnTimer. Regroupe toutes les opérations effectuées sur les objets de la scène (déplacement, rotation, etc.) ;

- MaxFPS : permet de spécifier le nombre maximal de FPS (Frames Per Second : images par seconde). Si vous ne voulez pas le limiter, spécifiez un grand nombre.

Ainsi, il est nécessaire d'écrire deux méthodes réagissant à OnTimer et à OnProcess.
Vous me direz, pourquoi découper ce OnTimer en OnTimer et en OnProcess ? La raison principale est purement pratique : vous verrez qu'à la longue, le code permettant de gérer les différents sprites enfle rapidement. De cette façon, le code est rangé de façon à le retrouver plus facilement. Dans OnTimer, on déclenche le rendu et OnProcess. Dans OnProcess, on a tous les traitements à effectuer.
Ces méthodes sont de type TNotifyEvent et, donc leur écriture se fait comme ceci :

procedure TimerEvent(Sender: TObject);
procedure ProcessEvent(Sender: TObject);

On les mettra encore une fois dans la section private de la fiche pour les mêmes raisons que la méthode ConfigureDevice.

III-B-2-a - OnTimer

Cet événement se produit automatiquement à chaque cycle du Timer. Comme nous l'avons vu plus haut, c'est ici que l'on appelle le rendu (dessin) de la scène et qu'on appelle la méthode OnProcess à l'aide de la méthode Process de Timer.

procedure TMainFrm.TimerEvent(Sender: TObject);
begin
DefDevice.Render(DrawView, Self, $000000);
Timer.Process;
end;

Analysons plus en détail cette procédure.

DefDevice.Render(DrawView, Self, $000000);

- DrawView : méthode appelée pour dessiner des données complémentaires. on peut comparer cette procédure à la procédure OnPaint de TForm : c'est là-dedans qu'on dessine les formes, les textes, etc.

- Self : objet sur lequel dessiner. Ici, la fiche principale.

- $000000 : couleur de fond. Ici, du noir.

La méthode DrawView s'écrit de la façon suivante :

procedure TMainFrm.DrawView(Sender: TAsphyreDevice; Tag: TObject);
begin
...
end;

Ensuite, nous avons :

Timer.Process();

Cette ligne a pour unique fonction de déclencher OnProcess du Timer.

III-B-2-b - OnProcess

Cet événement regroupe toutes les opérations à effectuer sur les différents objets affichés (ou non). Nous n'en sommes qu'au début du tutoriel, nous verrons son utilisation plus loin. Néanmoins, nous le déclarerons même s'il est vide pour acquérir le bon réflexe.

III-B-3 - Finaliser

Une fois le programme terminé, il faut libérer toutes les ressources allouées lors de l'initialisation.
Pour cela, rien de plus simple :

procedure TMainFrm.FormDestroy(Sender: TObject);
begin
Devices.Finalize;
end;

Et c'est tout ! Évidemment, si vous avez des ressources que vous avez créées au démarrage, il faut les libérer en choisissant l'événement adéquat comme vu dans la section III.B.1.

III-C - Premier programme: « Hello World! »

Passons maintenant à la création d'un petit programme !

Pour respecter la tradition de la programmation, nous allons faire un simple programme qui affiche « Hello World » en blanc sur un fond noir, le tout en mode fenêtré.

Tout d'abord, créez une nouvelle application avec fiche et sauvegardez-la.
Appelez la fiche principale MainFrm.

Première chose à faire: initialiser Asphyre. Comme nous l'avons vu, voici ce dont nous avons besoin :

- OnCreate de la fiche afin d'initialiser Asphyre ;

- une procédure ConfigureDevice qui sera appelée lors de l'initialisation pour paramétrer l'affichage ;

- une procédure TimerEvent pour le OnTimer du Timer ;

- une procédure ProcessEvent pour le OnProcess du Timer ;

- une procédure RenderCallback pour dessiner le texte lors de l'appel à la méthode Render ;

- OnDestroy de la fiche afin de finaliser Asphyre.

procedure TMainFrm.ConfigureDevice(Sender: TAsphyreDevice; Tag: TObject;
var Config: TScreenConfig);
begin
Config.Width   := ClientWidth;
Config.Height  := ClientHeight;
Config.Windowed:= True;

Config.WindowHandle:= Self.Handle;
Config.HardwareTL  := true;
end;

Voilà pour le ConfigureDevice.
Pour le OnCreate, on aura donc l'initialisation d'Asphyre et du Timer, ainsi que la création de la police de caractères :

procedure TMainFrm.FormCreate(Sender: TObject);
begin
if (not Devices.Initialize(ConfigureDevice, Self)) then
begin
MessageDlg('Asphyre n''a pas pu s''initialiser!', mtError, [mbOk], 0);
Close();
Exit;
end;

DefDevice.SysFonts.CreateFont('sys:tahoma', 'tahoma', 9, False, fwtHeavy,
fqtClearType, fctAnsi);

Timer.OnTimer  := TimerEvent;
Timer.OnProcess:= ProcessEvent;
Timer.MaxFPS   := 100;
Timer.Enabled  := True;
end;

Nous verrons plus en détail la façon d'écrire un texte plus loin dans le tutoriel.

Passons ensuite aux deux méthodes du Timer :

procedure TMainFrm.ProcessEvent(Sender: TObject);
begin
//Rien
end;

procedure TMainFrm.TimerEvent(Sender: TObject);
begin
DefDevice.Render(RenderCallback, self, $000000);
end;

Dans cette procédure RenderCallback, le texte sera écrit. Voici cette procédure :

procedure TMainFrm.RenderCallback(Sender: TAsphyreDevice; Tag: TObject);
begin
Sender.SysFonts.Font['sys:tahoma'].TextOut('Hello world!',
4, 10, $FFFFFFFF);
end;

Comme dit plus haut, la méthode d'écriture sera détaillée plus loin.

Évidemment, il ne faut surtout pas oublier de libérer toute la mémoire que l'on a allouée :

procedure TMainFrm.FormDestroy(Sender: TObject);
begin
Devices.Finalize;
end;

Compilez et exécutez. Vous devriez voir ceci :

Votre premier programme avec Asphyre

Voici le code complet :

interface

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

type
TMainFrm = class(TForm)
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
{ Déclarations privées }
procedure ConfigureDevice(Sender: TAsphyreDevice; Tag: TObject;
var Config: TScreenConfig);
procedure TimerEvent(Sender: TObject);
procedure ProcessEvent(Sender: TObject);
procedure RenderCallback(Sender: TAsphyreDevice; Tag: TObject);
public
{ Déclarations publiques }
end;

var
MainFrm: TMainFrm;

implémentation

uses
AsphyreEvents, AsphyreTimer, AsphyreSystemFonts;

{$R *.dfm}

procedure TMainFrm.ConfigureDevice(Sender: TAsphyreDevice; Tag: TObject;
var Config: TScreenConfig);
begin
Config.Width   := ClientWidth;
Config.Height  := ClientHeight;
Config.Windowed:= True;

Config.WindowHandle:= Self.Handle;
Config.HardwareTL  := true;
end;

procedure TMainFrm.FormCreate(Sender: TObject);
begin
if (not Devices.Initialize(ConfigureDevice, Self)) then
begin
MessageDlg('Asphyre n''a pas pu s''initialiser!', mtError, [mbOk], 0);
Close();
Exit;
end;

DefDevice.SysFonts.CreateFont('sys:tahoma', 'tahoma', 9, False, fwtHeavy,
fqtClearType, fctAnsi);

Timer.Enabled  := True;
Timer.OnTimer  := TimerEvent;
Timer.OnProcess:= ProcessEvent;
Timer.MaxFPS   := 100;
end;

procedure TMainFrm.FormDestroy(Sender: TObject);
begin
Devices.Finalize;
end;

procedure TMainFrm.ProcessEvent(Sender: TObject);
begin
//Rien
end;

procedure TMainFrm.RenderCallback(Sender: TAsphyreDevice; Tag: TObject);
begin
Sender.SysFonts.Font['sys:tahoma'].TextOut('Hello world!',
4, 10, $FFFFFFFF);
end;

procedure TMainFrm.TimerEvent(Sender: TObject);
begin
DefDevice.Render(RenderCallback, self, $000000);
Timer.Process;
end;

end.

Comme vous pouvez le voir, aucun composant n'est nécessaire. Toutefois, il faut tout de même déclarer les unités qui contiennent les routines que l'on a utilisées. Puisque justement, il n'y a pas de composants, leur rajout ne se fait pas de façon automatique.
Seul AsphyreDevices nécessite d'être déclarée en interface puisque les procédures que l'on ajoute contiennent des types qui y sont déclarés. Toutes les autres peuvent être déclarées en implémentation.

III-C-1 - Bug de librairie d3dx9d_31.dll

Si vous êtes sous Delphi 2007 ou RAD Studio 2007, il se peut que vous ayez une erreur telle que celle-ci :

Cette application n'a pas pu démarrer, car d3dx9d_31.dll est introuvable. La réinstallation de cette application peut corriger ce problème

C'est un bug connu d'Asphyre. La raison est simple: Yuriy Kotsarenko utilise le SDK de DirectX qui propose cette DLL pour le déboguage. Sa version Release est nommée « d3dx9_31.dll » (sans le dernier d). Delphi 2007, par défaut est en configuration de Debug. Dans le code de DXDX9.pas, si le mode debug est activé, c'est la DLL « d3dx9d_31.dll » qui sera utilisée, sinon, c'est l'autre.

Pour le résoudre, vous avez plusieurs solutions :

- récupérez cette DLL : allez sur MSDN , téléchargez le SDK et installez-le. Normalement, la librairie devrait s'ajouter dans System32 ;

- copiez « d3dx9_31.dll » et renommez-la en « d3dx9d_31.dll » ;

- passez votre projet en configuration Release (Projet\Options… section Compilation puis dans la boite déroulante « Config construction » en haut à droite, choisissez Release) puis reconstruisez (pas recompilez) le tout ;

- la méthode la plus simple : rendez-vous dans l'unité D3DX9.pas d'Asphyre et allez ligne 82. Vous y trouverez ce code :

d3dx9MicrosoftDebugDLL = 'd3dx9d_31.dll';

Vous l'avez compris : ce code définit la DLL lorsque le mode Debug est actif. Copiez cette ligne (ou remplacez-la) par ceci :

// On laisse la ligne commentée au cas où, mais ce n'est pas forcément
// nécessaire si vous ne comptez pas utiliser cette librairie de débogage
//  d3dx9MicrosoftDebugDLL = 'd3dx9d_31.dll';
d3dx9MicrosoftDebugDLL = 'd3dx9_31.dll';

Cette DLL ne peut pas être distribuée. Pour pallier ce problème, vous pouvez télécharger les DLL libres de droits.

III-D - Méthodes de dessin

Dans cette section, nous allons voir les principales méthodes de dessin, à savoir écrire du texte, dessiner des formes et afficher une image.

III-D-1 - Écrire du texte

Écrire du texte sur une surface DirectX reste assez simple grâce à Asphyre. Nous allons voir ici les deux méthodes qui permettent de dessiner du texte.

III-D-1-a - Écriture « simple » à partir d'une police système

La méthode ressemble à la méthode TextOut du TCanvas de la VCL. À ceci près que l'appel à ce TextOut se fait à un endroit bien précis.

Avant toute chose, il faut créer la police que l'on va utiliser. Pour cela, dans le OnCreate, il suffit d'appeler le constructeur de police. Dans notre exemple, on a cette ligne :

DefDevice.SysFonts.CreateFont('sys:tahoma', 'tahoma', 9, False, fwtHeavy,
fqtClearType, fctAnsi);

Voici la déclaration que l'on trouve dans AsphyreSystemFonts.pas :

function CreateFont(const Name, SystemFontName: string; Size: Integer;
Italic: Boolean = False; Weight: TFontWeightType = fwtNormal;
Quality: TFontQualityType = fqtClearType;
Charset: TFontCharsetType = fctAnsi): Integer;

- Name : le nom de la police. C'est ce nom que l'on va utiliser pour appeler cette police de caractères.

- SystemFontName : le nom de la police telle qu'elle est définie dans Windows.

- Size : taille de la police.

- Italic : police en italique ou non.

- Weight : épaisseur de la police.

- Quality : qualité de la police.

- Charset : charset de la police.

Weight définit l'épaisseur de la police. Il propose neuf différentes valeurs :

fwtThin, fwtExtraLight, fwtLight, fwtNormal, fwtMedium, fwtSemiBold, fwtBold, fwtExtraBold, fwtHeavy

Voici à quoi ressemblent ces valeurs avec Quality et Charset à leur valeur par défaut (respectivement fqtClearType et fctAnsi) et police Technic :

Les différentes épaisseurs de police

La différence est subtile, mais assez visible sur cette police à cette taille.

Une fois la police créée, on peut écrire sur la surface. Pour cela, il suffit d'écrire, comme dans notre exemple, la procédure suivante dans le Callback de Rendu :

Sender.SysFonts.Font['sys:tahoma'].TextOut('Hello world!',
4, 10, $FFFFFFFF);

On identifie notre police grâce à son nom (ici 'sys:tahoma') puis on appelle sa méthode TextOut dont voici les déclarations :

procedure TextOut(const Text: WideString; const Rect: TRect;
Format: TFontFormatType; Color: Cardinal); overload;

procedure TextOut(const Text: WideString; x, y: Integer;
Color: Cardinal); overload;

La première permet de définir la position du texte dans un cadre (TRect) ce qui permet de spécifier des effets particuliers pour le texte que l'on veut inscrire dedans. Asphyre propose les dix valeurs différentes de TFontFormatType suivantes :

fftTop, fftBottom, fftLeft, fftRight, fftCenter, fftVCenter, fftNoClip, fftExpandTabs, fftSingleLine, fftWordBreak

Ces valeurs peuvent être combinées afin, par exemple d'obtenir un texte centré verticalement et horizontalement dans une case de cette façon :

var
UneCase: TRect;
begin
//Initialisation de UneCase
...
Sender.SysFonts.Font['MaPolice'].TextOut('Mon texte', UneCase, [fftCenter, fftVCenter], $FFFFFFFF);
end;

La seconde permet de spécifier simplement la position du texte à l'aide de ses coordonnées en pixels sur la surface.

Le dernier paramètre est la couleur du texte. Il se décompose ainsi $AARRVVBB, AA étant la valeur de transparence, RR la valeur de rouge, VV, la valeur de vert et BB la valeur de bleu. On peut donc facilement définir la couleur ainsi que son opacité :

Différentes valeurs d'opacité

Sender.SysFonts.Font['sys:tahoma'].TextOut('Opacité = $FF (255)',
4, 10, $FFFF0000);
Sender.SysFonts.Font['sys:tahoma'].TextOut('Opacité = $CC (204)',
4, 30, $CCFF0000);
Sender.SysFonts.Font['sys:tahoma'].TextOut('Opacité = $99 (153)',
4, 50, $99FF0000);
Sender.SysFonts.Font['sys:tahoma'].TextOut('Opacité = $66 (102)',
4, 70, $66FF0000);
Sender.SysFonts.Font['sys:tahoma'].TextOut('Opacité = $33 (51)',
4, 90, $33FF0000);

III-D-1-a-i - Écrire le nombre d'images par seconde (FPS)

Une des utilités les plus intéressantes est de pouvoir afficher le nombre de FPS actuel. Cela peut nous informer de la qualité d'optimisation de notre programme.
Pour cela, il faut appeler la fonction FrameRate de Timer.

Le résultat de cette fonction est un entier. Il suffit donc de convertir la valeur et d'écrire, par exemple :

Sender.SysFonts.Font['sys:tahoma'].TextOut('FPS: ' + IntToStr(Timer.FrameRate),
4, 10, $FFFFFFFF);

III-D-1-b - Écriture à partir d'une police « complexe »

Comme nous l'avons vu, il est très facile de créer des polices simples grâce à quelques lignes de code. Voyons maintenant comment créer des polices un peu plus complexes grâce à l'un des outils fournis avec Asphyre : FontTool.exe.

Cette méthode nécessite l'emploi de cet outil et l'intégration à votre programme est un peu plus complexe.

III-D-1-b-i - Description de l'outil FontTool.exe

Cet outil se trouve à la base du répertoire dans lequel vous avez décompressé Asphyre. Voici de quoi il a l'air :

Interface de FontTool.exe

Depuis ce programme, on va pouvoir créer une police personnalisée avec toute sorte d'effets (ombrage, antialiasing, etc.).
Chaque police sera représentée par une image (en général PNG) contenant tous les caractères de la police et d'un fichier XML contenant toutes les données relatives à cette police.

Voyons maintenant en détail à quoi servent tous ces panneaux.

Panneau Windows Font Settings

Dans ce panneau, vous pouvez paramétrer la police, sa taille et ses caractéristiques (gras, italique) comme dans n'importe quel éditeur de texte. Le bouton Details ouvre la boite de dialogue de sélection de police de caractères que tout le monde connait.
Un aperçu de la police brute est affiché en dessous.

Panneau Font Preview

Ce panneau montre un aperçu de ce que donnera la police définitive. La mise à jour se fait au bout de 2-3 secondes après avoir modifié un paramètre.

Panneau Rendering Options

Ce panneau vous permet de sélectionner les effets que vous voulez intégrer à votre police. Les effets disponibles sont l'anticrénelage (Antialias), le contour (Outline), l'ombrage (Shadow), l'exagération des contours (Sharpen). Ces effets peuvent être combinés. Pour les activer, il suffit de cliquer sur le bouton correspondant.
Les deux listes déroulantes du dessus permettent de définir l'étendue des caractères qui seront intégrés à la police. En effet, comme nous l'avons vu plus haut, une image de la police est générée. Si vous ne comptez utiliser que les chiffres par exemple, il est inutile de surcharger les ressources pour rien : intégrez uniquement les chiffres dans votre police. Vous pouvez néanmoins intégrer tous les caractères que vous voulez.

Panneau Individual Letter Text

Ce panneau définit la luminosité (Brightness) et l'opacité (Opacity) des caractères. Si vous jouez sur la valeur de luminosité, vous verrez les couleurs s'assombrir ou s'éclaircir dans l'aperçu de gauche.
Le fond en carreau de cet aperçu permet de contrôler facilement l'opacité. Cette opacité vient en complément de celle que l'on peut définir dans TextOut. Ainsi, si on définit, dans FontTool, une opacité à 50 %, si dans TextOut, on la définit à 100 % ($FF), on aura tout de même une police transparente à 50 %.

Les deux panneaux suivants permettent de paramétrer les effets Border et Shadow.

Panneau Outer Letter Border

Ce panneau paramètre l'effet de bordure. Vous pouvez spécifier la luminosité, l'opacité et l'épaisseur de cette bordure.

Panneau Letter Depth Shadow

Ce panneau, quant à lui, permet de paramétrer l'effet d'ombrage. Vous pouvez ainsi modifier les valeurs d'adoucissement (les ombres seront moins marquées), de luminosité, d'opacité, et la distance de l'ombre par rapport à son objet en pixels. Si vous voulez donner l'effet d'un texte juste au-dessus de ce qu'il couvre, laissez une petite valeur. Comme pour tous les paramètres, vous pourrez vérifier le résultat dans l'aperçu de gauche.

Vous l'aurez compris, l'énorme avantage d'utiliser cette méthode pour créer une police – outre les effets disponibles – est que l'on ne dépend plus de la police elle-même : si vous utilisez une police non fournie avec Windows par exemple, vous n'avez plus à vous soucier de son déploiement puisque la police est intégrée à une image.

III-D-1-b-ii - Création d'une police

Voyons maintenant comment créer une police.
Exécutez FontTool.exe et paramétrez-le ainsi :

Panneau Windows Font Settings

Champ

Valeur

Font

Arial

Size

12

Italic

Non

Bold

Oui

Panneau Rendering Options

Champ

Valeur

Etendu de caractères

#032 à #126

Antialias

Oui

Outline

Oui

Shadow

Non

Sharpen

Non

Panneau Individual Letter Text

Champ

Valeur

Brightness

1.000

Opacity

0.600

Panneau Outer Letter Border

Champ

Valeur

Brightness

0.700

Opacity

1.000

Thickness

1

Pour saisir une valeur exacte sur les gauges, on peut cliquer avec le bouton droit de la souris pour la saisir manuellement.

Panneau Letter Depth Shadow
Puisque l'effet Shadow n'est pas utilisé, il n'est pas nécessaire de paramétrer cette zone.

Une fois la police paramétrée, il faut la sauvegarder. Pour cela, cliquez sur le bouton en bas à gauche « Save Font As… » puis sélectionnez un chemin et un nom pour vos fichiers. Créez un répertoire « Fonts » dans le répertoire de votre projet puis enregistrez votre police dedans en la nommant « MaPolice.png ».

L'outil enregistrera automatiquement les deux fichiers dans le répertoire sélectionné. Ces deux fichiers auront le même nom.
Vous devez donc avoir deux fichiers :

- MaPolice.png

Le fichier MaPolice.png

- MaPolice.xml : ce fichier contient les données nécessaires à l'utilisation de la police.

III-D-1-b-iii - Créer le fichier XML de ressources

Auparavant, dans Asphyre eXtreme, on utilisait un composant, le TAsDb qui se chargeait d'ouvrir un fichier de type .asdb qui contenait toutes les ressources du programme. Ce fichier de ressources était créé à l'aide d'un outil proposé avec cette version d'Asphyre et permettait de rajouter toute sorte de fichiers.

Dans la version 4.1, tout se fait en XML. Le format .asdb n'existe plus (au grand dam de beaucoup de gens et au contraire de Asphyre Casual qui a repris ce système).

Vous devez donc créer un fichier XML qui fera appel au fichier XML de la police. Ce fichier XML peut contenir les données de polices et d'images que nous verrons plus loin.

Créez donc un nouveau fichier XML dans Delphi et appelez-le Ressources.xml.
Voici le squelette de ce fichier XML :

<unires>

</unires>

Il est inutile de spécifier la version d'encodage des caractères.
Chaque ressource (image, police) sera incluse dans un groupe que l'on peut nommer.

Comme notre police est contenue dans un fichier image, nous devons d'abord la répertorier dans un groupe d'image :

<image-group name="default group">
</image-group>

Entre ces balises, nous devons spécifier l'image. Les images sous Asphyre sont des textures et il convient de les déclarer comme tels :

<image uid="mapolicepicture" type="image">
<format type="a8r8g8b8" miplevels="auto"/>

<textures count="1">
<texture num="0" source=".\fonts\mapolice.png" />
</textures>

</image>

Regardons plus en détail ces lignes. La première ligne ouvre un nouveau nœud image. L'attribut uid est l'identifiant de l'image. L'attribut type définit lui le type d'image. Ce type peut être image, surface ou dynamic.
Le nœud format définit les caractéristiques de l'image. type définit comment sont encodées les couleurs et miplevels définit le niveau de mip-mapping.
Comme nous l'avons vu, il faut maintenant déclarer les textures qui composent notre image. Dans notre exemple, il n'y a qu'une seule texture : l'image MaPolice.png. Comme vous le voyez, il est tout à fait possible d'utiliser des chemins relatifs.

Une fois l'image créée, on peut déclarer une police dans un groupe de police de cette façon:

<font-group name="default">
</font-group>

Dans ce groupe, on répertorie une nouvelle police en utilisant l'image répertoriée plus haut et le fichier XML que l'on a créé avec FontTool.exe :

<font uid="mapolice" type="bitmap" image="mapolicepicture" space="0.5" desc=".\fonts\mapolice.xml" />

Si on regarde cette ligne, excepté space (Sinon c'est trop facile!), on comprend rapidement à quoi correspondent les attributs :

- uid : identifiant de la police ;

- type : le type de ressource d'où elle est tirée ;

- image : l'uid de l'image qui contient la police créée précédemment ;

- space : facteur de multiplication pour la largeur du caractère espace (0.0 = pas d'espace ; 1.0 = un espace entier) ;

- desc : chemin du fichier XML qui définit la police.

Il est tout à fait possible de diviser les groupes en plusieurs fichiers XML. Par exemple, ici, on pourrait tout à fait avoir un fichier Images.xml et Fonts.xml, le premier contenant la déclaration du groupe d'images, le second du groupe de police.

En effet, une fois chargées, les ressources sont identifiées seulement par leur uid. Donc il importe peu qu'elles soient déclarées dans le même fichier !

Voilà le plus dur est fait ! À noter qu'il existe un utilitaire qui permet de faire ceci. Cet utilitaire, créé par le membre Ch@ser, est téléchargeable sur le site d'afterwarp ici. Les seuls inconvénients sont :

- quelle que soit l'image (texture complexe ou simple comme notre exemple) toutes les données sont ajoutées, même celles qui sont inutiles. Le XML en devient vite illisible ;

- l'interface pas franchement ergonomique à mon goût.

En revanche, on visualise plus facilement les données grâce à ce programme. L'autre avantage est que les possibilités du XML y sont – normalement – toutes. On peut donc utiliser ce programme pour savoir comment se constituent ces fichiers XML.

III-D-1-b-iv - Charger la police dans le projet

Une fois le fichier XML dûment créé, il faut maintenant charger dans le programme les données que l'on a définies dans ce fichier.
Comme toujours, Asphyre permet de faire assez simplement cette manœuvre. Contrairement à ce que l'on pourrait croire, le chargement de ces fichiers se fait avant l'initialisation et dans le OnCreate !

Le chargement se fait pour chacune des ressources de façon distincte :

procedure TMainFrm.FormCreate(Sender: TObject);
begin
ImageGroups.ParseLink('Ressources.xml'); //Chargement des images
FontGroups.ParseLink('Ressources.xml'); //Chargement des polices

if (not Devices.Initialize(ConfigureDevice, Self)) then //Initialisation
begin
MessageDlg('Asphyre n''a pas pu s''initialiser!', mtError, [mbOk], 0);
Close();
Exit;
end;

//Démarrage et paramétrage du Timer
Timer.Enabled  := True;
Timer.OnTimer  := TimerEvent;
Timer.OnProcess:= ProcessEvent;
Timer.MaxFPS   := 100;
end;

Notez bien l'ordre de chargement des ressources. Dans notre cas il est important !

En effet, dans le fichier XML, nous déclarons une image puis une police dépendante de cette image. Si l'on ne charge pas d'abord les images, le gestionnaire de polices ne va pas trouver l'image correspondante.

En général, puisque les images dépendent seulement de fichiers externes, on peut sans se soucier les charger en premier.

N'oubliez pas de déclarer les unités dans la clause uses :

- ImageGroups est déclaré dans MediaImages ;

- FontGroups est déclaré dans MediaFonts.

Une fois ces images chargées, on peut initialiser. En outre, il est également possible de vérifier que les ressources se soient chargées correctement.
Comme nous l'avons vu plus haut, il suffit de déclarer une méthode en cas d'erreur de chargement de cette façon :

procedure TMainFrm.ConfigureDevice(Sender: TAsphyreDevice; Tag: TObject;
var Config: TScreenConfig);
begin
Config.Width   := ClientWidth;
Config.Height  := ClientHeight;
Config.Windowed:= True;

Config.WindowHandle:= Self.Handle;
Config.HardwareTL  := true;

EventResolveFailed.Subscribe(OnResolveFailed, Sender); //Inscription de la méthode pour l'événement
end;

Il faut alors créer une nouvelle procédure OnResolveFailed de cette façon :

procedure TMainFrm.OnResolveFailed(Sender: TObject; EventParam: Pointer;
var Success: Boolean);
begin
MessageDlg('Erreur de chargement de ressources.', mtError, [mbOk], 0);

Devices.Finalize();
Timer.Enabled:= False;
Application.Terminate();
end;

Ainsi, si une erreur se produit, un message d'erreur apparait puis l'application se ferme.

Il ne faut pas libérer les images et polices créées: tout se fait dans des sections finalization dans leur unité respective.

III-D-1-b-v - Écrire avec la police

Nous voilà enfin parvenus à tout charger correctement! Il ne reste plus qu'à écrire avec cette police amoureusement créée.
De la même façon qu'avec l'écriture « simple », tout se passe encore dans le Callback de rendu. Par contre, cette fois-ci, ce n'est pas Sender.SysFonts, mais Sender.Fonts que l'on utilise :

procedure TMainFrm.RenderCallback(Sender: TAsphyreDevice; Tag: TObject);
begin
Sender.Fonts.Font['MaPolice'].TextOut('Ma chaine de caracteres',
4, 10, $FFFF0000 );
end;

procedure TMainFrm.TimerEvent(Sender: TObject);
begin
DefDevice.Render(RenderCallback, self, $FFFFFF);
Timer.Process;
end;

Pour des raisons esthétiques, j'ai simplement modifié la couleur de fond dans TimerEvent afin d'avoir un fond blanc.
Vous devriez avoir un résultat comme celui-ci :

Résultat d'écriture avec une police complexe

Il existe également une autre procédure TextOut qui, à la place d'un Cardinal pour la couleur demande un type TColor2.
Mais un TColor2, c'est quoi donc ?? C'est évidemment un type d'Asphyre qui est une combinaison de deux couleurs. Mais pourquoi deux couleurs ? Que de questions !! Pour la réponse, je vous laisse découvrir ce que ça donne avec ce code :

Sender.Fonts.Font['MaPolice'].TextOut('Ma chaine de caracteres',
4, 10, cColor2($FF00FF00, $FFFF0000) );

L'autre utilisation de TextOut

En effet, simplement en spécifiant deux couleurs et en utilisant la fonction cColor2, on obtient un dégradé le plus simplement du monde.

Cette fonction cColor2 est déclarée dans l'unité AsphyreTypes. Cette unité contient une quantité assez importante de constantes et autres routines de conversion.

Un bon conseil: si vous devez écrire une petite routine de conversion (de couleur par exemple) vérifiez d'abord que cette fonction n'existe pas déjà dans cette unité !

Par exemple, la fonction cColor2 utilisée ci-dessus renvoie un type TColor2 à partir de deux couleurs passées en paramètres.

On peut également modifier les options de dessin de la police. Par exemple, on voudrait modifier l'espace entre chaque caractère. Pour cela, la propriété Font propose une propriété Options qui permet ce genre de modifications.
Par exemple, ce code :

with Sender.Fonts.Font['MaPolice'] do
begin
Options.Kerning := -2;
TextOut('Ma chaine de caracteres',
4, 10, cColor2($FF00FF00, $FFFF0000) );
end;

va diminuer l'espace entre chaque caractère. D'autres paramètres sont disponibles comme la taille (attention: ce n'est qu'un facteur d'échelle : l'image d'origine reste la même !), afficher l'ombrage ou non, etc.

III-D-2 - Dessiner des formes

Le dessin de forme, comme on le fait sur un TCanvas à l'aide de MoveTo, LineTo, Rectangle, FillRect, etc. dans la VCL, se fait de manière assez similaire puisque le type TAsphyreDevice propose une propriété Canvas de type TAsphyreCanvas qui permet toute sorte de dessin.

Toutes les fonctions de dessin se font dans la procédure de Callback du rendu que l'on a appelé jusque là RenderCallback. Dans les codes qui suivront, Sender sera donc du type TAsphyreDevice.

À l'instar du TCanvas de la VCL, le TAsphyreCanvas permet de dessiner une grande variété de figures géométriques. Nous ne verrons ici que le point, la ligne, le rectangle et l'ellipse afin de bien s'imprégner de la méthode de dessin d'Asphyre.

III-D-2-a - Paramétrer le dessin

Avant de dessiner quoi que ce soit, on peut paramétrer des valeurs telles que la largeur des lignes, le type d'anticrénelage, etc.
Pour cela, à l'instar du TCanvas de la VCL, le TAsphyreCanvas offre des propriétés. Voyons les plus utiles :

- LineAntialias : permet de spécifier si on veut utiliser l'anticrénelage ou non ;

- LineWidth : épaisseur de la ligne.

Nous allons le voir plus loin, mais plusieurs procédures de dessin se déclinent en deux versions : Ex et Hw (par exemple LineEx et LineHw). La fonction type Hw est plus rapide puisqu'elle utilise directement le matériel (la carte graphique). En revanche, Hw n'est pas impacté par LineWidth et l'anticrénelage n'est pas paramétrable.

III-D-2-b - Dessiner un point

La méthode de dessin de base est de dessiner un pixel. En fait, on ne dessine pas un pixel à proprement dit, on lui définit une couleur. Cette opération se fait en utilisant la procédure PutPixel du TAsphyreCanvas. Deux méthodes surchargées sont disponibles et ont uniquement pour différence le type de donnée pour les coordonnées du pixel. L'une demande X et Y, l'autre demande un type TPoint2.

procedure PutPixel(const Point: TPoint2; Color: Cardinal;
DrawFx: Integer); overload;
procedure PutPixel(x, y: Single; Color: Cardinal; DrawFx: Integer); overload;

Les paramètres suivants sont la couleur du point et sa méthode de dessin que nous détaillerons plus bas.

III-D-2-c - Dessiner une ligne

Il existe plusieurs façons de dessiner une ligne. Nous allons voir les deux plus caractéristiques.

Le première utilise LineEx. Cette fonction accepte un point de départ, un point d'arrivée et une couleur :

Sender.Canvas.LineEx(4, 30, 200, 30, $FFFF0000);
Sender.Canvas.LineEx(Point(4, 30), Point(200, 30), $FFFF0000);
Sender.Canvas.LineEx(Point2(4, 30), Point2(200, 30), $FFFF0000);

Ces trois méthodes donnent rigoureusement le même résultat. À noter que la fonction Point2 ainsi que le type TPoint2 se trouve dans l'unité Vectors2.

La seconde méthode permet un peu plus de possibilités. On pourra en effet définir un dégradé et une transparence.

Sender.Canvas.LineHw(4, 35, 200, 35, $FFFF0000, $FF00FF00, fxuBlend);

Cette ligne dessine une ligne partant du rouge opaque ($FFFF0000) vers un vert opaque ($FF00FF00). Le dernier paramètre définit la façon dont va se dessiner la ligne. fxuBlend (unité AsphyreEffects.pas) spécifie d'utiliser les channels alpha (de transparence) des couleurs utilisées.
De la même façon que LineEx, LineHw propose plusieurs versions surchargées qui pourront être utiles suivant le type de données que vous avez au moment de l'appel (TPoint ou TPoint2).
Il est à noter que LineHw est plus rapide que LineEx.

III-D-2-d - Dessiner un rectangle

À l'instar du dessin de ligne, on retrouve deux types de procédures de TAsphyreCanvas: WireQuadEx et WireQuadHw. Les différences sont les mêmes : la première dessine un rectangle simple à l'aide de ses quatre côtés et d'une couleur.
La seconde dessine un rectangle avec les quatre côtés, un type TColor4 (combinaison de quatre couleurs pour définir un dégradé entre chaque « branche » du rectangle) et une méthode de dessin.

Exemple d'utilisation :

Sender.Canvas.WireQuadEx(Point4(Point2(4, 10), Point2(200, 10), Point2(200, 30), Point2(4, 30)), $FF00FF00);

Sender.Canvas.WireQuadHw(
Point4(Point2(4, 50), Point2(200, 50), Point2(200, 70), Point2(4, 70)),
cColor4($FFFFFFFF, $FFFF0000, $FF00FF00, $FF0000FF),
fxuBlend);

La fonction Point4 est déclarée dans l'unité AsphyreTypes.
Voici ce que donne ce code :

Rectangles utilisant les deux méthodes

Les quatre points donnés doivent être dans l'ordre afin d'avoir un rectangle. Il est tout à fait possible d'obtenir ceci :

Rectangle avec les points dans le désordre

avec ce code :

Sender.Canvas.WireQuadHw(
Point4(Point2(4, 50), Point2(200, 70), Point2(200, 50), Point2(4, 70)),
cColor4($FFFFFFFF, $FFFF0000, $FF00FF00, $FF0000FF),
fxuBlend);

En fait, les procédures WireQuadEx et WireQuadHw permettent de dessiner des quadrilatères, quels qu'ils soient.

À noter qu'il existe également des procédures permettant de dessiner des rectangles pleins. En particulier, FillQuad qui s'approche beaucoup de WireQuad :

Sender.Canvas.FillQuad(
Point4(Point2(4, 50), Point2(200, 50), Point2(200, 70), Point2(4, 70)),
cColor4($FFFFFFFF, $FFFF0000, $FF00FF00, $FF0000FF),
fxuBlend);

Donne un rectangle rempli de beaux dégradés :

Résultat de FillQuad

III-D-2-e - Dessiner un cercle et une ellipse

Deux procédures sont fournies pour cette opération : Circle et Ellipse (oui les noms sont bien choisis…). Voici les paramètres demandés par cette procédure :

- x et y : la position du centre du cercle ;

- Radius : le rayon du cercle (type single) ;

- Steps : précision de l'arrondi. Plus la valeur sera grande, mieux l'arrondi sera dessiné. En revanche, le dessin sera plus lourd ;

- Color : la couleur de type cardinal ($AARRGGBB).

Exemple d'utilisation :

Sender.Canvas.Circle(50, 50, 40, 20, $FF0000FF);

Comme toujours, voici le résultat :

La procédure Circle

La procédure Ellipse marche rigoureusement de la même façon sauf qu'évidemment, au lieu du rayon seulement, on définit le grand axe et le petit axe.
À noter que la procédure Circle ne fait rien d'autre qu'appeler la procédure Ellipse avec Radius en petit axe et grand axe !

De la même façon que pour le quadrilatère, il existe des procédures équivalentes pour dessiner un cercle et une ellipse remplis. Ces fonctions sont FillCircle et FillEllipse et s'utilisent de la même façon que FillQuad avec les paramètres de Circle et Ellipse à savoir un type TColor4 (pour faire des dégradés) et une méthode de dessin.
Pour illustrer ceci, voici le résultat que donne ce code :

with Sender.Canvas do
begin
FillEllipse(50, 50, 40, 20, 20,
cColor4($FFFFFFFF, $FFFF0000, $FF00FF00, $FF0000FF),
fxuBlend);
FillCircle(150, 50, 40, 20,
cColor4($FFFF00FF, $FFFF00FF, $FFFFFF00, $FF00FFFF),
fxuBlend);
end;

Résultat de FillEllipse et FillCircle

III-D-3 - Afficher une image

Passons maintenant à l'affichage des images.
La première chose à prendre en compte est la taille de l'image. En effet, Asphyre n'affichera que des images dont la taille est une puissance de 2 (2, 4, 8, 16, 32, 64, 128, 256, etc.).
Donc si vous avez par exemple une image de 300x100, il vous faudra l'inscrire dans une image dont la taille est une puissance de 2 supérieure. Dans cet exemple, une image de 512x128 convient. Évidemment, il faut choisir correctement la taille de son image originale pour éviter d'avoir une image dont une bonne partie n'affiche rien. Par exemple, si une image a un de ses côtés à 513 pixels, l'image affichée dans Asphyre devra être de 1024 pixels puisque dans 512, cela ne rentre pas.
Si vous utilisez une image dont la taille ne correspond pas à cette règle, votre image sera déformée afin de correspondre à sa taille puissance de 2 minimale.

Heureusement, un outil est fourni pour modifier les images. Il s'agit de TextureMaker.exe. Cet outil va permettre de créer une image qui respecte cette règle depuis une autre image.

Si nous utilisons ce programme, cela est uniquement dû à la taille de l'image non conforme. Il va sans dire que cette manipulation peut être faite depuis n'importe quel logiciel de dessin en copiant l'image dans une autre de taille conforme.

Si l'image est déjà à la bonne taille, il est inutile de faire cette opération.

III-D-3-a - Utilisation de TextureMaker

Interface de TextureMaker

Ce programme sert à modifier les images de façon à ce qu'elles soient de taille d'une puissance de 2. Il peut également découper une image en plusieurs pour optimiser la place de vos sprites sur l'image.
Vous pouvez aussi extraire des images depuis une image source suivant un schéma défini.

III-D-3-a-i - Qu'est-ce qu'un pattern ?

Dans l'animation, le pattern est une notion très importante. En effet, lorsque vous animez un personnage par exemple, vous ne faites « que » succéder des images donnant l'illusion du mouvement. Donc deux options s'offrent à nous pour arriver à ce résultat :

- utiliser une image par mouvement ;

- utiliser une image qui regroupe tous les mouvements.

Il est évident qu'en termes d'utilisation de ressources, la seconde méthode est à privilégier !
Ce genre d'image s'appelle une feuille de sprites.
Voici donc à quoi pourrait ressembler une telle image :

Cette image est extraite de l'excellent tutoriel de Loka que vous trouverez ici.
Comme vous le voyez, chaque décomposition du mouvement est sur la même image. Chacune de ses décompositions est un pattern ! Par exemple, dans notre exemple la taille du pattern à utiliser est de la taille de l'image puisqu'il n'y a pas d'animation. En revanche, sur une feuille de sprites, la taille du pattern est celle de chacun des éléments.

III-D-3-a-ii - Paramétrage de TextureMaker

Comme d'habitude, analysons chaque partie du programme séparément.

Première chose à faire : charger l'image. Cela se fait à l'aide du bouton Browse. Une fois l'image chargée, il faut spécifier la taille du pattern.

Donc, si vous avez bien suivi, vous aurez compris que dans l'exemple qui nous intéresse, la taille du pattern est la taille de l'image. Vous remarquerez le « Number of patterns » grisé en dessous qui indique le nombre de patterns d'après la taille que vous avez spécifiée au-dessus.

Cette partie peut être intéressante si vous n'utilisez pas de format d'image transparent. Vous pouvez en effet choisir de garder les informations de transparence de l'image utilisée (channel Alpha) ou bien spécifier une couleur qui va servir de transparence avec une tolérance.
En règle générale, je vous conseille d'utiliser un format contenant des informations de transparence comme le PNG par exemple.

Cette partie permet de spécifier la taille de sortie de l'image. Comme nous l'avons vu, la taille doit être une puissance de 2. En cliquant sur le bouton , le programme calculera lui-même la taille la plus adaptée.

Ce paramètre correspond à l'écart que vous voulez donner entre vos patterns. Si vous voulez qu'ils soient accolés, laissez à 0, sinon, vous pouvez spécifier une autre valeur. Dans notre cas, laissons 0 puisque nous n'avons qu'un seul pattern sur la texture.

Cette partie permet de visualiser l'organisation finale et les pixels inutilisés d'après les paramètres que l'on a fournis.

III-D-3-b - Déclarer l'image dans le XML

Comme nous avons vu dans le chapitre III.D.1.b.iii, il faut déclarer l'image dans le fichier XML de ressources.

<image uid="MonImage" type="image">
<format type="a8r8g8b8" miplevels="auto"/>
<textures count="1">
<texture num="0" source=".\Images\MonImage.png"/>
</textures>
</image>

Si l'image utilise des patterns, on peut les spécifier en les déclarant juste après le nœud format :

<pattern width="300" height="100" count="1" padx="0" pady="0" />

Dans notre cas, c'est évidemment inutile.

III-D-3-c - Charger l'image dans le programme

Nous l'avons déjà vu, le chargement des images depuis le fichier ressources se fait dans le OnCreate de la fiche avant l'initialisation d'Asphyre.

procedure TMainFrm.FormCreate(Sender: TObject);
begin
ImageGroups.ParseLink('Ressources.xml');

if (not Devices.Initialize(ConfigureDevice, Self)) then
begin
MessageDlg('Asphyre n''a pas pu s''initialiser!', mtError, [mbOk], 0);
Close();
Exit;
end;

Timer.Enabled  := True;
Timer.OnTimer  := TimerEvent;
Timer.OnProcess:= ProcessEvent;
Timer.MaxFPS   := 100;
end;

III-D-3-d - Dessiner l'image

Nous voilà enfin à l'étape du dessin. De la même façon que toutes les opérations de dessin sur le TAsphyreCanvas, cela se fait dans la procédure de Callback du rendu.
L'affichage d'une image se fait en deux temps :

- UseMap : on identifie d'abord l'image à dessiner ;

- TexMap : on l'affiche sur le TasphyreCanvas.

Voilà ce que donne le code :

procedure TMainFrm.RenderCallback(Sender: TAsphyreDevice; Tag: TObject);
const
//Constante pour donner la position de l'image
X = 10;
Y = 10;
var ImageIndex: integer;
p4: TPoint4;
begin
//On retrouve l'index de l'image que l'on veut utiliser
ImageIndex := Sender.Images.Image['MonImage'].ImageIndex;

//On retrouve le rectangle d'affichage de l'image à l'aide des constantes
//de position et de la taille de la texture.
//PpBounds4 convertit 4 entiers (gauche, droite, largeur, hauteur) en un type TPoint4
p4 := pBounds4(X, Y,
Sender.Images[ImageIndex].Texture[0].Size.X,
Sender.Images[ImageIndex].Texture[0].Size.Y);

with Sender.Canvas do
begin
//Pour vérifier que l'image est correcte, on peut désactiver l'anticrénelage
Antialias := atNone;

//On définit l'image à utiliser et son pattern (0)
UseImage(Sender.Images[ImageIndex], 0);
//On dessine l'image sélectionnée
TexMap(p4, clWhite4, fxuBlend);
end;
end;

Regardons un peu plus en détail ce code.
UseImage sert donc à sélectionner l'image à utiliser. Il existe quatre versions surchargées de cette procédure suivant les données que vous avez. Celle que j'ai choisie est celle faisant appel au pattern. Il n'y en a qu'un, c'est le premier, son index est donc 0.
Il est aussi possible de définir ce que l'on veut afficher en utilisant des coordonnées de texture :

procedure UseImage(Image: TAsphyreCustomImage;
const Mapping: TPoint4; TexNum: Integer = 0); overload;

Cette version accepte un paramètre de type TPoint4 qui représente les coordonnées de texture. Ici, puisque l'on veut afficher la totalité de l'image, on peut définir des coordonnées de textures qui utilisent cette totalité. Comme je vous le disais, vérifiez d'abord que ce que vous voulez faire n'existe pas déjà dans AsphyreTypes ! On utilisera donc

TexFull4: TPoint4 = ((x: 0.0; y: 0.0), (x: 1.0; y: 0.0), (x: 1.0; y: 1.0),
(x: 0.0; y: 1.0));

Le code sera donc :

UseImage(Sender.Images[ImageIndex], TexFull4);

Passons ensuite à TexMap.

procedure TexMap(const Points: TPoint4; const Colors: TColor4;
DrawFx: Integer);

Le premier paramètre est de type TPoint4 (un tableau de quatre TPoint) qui donne le cadre dans lequel dessiner l'image. Vous l'aurez compris, la fonction pRect4 de l'unité AsphyreTypes (encore elle) convertit un type TRect en type TPoint4.
Le paramètre suivant est un type TColor4 (un tableau de quatre couleurs en cardinal). clWhite4 est encore une constante de l'unité AsphyreTypes qui représente 4 blancs.
Le dernier paramètre de type Integer, nous le connaissons maintenant, il s'agit de la méthode de dessin.

Voici ce que peut donner ce code :

Dessin du logo de developpez.com avec TexMap

Il est évidemment possible de faire varier le paramètre Colors de façon à obtenir des effets intéressants sur l'image. Par exemple, voyons ce que donne ce code :

TexMap(pRect4(aRect), cColor4($66FFFFFF, $99FF0000, $CC00FF00, $FF0000FF), fxuMultiBlend);

La fonction cColor4 construit un type TColor4 à l'aide de une ou de quatre couleurs.
Le paramètre fxuMultiBlend permet de combiner les transparences entre celles de l'image et celles des couleurs définies ainsi que les couleurs elles-mêmes.
Nous obtenons alors cette image :

Effets de couleur avec fxuMultiBlend et des couleurs personnalisées

III-D-3-e - Les effets de déformations

Comme nous l'avons vu, la procédure TexMap utilise un type TPoint4 qui est simplement un tableau de quatre TPoint2 qui définissent chacun un angle de l'image. Il est donc tout à fait possible de modifier un ou plusieurs angles afin de déformer l'image. Ce code par exemple modifie le premier point :

procedure TMainFrm.RenderCallback(Sender: TAsphyreDevice; Tag: TObject);
const
X = 10;
Y = 10;
var
t: TAsphyreCustomImage;
p: TPoint4;
begin
with Sender.Canvas do
begin
t := Sender.Images.Image['ImageDVP'];
UseImage(t, TexFull4);

p := pBounds4(X, Y,
t.Texture[0].Size.X,
t.Texture[0].Size.Y);

p[0].y := p[0].y - 50;

TexMap(p, clWhite4, fxuBlend);
end;
end;

Voci le résultat d'une telle modification :

Résultat de la modification d'un des points

On peut également effectuer une rotation. À l'aide des fonctions Rotate de AsphyreTypes : pRotate4, pRotate4se, pRotate4c.
Voici par exemple ce code :

procedure TMainFrm.RenderCallback(Sender: TAsphyreDevice; Tag: TObject);
const
X = 100;
Y = 50;
var
t: TAsphyreCustomImage;
p: TPoint4;
begin
with Sender.Canvas do
begin
t := Sender.Images.Image['ImageDVP'];
UseImage(t, TexFull4);

p := pBounds4(X, Y,
t.Texture[0].Size.X,
t.Texture[0].Size.Y);

p := pRotate4(
Point2(X, Y),
Point2(t.Texture[0].Size.X, t.Texture[0].Size.Y),
Point2(0, 0),
45);

TexMap(p, clWhite4, fxuBlend);
end;
end;

donne ce résultat :

Rotation d'une image

Il est aussi possible de modifier l'échelle, de faire un miroir horizontal et vertical (respectivement pMirror4 et pFlip4). Leur utilisation ressemble à celle que l'on vient de voir, je ne reviendrai donc pas dessus.

III-E - Téléchargement du programme d'exemple

Vous pouvez télécharger le programme d'exemple ici : Exemple2D.zip

III-F - Conclusion

Nous venons donc de voir les principales méthodes de dessin en 2D. Ce sont les bases qui vous permettront de créer votre premier jeu en 2D avec Asphyre. Pourquoi par un shoot-them-up ou un jeu de plateformes ? Voir un casse-briques ! Bref, une fois que vous avez assimilé ce tutoriel, seule votre imagination sera votre limite !

Éclatez-vous et n'hésitez pas à me proposer vos créations !

IV - La 3D

IV-A - Introduction

Passons maintenant au vif du sujet : la 3D.
Nous allons voir comment insérer un maillage (mesh) et l'afficher avec des effets.
Pour cela, nous utiliserons une technologie incontournable aujourd'hui : les shaders.

IV-B - Les shaders

IV-B-1 - Qu'est-ce que c'est ?

Vous pouvez consulter l'excellent article de Laurent Gomila à ce sujet : https://loulou.developpez.com/tutoriels/moteur3d/partie8/ spécialement, la section 2 qui vous l'expliquera bien mieux que moi.

Ce que vous avez à retenir : les shaders sont des mini programmes exécutés par la carte graphique acceptant des paramètres en entrée et des données en sortie.

Le langage utilisé pour DirectX est le HLSL (High Level Shader Language) qui est très proche du C (je vous avais prévenus !).

Ce que j'appelle shader se divise en deux parties distinctes : le vertex shader et le pixel shader.

Ces programmes de shader sont contenus dans un fichier texte d'extension .fx.

Dans cette section, je partirai du principe que vous avez des notions sur les shaders et leur action ainsi qu'une bonne notion de la 3D en général. Pour information, vous pourrez trouver toute sorte d'informations dans la FAQ de la section jeux vidéo de developpez.com ici.

IV-B-2 - Créer un shader

Diverses possibilités nous sont offertes pour créer un shader.

IV-B-2-a - Bloc-notes

Puisque les shaders sont des fichiers texte, il est tout à fait possible de les écrire avec n'importe quel éditeur de texte ! En revanche, vous n'aurez aucune information sur la syntaxe.

IV-B-2-b - Delphi

Puisque notre EDI favori permet la coloration syntaxique et que – comme je vous l'ai dit – la syntaxe du shader est très proche du C, nous pouvons tout à fait insérer un fichier fx dans Delphi !
Pour cela, ouvrez un fichier .fx du SDK de DirectX par exemple, faites un clic droit sur la source, puis Propriétés. Allez dans la section Options de l'éditeur \ Options du source. Dans la boite déroulante Fichier source, choisissez C/C++. Si vous voulez que le type C/C++ s'applique à chaque fois que vous ouvrez un fichier .fx, ajoutez :

;fx

à la fin de la zone de saisie Extensions. Validez et vous verrez la syntaxe colorée dans le fichier .fx.

IV-B-2-c - FX Composer

Puisque de nos jours, les effets dans les jeux vidéo sont de plus en plus complexes, les fichiers fx deviennent vite longs et difficiles à écrire.
Heureusement, nVidia propose un EDI pour écrire des shaders. Ce programme s'appelle FX Composer et se télécharge ici.
Grâce à ce programme, vous pourrez créer toute sorte d'effets avec vos shaders et avoir une visualisation en temps réel de vos modifications.

IV-C - La 3D dans Asphyre

IV-C-1 - Les matrices

Dans Asphyre et dans DirectX, il existe trois types de matrices qui permettent de paramétrer sa scène :

- Matrice de transformation : cette matrice permet de transformer l'objet que l'on veut afficher (translation, rotation, échelle, etc.) ;

- Matrice de point de vue : cette matrice représente tout simplement la caméra ;

- Matrice de projection : cette matrice est utilisée pour définir la transformation de la 3D vers la surface 2D que représente votre écran.

Sous Asphyre, les transformations de matrice sont facilitées grâce à la classe TAsphyreMatrix (unité AsphyreMatrices.pas). Cette classe regroupe des opérations de matrice indispensable telles que la translation, la rotation, l'échelle, la transformation en matrice identité, etc.
Lorsque l'on utilise les shaders sous Asphyre, il suffit d'utiliser les matrices WorldMtx, ViewMtx et ProjMtx qui sont des TAsphyreMatrix correspondant aux trois matrices indispensables. Il est inutile de les créer et de les détruire : Asphyre s'en charge tout seul. Elles sont déclarées, créées et détruites dans l'unité AsphyresScene.pas.

IV-C-2 - Les maillages

Les maillages sont encapsulés par la classe TAsphyreMeshX (unité AsphyreMeshes.pas). La méthode LoadFromFile permet – comme son nom l'indique – de charger un fichier contenant un ou plusieurs maillages au format .x.
Vous pouvez soit regrouper tous vos maillages de la scène dans un fichier X, soit les séparer dans plusieurs fichiers. Il est préférable d'utiliser la seconde méthode afin de pouvoir paramétrer chaque maillage séparément. Si toutefois plusieurs maillages doivent réagir de la même façon (transformation et affichage), vous pouvez les regrouper dans un seul fichier.

IV-C-2-a - La classe de maillage utilisée

Le programme de shader que nous allons utiliser va simplement afficher une couleur unie sur le maillage.
Il nous faut donc définir une couleur pour chaque maillage.
Pour cela, nous allons créer une nouvelle classe dérivant de TAsphyreMeshX (unité AsphyreMeshes.pas) en y ajoutant une propriété Color de type TAsphyreColor (unité AsphyreColors.pas).

Voici l'unité complète :

unit MyMesh;

interface

uses
AsphyreMeshes, AsphyreColors;

type
TMyMesh = class(TAsphyreMeshX)
private
FColor: TAsphyreColor;
published
public
property Color: TAsphyreColor read FColor write FColor;
end;

implémentation

end.

Et c'est tout ! Comme nous l'avons vu dans la section 2D, les transformations se font quasiment en temps réel. Il est donc inutile d'écrire un SetColor ! Le changement de la couleur sera immédiatement visible.
Enregistrez cette unité en MyMesh.pas. Voici le fichier mesh que nous utiliserons tout le long de ce tutoriel : Mesh.zip

IV-C-3 - L'éclairage

Auparavant, dans Asphyre eXtreme, il fallait déclarer une source de lumière, la créer, la paramétrer et la détruire. Aujourd'hui, rien de tout cela. Ce sont les shaders qui s'occupent de la mise en lumière des objets.
Donc, à part paramétrer sa source de lumière et l'injecter dans le shader si c'est nécessaire (un shader peut tout à fait contenir les données de la source de lumière), il n'y a rien à faire d'autre dans le programme.

IV-D - Utilisation des shaders dans Asphyre : La classe TAsphyreShaderEffect

Nous allons maintenant voir comment inclure un shader à notre programme. Ce programme chargera un maillage au format .x puis l'affichera en utilisant un shader simple.

Sous Asphyre, tout classe de shader hérite de la classe TAsphyreShaderEffect (unité AsphyreShaderFX.pas).

IV-D-1 - Le programme

Avant de commencer, il faut évidemment avoir un programme qui fasse tourner tout ça!
Nous avons vu dans la section précédente, comment écrire un programme qui affiche de la 2D. Eh bien, pour la 3D, c'est exactement pareil.
Dans nos exemples, nous aurons besoin d'une police pour afficher les FPS, un fichier de maillage (.x) et un fichier d'effet qu'il faudra charger.

Voyons donc ce code :

unit Asphyre3D1;

interface

uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, AsphyreDevices, AsphyreSystemFonts, AsphyreTimer, AsphyreScene,
Vectors3, AsphyreTypes, MyShader, MyMesh;

type
TMainFrm = class(TForm)
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure FormKeyPress(Sender: TObject; var Key: Char);
private
{ Déclarations privées }
Shader : TMyShader;
//Le paramètre GameTicks sera un compteur que l'on incrémentera
//afin d'animer le maillage
GameTicks: Integer;
procedure ConfigureDevice(Sender: TAsphyreDevice; Tag: TObject; var Config: TScreenConfig);
procedure TimerEvent(Sender: TObject);
procedure ProcessEvent(Sender: TObject);
procedure RenderPrimary(Sender: TAsphyreDevice; Tag: TObject);
public
{ Déclarations publiques }
end;

var
MainFrm: TMainFrm;
//Maillage que nous allons utiliser
Mesh: TMyMesh;

implémentation

uses
Matrices4, Direct3D9;

{$R *.dfm}

procedure TMainFrm.ConfigureDevice(Sender: TAsphyreDevice; Tag: TObject;
var Config: TScreenConfig);
begin
//Paramétrage de l'affichage
Config.WindowHandle := Handle;
Config.Width := ClientWidth;
Config.Height := CLientHeight;
Config.Windowed := true;
Config.HardwareTL := true;
Config.BitDepth := bd24bit;
end;

procedure TMainFrm.FormCreate(Sender: TObject);
var s: string;
MustClose: boolean;
begin
MustClose := false;
//Initialisation de DirectX
if (not Devices.Initialize(ConfigureDevice, Self)) then
begin
//L'initialisation a échoué
s := 'L''initialisation a échoué.';
MustClose := true;
end;

if not MustClose then
begin
//Création du maillage avec notre classe de maillage créée précédemment
Mesh := TMyMesh.Create(DefDevice);
//Chargement du fichier de maillage .x
if not Mesh.LoadFromFile('dvp.x') then
begin
//Erreur de chargement
s := 'Le chargement du fichier de maillage a échoué';
MustClose := true;
end;
//Spécification de la couleur du maillage
Mesh.Color := $FF0000FF;
end;

if not MustClose then
begin
//Création de la police système qui va servir à afficher le nombre de FPS
DefDevice.SysFonts.CreateFont('s/arial', 'arial', 9, False, fwtBold,
fqtClearType, fctAnsi);

//Création du shader
Shader := TMyShader.Create(DefDevice);
//Tentative de chargement du shader
if not Shader.LoadFromFile('Shader.fx') then
begin
//Erreur de chargement
s := 'Impossible de charger le shader!';
MustClose := true;
end;
end;

if not MustClose then
begin
//Paramétrage et déclenchement du Timer
Timer.MaxFPS := 200;
Timer.OnTimer := TimerEvent;
Timer.OnProcess := ProcessEvent;
Timer.Enabled := True;
end;

//Si une erreur est apparue, quitter l'application
if MustClose then
begin
MessageDlg(s, mtError, [mbOk], 0);
Devices.Finalize();
Close();
Exit;
end;
end;

procedure TMainFrm.FormDestroy(Sender: TObject);
begin
//Destruction des objets
Mesh.Destroy;
Shader.Destroy;
//Finalisation de DirectX
Devices.Finalize;
end;

procedure TMainFrm.FormKeyPress(Sender: TObject; var Key: Char);
begin
//Si l'on appuie sur Echap, quitter l'application
if Key = Chr(vk_escape) then
Close;
end;

procedure TMainFrm.ProcessEvent(Sender: TObject);
begin
//Incrémentation de GameTicks pour l'animation
Inc(GameTicks);
end;

procedure TMainFrm.RenderPrimary(Sender: TAsphyreDevice; Tag: TObject);
begin
//Paramétrage de l'affichage
//Cette étape n'est pas forcément nécessaire
with DefDevice.Dev9 do
begin
SetRenderState(D3DRS_CULLMODE, D3DCULL_CCW);
SetRenderState(D3DRS_ZENABLE, D3DZB_TRUE);
end;

//Paramétrage de la matrice de transformation
//Chargement de la matrice identité
WorldMtx.LoadIdentity;
//Application d'une échelle de 0.2 dans toutes les directions
WorldMtx.Scale(Vector3(0.2, 0.2, 0.2));

//Paramétrage de la caméra
ViewMtx.LoadIdentity();
//Rotation de la caméra suivant le paramètre GameTicks pour animer la scène
Viewmtx.RotateY(GameTicks / 100.0);
//Paramétrage de la caméra afin qu'elle regarde vers le point (0, 0, 0)
ViewMtx.LookAt(Vector3(-15.0, 0.0, 0.0), ZeroVec3, AxisYVec3);

//Paramétrage de la matrice de projection
ProjMtx.LoadIdentity();
//Ces paramètres peuvent servir pour tous les rendus classiques
//De cette façon, la projection utilise la taille de la fenêtre
ProjMtx.PerspectiveFovY(Pi / 4.0, ClientWidth / ClientHeight, 1.0, 1000.0);

//Initialisation du shader
Shader.BeginAll;
//Dessin du maillage par le shader
Shader.Draw(Mesh);
//Finalisation du shader
Shader.EndAll;

//Affichage du nombre de FPS
Sender.SysFonts.Font['s/arial'].TextOut('FPS : ' + IntToStr(Timer.FrameRate),
10, 10, $99009900);
end;

procedure TMainFrm.TimerEvent(Sender: TObject);
begin
//Rien de particulier ici
DefDevice.Render(RenderPrimary, Self, clBlack1, 1.0, 0);
Timer.Process;
end;

end.

Comme vous le voyez, il faut paramétrer les matrices de transformation, vue et projection et ensuite déclencher le rendu par le shader.
Nous utiliserons ce code comme base dans toutes les sections suivantes.

Vous verrez certainement une erreur sous la classe TMyShader ainsi que tous ses membres.
C'est normal, notre classe de shader n'est pas encore faite. Nous allons l'écrire plus bas.

IV-D-2 - Shader simple

Voici le code HLSL du shader que nous allons utiliser dans notre premier programme :

//--------------------------------------------------------------------------------------
// Global variables
//--------------------------------------------------------------------------------------
float4x4 g_WorldViewProj;    // World * View * Projection matrix
float3 MeshRGBColor; //Mesh Color

//--------------------------------------------------------------------------------------
// Vertex shader output structure
//--------------------------------------------------------------------------------------
struct VS_OUTPUT
{
float4 Position   : POSITION;   // vertex position
};

//--------------------------------------------------------------------------------------
// This shader computes standard transform
//--------------------------------------------------------------------------------------
VS_OUTPUT RenderVS(float4 vPos : POSITION)
{
VS_OUTPUT Output;

Output.Position = mul(vPos, g_WorldViewProj);

return Output;
}

//--------------------------------------------------------------------------------------
// Pixel shader output structure
//--------------------------------------------------------------------------------------
struct PS_OUTPUT
{
float4 RGBColor : COLOR;  // Pixel color
};

//--------------------------------------------------------------------------------------
// This shader outputs the pixel's color
//--------------------------------------------------------------------------------------
PS_OUTPUT RenderPS( VS_OUTPUT In )
{
PS_OUTPUT Output;

Output.RGBColor = float4(MeshRGBColor, 1.0);

return Output;
}

//--------------------------------------------------------------------------------------
// Renders scene to render target
//--------------------------------------------------------------------------------------
technique Render
{
pass P0
{
VertexShader = compile vs_2_0 RenderVS( );
PixelShader  = compile ps_2_0 RenderPS( );
}
}

Comme vous pouvez le voir, ce shader est volontairement simple : il ne fait qu'utiliser la transformation standard et afficher une couleur pleine définie sans aucun ombrage ni effet.
Enregistrez ce fichier en Shader.fx.

Ce qui est intéressant pour nous est de voir ce que le shader attend comme paramètres :

//--------------------------------------------------------------------------------------
// Global variables
//--------------------------------------------------------------------------------------
float4x4 g_WorldViewProj;    // World * View * Projection matrix
float3 MeshRGBColor; //Mesh Color

ainsi que le ou les noms des techniques utilisées :

//--------------------------------------------------------------------------------------
// Renders scene to render target
//--------------------------------------------------------------------------------------
technique Render
{
pass P0
{
VertexShader = compile vs_2_0 RenderVS( );
PixelShader  = compile ps_2_0 RenderPS( );
}
}

Ce sera en effet à nous de spécifier au programme de shader quelles sont ces variables et ces techniques.

IV-D-2-a - Les techniques

Les techniques permettent de spécifier d'une part les fonctions utilisées pour le vertex shader et le pixel shader et d'autre part, la version de shader utilisée. Nous avons donc :

VertexShader = compile vs_2_0 RenderVS( );

qui signifie: pour le vertex shader, utiliser la procédure RenderVS et compiler en version shader 2.0 (paramètre vs_2_0).
La ligne suivante se traduit de la même façon pour le vertex shader.
Cette déclaration est indispensable.
Il est à noter que l'on peut tout à fait déclarer plusieurs techniques. Cela permet, par exemple, d'adapter son rendu aux machines plus ou moins performantes.
Exemple :

//--------------------------------------------------------------------------------------
// Renders scene to render target
//--------------------------------------------------------------------------------------
technique Render_2_0
{
pass P0
{
VertexShader = compile vs_2_0 RenderVS( );
PixelShader  = compile ps_2_0 RenderPS( );
}
}

technique Render_1_1
{
pass P0
{
VertexShader = compile vs_1_1 RenderVS( );
PixelShader  = compile ps_1_1 RenderPS( );
}
}

Suivant la technique que l'on utilise, le code HLSL sera compilé en version shader 2.0 ou 1.1.

On peut aussi écrire des procédures RenderVS ou RenderPS différentes, avec des paramètres différents, etc.
Dans ce tutoriel, nous utiliserons la version de shader 2.0 puisque la grande majorité des cartes vidéo de nos jours supporte ce format.

IV-D-2-b - Utilisation des paramètres et des techniques

Créons donc une nouvelle classe de shader que nous appellerons MyShader.pas :

unit MyShader1;

interface

//Unités nécessaires pour la suite
uses
Direct3D9, d3dx9, Vectors2, Vectors3, Matrices4, AsphyreColors,
AsphyreDevices, AsphyreShaderFX, AsphyreMeshes, AsphyreMatrices;

type
TMyShader = class(TAsphyreShaderEffect)
private
protected
public
end;

implémentation

end;

Comme nous l'avons vu, le programme de shader nécessite deux variables : g_WorldViewProj qui est la matrice de transformation. Cette matrice est tout simplement la matrice WorldMtx que nous avons vue plus haut. Il est donc inutile de la déclarer dans la classe.
En revanche, nous devons tout de même spécifier la couleur du maillage pour pouvoir la transférer au shader. Pour cela, il nous faut surchager ces deux méthodes :

- Describe : permet de définir quels sont les paramètres et les techniques utilisés par le programme de shader et leur type de donnée ;

- UpdateParam : permet de spécifier la valeur du paramètre.

Si vous ne paramétrez pas correctement ou que vous oubliez un paramètre, le shader ne pourra pas se charger.

IV-D-2-b-i - La méthode Describe

Cette méthode n'a pas de paramètre. Elle se déclare ainsi dans notre classe :

TMyShader = class(TAsphyreShaderEffect)
private
protected
procedure Describe; override;
public
end;

Cette méthode va spécifier chacun des paramètres utilisés par le shader.
Dans notre cas, nous en avons trois (la matrice de transformation, la couleur et la technique).
Voici comment se présente le contenu de Describe pour ce shader :

procedure TMyShader.Describe;
begin
//Description de la matrice de transformation
DescParam(sptWorldViewProjection, 'g_WorldViewProj');

//Description de la couleur
DescParam(sptCustom, 'MeshRGBColor', 1);

//Description de la technique
DescTechnique('Render', 2);
end;

Regardons un peu plus en détail ce code.
Tout d'abord, nous déclarons la matrice de transformation avec

//Description de la matrice de transformation
DescParam(sptWorldViewProjection, 'g_WorldViewProj');

Le premier paramètre de la procédure DescParam est le type de données. Il en existe plusieurs types que voici :

TShaderParameterType = (sptCustom, sptTexture, sptWorldViewProjection,
sptViewProjection, sptWorldInverseTranspose, sptCameraPosition, sptWorld,
sptWorldInverse, sptWorldView, sptProjection);

Ainsi, puisque nous définissons un type « connu » (la matrice de transformation), nous pouvons spécifier le paramètre sptWorldViewProjection.
La chaine de caractères suivante est le nom de la variable dans le fichier fx
Prenons les lignes suivantes :

//Description de la couleur
DescParam(sptCustom, 'MeshRGBColor', 1);

Cette fois-ci, la procédure DescParam doit prendre un paramètre supplémentaire (son identifiant).
Pourquoi? Tout simplement parce que le paramètre du shader que l'on définit n'est pas standard. D'ailleurs, vous remarquerez le premier paramètre sptCustom qui traduit cet état de fait.

//Description de la technique
DescTechnique('Render', 2);

Ici, nous déclarons la technique et son identifiant.

Que ce soit dans DescParam ou DescTechnique, chaque identifiant doit être unique.

En aucun cas, l'identifiant de la technique ne pourra être 0. Pour une simple raison : si vous spécifiez dans votre rendu, d'utiliser la technique 0, cela veut dire que vous n'utilisez aucune technique !

Pour ne pas être embêté avec ça, prenez l'habitude de commencer vos indices à 1.

Comme vous pouvez le remarquer, ces entiers sont peu parlants. De plus, nous allons les utiliser dans d'autres endroits du code. Prenons dès lors l'habitude d'utiliser des constantes :

const
//Constantes d'indentification
shdrRGBColor = 1;

shdrRender = 2; //Rappel : ne jamais utiliser 0

Le code de Describe devient donc :

procedure TMyShader.Describe;
begin
//Description de la matrice de transformation
DescParam(sptWorldViewProjection, 'g_WorldViewProj');

//Description de la couleur
DescParam(sptCustom, 'MeshRGBColor', shdrRGBColor);

//Description de la technique
DescTechnique('Render', shdrRender);
end;

IV-D-2-b-ii - La méthode UpdateParam

Cette méthode va servir à spécifier au shader la valeur des paramètres. Attention : seuls les paramètres non standards seront à spécifier.
Cette procédure se déclare de la façon suivante :

TMyShader = class(TAsphyreShaderEffect)
private
protected
procedure UpdateParam(Code: Integer; out DataPtr: Pointer;
out DataSize: Integer); override;
public
end;

Pour chaque paramètre identifié par Code, on spécifie la valeur que l'on retourne dans DataPtr. DataSize est la taille du paramètre retourné.
Pour cette manipulation, nous avons besoin d'une variable couleur représentant ladite couleur ainsi qu'une variable de type TD3DColorValue (unité Direct3D9) que nous déclarerons dans la classe (et non local à la procédure) afin d'éviter de pointer sur une zone vide (nil) :

TMyShader = class(TAsphyreShaderEffect)
private
TempColor : TD3DColorValue;
FColor: TAsphyreColor;
protected
procedure UpdateParam(Code: Integer; out DataPtr: Pointer;
out DataSize: Integer); override;
public
end;

Le type TD3DColorValue est le type qui permet de faire la jonction entre une TAsphyreColor et le shader.
Vous aurez noté que le type TAsphyreColor représente quatre valeurs : le channel alpha, rouge, vert et bleu. Nous n'utiliserons pas le channel alpha dans cet exemple (d'où le float3 comme type pour MeshRGBColor dans le code HLSL de notre shader)
On a donc le code suivant :

procedure TMyShader.UpdateParam(Code: Integer; out DataPtr: Pointer;
out DataSize: Integer);
begin
case Code of
//Paramètre de la couleur
shdrRGBColor :
begin
TempColor := FColor;

DataPtr := @TempColor;
DataSize := SizeOf(TD3DColorValue) - SizeOf(Single);
end;
end;
end;

évidemment, dans ce cas, le case of n'est pas réellement nécessaire puisqu'il n'y a qu'un seul paramètre. Toutefois, il faut prendre l'habitude de l'utiliser pour faciliter l'ajout de paramètres.
Détaillons un peu ce code.

case Code of
//Paramètre de la couleur
shdrRGBColor :

Dans le paramètre Code se trouve l'identifiant du paramètre du shader : celui que l'on a spécifié dans la procédure Describe.

TempColor := FColor;

DataPtr := @TempColor;

Le type TAsphyreColor se caste directement avec le type TD3DColorValue. On renvoie ensuite le pointeur vers ce TD3DColorValue dans DataPtr.

DataSize := SizeOf(TD3DColorValue) - SizeOf(Single);

On renvoie la taille de la valeur. Comme nous l'avons vu plus haut, nous n'utiliserons pas le channel alpha de la couleur. C'est pour cela que l'on enlève

SizeOf(Single)

à la taille renvoyée.

IV-D-2-c - Le rendu

Maintenant que le shader est paramétré, il convient de créer une procédure Render afin de paramétrer le shader juste avant le rendu.
Déclarons donc cette procédure dans la section public puisque c'est cette procédure qui va déclencher le rendu du maillage. Il faut donc qu'elle soit accessible depuis mes autres unités :

TMyShader = class(TAsphyreShaderEffect)
private
protected
public
procedure Draw(Mesh: TAsphyreCustomMesh);
end;

Voici son implémentation :

procedure TMyShader.Draw(Mesh: TAsphyreCustomMesh);
var
PassNo: Integer;
begin
//Définition de la technique utilisée
UseTechnique(shdrRender);

//Récupération de la couleur
FColor := TMyMesh(Mesh).Color;

//Mise à jour des paramètres
UpdateAll;

//Rendu du mesh pour chaque passe
for PassNo:= 0 to NumPasses - 1 do
begin
if (not BeginPass(PassNo)) then Break;

Mesh.Draw();

EndPass();
end;
end;

La première procédure utilisée est UseTechnique. Vous l'aurez compris, cela permet de spécifier la technique que l'on va utiliser à l'aide de son identifiant.
Si vous écrivez :

//Définition de la technique utilisée
UseTechnique(0);

Cela signifie, comme je vous le disais plus haut, que vous n'utilisez aucune technique et donc aucun shader.
Il est possible d'utiliser plusieurs techniques, mais une seule à la fois en appelant successivement UseTechnique avec des techniques différentes.

La procédure UpdateAll appelle UpdateParam et UpdateTexture que nous verrons plus loin autant de fois qu'il y a de paramètres pour le shader.
Son appel est donc indispensable. C'est une fonction membre de la classe TAsphyreShaderEffect.

La boucle permet de faire plusieurs passes de rendu suivant le paramètre global NumPasses (nombre de passes).

Il est temps maintenant d'exécuter notre programme et de voir le résultat !

Rendu final avec un shader simple

IV-D-2-d - Débogage du ficher .fx

Lorsque vous exécutez votre programme, le fichier fx est compilé par DirectX. On peut ainsi contrôler s'il n'y a pas d'erreur.
Reprenons notre fichier .fx et mettons-y volontairement une erreur : remplaçons

float4x4 g_WorldViewProj;    // World * View * Projection matrix

du tout début (ligne 4) par

//Ajout volontaire d'une parenthèse pour provoquer
//une erreur de syntaxe
float4x4 (g_WorldViewProj;    // World * View * Projection matrix

Exécutez, vous devriez avoir, outre la boite de dialogue d'avertissement « Impossible de charger le shader », dans le journal d'événement de Delphi, une ligne supplémentaire qui devrait ressembler à ceci :

Sortie du débogage : ...Shader.fx(4): error X3000: syntax error: unexpected token '(' Processus Asphyre3D.exe (2972)

Cette ligne a été rajoutée par DirectX grâce à Asphyre. Elle vous permet de savoir quelle est l'erreur ainsi que la ligne où elle se produit : le numéro de la ligne est le numéro qui suit le nom du fichier .fx. Dans notre cas, c'est bien à la ligne 4 que nous avons volontairement ajouté une parenthèse qui provoque une erreur de syntaxe.
Ainsi, il est tout à fait possible de développer le shader avec Delphi.

IV-D-2-e - Téléchargement du projet

Vous pouvez télécharger l'intégralité du projet de cette section ici : Shader1.zip

IV-D-3 - Shader avec paramètre

Nous avons vu comment créer un shader simple avec Asphyre. Corsons un peu la chose.
Nous allons maintenant créer un shader qui va faire varier la couleur à partir d'un paramètre.

IV-D-3-a - Le shader

Voici le code HLSL du shader :

//--------------------------------------------------------------------------------------
// Global variables
//--------------------------------------------------------------------------------------
float4x4 g_WorldViewProj;    // World * View * Projection matrix
float3 MeshRGBColor; //Mesh Color

float Ticks;

//--------------------------------------------------------------------------------------
// Vertex shader output structure
//--------------------------------------------------------------------------------------
struct VS_OUTPUT
{
float4 Position   : POSITION;   // vertex position
};

//--------------------------------------------------------------------------------------
// This shader computes standard transform
//--------------------------------------------------------------------------------------
VS_OUTPUT RenderVS(float4 vPos : POSITION)
{
VS_OUTPUT Output;

Output.Position = mul(vPos, g_WorldViewProj);

return Output;
}

//--------------------------------------------------------------------------------------
// Pixel shader output structure
//--------------------------------------------------------------------------------------
struct PS_OUTPUT
{
float4 RGBColor : COLOR;  // Pixel color
};

//--------------------------------------------------------------------------------------
// This shader outputs the pixel's color by using the texture's color
//--------------------------------------------------------------------------------------
PS_OUTPUT RenderPS( VS_OUTPUT In )
{
PS_OUTPUT Output;
float4 OutColor = {MeshRGBColor, 1.0};
float fact = (sin(Ticks) + 1) / 2;

Output.RGBColor = float4(
MeshRGBColor.x * fact,
MeshRGBColor.y * fact,
MeshRGBColor.z * fact,
1.0);

return Output;
}

//--------------------------------------------------------------------------------------
// Renders scene to render target
//--------------------------------------------------------------------------------------
technique Render
{
pass P0
{
VertexShader = compile vs_2_0 RenderVS( );
PixelShader  = compile ps_2_0 RenderPS( );
}
}

Comme vous pouvez le remarquer, les seules modifications apportées sont :

- l'ajout d'une nouvelle variable Ticks ;

- la modification du pixel shader afin qu'il utilise cette nouvelle variable.

Le pixel shader utilise donc ici le sinus de Ticks, qu'il ramène entre 0 et 1. Il multiplie ensuite chaque composante de la couleur par la valeur obtenue. Ceci aura pour effet une couleur qui va s'assombrir au fur et à mesure pour revenir progressivement à son niveau d'origine.

IV-D-3-b - Les paramètres et les techniques

Il faut maintenant ajouter ce paramètre dans le code de notre classe de shader.
Rajoutons donc une variable privée FTicks à notre classe TMyShader ainsi qu'une surcharge du constructeur afin d'initialiser FTicks à 0 :

TMyShader = class(TAsphyreShaderEffect)
private
TempColor : TD3DColorValue;
FColor: TAsphyreColor;
//Variable FTicks pour l'animation
FTicks: single;
protected
procedure Describe; override;
procedure UpdateParam(Code: Integer; out DataPtr: Pointer;
out DataSize: Integer); override;
public
procedure Draw(Mesh: TAsphyreCustomMesh);
constructor Create(ADevice: TAsphyreDevice);
end;

...

implémentation

...

constructor TMyShader.Create(ADevice: TAsphyreDevice);
begin
inherited;
//Initialisation de FTicks
FTicks := 0;
end;

Notez bien la déclaration du constructeur de la classe et son paramètre.

Ensuite, il suffit d'ajouter ce paramètre dans la procédure Describe de cette façon :

const
shdrRGBColor = 1;
shdrTicks = 2;

shdrRender = 3;

...

procedure TMyShader.Describe;
begin
//Description de la matrice de transformation
DescParam(sptWorldViewProjection, 'g_WorldViewProj');

//Description de la couleur
DescParam(sptCustom, 'MeshRGBColor', shdrRGBColor);

//Description du paramètre Ticks
DescParam(sptCustom, 'Ticks', shdrTicks);

//Description de la technique
DescTechnique('Render', shdrRender);
end;

Nous avons donc déclaré une nouvelle constante pour ce paramètre, modifié celui de la technique et ajouté une procédure DescParam avec sptCustom en paramètre.

Il nous faut ensuite modifier la procédure UpdateParam pour y ajouter ce nouveau paramètre :

procedure TMyShader.UpdateParam(Code: Integer; out DataPtr: Pointer;
out DataSize: Integer);
begin
case Code of
//Paramètre de la couleur
shdrRGBColor :
begin
TempColor:= FColor;

DataPtr := @TempColor;
DataSize:= SizeOf(TD3DColorValue) - SizeOf(Single);
end;
//Paramètre Ticks
shdrTicks :
begin
DataPtr := @FTicks;
DataSize := SizeOf(Single);
end;
end;

Rien de bien sorcier, je ne reviendrai pas dessus.

À ce moment-là, le paramètre Ticks du shader est bien pris en compte, mais n'est pas modifié afin d'obtenir l'effet voulu.
Pour cela, il faut modifier la procédure Draw et y ajouter la modification de Fticks :

procedure TMyShader.Draw(Mesh: TAsphyreCustomMesh);
var
PassNo: Integer;
begin
//Définition de la technique utilisée
UseTechnique(shdrRender);

//Modification de FTicks
FTicks := FTicks + 0.01;

//Récupération de la couleur
FColor := TMyMesh(Mesh).Color;

//Mise à jour des paramètres
UpdateAll;

//Rendu du mesh pour chaque passe
for PassNo:= 0 to NumPasses - 1 do
begin
if (not BeginPass(PassNo)) then Break;

Mesh.Draw();

EndPass();
end;
end;

Et c'est tout : la mise à jour du paramètre du shader se fera avec UpdateAll.
Exécutez maintenant votre programme, vous devriez avoir ceci :

Résultat du shader avec paramètre

Évidemment, sur une image fixe, on ne voit pas l'effet produit. Mais le logo tourne et sa couleur s'estompe puis réapparait indéfiniment.

IV-D-3-c - Téléchargement du projet

Vous pouvez télécharger l'intégralité du projet de cette section ici : Shader2.zip

IV-D-4 - Shader avec texture

Voyons maintenant comment faire un shader qui utilise une texture.
Avant toute chose, il faut charger une image. Nous avons vu dans la section 2D de ce tutoriel comment faire :

- créer un fichier XML pour décrire l'image ;

- charger l'image avec ImageGroups.ParseLink.

Voici ce que donne le fichier XML :

<unires>
<image-group name="default group">
<image uid="TextureImg" type="image">
<format type="x8r5g6b5" miplevels="auto"/>
<textures count="1">
<texture num="0" source=".\Images\Texture.jpg"/>
</textures>
</image>
</image-group>
</unires>

On charge donc une image Texture.jpg dans le répertoire Images que l'on appellera TextureImg.

IV-D-4-a - Le shader

Le shader que nous allons utiliser a été créé sous FX Composer. C'est un matériau avec texture utilisant l'ombrage Lambert.
Vous pouvez télécharger ici ce shader : ShaderLambert.zip

IV-D-4-a-i - Les paramètres

Si vous ouvrez ce fichier, vous aurez ceci :

float4x4 WorldITXf : WorldInverseTranspose < string UIWidget="None"; >;
float4x4 WvpXf : WorldViewProjection < string UIWidget="None"; >;
float4x4 WorldXf : World < string UIWidget="None"; >;
float4x4 ViewIXf : ViewInverse < string UIWidget="None"; >;

...

texture ColorTexture : DIFFUSE <
string UIName =  "Diffuse Texture";
string ResourceType = "2D";
>;

On sait donc que ce shader nécessite :

- WorldITXf : matrice monde inversée et transposée ;

- WvpXf : matrice de vue monde ;

- WorldXf : matrice monde ;

- ViewIXf : matrice de vue inversée ;

- ColorTexture : la texture utilisée.

Si vous avez bien suivi, vous aurez donc compris que les quatre premières matrices peuvent être directement reliées depuis Asphyre sans grande intervention de notre part. Mais en sommes-nous si surs ?
Reprenons la liste des matrices standards disponibles dans AsphyreShaderFX.pas :

TShaderParameterType = (sptCustom, sptTexture, sptWorldViewProjection,
sptViewProjection, sptWorldInverseTranspose, sptCameraPosition, sptWorld,
sptWorldInverse, sptWorldView, sptProjection);

Eh oui ! Le quatrième type de matrice (vue inversée) n'y est pas ! Un oubli du développeur ? Bref, quoi qu'il en soit, il faudra donc passer par une autre méthode : considérer cette matrice comme une variable non connue.
Prenons donc maintenant notre classe de shader et modifions sa méthode Describe de la façon suivante :

procedure TMyShader.Describe;
begin
//Description de la matrice de transformation inversée et transposée
DescParam(sptWorldInverseTranspose, 'WorldITXf');
//Description de la matrice de vue monde
DescParam(sptWorldViewProjection, 'WvpXf');
//Description de la matrice de monde
DescParam(sptWorld, 'WorldXf');
//Description de la matrice de vue inversée
DescParam(sptCustom, 'ViewIXf', shdrViewInv);

//Description de la texture
DescParam(sptTexture, 'ColorTexture', shdrSkinTexture);

//Description de la technique
DescTechnique('Main', shdrRender);
end;

Comme vous le voyez, nous avons bien mis sptCustom ainsi qu'un indice à la description de la matrice de vue inversée. Il nous faut donc modifier aussi la procédure UpdateParam :

procedure TMyShader.UpdateParam(Code: Integer; out DataPtr: Pointer;
out DataSize: Integer);
begin
case Code of
shdrViewInv:
begin
//Envoi de la valeur de la matrice vue inversée
FTempMtx := TD3DXMatrix(InvertMtx4(ViewMtx.RawMtx^).Data);
DataPtr := @FTempMtx;
DataSize:= SizeOf(TD3DXMatrix);
end;
end;
end;

Mais que faisons-nous dans cette procédure ? C'est très simple : nous construisons cette matrice de vue inversée à partir de ViewMtx (matrice de vue).
La variable FTempMtx est de type TD3DXMatrix et est membre privé de notre classe de shader.
Nous récupérons d'abord la matrice de vue ViewMtx et ses données brutes RawMtx qui est un pointeur sur un TMatrix4, d'où le « ^ ». À l'aide de InvertMtx4, nous inversons la matrice. Cette fonction renvoie un TMatrix4.
Enfin, nous castons le membre Data (données brutes de TMatrix4) en TD3DXMatrix.
La suite, nous la connaissons : renvoi du pointeur et de la taille de notre matrice.

IV-D-4-a-ii - Les textures

Pour les textures, ce n'est pas dans UpdateParam qu'elles sont mises à jour, mais dans UpdateTexture :

procedure UpdateTexture(Code: Integer;
out ParamTex: IDirect3DTexture9); override;

Que devons-nous faire dans cette procédure ? Renvoyer simplement un type IDirect3DTexture9 qui est directement accessible depuis un TAsphyreCustomTexture.
Déclarons donc un type TAsphyreCustomTexture que nous appellerons FSkinTexture :

private
FSkinTexture: TAsphyreCustomTexture;

Cette texture contiendra la texture utilisée au moment du rendu. Voyons maintenant l'implémentation de UpdateTexture :

procedure TMyShader.UpdateTexture(Code: Integer;
out ParamTex: IDirect3DTexture9);
begin
ParamTex:= nil;

case Code of
shdrSkinTexture:
//Envoi de la texture
if (FSkinTexture <> nil) then ParamTex:= FSkinTexture.Tex9;
end;
end;

Comme vous le voyez, rien de plus simple. De cette façon, la texture est transférée dans le programme de shader.

IV-D-4-a-iii - Le rendu

Afin de spécifier la texture utilisée dans le shader, il nous faut légèrement modifier la procédure Draw de cette façon :

procedure Draw(Mesh: TAsphyreCustomMesh; SkinTex: TAsphyreCustomImage);

Le paramètre SkinTex contient la texture qu'il nous faudra passer dans FSkinTexture avant d'appeler UpdateAll;

procedure TMyShader.Draw(Mesh: TAsphyreCustomMesh;
SkinTex: TAsphyreCustomImage);
var
PassNo: Integer;
begin
//Définition de la technique utilisée
UseTechnique(shdrRender);

//Passage de la texture dans SkinTexture
FSkinTexture:= nil;
if (SkinTex <> nil) then FSkinTexture:= SkinTex.Texture[0];

//Mise à jour des paramètres
UpdateAll;

//Rendu du mesh pour chaque passe
for PassNo:= 0 to NumPasses - 1 do
begin
if (not BeginPass(PassNo)) then Break;

Mesh.Draw();

EndPass();
end;
end;

Comme la texture n'utilise qu'un seul pattern, la texture utilisée aura pour indice 0.

IV-D-4-b - Le programme

Commençons par modifier le OnCreate de la fiche pour charger l'image depuis ce XML. Pour cela, nous le savons maintenant, il suffit d'ajouter :

//Chargement des images
ImageGroups.ParseLink('ressources.xml');

au début du OnCreate de la Form. Le reste ne change pas.

Pour la suite du code, nous avons besoin de l'indice de cette image. Bien que ce soit la seule image et a donc un indice égal à 0, prenons l'habitude dès maintenant de prévoir des ressources plus importantes.
L'endroit idéal pour récupérer cet indice est l'événement OnDeviceCreate.

Cette procédure OnDeviceCreate se déclare ainsi, toujours dans la section private de notre Form :

procedure OnDeviceCreate(Sender: TObject; EventParam: Pointer;
var Success: Boolean);

Voici son implémentation :

procedure TMainFrm.OnDeviceCreate(Sender: TObject; EventParam: Pointer;
var Success: Boolean);
begin
with Sender as TAsphyreDevice do
begin
//Récupération de l'indice et chargement de la texture utilisée
ImageSkin:= Images.ResolveImage('TextureImg');
end;
end;

ImageSkin est un integer que nous avons déclaré en variable globale et initialisé à -1 :

var
ImageSkin: Integer = -1;

La chaine de caractères passée en paramètre est la chaine déclarée dans le XML dans l'attibut uid de l'image.

Afin de vérifier le bon chargement de la ressource, utilisons aussi l'événement OnResolveFailed, toujours dans private :

procedure OnResolveFailed(Sender: TObject; EventParam: Pointer;
var Success: Boolean);

Et son implémentation :

procedure TMainFrm.OnResolveFailed(Sender: TObject; EventParam: Pointer;
var Success: Boolean);
begin
//Si le chargement d'une ressource échoue, quitte l'application
MessageDlg('Erreur lors du chargement d''une ressource', mtError, [mbOk], 0);
Devices.Finalize;
Close;
Exit;
end;

Nous avons également vu comment utiliser ces événements, je ne reviendrai donc pas dessus :

procedure TMainFrm.ConfigureDevice(Sender: TAsphyreDevice; Tag: TObject;
var Config: TScreenConfig);
begin
//Paramétrage de l'affichage
Config.WindowHandle := Handle;
Config.Width := ClientWidth;
Config.Height := ClientHeight;
Config.Windowed := true;
Config.HardwareTL := true;
Config.BitDepth := bd24bit;
//Activation de l'anticrénelage
Config.MultiSamples := 2;

//Inscription de l'événement OnDeviceCreate
EventDeviceCreate.Subscribe(OnDeviceCreate, Sender);
//Inscription de l'événement OnResolveFailed
EventResolveFailed.Subscribe(OnResolveFailed, Sender);
end;

La dernière chose à modifier est le callback de rendu afin d'utiliser la nouvelle procédure Draw de notre classe de shader. Pour cela, remplacez la ligne correspondante par :

Shader.Draw(Mesh, Sender.Images[ImageSkin]);

Ainsi, nous passons notre texture dans notre classe de shader qui se chargera de l'envoyer au shader lui-même.

Vous pouvez maintenant exécuter votre code !

Rendu final avec texture et ombrage

IV-D-4-c - Téléchargement du projet

Vous pouvez télécharger l'intégralité du projet de cette section ici : Shader3.zip

IV-E - Le post processing

Mais qu'est-ce donc ? C'est très simple : c'est une méthode qui permet de faire un traitement sur l'image rendue. En d'autres termes, vous devez effectuer le rendu final de votre scène sur une surface tampon (à l'aide des shaders ou non), puis réinjecter cette image tampon dans un shader qui va s'occuper d'appliquer l'effet.

Malheureusement, je n'ai pas encore réussi à ce jour à faire une telle chose avec Asphyre. L'auteur m'a certifié que c'était possible. Mais tous mes essais ont été infructueux.
Donc si vous avez des informations sur cette méthode, n'hésitez pas et faites-m'en part afin que je complète ce tutoriel !

IV-F - Conclusion

Ce tutoriel sur la 3D avec Asphyre est maintenant terminé. Normalement, vous devriez pouvoir utiliser n'importe quel type de shader. Les effets que vous voulez utiliser ne seront limités que par votre imagination !

N'hésitez pas à vous entrainer à charger des shaders construits avec FX Composer. Certains peuvent être importés depuis la Shader Library de nVidia et sont réellement magnifiques !

Rendu avec shader créé depuis FX Composer

V - Les entrées utilisateurs

V-A - Introduction

Maintenant que vous savez afficher des vues 2D et 3D à couper le souffle (allons, pas de fausse modestie), il reste une chose importante: pouvoir faire interagir la scène avec le joueur.
Pour cela, il existe trois interfaces principales disponibles avec DirectX :

- le clavier ;

- la souris ;

- les manettes (joystick, joypad, etc.).

Comme d'habitude, nous allons voir leur fonctionnement tout au long de l'écriture d'un programme simple. Celui-ci devra afficher un carré blanc. L'utilisateur pourra déplacer ce carré à l'aide du clavier et d'une des manettes. Évidemment, il pourra sélectionner la manette directement pendant le « jeu ».
Afin d'aller plus, loin, on affichera quelques données sur les manettes afin d'identifier plus facilement la manette courante.

Maintenant que vous êtes familier avec l'affichage avec Asphyre, nous ne rentrerons pas dans les détails de l'affichage.

Le projet de départ que nous utiliserons ne fait pour l'instant qu'afficher un fond noir ainsi que le nombre de FPS.

V-B - Les entrées utilisateur dans Asphyre

Comme toujours, Asphyre propose des classes déjà toutes faites. Il nous incombe tout de même de les initialiser. Tout se passe dans les unités suivantes :

- AsphyreInputs.pas : déclare la variable AsphyreInput de type TAsphyreInput qui centralise les entrées utilisateurs ;

- AsphyreKeyboard.pas : unité relative au clavier ;

- AsphyreJoystick.pas : unité relative aux manettes ;

- AsphyreMouse.pas : unité relative à la souris ;

- DirectInput.pas : unité contenant toutes les constantes ainsi que les implémentations de DirectX.

Ajoutons donc ces unités à la clause uses de notre projet.

Pour commencer, ajoutons également ces variables privées nécessaires à la suite du programme :

- OriginRectPos : TPoint : coordonnées de l'origine du carré ;

- DispRectPos : TPoint : coordonnées finales du carré.

Voyons maintenant l'initialisation :

//Initialisation de AsphyreInput
AsphyreInput.Initialize;
AsphyreInput.WindowHandle := Handle;
//Initialisation des interfaces
AsphyreInput.Joysticks.Initialize;
AsphyreInput.Keyboard.Initialize;

N'oubliez surtout pas d'assigner la propriété WindowHandle !

La finalisation se fait aussi simplement :

AsphyreInput.Finalise;

à noter que la finalisation de AsphyreInput déclenche aussi la finalisation de Joysticks, Keyboard et Mouse. Cette simple ligne suffit donc.

L'affichage du carré se fera de cette façon dans le callback de rendu :

procedure TMainFrm.RenderCallback(Sender: TAsphyreDevice; Tag: TObject);
const
RectSize = 40;
var
p: TPoint4;
cl: TColor4;
begin
//Affichage du nombre de FPS
Sender.SysFonts.Font['s/arial'].TextOut('FPS : ' + IntToStr(Timer.FrameRate),
10, 10, $99009900);
with Sender.Canvas do
begin
//Spécification des coordonnées du carré
p := Point4(DispRectPos.X, DispRectPos.Y,
DispRectPos.X + RectSize, DispRectPos.Y,
DispRectPos.X + RectSize, DispRectPos.Y + RectSize,
DispRectPos.X, DispRectPos.Y + RectSize);

//Affichage du carré
FillQuad(p, clWhite4, fxuBlend);
end;
end;

Vous devriez avoir ceci :

V-C - Le clavier

Tachons maintenant de déplacer ce carré blanc lorsque l'on appuie sur les flèches du pavé directionnel.
L'idéal est de tout regrouper ces opérations dans OnProcessEvent.
La première chose à faire est de mettre à jour les informations en appelant Update :

if AsphyreInput.Keyboard.Update then

Si l'Update s'est correctement déroulé, on peut passer à la suite.
La détection des touches sous DirectX ne se passe pas de la même façon que la VCL. En effet, dans la VCL, vous avez un événement OnKeyPress (ou Down et Up) qui passe la touche appuyée en paramètre. Sous DirectX, c'est à vous de tester si chaque touche qui vous intéresse est pressée ou non.
Pour cela, la propriété Key de AsphyreInput.Keyboard nous permet de savoir facilement si la touche spécifiée est pressée ou non de cette façon :

procedure TMainFrm.ProcessEvent(Sender: TObject);
const
DecValue = 40;
var
Decalage: TPoint;
begin
//Mise à zéro du décalage
Decalage := Point(0, 0);

with AsphyreInput.Keyboard do
begin
//Appui sur droite ou gauche
if Key[DIK_RIGHT] then
Decalage.X := DecValue else
if Key[DIK_LEFT] then
Decalage.X := -DecValue;

//Appui sur bas ou haut
if Key[DIK_DOWN] then
Decalage.Y := DecValue else
if Key[DIK_UP] then
Decalage.Y := -DecValue;

//Appui sur Echap pour fermer l'application
if Key[DIK_ESCAPE] then
Close;
end;

//Assignation de la position du carré
DispRectPos.X := OriginRectPos.X + Decalage.X;
DispRectPos.Y := OriginRectPos.Y + Decalage.Y;
end;

Les constantes DIK_[Touche] se trouvent dans l'unité DirectInput.pas. Ce code est simple : suivant la direction pressée, le décalage est assigné. Vous aurez aussi remarqué que nous vérifions l'état de la touche Echap afin de fermer l'application. Son interception dans OnKeyPress est donc inutile ici!
Les deux dernières lignes calculent la position finale du carré.

Et c'est tout !
Exécutez votre programme et essayez d'appuyer sur les touches du pavé directionnel, le carré se déplacera et reviendra à sa place lorsque vous lâchez la touche.

Il existe également des propriétés KeyPressed et KeyReleased, mais comme stipulé en commentaire dans l'unité AsphyreKeyboard.pas, elles sont tributaires des appels à Update. Donc si vous utilisez ces méthodes, votre programme peut perdre des événements du clavier.

Il vaut donc mieux utiliser la propriété Key.

V-D - Les joysticks

Afin d'afficher les données de chaque joystick (pour peu que vous en ayez plusieurs), nous avons besoin d'un record ainsi que d'un tableau dynamique contenant ces records.
Voici la structure :

TJoyInfos = record
Axis: integer;
Buttons: integer;
POVs: integer;
end;

Cette structure contiendra les capacités de chacun des joystick branché.

Nous aurons également des variables privées suivantes :

- CurrentJoystick : integer : indice du joystick courant ;

- JoyPos : TPoint : amplitude du déplacement du joystick pour l'affichage ;

- Joys : array or TJoyInfos : contient les données de chaque joystick ;

- JoyDown : boolean : si le bouton est appuyé ou non.

Ajoutons donc une fonction qui va récupérer les informations des joysticks :

function TMainFrm.GetJoyInfos(const JoyIndex: integer): TJoyInfos;
begin
with AsphyreInput.Joysticks[JoyIndex].DeviceCaps do
begin
//Nombre d'axes
Result.Axis := dwAxes;
//Nombre de boutons
Result.Buttons := dwButtons;
//Nombre de POVs
Result.POVs := dwPOVs;
end;
end;

Les capacités du joystick se trouvent dans la propriété DeviceCaps.

Vous pouvez également afficher le nom du joystick en utilisant la méthode GetDeviceInfo de AsphyreJoystick.InputDevice, mais je n'ai pas réussi à la faire fonctionner: soit elle ne marche pas soit je m'y suis pris comme un pied.

Bref, si vous avez la solution à ce problème, je suis preneur.

Étoffons un peu l'initialisation afin de récupérer toutes ces données :

//Initialisation de AsphyreInput
AsphyreInput.Initialize;
AsphyreInput.WindowHandle := Handle;

//Initialisation des interfaces
AsphyreInput.Joysticks.Initialize;
AsphyreInput.Keyboard.Initialize;

//Initialise le joystick courant
CurrentJoystick := 0;

//Spécifie la longueur du tableau
SetLength(Joys, AsphyreInput.Joysticks.Count);

//Initialise chaque information de joystick
for I := 0 to AsphyreInput.Joysticks.Count - 1 do
Joys[I] := GetJoyInfos(i);

//Initialisation de l'état appuyé à faux
JoyDown := false;

Ajoutons également la libération du tableau dans OnDestroy :

procedure TMainFrm.FormDestroy(Sender: TObject);
begin
//Vidage du tableau
SetLength(Joys, 0);

//Finalisation de AsphyreInput
AsphyreInput.Finalize;

//Finalisation de DirectX
Devices.Finalize;
end;

Afin de faciliter l'écriture du code, nous allons également séparer l'affichage des informations des joysticks :

procedure TMainFrm.DrawJoyInfos(Sender: TAsphyreDevice);
var
i: integer;
Color: Cardinal;
begin
//Y a-t-il des joysticks?
if AsphyreInput.Joysticks.Count > 0 then
begin
//Affichage du nombre de joystick(s) et des amplitudes de déplacement
Sender.SysFonts.Font['s/arial'].TextOut(
Format('Joysticks : %u (X=%d, Y=%d)',
[AsphyreInput.Joysticks.Count, JoyPos.X, JoyPos.Y]),
10, 25, $FF777777);
//Affichage des données de chaque joystick
with Sender.Canvas do
for i := 0 to AsphyreInput.Joysticks.Count - 1 do
begin
//Si le joystick est le joystick courant
if i = CurrentJoystick then
begin
//Aficher un cercle rouge en face du texte
FillCircle(10, 51 + i * 20, 5, 10, clRed4, fxuBlend);
//Couleur du texte en blanc
Color := $FFFFFFFF;
end else
//Couleur du texte en gris
Color := $FF777777;

//Affichage du texte
Sender.SysFonts.Font['s/arial'].TextOut(
Format('%d :  %d axe(s),  %d bouton(s),  %d POV(s)',
[i + 1, Joys[i].Axis, Joys[i].Buttons, Joys[i].POVs]),
20, 45 + i * 20, Color);
end;
end else
//S'il n'y a pas de joystick
Sender.SysFonts.Font['s/arial'].TextOut('Aucun joystick détecté.',
10, 25, $FF777777);
end;

Ajoutons cette procédure dans le callback de rendu :

procedure TMainFrm.RenderCallback(Sender: TAsphyreDevice; Tag: TObject);
const
RectSize = 40;
var
p: TPoint4;
cl: TColor4;
begin
//Affichage du nombre de FPS
Sender.SysFonts.Font['s/arial'].TextOut('FPS : ' + IntToStr(Timer.FrameRate),
10, 10, $99009900);

//Affichage des données des joysticks
DrawJoyInfos(Sender);

with Sender.Canvas do
begin
//Spécification des coordonnées du carré
p := Point4(DispRectPos.X, DispRectPos.Y,
DispRectPos.X + RectSize, DispRectPos.Y,
DispRectPos.X + RectSize, DispRectPos.Y + RectSize,
DispRectPos.X, DispRectPos.Y + RectSize);

//Spécification de la couleur suivant que l'état appuyé soit vrai ou faux
if JoyDown then
cl := clRed4
else
cl := clWhite4;

//Affichage du carré
FillQuad(p, cl, fxuBlend);
end;
end;

Vous devriez avoir quelque chose comme ceci :

Écrivons maintenant le code pour utiliser les données des joysticks.
Contrairement aux souris et clavier, il est courant d'avoir plusieurs joysticks/pads branchés en même temps sur l'ordinateur. De plus, le joystick que l'utilisateur voudrait utiliser n'est pas forcément le premier branché !
Vous devez donc prendre compte de tous ces paramètres lorsque que vous codez un jeu.
Heureusement, Asphyre gère ce problème de façon très simple. En effet, chaque joystick a un indice différent (de 0 à n-1); il est donc très facile d'utiliser le bon joystick.
Regardons le code pour notre exemple :

const
JoyDeadZone = 3000;
...
//Vérification de la présence d'un joystick
if AsphyreInput.Joysticks.Count > 0 then
begin
//Mise à jour des données des joysticks
if AsphyreInput.Joysticks.Update then
with AsphyreInput.Joysticks[CurrentJoystick].JoyState do
begin
//Si l'amplitude est supérieure à la deadzone alors effectuer le déplacement

//en X
if Abs(lX) > JoyDeadZone then
Decalage.X := lX div 200;

//en Y
if Abs(lY) > JoyDeadZone then
Decalage.Y := lY div 200;

//Pour l'affichage des données
JoyPos := Point(lX, lY);
end;
end;

Vous aurez remarqué cette constante JoyDeadZone. C'est une constante assez importante puisque c'est elle qui définit la zone morte (dead zone) de votre joystick. En effet, si le joystick est sensible, à peine vous aurez bougé le manche que l'affichage réagira. C'est une bonne chose me direz-vous. Je vous répondrais oui, sauf qu'un joystick est rarement centré pile au centre lorsqu'il est au repos. Vous aurez donc des mouvements parasites que vous pourrez supprimer avec cette zone morte. Elle définit l'amplitude minimale à prendre en compte.
Par exemple, pour éviter les mouvements parasites avec mon pad, j'ai dû paramétrer la deadzone à 3000…

Nous testerons donc pour chaque direction si l'amplitude est supérieure à la deadzone. Nous diviserons l'amplitude par 200 afin d'éviter que le carré sorte complètement de la zone d'affichage (l'amplitude d'un joystick allant de -32768 à 32767).
La dernière ligne ne sert qu'à assigner JoyPos à l'amplitude actuelle afin de l'afficher.

Vous pouvez d'ores et déjà exécuter votre code. Vous verrez que si vous agissez sur votre premier joystick (dans l'ordre de la liste des Contrôleurs de jeu du Panneau de configuration de Windows) le carré bougera dans le sens que vous lui donnez.

La dernière chose à faire est de permettre de spécifier le joystick utilisé. Pour cela, l'utilisateur devra taper sur le pavé numérique du clavier le numéro du joystick.
Nous devons donc ajouter les tests des touches 1 à 9 (oui voyons grand !) afin de donner à CurrentJoystick l'indice du joystick :

if AsphyreInput.Keyboard.Update then
with AsphyreInput.Keyboard do
begin
//Appui de 1 à 9 sur le pavé numérique pour spécifier le joystick utilisé
if Key[DIK_NUMPAD1] then CurrentJoystick :=
Min(0, AsphyreInput.Joysticks.Count - 1) else
if Key[DIK_NUMPAD2] then CurrentJoystick :=
Min(1, AsphyreInput.Joysticks.Count - 1) else
if Key[DIK_NUMPAD3] then CurrentJoystick :=
Min(2, AsphyreInput.Joysticks.Count - 1) else
if Key[DIK_NUMPAD4] then CurrentJoystick :=
Min(3, AsphyreInput.Joysticks.Count - 1) else
if Key[DIK_NUMPAD5] then CurrentJoystick :=
Min(4, AsphyreInput.Joysticks.Count - 1) else
if Key[DIK_NUMPAD6] then CurrentJoystick :=
Min(5, AsphyreInput.Joysticks.Count - 1) else
if Key[DIK_NUMPAD7] then CurrentJoystick :=
Min(6, AsphyreInput.Joysticks.Count - 1) else
if Key[DIK_NUMPAD8] then CurrentJoystick :=
Min(7, AsphyreInput.Joysticks.Count - 1) else
if Key[DIK_NUMPAD9] then CurrentJoystick :=
Min(8, AsphyreInput.Joysticks.Count - 1);
end;

C'est long, mais c'est indispensable. Notez que nous testons à chaque fois si l'indice ne dépasse pas le nombre de joysticks ! Mais pourquoi le faire à chaque fois et pas une bonne fois pour toutes à la fin ? La raison en est très simple : on utilise CurrentJoystick dans le callback de rendu afin de mettre en valeur le joystick utilisé. Or, si on ne paramètre cette variable qu'à la fin, pendant un court instant, elle aura une valeur potentiellement hors de l'étendue (supérieure au nombre de joysticks). Eh bien ce court instant a suffi à faire planter systématiquement lorsque j'ai voulu faire comme ceci : le callback de rendu faisait appel à cette variable au moment où elle n'était pas encore vérifiée.
C'est pour cette raison que j'ai volontairement utilisé cette méthode qui n'assignera jamais CurrentJoystick à autre chose qu'un indice valable.

Ainsi, l'utilisateur pourra choisir le joystick utilisé :

Programme avec le joystick n°2 (indice 1) sélectionné

Il ne reste qu'une chose : Détecter l'appui du bouton.
Pour cela, une ligne suffit :

if AsphyreInput.Joysticks.Update then
with AsphyreInput.Joysticks[CurrentJoystick].JoyState do
begin
//Etat baissé si le bouton 0 est pressé
JoyDown := rgbButtons[0] > 0;
end;

La propriété rgbButtons recense tous les boutons du contrôleur. Ils sont identifiés par leur indice.

Vous pouvez trouver très facilement l'indice de chacun des boutons de votre contrôleur. Pour cela, rendez-vous dans la boite de dialogue Contrôleur de jeu du Panneau de configuration de Windows. Sélectionnez le contrôleur et cliquez sur Propriétés. Vous devrez avoir la liste de tous les boutons numérotés. Cliquez sur le bouton que vous voulez utiliser, repérez son numéro et retranchez 1 à ce numéro : vous avez l'indice !

Vous remarquerez aussi une particularité : la propriété rgbButtons[Index] n'est pas de type booléen. Il est donc tout à fait possible d'utiliser des boutons avec une amplitude. Dans notre cas, le bouton est du type on/off, nous testerons donc simplement si sa valeur est 0 (relâché) ou supérieur à 0 (appuyé).

Vous pouvez exécuter votre programme, lorsque vous appuyez sur le bouton, le carré deviendra rouge :

Le bouton est enfoncé

V-E - La souris

Passons maintenant à la dernière interface disponible: la souris. Son utilisation sous Asphyre ressemble beaucoup à celle du joystick. À ceci près, que l'on n'a pas une valeur d'amplitude pour les déplacements, mais une différence (delta).
Premièrement, il faut initialiser le gestionnaire de souris. Nous avons maintenant l'habitude, cela se fait comme ceci :

//Initialisation de AsphyreInput
AsphyreInput.Initialize;
AsphyreInput.WindowHandle := Handle;

//Initialisation des interfaces
AsphyreInput.Joysticks.Initialize;
AsphyreInput.Keyboard.Initialize;
AsphyreInput.Mouse.Initialize;

Comme son nom l'indique, la dernière ligne initialise le gestionnaire de souris. Nous pouvons dès lors utiliser la souris avec Asphyre.
Lorsque l'utilisateur appuiera sur le bouton gauche de la souris, cela aura le même effet que le bouton du joystick : le carré devient rouge et redevient blanc lorsque le bouton est lâché. Ajoutons une variable privée MouseDown : boolean afin de ne pas interférer avec la variable JoyDown pour le joystick. Cette variable MouseDown sera initialisée à false dans OnCreate
Modifions donc notre procédure ProcessEvent afin d'utiliser la souris :

//Mise à jour des données de la souris
if AsphyreInput.Mouse.Update then
with AsphyreInput.Mouse do
begin
//Assignation des valeurs des mouvements
Decalage.X := DeltaX * 2;
Decalage.Y := DeltaY * 2;

//Si le bouton gauche est enfoncé
if Pressed[0] then
MouseDown := true;
//S'il est relâché
if Released[0] then
MouseDown := false;
end;

La question qui se pose ici est de savoir où placer cette portion de code par rapport aux autres gestionnaires que nous avons écrits précédemment (clavier et joysticks). Tel que nous l'avons écrit, le dernier des périphériques donnera sa valeur à Decalage.X et Decalage.Y. C'est donc à vous de savoir quel périphérique sera prioritaire par rapport aux autres. En général, les contrôleurs d'un jeu ne sont pas aussi redondants et on doit choisir entre le couple clavier/souris ou le joystick par exemple pour les jeux tirés des consoles. Ce problème se pose donc uniquement dans notre exemple. J'ai personnellement placé cette gestion de la souris en premier.
Rien ne vous empêche également de cumuler tous ces contrôleurs en ajoutant à chaque fois la valeur de Decalage.X et Decalage.Y. Bref, vous l'aurez compris, il est très facile de gérer les contrôleurs de façon adaptée à vos besoins.

Regardons le code plus en détail. On commence comme d'habitude par faire un Update. Cette opération est indispensable sinon, les buffers ne sont pas mis à jour et aucun mouvement n'est détecté !
Les lignes suivantes utilisent DeltaX et DeltaY qui sont comme je vous le disais les deux différences (et non amplitudes) de déplacement de la souris. Dans notre exemple volontairement simple, nous utiliserons directement la valeur des deltas pour notre position.
La multiplication par 2 ne sert qu'à avoir une amplitude de mouvement plus grand.

Passons aux lignes du dessous. Elles gèrent l'état du bouton 0 (le bouton gauche) appuyé ou relâché.
Comme vous le constatez, le système est différent du joystick. Pour celui-ci, nous devions tester si la valeur d'un bouton est supérieure à zéro. Si oui, le bouton est pressé et dès que la valeur repasse à 0, le bouton vient d'être relâché.
Ici, il faut tester lorsque le bouton est appuyé (Pressed) et lorsqu'il est relâché (Released). C'est pour cette raison que je n'ai pas écrit :

MouseDown := Pressed[0];

Si vous essayez ce code, vous verrez le cube rouge qu'une fraction de seconde : Simplement au moment où le bouton est pressé et non pendant que le bouton est pressé ! La nuance est subtile.
À noter qu'il existe également la propriété DeltaWheel de Mouse qui, comme son nom l'indique permet d'utiliser la molette.

Pour finir, ajoutons la prise en compte de la nouvelle variable MouseDown dans le callback de rendu :

procedure TMainFrm.RenderCallback(Sender: TAsphyreDevice; Tag: TObject);
const
RectSize = 40;
var
p: TPoint4;
cl: TColor4;
begin
//Affichage du nombre de FPS
Sender.SysFonts.Font['s/arial'].TextOut('FPS : ' + IntToStr(Timer.FrameRate),
10, 10, $99009900);

//Affichage des données des joysticks
DrawJoyInfos(Sender);

with Sender.Canvas do
begin
//Spécification des coordonnées du carré
p := Point4(DispRectPos.X, DispRectPos.Y,
DispRectPos.X + RectSize, DispRectPos.Y,
DispRectPos.X + RectSize, DispRectPos.Y + RectSize,
DispRectPos.X, DispRectPos.Y + RectSize);

//Spécification de la couleur suivant que l'état appuyé soit vrai ou faux
if JoyDown or MouseDown then
cl := clRed4
else
cl := clWhite4;

//Affichage du carré
FillQuad(p, cl, fxuBlend);
end;
end;

Vous pouvez exécuter votre programme et voir votre carré blanc trembler suivant la vitesse de déplacement de votre souris.

V-F - Téléchargement du programme d'exemple

Vous pouvez télécharger le programme d'exemple ici: Exemple2D.zip

V-G - Conclusion

Voilà, dorénavant, vous êtes prêt à vous lancer dans la création d'un jeu avec votre EDI préféré ! N'hésitez pas à me faire part de vos créations, je me ferai une joie de les ajouter en téléchargement à la suite de ce tutoriel !
Bon dev à tous !