Version imprimable       Envoyer     
Cliquez pour évaluer et commenter
MSDN
MSDN Library
Articles Techniques
Développement Win32 et COM
Graphiques et Multimedia
DirectX
DirectHLSL
 Introduction aux Shaders avec Manag...
Introduction aux Shaders avec Managed DirectX Partie 1

Introduction et Cartes 3D modernes et Shaders

Paru le 31 août 2006
Par Guillaume Randon - Microsoft Services

Sur cette page

Introduction Introduction
Cartes 3D modernes et Shaders Cartes 3D modernes et Shaders

Introduction

Ce document présente les Shaders et explique jusqu’à un certain point comment les mettre en œuvre à partir d’une application s’appuyant sur le Framework .Net et sur le SDK DirectX.

Il fait suite au papier DirectX Managed : Rappel des concepts de bases auquel on pourra se reporter pour un bref rappel des concepts de géométrie euclidienne en trois dimensions utilisés ici ainsi que pour une introduction à Managed DirectX (MDX).

Cartes 3D modernes et Shaders

Par Shaders on désigne des petits programmes qui vont être exécuté directement par la carte graphique. Il s’agit de code qui ne sera pas exécuté par l’unité centrale de votre machine (CPU) mais bien plutôt par le processeur présent sur la carte vidéo (GPU). Ce processeur est spécialisé pour ce type de traitements pour lequel il est exceptionnellement plus performant. Comme nous allons le voir ces petits programmes se rangent dans deux catégories distinctes, les Vertex Shader et les Pixel Shader. En plus de permettre le déport de certains traitement du CPU vers les GPU, ils permettent de spécifier de manière très fine le travail que la carte graphique va effectuer pour calculer la couleur de tous les points de l’écran à partir des données qui lui sont fournis. Cette flexibilité d’emploi est inaccessible aux générations antérieures de cartes graphiques qui ne les supportent pas.

