Lumière ambiante
Adapté à XNA 4.0 par A. Nadif
Bonjour et bienvenue. Voici le premier tutorial de ma série de tutoriaux sur les shaders dans XNA. Je m'appelle Petri Wilhelmsen et je suis un membre des studios Dark Codex. Nous avons l'habitude de participer à de diverses compétitions concernant le graphisme et le développement de jeux, à The Gathering, Assembly, Solskogen, Dream-Build-Play, NGA, etc.
La série sur la programmation de shaders dans XNA couvrira différents aspects d'XNA, et comment écrire des shaders HLSL en utilisant XNA et votre GPU. Je vais commencer avec les aspects basiques de la théorie, puis je passerai à une approche plus pratique de la programmation de shaders.
La parie théorique ne sera pas couverte en détail, mais devrait être suffisante pour se lancer dans les shaders, et être capable d'apprendre par vous même. Cela couvrira les notions basiques d'HLSL, comment le langage HLSL fonctionne et quelques mots-clés qu'il est bon de connaître.
Aujourd'hui, je couvrirai XNA et HLSL, ainsi qu'un simple algorithme de lumière ambiante.
La série sur la programmation de shaders dans XNA couvrira différents aspects d'XNA, et comment écrire des shaders HLSL en utilisant XNA et votre GPU. Je vais commencer avec les aspects basiques de la théorie, puis je passerai à une approche plus pratique de la programmation de shaders.
La parie théorique ne sera pas couverte en détail, mais devrait être suffisante pour se lancer dans les shaders, et être capable d'apprendre par vous même. Cela couvrira les notions basiques d'HLSL, comment le langage HLSL fonctionne et quelques mots-clés qu'il est bon de connaître.
Aujourd'hui, je couvrirai XNA et HLSL, ainsi qu'un simple algorithme de lumière ambiante.
I Prérequis
Une certaine connaissance de XNA, puisque je ne vais pas beaucoup détailler le chargement des textures, des modèles 3D, les matrices et les maths.
II Un bref historique sur les shaders
Avant DirectX 8, les GPUs avaient une manière fixe de transformer pixels et vertices, appelée "The fixed pipeline". Ceci rendait impossible pour les développeurs, de changer la façon dont les pixels et les vertices étaient transformés et traités, après les avoir transmis au GPU, et ceci faisait que les jeux semblait tous "sage" graphiquement.
DirectX 8 introduit les vertex et pixel shaders, qui constituaient une méthode que les développeurs pouvaient utiliser pour décider de la manière avec laquelle les vertices et les pixels devraient être traités en parcourant le pipeline, leurs donnant ainsi beaucoup de flexibilité.
Un langage assembleur était utilisé pour programmer les shaders, quelque chose qui rendait assez dûre la tâche des programmeurs de shaders, et la seule version supportée était le "shader model 1.0". Mais quelque chose changea dès queDirectX 9 fut sortie, donnant aux programmeurs l'opportunité de développer des shaders dans un langage de haut niveau, appelé High Level Shader Language (HLSL), remplaçant le langage d'assemblage avec quelque chose proche du langage C. Cela rendit les shaders bien plus faciles à écrire, lire et apprendre.
DirectX10.0 introduisit un nouveau shader, le "Geometry Shader", et qui faisait partie du "shader model 4.0". Mais ceci réclamait une nouvelle carte graphique de pointe, ainsi que Windows Vista.XNA supporte les "shader model" de 1.0 à 3.0, mais fonctionne sur XP, Vista et XBox360 !
DirectX 8 introduit les vertex et pixel shaders, qui constituaient une méthode que les développeurs pouvaient utiliser pour décider de la manière avec laquelle les vertices et les pixels devraient être traités en parcourant le pipeline, leurs donnant ainsi beaucoup de flexibilité.
Un langage assembleur était utilisé pour programmer les shaders, quelque chose qui rendait assez dûre la tâche des programmeurs de shaders, et la seule version supportée était le "shader model 1.0". Mais quelque chose changea dès queDirectX 9 fut sortie, donnant aux programmeurs l'opportunité de développer des shaders dans un langage de haut niveau, appelé High Level Shader Language (HLSL), remplaçant le langage d'assemblage avec quelque chose proche du langage C. Cela rendit les shaders bien plus faciles à écrire, lire et apprendre.
DirectX10.0 introduisit un nouveau shader, le "Geometry Shader", et qui faisait partie du "shader model 4.0". Mais ceci réclamait une nouvelle carte graphique de pointe, ainsi que Windows Vista.XNA supporte les "shader model" de 1.0 à 3.0, mais fonctionne sur XP, Vista et XBox360 !
III Shaders?
Bon, assez d'histoire... Dans les faits, c'est quoi un shader ?
Comme je l'ai dit, les shaders peuvent être utilisés pour personnaliser les étapes dans le pipeline, pour faire en sorte que ce soit les développeurs qui implémentent comment les pixels/vertices doivent être traitées.
Comme nous le voyons sur la figure ci après, une application s'initialise et utilise un shader quand elle effectue le rendu, le vertex buffer travaille avec le pixel shader en lui envoyant les informations requises, depuis le vertex shader, travaillant ensemble pour créer une image dans le framebuffer.
Comme je l'ai dit, les shaders peuvent être utilisés pour personnaliser les étapes dans le pipeline, pour faire en sorte que ce soit les développeurs qui implémentent comment les pixels/vertices doivent être traitées.
Comme nous le voyons sur la figure ci après, une application s'initialise et utilise un shader quand elle effectue le rendu, le vertex buffer travaille avec le pixel shader en lui envoyant les informations requises, depuis le vertex shader, travaillant ensemble pour créer une image dans le framebuffer.
Une chose importante à retenir est que beaucoup de GPUs ne supportent pas tous les "shader models". Ceci doit être prit en compte lorsqu'on développe des shaders. Un shader doit avoir des méthodes alternatives pour archiver des effets similaires, plus simples, rendant l'application utilisable sur de plus vieux ordinateurs.
IV Vertex Shaders
Le vertex shader est utilisé pour manipuler des informations sur les vertices, par vertex. Cela peut, par exemple, être un shader qui fait que le model est "plus gros" durant le rendu, en bougeant les vertices le long de leurs normales, jusqu'à une nouvelle position, et ceci pour chaque vertex dans le model (on appelle cela un "deform shader").
Le vertex shader tient son entrée d'informations d'une structure (la "vertex strcture"), qui définit dans le code d'application, et charge cela depuis le vertex buffer, et l'envoie au shader. Ceci décrit quelles propriétés chaque vertex aura lors du shading : Position, Couleur, Normale, Tangente...
Le vertex shader envoie sa sortie d'information au pixel shader, pour l'utiliser plus tard. Définir quelles informations le vertex shader va envoyer à la prochaine étape, peut être fait en définissant une structure dans le shader, contenant les informations que vous voulez stocker, et fait que le vertex shader les retourne, ou en définissant des paramètres dans le shader, en utilisant le mot-clé "out". La sortie de donnée (output) peut concerner : une position, du "brouillard", des coordonnées de texture, des tangentes, des positions de lumières, etc.
Le vertex shader tient son entrée d'informations d'une structure (la "vertex strcture"), qui définit dans le code d'application, et charge cela depuis le vertex buffer, et l'envoie au shader. Ceci décrit quelles propriétés chaque vertex aura lors du shading : Position, Couleur, Normale, Tangente...
Le vertex shader envoie sa sortie d'information au pixel shader, pour l'utiliser plus tard. Définir quelles informations le vertex shader va envoyer à la prochaine étape, peut être fait en définissant une structure dans le shader, contenant les informations que vous voulez stocker, et fait que le vertex shader les retourne, ou en définissant des paramètres dans le shader, en utilisant le mot-clé "out". La sortie de donnée (output) peut concerner : une position, du "brouillard", des coordonnées de texture, des tangentes, des positions de lumières, etc.
struct VS_OUTPUT { float4 Pos: POSITION; }; VS_OUTPUT VS( float4 Pos: POSITION ) { VS_OUTPUT Out = (VS_OUTPUT) 0; ... return Out; } // or float3 VS(out float2 tex : TEXCOORD0) : POSITION { tex = float2(1.0, 1.0); return float3(0.0, 1.0, 0.0); }
Vous ne devriez jamais appeler vos Vertex et Pixel Shaders "VertexShader" et "PixelShader". L'utilisation de tels noms provoqueraient des erreurs.
V Pixel Shaders
Le Pixel shader manipule tous les pixels (par pixel) pour un model/object/collection vertices donné. On peut le comparer à une boite en métal, où l'on souhaite personnaliser l'algorithme d'éclairage, les couleurs, etc. Le pixel shader reçoit des informations des valeurs des sorties du vertex shader, comme la position, les normales, les coordonnées de textures :
float4 PS(float vPos : VPOS, float2 tex : TEXCOORD0) : COLOR { ... return float4(1.0f, 0.3f, 0.7f, 1.0f); }
Le pixel shader peut avoir deux valeurs de sorties uniquement, Color et Depth.
VI HLSL
Le High Level Shading Language est utilisé our développer des shaders. En HLSL, vous pouvez déclarer des variables, des fonctions, des tests (if/else), des boucles (for, do/while) et beaucoup d'autres choses,afin de créer une logique pour les vertices et les pixels. Ci-dessous, voici un tableau contenant des mots-clés existant encHLSL. Ils n'y sont pas tous, mais seulement les plus importants.
Exemples de types de données en HLSL
bool true or false
int 32-bit integer
half 16bit integer
float 32bit float
double 64bit double
Exemples de vecteurs in HLSL
float3 vectorTest
float vectorTest[3]
vector vectorTest
float2 vectorTest
bool3 vectorTest
Matrices en HLSL
float3x3: a 3x3 matrix, type float
float2x2: a 2x2 matrix, type float
Il y a aussi beaucoup de fonctions d'aide en HLSL, ce qui permet d'utiliser de complexes expressions mathématiques.
cos(x) Retourne cosinus de x
sin(x) Retourne sinus de x
cross(a, b) Retourne le produit vectoriel de deux vecteurs a et b
dot(a, b) Retourne le produit scalaire de deux vecteurs a et b
normalize(v) Retourne un vecteur colinéaire, de même sens et de norme 1
Pour une liste complète : http://msdn2.microsoft.com/en-us/library/bb509611.aspx
HLSL offre quantités de fonctions n'attendant que d'être utilisées par vous ! Apprenez les, comme ça vous saurez résoudre des problèmes divers.
Exemples de types de données en HLSL
bool true or false
int 32-bit integer
half 16bit integer
float 32bit float
double 64bit double
Exemples de vecteurs in HLSL
float3 vectorTest
float vectorTest[3]
vector vectorTest
float2 vectorTest
bool3 vectorTest
Matrices en HLSL
float3x3: a 3x3 matrix, type float
float2x2: a 2x2 matrix, type float
Il y a aussi beaucoup de fonctions d'aide en HLSL, ce qui permet d'utiliser de complexes expressions mathématiques.
cos(x) Retourne cosinus de x
sin(x) Retourne sinus de x
cross(a, b) Retourne le produit vectoriel de deux vecteurs a et b
dot(a, b) Retourne le produit scalaire de deux vecteurs a et b
normalize(v) Retourne un vecteur colinéaire, de même sens et de norme 1
Pour une liste complète : http://msdn2.microsoft.com/en-us/library/bb509611.aspx
HLSL offre quantités de fonctions n'attendant que d'être utilisées par vous ! Apprenez les, comme ça vous saurez résoudre des problèmes divers.
VII Fichiers Effet
Les fichiers effets (.fx) rendent plus facile le développement de shaders en HLSL, et vous pouvez stocker presque tout ce qui concerne les shaders dans un fichier .fx. Ceci inclu les variables globales, fonctions, structures, vertex shader, pixel shader, différentes techniques, différentes "passes", textures etc.
Nous avons déjà vu comment déclarer des variables et des structures dans un shader, mais qu'est ce que ces techniques/passes ? C'est très simple. Un shader peut avoir une ou plusieurs techniques. Chacune à un nom unique, et, depuis l'application ou le jeu, on peut choisir quelle technique du shader nous souhaitons utiliser, en ajustant la propriété CurrentTechnique de la classe Effect.
Nous avons déjà vu comment déclarer des variables et des structures dans un shader, mais qu'est ce que ces techniques/passes ? C'est très simple. Un shader peut avoir une ou plusieurs techniques. Chacune à un nom unique, et, depuis l'application ou le jeu, on peut choisir quelle technique du shader nous souhaitons utiliser, en ajustant la propriété CurrentTechnique de la classe Effect.
effect.CurrentTechnique = effect.Techniques["AmbientLight"];
Ici, on ajuste l'objet "effect", de façon à ce qu'il utilise la technique "AmbientLight". Une technique peut avoir un ou plusieurs passes, et nous devons nous rappeler de traiter toutes les passes de manière à obtenir le résultat désiré.
Ceci est un exemple de shader n'utilisant qu'une technique et qu'une passe :
Ceci est un exemple de shader n'utilisant qu'une technique et qu'une passe :
technique Shader { pass P0 { VertexShader = compile vs_2_0 VS(); PixelShader = compile ps_2_0 PS(); } }
Ceci est un exemple de shader utilisant une technique et deux passe :
technique Shader { pass P0 { VertexShader = compile vs_2_0 VS(); PixelShader = compile ps_2_0 PS(); } pass P1 { VertexShader = compile vs_2_0 VS_Other(); PixelShader = compile ps_2_0 PS_Other(); } }
Ceci est un exemple de shader utilisant deux technique et une passe :
technique Shader_11 { pass P0 { VertexShader = compile vs_2_0 VS(); PixelShader = compile ps_2_0 PS(); } } technique Shader_2a { pass P0 { VertexShader = compile vs_2_0 VS2(); PixelShader = compile ps_2_a PS2(); } }
On peut noter qu'une technique possède deux fonctions, une pour le pixel shader et une pour le vertex shader.
VertexShader = compile vs_2_0 VS2(); PixelShader = compile ps_2_0 PS2();
Ceci nous indique que la technique va utiliser VS2() comme vertex shader, PS2 comme pixel shader, et va supporter les shader model 1.1 ou plus. Ceci permet l'obtention de shaders différents et plus complexes pour les GPUs supportant une versionplus élevée de shader model.
Notez que les APIs de shaders bas niveau ne sont plus supportés dans XNA 4.0
Notez que les APIs de shaders bas niveau ne sont plus supportés dans XNA 4.0
VIII Implémention des shaders dans XNA
Il est vraiment facile d'implémenter des shaders dans XNA. En fait, seules quelques lignes de code sont nécessaires pour charger et utiliser un shader. Voici une listes des étapes à suivre quand on fait une shader :
- Faire le shader
- Mettre le fichier effet contenant le shader (.fx) dans le dossier “Content”
- Faire une instance de la classe Effect
- Initialiser l'objet fait à partir de la classe Effect
- Lancer le shader et choisir les techniques que vous souhaitez utiliser
- Transmettre les paramètres au shader
- Dessiner la scène, l'objet
Ces étapes plus en détail :
1. En faisant un shader, certains programmes comme notepad, visual studio editor et bien d'autres, peuvent être utilisés. Sont aussi disponibles, des shaders IDEs, et personnellement, j'aime utiliser nVidias FX Composer :
http://developer.nvidia.com/object/fx_composer_home.html
2. Lorsque le shader est créé, mettez le dans le dossier ”Content”, afin qu'il ait un "asset name".
NB : L'asset name est le nom par lequel on va désigner les fichier dans le code :
1. En faisant un shader, certains programmes comme notepad, visual studio editor et bien d'autres, peuvent être utilisés. Sont aussi disponibles, des shaders IDEs, et personnellement, j'aime utiliser nVidias FX Composer :
http://developer.nvidia.com/object/fx_composer_home.html
2. Lorsque le shader est créé, mettez le dans le dossier ”Content”, afin qu'il ait un "asset name".
NB : L'asset name est le nom par lequel on va désigner les fichier dans le code :
Shader est l'asset name, utilisé pour faire référence au fichier se trouvant dans le ossier Content.
3. Dans le XNA Framework se trouve la classe Effect qui est utilisée pour charger et compiler les shaders. Voici le code pour faire une instance de cette classe :
Effect effect;
Effect fait partie de la bibliothèque “Microsoft.Xna.Framework.Graphics”, donc n'oubliez pas d'ajouter cette ligne en haut de votre code :
using Microsoft.Xna.Framework.Graphics
4. Pour initialiser l'effet, chargez le depuis le dossier Content :
effect = Content.Load("Shader");
5. Choisissez la techniques que vous voulez utiliser :
effect.CurrentTechnique = effect.Techniques["AmbientLight"];
Vous devez aussi débuter toutes les passes du shader.
foreach (EffectPass pass in effect.CurrentTechnique.Passes) { // Begin the pass (index value - here 0 - selects the pass) effect.CurrentTechnique.Passes[0].Apply();
6. Il y a plusieurs manières de fixer un paramètre du shader, mais la suivante et suffisante pour ce tutorial.
Note : Ce n'est pas la manière la plus rapide, mais nous y reviendrons dans un futur tutorial.
Note : Ce n'est pas la manière la plus rapide, mais nous y reviendrons dans un futur tutorial.
effect.Parameters["matWorldViewProj"].SetValue(worldMatrix * viewMatrix * projMatrix);
où "matWorldViewProj" est défini dans le shader : float4x4 matWorldViewProj; et worldMatrix * viewMatrix * projMatrix est la valeur qu'on assigne à matWorldViewProj.
SetValue définit la valeur du paramètre et l'envoie au shader et GetVlue<Type> retire une valeur du shader, où Type est le type de la donnée à retirer. Par exemple, GetValue<Int32>() reçoit un entier du shader.
7. Rendre la scène ou l'objet que vous souhaitez traiter/modifier avec ce shader à l'écran.
Pour mieux comprendre, ouvrez le code source et voyez les étapes en action.
SetValue définit la valeur du paramètre et l'envoie au shader et GetVlue<Type> retire une valeur du shader, où Type est le type de la donnée à retirer. Par exemple, GetValue<Int32>() reçoit un entier du shader.
7. Rendre la scène ou l'objet que vous souhaitez traiter/modifier avec ce shader à l'écran.
Pour mieux comprendre, ouvrez le code source et voyez les étapes en action.
IX Lumière ambiante
Ok, nous arrivons enfin à la dernière étape, l'implémentation du shader ! Pas mal, hein ?
Tout d'abord, qu'est ce qu'une "lumière ambiante" ?
La lumière ambiante est la lumière la plus basique d'une scène. Si vous traversez une salle complètement sombre, la lumière ambiante est à zero, mais quand vous en sortez, il y a presque toujours une lumière ce vous permez de voir. Cette lumière n'a pas de direction et son rôle est de s'assurer que les objets non éclairés ont une couleur de base.
La formule de la lumière ambiante est :
I = Aintensity x Acolor ( 1.1)
où L est la lumière, Aintensity est l'intensité lumineuse (entre 0.0 et 1.0), et Acolor est la couleur de la lumière ambiente. Cette couleur peut être fixée dans le code ou à l'aide d'une texture.
Ok, commençons l'implémentation du shader. Tout d'abord, nous avons besoin d'une matrice qui représente la matrice du monde :
Tout d'abord, qu'est ce qu'une "lumière ambiante" ?
La lumière ambiante est la lumière la plus basique d'une scène. Si vous traversez une salle complètement sombre, la lumière ambiante est à zero, mais quand vous en sortez, il y a presque toujours une lumière ce vous permez de voir. Cette lumière n'a pas de direction et son rôle est de s'assurer que les objets non éclairés ont une couleur de base.
La formule de la lumière ambiante est :
I = Aintensity x Acolor ( 1.1)
où L est la lumière, Aintensity est l'intensité lumineuse (entre 0.0 et 1.0), et Acolor est la couleur de la lumière ambiente. Cette couleur peut être fixée dans le code ou à l'aide d'une texture.
Ok, commençons l'implémentation du shader. Tout d'abord, nous avons besoin d'une matrice qui représente la matrice du monde :
float4x4 matWorldViewProj;
Déclarez ceci en haut du shader, en tant que déclaration globale.
Ensuite, on doit savoir quelles valeurs le vertex shader doit passer au pixel shader. Ceci est fait en créant une structure (qui vous pouvez appeler comme vous voulez) :
Ensuite, on doit savoir quelles valeurs le vertex shader doit passer au pixel shader. Ceci est fait en créant une structure (qui vous pouvez appeler comme vous voulez) :
struct OUT { float4 Pos: POSITION; };
On crée une structure appellée OUT qui contient une variable de type float4 avec le nom Pos. "POSITION", à la fin, dit au GPU dans quel registre il doit mettre cette valeur. Mais, qu'est ce qu'un registre ? En fait, un registre est simplement quelque chose dans le GPU qui contient des informations. Le GPU possède différents registres pour stocker des positions, des normales, des coordonnées de exture, etc. Et quand on déinit une variable que le vertex shader va passer au pixel shader, on doit aussi décider de l'endroit dans le GPU où cette valeur est stockée.
Jetons un oeil au vertex shader :
Jetons un oeil au vertex shader :
OUT VertexShaderFunction( float4 Pos: POSITION ) { OUT Out = (OUT) 0; Out.Pos = mul(Pos, matWorldViewProj); return Out; }
On crée une fonction vertex shader de type OUT, qui a pour paramètre float4 Pos:POSITION. C'est la position de la vertice définie dans le fichier du model, dans l'application ou le jeu.
Ensuite, on fait une instance de la structure OUT, qu'on appelle Out. Cette structure doit être remplie et retournée par la fonction pour traitement ultérieur.
La position qu'on a dans les parmètres d'entrée n'est pas traitée, et doit être multipliée par la matrice worldviewprojection pour être placée correctement à l'écran.
Comme c'est l'unique varaible dans OUT, on peut la retourner, et passer à l'étape suivante.
Maintenant, c'est au pixel shader d'agir. On déclare une fonction de type float4, qui retourne une valeur de type float4 stockée dans le registre COLOR du GPU.
Dans le pixel shader, on calcule l'algorithme de la lumière ambiante :
Ensuite, on fait une instance de la structure OUT, qu'on appelle Out. Cette structure doit être remplie et retournée par la fonction pour traitement ultérieur.
La position qu'on a dans les parmètres d'entrée n'est pas traitée, et doit être multipliée par la matrice worldviewprojection pour être placée correctement à l'écran.
Comme c'est l'unique varaible dans OUT, on peut la retourner, et passer à l'étape suivante.
Maintenant, c'est au pixel shader d'agir. On déclare une fonction de type float4, qui retourne une valeur de type float4 stockée dans le registre COLOR du GPU.
Dans le pixel shader, on calcule l'algorithme de la lumière ambiante :
float4 PixelShaderFunction() : COLOR { float Ai = 0.8f; float4 Ac = float4(0.075, 0.075, 0.2, 1.0); return Ai * Ac; }
Ici, on utilise 1.1 pour calculer la couleur du pixel considéré. Ai est l'intensité de la lumière ambiante et Ac est la couleur ambiante.
Enfin, p, doit définir la technique et lier les fonctions vertex shader et pixel shader à la technique :
Enfin, p, doit définir la technique et lier les fonctions vertex shader et pixel shader à la technique :
technique AmbientLight { pass P0 { VertexShader = compile vs_1_1 VertexShaderFunction(); PixelShader = compile ps_1_1 PixelShaderFunction(); } }
Ok, c'est tout !
Maintenant, je vous recommande de lire le code source, et de jouer un peu avec les valeurs, pour comprendre comment mettre en place et implémenter un shader en utilisant XNA
Télécharger le code source
Maintenant, je vous recommande de lire le code source, et de jouer un peu avec les valeurs, pour comprendre comment mettre en place et implémenter un shader en utilisant XNA
Télécharger le code source