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 !
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 :
É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 !
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 !