Pour mieux mettre ces points en lumière je vais justement dans un premier temps décrire le fonctionnement de ces cartes graphique d’ancienne génération, en m’attachant à présenter la notion de pipeline graphique, et les évolutions qui ont pu être apportées à ces cartes au cours des dernières années. Il s’agit juste pour moi de donnée ici assez d’information pour qu’il devienne aisé de bien comprendre ce que sont les Shaders et où ils prennent leur place dans ces évolutions. Pour des descriptions plus détaillées des cartes graphiques et de leur architecture matérielle, il est intéressant de consulter les sites des grands constructeurs de cartes 3D (les sites ATI et nVidia (www.ati.com / www.nvidia.com contiennent des informations très précises sur ce sujet).


Le ‘Pipeline’ graphique

Le terme anglo-saxon pipeline donne une idée de suite de traitements exécutés les uns après les autres. Le fonctionnement d’une carte graphique et les opérations qu’elle effectue pour obtenir le résultat visible à l’écran peut de fait être découpé en étapes successives. Si l’on se rapporte au schéma ci-dessous extrait de la documentation du SDK DirectX il donne une vision de très haut niveau de cette suite d’opérations.

Comme cela a été décrit dans le document précédent DirectX Managed : Rappel des concepts de bases, pour obtenir l’affichage d’objet en 3 dimensions à l’écran, il est nécessaire de fournir à la carte graphique par le biais d’un API graphique comme DirectX (ou OpenGL) une suite de primitives graphiques. Ces primitives sont elles-mêmes une suite de ligne ou de triangles, des textures peuvent être plaquées sur ces primitives et vont impacter la couleur finale de celles-ci à l’écran. Si on fait le bilan on fournit donc à notre carte des points de l’espace, des tableau d’indices permettant de relier ces points et constituant les primitives, des textures. Les opérations effectuées sur ces données apparaissent dans le schéma ci-dessus.

Si l’on regarde les différentes étapes plus en détails, d’abord les points (les vertex contenus dans le VertexBuffer et passés à la carte) sont transformés en fonction des matrices 4x4 qui leur sont associées ainsi que des matrices View et Projection décrites dans mon document précédent. Dans le schéma ci-dessus il s’agit de l’étape appelée « Vertex Processing ».

Ensuite les primitives sont reconstituées à partir de tableau d’indices qui sont eux aussi passés à la carte et qui définissent les indices des points dans le tableau de points précédemment traités qui constituent par exemple les sommets de nos triangles. Une fois les primitives à affiché reconstituées, on détermine les portions de la surface de rendu couverts par telle ou telle primitive graphique, la couleur aux sommets ou des extrémités des primitives est calculé et extrapolée pour tout les autres points de la primitive tout en tenant compte de la texture que l’on a décidé d’appliquer, il s’agit de l’étape appelée « Primitive Processing » (cf. diagramme ci-dessus).

On voit bien que la suite d’opérations décrite ici est relativement fixe. En particulier, même si je ne donne pas plus de détails sur la façon dont la couleur en tout point d’une primitive est calculé en fonction de la couleur aux sommets et de la texture, dans le ‘pipeline’ décrit ici, cette opération est systématiquement la même quelques soit la primitive en cours de traitement. Grosso modo il s’agit peut ou prou d’un mode de fonctionnement commun à toute carte graphique dite 3D et à la suite d’opérations effectuées par la carte pour afficher les objets de l’exemple de code accompagnant mon précédent document. Ce mode de fonctionnement, aussi abouti soit-il pour afficher un certain type d’objets 3D, peut se révéler contraignant dans la mesure où il est inaltérable (et ce même si certaines variables permettent toutefois de jouer sur la façon dont ces opérations pré déterminées vont être exécutées et ainsi de modifier le résultat final). Dans toute la littérature anglo-saxonne traitant du sujet il est fait référence à ce pipeline sous l’appellation de Fixed-Function Pipeline.

C’est pour lever cette limitation que les Shaders ont fait leur apparition. Au lieu d’appliquer à tout point de l’espace en 3D puis à tout pixel de la surface de rendu couvert par une primitive une opération pré déterminée et gravée dans le silicium des processeurs graphiques, les cartes modernes offrent la possibilité d’exécuter une suite d’opérations spécifiées par le programmeur par le biais de programmes qui seront exécutés directement par le processeur graphique, nos fameux Shaders.


Les Shaders aujourd’hui dans DX9

Aujourd’hui (DX9) les Shaders permettent d’intervenir à deux niveaux. D’une pars pour le traitement des points de l’espace en 3D qui ont été fournis à la carte graphique (Vertex Processing dans le diagramme plus haut) : il s’agit des Vertex Shaders. D’autre pars au niveau des traitements effectués pour le calcul final de la couleur des pixels couverts par une primitive donnée (Pixel Processing) : il s’agit des Pixel Shader.

Notre description se base sur le système d’effets de DirectX et sur HLSL (High Level Shader Language). Voici un premier exemple de Shader :

//--------------------------------------------------------------------------------------
// Global variables
//--------------------------------------------------------------------------------------
texture g_BaseTexture;

float4x4 g_WorldViewProj;    // World * View * Projection matrix



//--------------------------------------------------------------------------------------
// Texture samplers
//--------------------------------------------------------------------------------------
sampler BaseTextureSampler = 
sampler_state
{
    Texture = <g_BaseTexture>;
	MinFilter = Linear;
	MagFilter = Linear;
	MipFilter = Linear;
};

//--------------------------------------------------------------------------------------
// Vertex shader output structure
//--------------------------------------------------------------------------------------
struct VS_OUTPUT
{
    float4 Position   : POSITION;   // vertex position
    float2 TextureUV  : TEXCOORD0;  // vertex texture coords 
};


//--------------------------------------------------------------------------------------
// This shader computes standard transform
//--------------------------------------------------------------------------------------
VS_OUTPUT RenderSVS( float4 vPos : POSITION, 
                         float3 vTexCoord0 : TEXCOORD0 )
{
    VS_OUTPUT Output;
    
    Output.Position = mul(vPos, g_WorldViewProj);
    Output.TextureUV = vTexCoord0;
    
    return Output;    
}


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


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

    Output.RGBColor = tex2D(BaseTextureSampler, In.TextureUV);
	
    return Output;
}


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

