|
|
| Lumière ambiante par Petri Wilhelmsen 
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. I Prérequis Une certaine connaissance d'XNA, puisque je ne vais pas beaucoup détailler le chargement des textures, des modèles 3D, les matrices et les maths. II Une courte histoire à propos des 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 d'assemblage é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 graohique de pointe, ainsi que Windows Vista. XNA supporte les "shader model" de 1.0 à 3.0, mais donctionne 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. 
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. 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); }
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); } The pixel shader can have two output values, Color and 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 en 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 des deux vecteurs a et b dot( a,b ) retourne le produit scalaire de a et b normalize( v ) retourne un vecteur colinéaire, de même sens et de norme 1
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 Effect files 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. 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 :
technique Shader { pass P0 { VertexShader = compile vs_1_1 VS(); PixelShader = compile ps_1_1 PS(); } } Ceci est un exemple de shader utilisant une technique et deux passe :
technique Shader { pass P0 { VertexShader = compile vs_1_1 VS(); PixelShader = compile ps_1_1 PS(); } pass P1 { VertexShader = compile vs_1_1 VS_Other(); PixelShader = compile ps_1_1 PS_Other(); } } Ceci est un exemple de shader utilisant deux technique et une passe :
technique Shader_11 { pass P0 { VertexShader = compile vs_1_1 VS(); PixelShader = compile ps_1_1 PS(); } }
technique Shader_2a { pass P0 { VertexShader = compile vs_1_1 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_1_1 VS2(); PixelShader = compile ps_1_1 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.
VIII Implementation 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 : 1. Faire le shader 2. Mettre le fichier effet contenant le shader ( .fx ) dans le dossier “Content” 3. Faire une instance de la classe Effect
4. Initialiser l'objet fait à partir de la classe Effect 5. Choisir les techniques que vous souhaitez utiliser 6. Lancer le shader 7. Transmettre les paramètres au shader 8. Dessiner la scène, l'objet 9. Arrêterle shader Ces étapes plus endé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 : 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<Effect>("Shader"); 5. Choisissez la techniques que vous voulez utiliser :
effect.CurrentTechnique = effect.Techniques["AmbientLight"];
6. Pour lancer l'effet, appelez la fonction Begin() :
effect.Begin();
Vous devez aussi lancer toutes les passes.
foreach (EffectPass pass in effect.CurrentTechnique.Passes) { // Begin current pass pass.Begin();
7. Il y a beaucoup de façons de régler les paramètre du shader, mais ce qui suit est suffisant pour ce tutorial. Note : Ceci n'est pas le moyen le plus rapide de le faire, et j'y reviendrai lors d'un prochain tutorial. effect.Parameters["matWorldViewProj"].SetValue( worldMatrix * viewMatrix * projMatrix);
Où "matWorldViewProj" est définit dans le shader : float4x4 matWorldViewProj; et worldMatrix * viewMatrix * projMatrix est une matrice dont la valeur est attribuée à matWorldViewProj. SetValue attribut une valeur au paramètre et l'envoie au shader, et GetValue<Type> retire une valeur du shader, où Type est le type de la donnée à retirer. Par exemple, GetValueInt32() retire un entier du shader. 8. Dessinez la scène. 9. Pour arrêter la passe, appelez pass.End() et pour arrêter le shader, appelez la méthode End() de la classe Effect : pass.End(); effect.End();
Pour mieux comprendre ceci, ouvrez le code source procuré et regardeez les étapes en action.
| |
|