IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Utilisation de DirectX sous Delphi : Asphyre 4.1

Utilisation de DirectX sous Delphi : Asphyre 4.1


précédentsommairesuivant

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 2 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:

 
Sélectionnez
;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 video 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 3 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 3 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 su 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 :

 
Sélectionnez
unit MyMesh;

interface

uses
  AsphyreMeshes, AsphyreColors;

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

implementation

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:

 
Sélectionnez
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;

implementation

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'initalisation 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 mailage .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:

 
Sélectionnez
//--------------------------------------------------------------------------------------
// 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:

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

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

 
Sélectionnez
//--------------------------------------------------------------------------------------
// 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ée pour le vertex shader et le pixel shader et d'autre part, la version de shader utilisée. Nous avons donc :

 
Sélectionnez
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:

 
Sélectionnez
//--------------------------------------------------------------------------------------
// 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 video 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:

 
Sélectionnez
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;
  
implementation

end;

Comme nous l'avons vu, le programme de shader nécessite 2 variables : g_WorldViewProj qui est la matrice de transformation. Cette matrice est tout simplement la matrice WorldMtx que nous avons vu 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 2 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:

 
Sélectionnez
  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 3 (la matrice de transformation, la couleur et la technique).
Voici comment se présente le contenu de Describe pour ce shader :

 
Sélectionnez
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

 
Sélectionnez
//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:

 
Sélectionnez
 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.
Le chaine de caractères suivante est le nom de la variable dans le fichier fx
Prenons les lignes suivantes :

 
Sélectionnez
//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.

 
Sélectionnez
//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:

 
Sélectionnez
const
  //Constantes d'indentification
  shdrRGBColor = 1;

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

Le code de Describe devient donc :

 
Sélectionnez
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:

 
Sélectionnez
  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 la dite 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) :

 
Sélectionnez
  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 4 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:

 
Sélectionnez
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;

Evidemment, 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.

 
Sélectionnez
  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.

 
Sélectionnez
        TempColor := FColor;

        DataPtr := @TempColor;

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

 
Sélectionnez
        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

 
Sélectionnez
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 :

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

Voici son implémentation :

 
Sélectionnez
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:

 
Sélectionnez
  //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!

Image non disponible
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

 
Sélectionnez
float4x4 g_WorldViewProj;    // World * View * Projection matrix

du tout début (ligne 4) par

 
Sélectionnez
//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 :

 
Sélectionnez
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 :

 
Sélectionnez
//--------------------------------------------------------------------------------------
// 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 niveu 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 contructeur afin d'initialiser FTicks à 0:

 
Sélectionnez
  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;
  
...

implementation

...

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:

 
Sélectionnez
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 :

 
Sélectionnez
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.

A 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:

 
Sélectionnez
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:

Image non disponible
Résultat du shader avec paramètre

Evidemment, 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 :

 
Sélectionnez
<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

 
Sélectionnez
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 4 première matrices peuvent être directement reliées depuis Asphyre sans grande intervention de notre part. Mais en sommes-nous si sûr?
Reprenons la liste des matrices standards disponibles dans AsphyreShaderFX.pas:

 
Sélectionnez
 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:

 
Sélectionnez
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 :

 
Sélectionnez
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 "^". A 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 :

 
Sélectionnez
    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 appelerons FSkinTexture :

 
Sélectionnez
  private
    FSkinTexture: TAsphyreCustomTexture;

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

 
Sélectionnez
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 :

 
Sélectionnez
    procedure Draw(Mesh: TAsphyreCustomMesh; SkinTex: TAsphyreCustomImage);

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

 
Sélectionnez
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 :

 
Sélectionnez
  //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 :

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

Voici son implémentation :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

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

Et son implémentation :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
  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!

Image non disponible
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!

Image non disponible
Rendu avec shader créé depuis FX Composer

précédentsommairesuivant

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 ni 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.