Il s’agit là juste d’un exemple inclus ici car il permet de mieux apprécier les opérations qui peuvent être réalisées avec un Vertex Shader d’une part, et avec un Pixel Shader d’autre part. Les détails de l’implémentation des Shaders seront dans la seconde partie de ce document : « Les Shaders avec DirectX ». Nous allons avant tout présenter le rôle des Shaders et leur domaine d’utilisation.

Dans l’exemple ci-dessus les variables globales définies au début de notre fichier d’effet DirectX peuvent être modifiées à partir de notre programme principal exécuté sur le CPU de notre machine. Je donnerai plus de détails à ce sujet dans la partie concernant l’utilisation des Shaders à partir de DirectX. Pour l’instant il est juste intéressant de noter que l’on peut fournir des valeurs à notre Shader à partir du programme principal. Le Shader ci-dessus défini par ailleurs deux structures VS_OUTPUT et PS_OUTPUT qui permettent de spécifier le format des données qui sera retourné par l’exécution d’une part du Vertex Shader et d’autre part du Pixel Shader.

Dans l’exemple ci-dessus on définit aussi un sampler_state qui nous permet de spécifier à la fois une texture ainsi que certaines variables d’état de notre pipeline permettant d’impacter la façon dont la texture sera utilisée.

Une fois tous ces éléments en place, notre Vertex Shader d’exemple se limite à effectuer les transformations successives World View et Projection à tous les points (vertex) qui lui sont fournis (se reporter au premier document sur les concepts de base pour une description de ces transformations et de leur rôle). Par ailleurs pour chacun de ces points, les coordonnées de texture sont copiées dans l’instance de la structure de donnée VS_OUTPUT qui va être retournée en sortie. Cette structure VS_OUTPUT définie le format de sortie du Vertex Shader et finalement le format sous lequel les données vont être transmises aux autres unités de traitement en aval dans notre pipeline et entre autre à notre Pixel Shader.

Ici, le Pixel Shader de notre exemple s’occupe quant à lui d’utiliser les coordonnées de texture qui lui sont fourni pour accéder à la texture et récupérer la couleur du point correspondant.

Enfin pour finir les mots clés Technique et Pass à la fin du fichier d’effet donné ci-dessus, sont propres au système de fichiers d’effets de DirectX. Je les détaillerai plus tard ; notons simplement que cela permet d’indiquer que lorsque l’on précise par le biais de DirectX que l’on souhaite utiliser la Technique Render définie dans ce fichier, il faudra exécuter lors de la première passe les Shaders qui sont spécifiés ici. Un rendu peut-être le résultat de plusieurs passes, c’est à dire plusieurs passage des données au travers du pipeline, étant entendu que cela n’est intéressant que si des variables de celui-ci ou les Shaders qui vont être utilisés sont modifiés d’une passe à une autre.

Revenons maintenant un peu sur les concepts de base : le Viewing Frustrum est une pyramide englobant tous ce qui est visible d’une scène 3D à un instant t en fonction de la position de la caméra et de son angle d’ouverture.

Ce graphique extrait de la documentation du SDK DirectX permet de bien visualiser de quoi il s’agit.

Après la transformation par la matrice dite de Projection, cette portion de l’espace en 3D qui contient tout ce qui est visible de notre scène 3D, est en fait transformée en un cube.

Celui-ci a pour sommets de sa diagonale (-1, -1, 0) et (1, 1, 1). Ensuite, si l’on aplati ce cube et que l’on regarde la surface ainsi créée à partir d’un point en (0, 0, 0) on voit finalement ce qui est affiché sur notre écran.

En ayant bien ces opérations en tête, on peut finalement dire que le Vertex Shader a pour responsabilité d’effectuer sur tous les points qui lui sont passés les opérations nécessaires pour positionner ceux-ci où on le désire dans le cube décrit ci-dessus et résultant de la transformation du Viewing Frustrum par la matrice de projection. Si l’on souhaite les positionner de façon identique à ce que l’on obtient avec le Fixed-Function Pipeline décrit plus tôt, il faut alors juste opérer les transformations World View Projection sur tous ces points (c’est ce qui est fait dans l’exemple donné plus haut). Maintenant on peut tout à fait imaginer qu’en fonction de l’effet désiré, nous appliquions à ces points des transformations complètement différentes. Par exemple si nous étions en train d’afficher des primitives représentant de la végétation et que l’on souhaite que celle-ci subisse les effets du vent, il serait tout à fait possible de déplacer les sommets de nos primitives correspondant par exemple à la c ime des arbres en fonction d’une variable globale définie dans notre fichier d’effet. L’application principale pourrait faire varier cette variable pour chaque rendu afin d’obtenir le résultat visuel désiré. Il ne s’agit que d’un exemple parmi tant d’autres, un Vertex Shader permet d’appliquer les traitements que l’on désire pour positionner les points qui lui sont fournis ; nous ne sommes plus limités que par le nombre de variables qu’il est possible de communiquer à un Shader à partir de l’application principale, le nombre d’instructions maximum autorisé dans les Shader en lui-même, les performances des cartes et notre imagination.

Pour résumer tout ce que nous venons de décrire, le Shader donné en exemple se limite à effectuer les opérations nécessaires sur les points qui lui sont fournis pour que ceux-ci finissent à l’endroit de l’écran où ils apparaitraient si l’on utilisait le Fixed-Function Pipeline et utilise juste la couleur stockée dans la texture pour déterminer la couleur du pixel en cours de traitement qui apparaitra au final à l’écran. Il est intéressant de noter que cette partie là effectue des opérations assez limitées (par exemple le positionnement de lumières dans notre scène n’est pas pris en compte) et n’est pas utilisé pour calculer les couleurs des points de nos objets à l’écran. En cela les opérations effectués par le Fixed-Function Pipeline dans la phase Pixel Processing (se reporter au schéma plus haut) sont finalement plus complètes. Cet exemple est toutefois intéressant dans la mesure où il permet d’illustrer ce que sont les Shaders sans entrer pour le moment dans le détail d’opérations complexes dont il s peuvent éventuellement avoir la charge. Rappelons que l’utilité des Shaders est le gain de performance… Donc, même dans le cas de notre exemple où l’opération effectuée revient au traitement du Fixed-Function Pipeline, il y a un certain intérêt à utiliser les Shaders !


OPERATIONS POSSIBLES DANS UN VERTEX SHADER

Comme nous venons de le voir, un Vertex Shader s’attache à traiter les points de l’espace en trois dimensions qui lui sont fournis en entrée et à leur appliquer une suite de transformations. Tous les points définissant nos géométries vont être traités par le vertex Shader actif.

Dans l’exemple proposé plus haut, le Vertex Shader se borne à appliquer les transformations world View Projection qui lui sont fournis mais on peut imaginer bien d’autres types de traitements. J’ai déjà donné un exemple concernant l’affichage de végétation mais on peut aussi mentionner d’autres types de transformation ; par exemple Hugues Hoppe propose de faire calculer par le Vertex Shader les points d’un terrain en fonction des valeurs stockée dans une texture et définissant la hauteur du terrain à un endroit donné (http://research.microsoft.com/~hoppe/ ; voir le papier Terrain rendering using GPU-based geometry clipmaps.). Cet exemple est intéressant car il montre finalement bien que l’on peut utiliser le Vertex Shader non seulement pour appliquer les transformations que l’on désire à des géométries existantes, mais aussi pour créer entièrement, et de façon procédurale, ces géométries au moment de l’affichage et sans impliquer notre CPU dans ce travail. Même s’il est toujours nécessaire de fournir le bon nombre de points en entrée pour que ceux-ci soient traités et déclenchent l’exécution du Vertex Shader finalement en fonction des opérations effectuées dans notre Shader, la position de ces points peut être sans importance.

Finalement il y a peu de limites aux emplois possibles. Dans un article ultérieur j’espère avoir le loisir de décrire un Vertex Shader qui génère automatiquement des plans de coupe d’une géométrie, ce qui peut par exemple être utilisé pour des données volumiques collectés à l’aide de scanner. Il s’agit toutefois d’un exemple plus avancé d’utilisation des Shaders qu’il m’était difficile d’inclure dans cet article d’introduction.

Pour se faire une idée juste des possibilités, il est intéressant de confronter sa propre imagination à la liste des opérations mathématiques possibles dans ces programmes que sont les Shaders. La documentation du SDK DirectX contient toutes les informations nécessaires à ce sujet, en particulier dans la section intitulée « HLSL Intrinsic Functions ».


OPERATIONS POSSIBLES DANS UN PIXEL SHADER

Le Pixel Shader permet quant à lui de calculer la couleur de tout point de la surface de rendu couvert par une des primitives que nous avons envoyées à notre carte graphique. Par exemple dans le cas où nous envoyons à la carte une liste de triangles, nous allons en fait lui envoyer une liste de sommets et une liste d’indices précisant comment ces sommets doivent être reliés entre eux pour constituer les triangles désirés. Tous les sommets vont être transformés par notre Vertex Shader ce qui abouti à la même liste de points mais cette fois positionné dans le cube (-1, -1, 0) (1, 1, 1) résultant de la transformation du Viewing Frustrum par la matrice de projection. Les points en même tant que la liste d’indice sont ensuite passés dans le pipeline à ce qui est appelé dans notre diagramme « Primitive Processing ». Pour résumer, les traitements regroupés sous ce nom récupèrent la liste d’indices afin d’en déduire les sommets qui forment triangles et autres primitives que nous aurions pu passer à la carte.

Ensuite cette unité de traitement génère et passe au Pixel Shader les points de la surface de rendu couvert par cette primitive en fonction des informations qui lui sont passés par le biais de la structure VS_OUTPUT.

Nous voyons donc bien que le Pixel Shader nous permet de jouer directement, de calculer la couleur des points affichés à l’écran (ou tout du moins certains d’entre eux). Là encore un grand champ d’opérations différentes peut être effectué. On peut à nouveau se repporter à la section intitulée « HLSL Intrinsic Functions » de la documentation du SDK pour bien cerner ce qui est possible. Dans la suite de cet article je me propose de modifier l’exemple de mon premier document (DirectX Managed : Rappel des concepts de bases) pour obtenir une eau et un ciel un tout petit peu plus crédible que ce qu’ils étaient initialement.

Par exemple, pour le ciel, plusieurs textures seront utilisées et mélangées par le Shader. Les coordonnées de textures seront calculées à la volée pour animer le positionnement des textures plaquées sur notre ciel. Pour l’eau aussi, plusieurs textures seront utilisées, ainsi que le contenu du back Buffer pour obtenir une eau qui soit à la fois réflective et réfractives.


BILAN

J’espère que la description que j’ai faite de Shaders permet de bien mesurer l’intérêt de ces programmes exécutés directement sur notre carte graphique. Ils permettent d’appliquer nos propres traitements à la fois pour positionner les points de nos géométries et pour calculer la couleur de chaque point de l’écran. Le tout se fait en utilisant la puissance de la carte graphique. Pour un développeur d’applications 3D il s’agit d’une liberté retrouvée dans la mesure où il reprend le contrôle sur des étapes importantes des traitements effectués pour l’affichage d’une scène en trois dimensions. Un des apports indéniables des Shaders est ainsi de rompre l’uniformité des rendus que l’on pouvait obtenir avec le Fixed-Function Pipeline.

En revanche, il est intéressant de noter que malgré leurs avantages indéniables, ces petits programmes souffrent encore aujourd’hui de quelques limitations :

  • Le nombre de valeurs que l’on peut passer de l’application principale à notre Shader reste limité.

  • Le nombre d’opérations que l’on effectue dans les Shaders n’est pas infini.

  • Le nombre d’interactions entre la carte et l’application principale doit rester dans des limites raisonnables. Cela peut forcer à utiliser des Shaders plus simples que ceux que l’on souhaiterait, ou en tout cas à en utiliser moins que ce que l’on souhaiterait.

  • On peut jouer sur chaque point qui est passé au Vertex Shader et sur chaque point qui va être affiché dans notre Pixel Shader, mais finalement la liste de primitives à afficher reste fixe (même si certaines techniques souvent regroupées sous l’appellation d’« instanciation » permettent de traiter plusieurs fois la même suite de sommets).


Demain DX10

La version 10 du SDK DirectX arrive bientôt. Elle va s’appuyer sur un modèle de driver différent de DX9. Aujourd’hui le modèle de drivers pour DX9 permet difficilement de partager les ressources de la carte graphique entre différentes applications. Par ailleurs le coût d’un appel à une méthode d’un composant DX qui effectue un appel au driver de la carte est important. Le nouveau modèle de driver accompagnant DX10 apporte des avancées significatives dans ces domaines.

La mémoire vidéo est virtualisée elle est partageable entre différents processus, l’ensemble est interruptible. Tous cela permet un meilleur partage du GPU et de sa mémoire entre différentes applications. Les performances devraient être considérablement améliorées, tout du moins en mode fenêtré (en mode plein écran une application fait généralement un usage exclusif de la carte et donc les nouveautés évoquées devraient avoir moins d’impact). Au-delà de ces changements assez « bas niveau » qui devraient être bénéfiques pour les performances en général, il est intéressant de jeter rapidement un œil à l’évolution du pipeline en lui-même et de regarder certaines des nouvelles fonctionnalités qu’il apporte avec lui, particulièrement concernant les Shaders.

Tout d’abord pour harmoniser l’API il n’y a plus avec DX10 de Fixed-Function Pipeline. L’utilisation des Shaders est donc de fait obligatoire (mais des Shaders correspondants aux opérations qui étaient effectuées par le FFP sont fournies avec le SDK). Par ailleurs, outre l’augmentation considérable du nombre de registres pour passer des valeurs entre le CPU et le GPU et du nombre d’instructions possibles dans un Shader, on voit aussi l’apparition d’un nouveau type de Shader répondant au nom de « Geometry Shader ». J’avais signalé à la fin de la partie précédente qu’une limitation du modèle actuel était qu’il ne permettait pas de modifier la liste de primitives à afficher, ce point change avec DX10 puisque ce nouveau type de Shader permet justement de générer sur le GPU de façon procédurale de nouvelles primitives.

Pour se faire une meilleure idée de l’endroit ou les Geometry Shader interviennent dans notre pipeline graphique voici un diagramme tout droit extrait de la documentation concernant DX10 et présente sur le site http://msdn.microsoft.com/directX:

Pour l’instant je ne vais pas donner plus de détails concernant DX10, j’espère pouvoir traiter ce sujet de manière plus approfondie dans un prochain article.

<< 1   2   3   >>

Introduction aux Shaders avec Managed DirectX
Téléchargez

Le code source
TutMDXShaders.zip
2,08 Mo

© 2009 Microsoft Corporation. Tous droits réservés. Conditions d'utilisation | Marques | Confidentialité
Page view tracker