Écriture de nuanceurs HLSL dans Direct3D 9

Notions de base Vertex-Shader

Lorsqu’il est en fonctionnement, un nuanceur de vertex programmable remplace le traitement de vertex effectué par le pipeline graphique Microsoft Direct3D. Lors de l’utilisation d’un nuanceur de vertex, les informations d’état relatives aux opérations de transformation et d’éclairage sont ignorées par le pipeline de fonction fixe. Lorsque le nuanceur de vertex est désactivé et que le traitement de la fonction fixe est retourné, tous les paramètres d’état actuel s’appliquent.

Le pavage des primitives d’ordre élevé doit être effectué avant l’exécution du nuanceur de vertex. Les implémentations qui effectuent un pavage de surface après le traitement du nuanceur doivent le faire d’une manière qui n’est pas apparente au code de l’application et du nuanceur.

Au minimum, un nuanceur de vertex doit afficher la position du vertex dans l’espace clip homogène. Si vous le souhaitez, le nuanceur de vertex peut générer les coordonnées de texture, la couleur du vertex, l’éclairage des vertex, les facteurs de brouillard, etc.

Notions de base Pixel-Shader

Le traitement des pixels est effectué par des nuanceurs de pixels sur des pixels individuels. Les nuanceurs de pixels fonctionnent de concert avec les nuanceurs de vertex ; La sortie d’un nuanceur de vertex fournit les entrées d’un nuanceur de pixels. D’autres opérations de pixels (fusion de brouillard, opérations de gabarit et fusion de cible de rendu) se produisent après l’exécution du nuanceur.

États de l’étape de texture et de l’échantillonneur

Un nuanceur de pixels remplace complètement la fonctionnalité de fusion de pixels spécifiée par le mélangeur multi texture, y compris les opérations précédemment définies par les états de l’étape de texture. Les opérations d’échantillonnage et de filtrage des textures qui étaient contrôlées par les états d’étape de texture standard pour la réduction, l’agrandissement, le filtrage mip et les modes d’adressage wrap peuvent être initialisées dans les nuanceurs. L’application est libre de modifier ces états sans nécessiter la régénération du nuanceur actuellement lié. La définition de l’état peut être encore plus facile si vos nuanceurs sont conçus dans un effet.

Entrées du nuanceur de pixels

Pour les versions de nuanceur de pixels ps_1_1 - ps_2_0, les couleurs diffuses et spéculaires sont saturées (serrées) dans la plage de 0 à 1 avant l’utilisation par le nuanceur.

Les valeurs de couleur entrées dans le nuanceur de pixels sont supposées être correctes en perspective, mais cela n’est pas garanti (pour tout le matériel). Les couleurs échantillonnées à partir des coordonnées de texture sont itérées de manière correcte et limitées à la plage de 0 à 1 pendant l’itération.

Sorties du nuanceur de pixels

Pour les versions de nuanceur de pixels ps_1_1 - ps_1_4, le résultat émis par le nuanceur de pixels est le contenu du registre r0. Tout ce qu’il contient lorsque le nuanceur termine le traitement est envoyé à l’étape de brouillard et au mélangeur de cible de rendu.

Pour les versions de nuanceur de pixels ps_2_0 et ultérieures, la couleur de sortie est émise à partir de oC0 - oC4.

Entrées de nuanceur et variables de nuanceur

Déclaration de variables de nuanceur

La déclaration de variable la plus simple inclut un type et un nom de variable, comme cette déclaration à virgule flottante :

float fVar;

Vous pouvez initialiser une variable dans la même instruction.

float fVar = 3.1f;

Un tableau de variables peut être déclaré,

int iVar[3];

ou déclaré et initialisé dans la même instruction.

int iVar[3] = {1,2,3};

Voici quelques déclarations qui illustrent un grand nombre des caractéristiques des variables HLSL (high-level shader language) :

float4 color;
uniform float4 position : POSITION; 
const float4 lightDirection = {0,0,1};

Les déclarations de données peuvent utiliser n’importe quel type valide, notamment :

Un nuanceur peut avoir des variables, des arguments et des fonctions de niveau supérieur.

// top-level variable
float globalShaderVariable; 

// top-level function
void function(
in float4 position: POSITION0 // top-level argument
              )
{
  float localShaderVariable; // local variable
  function2(...)
}

void function2()
{
  ...
}

Les variables de niveau supérieur sont déclarées en dehors de toutes les fonctions. Les arguments de niveau supérieur sont des paramètres d’une fonction de niveau supérieur. Une fonction de niveau supérieur est toute fonction appelée par l’application (par opposition à une fonction appelée par une autre fonction).

Entrées du nuanceur uniforme

Les nuanceurs de vertex et de pixels acceptent deux types de données d’entrée : variables et uniformes. L’entrée variable est les données propres à chaque exécution du nuanceur. Pour un nuanceur de vertex, les données variables (par exemple : position, normale, etc.) proviennent des flux de vertex. Les données uniformes (par exemple , couleur de matériau, transformation du monde, etc.) sont constantes pour plusieurs exécutions d’un nuanceur. Pour ceux qui connaissent les modèles de nuanceur d’assembly, les données uniformes sont spécifiées par des registres constants et des données variables par les registres v et t.

Les données uniformes peuvent être spécifiées par deux méthodes. La méthode la plus courante consiste à déclarer des variables globales et à les utiliser dans un nuanceur. Toute utilisation de variables globales dans un nuanceur entraîne l’ajout de cette variable à la liste des variables uniformes requises par ce nuanceur. La deuxième méthode consiste à marquer un paramètre d’entrée de la fonction de nuanceur de niveau supérieur comme uniforme. Ce marquage spécifie que la variable donnée doit être ajoutée à la liste des variables uniformes.

Les variables uniformes utilisées par un nuanceur sont communiquées à l’application via la table constante. La table constante est le nom de la table de symboles qui définit la façon dont les variables uniformes utilisées par un nuanceur s’intègrent dans les registres de constantes. Les paramètres de fonction uniforme apparaissent dans la table constante avec un signe dollar ($), contrairement aux variables globales. Le signe dollar est requis pour éviter les collisions de noms entre les entrées uniformes locales et les variables globales du même nom.

La table constante contient les emplacements de registre de constantes de toutes les variables uniformes utilisées par le nuanceur. La table inclut également les informations de type et la valeur par défaut, si elles sont spécifiées.

Entrées et sémantiques variables du nuanceur

Différents paramètres d’entrée (d’une fonction de nuanceur de niveau supérieur) doivent être marqués avec une mot clé sémantique ou uniforme indiquant que la valeur est constante pour l’exécution du nuanceur. Si une entrée de nuanceur de niveau supérieur n’est pas marquée avec une mot clé sémantique ou uniforme, la compilation du nuanceur échoue.

La sémantique d’entrée est un nom utilisé pour lier l’entrée donnée à une sortie de la partie précédente du pipeline graphique. Par exemple, la sémantique d’entrée POSITION0 est utilisée par les nuanceurs de vertex pour spécifier où les données de position de la mémoire tampon de vertex doivent être liées.

Les nuanceurs de pixels et de vertex ont des jeux de sémantiques d’entrée différents en raison des différentes parties du pipeline graphique qui alimentent chaque unité de nuanceur. La sémantique d’entrée du nuanceur de vertex décrit les informations par vertex (par exemple : position, normale, coordonnées de texture, couleur, tangente, binormal, etc.) à charger à partir d’une mémoire tampon de vertex dans un formulaire qui peut être consommé par le nuanceur de vertex. La sémantique d’entrée est directement mappée à l’utilisation de la déclaration de vertex et à l’index d’utilisation.

La sémantique d’entrée du nuanceur de pixels décrit les informations fournies par pixel par l’unité de rastérisation. Les données sont générées en interpolant entre les sorties du nuanceur de vertex pour chaque sommet de la primitive actuelle. La sémantique d’entrée du nuanceur de pixels de base lie la couleur de sortie et les informations de coordonnées de texture aux paramètres d’entrée.

La sémantique d’entrée peut être affectée à l’entrée du nuanceur par deux méthodes :

  • Ajout d’un signe deux-points et du nom sémantique à la déclaration de paramètre.
  • Définition d’une structure d’entrée avec la sémantique d’entrée affectée à chaque membre de la structure.

Les nuanceurs de vertex et de pixels fournissent des données de sortie à l’étape de pipeline graphique suivante. La sémantique de sortie est utilisée pour spécifier la façon dont les données générées par le nuanceur doivent être liées aux entrées de l’étape suivante. Par exemple, la sémantique de sortie d’un nuanceur de vertex est utilisée pour lier les sorties des interpolateurs dans le rastériseur afin de générer les données d’entrée pour le nuanceur de pixels. Les sorties du nuanceur de pixels sont les valeurs fournies à l’unité de fusion alpha pour chacune des cibles de rendu ou la valeur de profondeur écrite dans la mémoire tampon de profondeur.

La sémantique de sortie du nuanceur de vertex est utilisée pour lier le nuanceur au nuanceur de pixels et à l’étape de rastériseur. Un nuanceur de vertex consommé par le rastériseur et non exposé au nuanceur de pixels doit générer au minimum des données de position. Les nuanceurs de vertex qui génèrent des données de coordonnées de texture et de couleur fournissent ces données à un nuanceur de pixels après l’interpolation.

La sémantique de sortie du nuanceur de pixels lie les couleurs de sortie d’un nuanceur de pixels à la cible de rendu appropriée. La couleur de sortie du nuanceur de pixels est liée à la phase de fusion alpha, qui détermine la façon dont les cibles de rendu de destination sont modifiées. La sortie de profondeur du nuanceur de pixels peut être utilisée pour modifier les valeurs de profondeur de destination à l’emplacement raster actuel. La sortie de profondeur et plusieurs cibles de rendu sont uniquement prises en charge avec certains modèles de nuanceur.

La syntaxe de la sémantique de sortie est identique à la syntaxe de spécification de la sémantique d’entrée. La sémantique peut être spécifiée directement sur les paramètres déclarés en tant que paramètres « out » ou attribuée lors de la définition d’une structure retournée en tant que paramètre « out » ou la valeur de retour d’une fonction.

La sémantique identifie d’où proviennent les données. La sémantique est des identificateurs facultatifs qui identifient les entrées et sorties du nuanceur. La sémantique apparaît à l’un des trois emplacements suivants :

  • Après un membre de structure.
  • Après un argument dans la liste d’arguments d’entrée d’une fonction.
  • Après la liste d’arguments d’entrée de la fonction.

Cet exemple utilise une structure pour fournir une ou plusieurs entrées de nuanceur de vertex et une autre structure pour fournir une ou plusieurs sorties de nuanceur de vertex. Chacun des membres de la structure utilise une sémantique.

vector vClr;

struct VS_INPUT
{
    float4 vPosition : POSITION;
    float3 vNormal : NORMAL;
    float4 vBlendWeights : BLENDWEIGHT;
};

struct VS_OUTPUT
{
    float4  vPosition : POSITION;
    float4  vDiffuse : COLOR;

};

float4x4 mWld1;
float4x4 mWld2;
float4x4 mWld3;
float4x4 mWld4;

float Len;
float4 vLight;

float4x4 mTot;

VS_OUTPUT VS_Skinning_Example(const VS_INPUT v, uniform float len=100)
{
    VS_OUTPUT out;

    // Skin position (to world space)
    float3 vPosition = 
        mul(v.vPosition, (float4x3) mWld1) * v.vBlendWeights.x +
        mul(v.vPosition, (float4x3) mWld2) * v.vBlendWeights.y +
        mul(v.vPosition, (float4x3) mWld3) * v.vBlendWeights.z +
        mul(v.vPosition, (float4x3) mWld4) * v.vBlendWeights.w;
    // Skin normal (to world space)
    float3 vNormal =
        mul(v.vNormal, (float3x3) mWld1) * v.vBlendWeights.x + 
        mul(v.vNormal, (float3x3) mWld2) * v.vBlendWeights.y + 
        mul(v.vNormal, (float3x3) mWld3) * v.vBlendWeights.z + 
        mul(v.vNormal, (float3x3) mWld4) * v.vBlendWeights.w;
    
    // Output stuff
    out.vPosition    = mul(float4(vPosition + vNormal * Len, 1), mTot);
    out.vDiffuse  = dot(vLight,vNormal);

    return out;
}

La structure d’entrée identifie les données de la mémoire tampon de vertex qui fournira les entrées du nuanceur. Ce nuanceur mappe les données des éléments position, normal et blendweight de la mémoire tampon de vertex dans des registres de nuanceur de vertex. Le type de données d’entrée n’a pas besoin de correspondre exactement au type de données de déclaration de vertex. Si elles ne correspondent pas exactement, les données de vertex sont automatiquement converties en type de données hlsL lorsqu’elles sont écrites dans les registres du nuanceur. Par instance, si les données normales étaient définies comme étant de type UINT par l’application, elles seraient converties en float3 lors de la lecture par le nuanceur.

Si les données du flux de vertex contiennent moins de composants que le type de données du nuanceur correspondant, les composants manquants sont initialisés sur 0 (à l’exception de w, qui est initialisé sur 1).

La sémantique d’entrée est similaire aux valeurs du D3DDECLUSAGE.

La structure de sortie identifie les paramètres de sortie du nuanceur de vertex de position et de couleur. Ces sorties seront utilisées par le pipeline pour la rastérisation triangle (dans le traitement primitif). La sortie marquée comme données de position indique la position d’un sommet dans un espace homogène. Au minimum, un nuanceur de vertex doit générer des données de position. La position de l’espace d’écran est calculée une fois le nuanceur de vertex terminé en divisant la coordonnée (x, y, z) par w. Dans l’espace d’écran, -1 et 1 sont les valeurs x et y minimales et maximales des limites de la fenêtre d’affichage, tandis que z est utilisé pour le test de la mémoire tampon z.

La sémantique de sortie est également similaire aux valeurs dans D3DDECLUSAGE. En général, une structure de sortie pour un nuanceur de vertex peut également être utilisée comme structure d’entrée pour un nuanceur de pixels, à condition que le nuanceur de pixels ne lise pas à partir d’une variable marquée avec la sémantique de position, de taille de point ou de brouillard. Ces sémantiques sont associées à des valeurs scalaires par sommet qui ne sont pas utilisées par un nuanceur de pixels. Si ces valeurs sont nécessaires pour le nuanceur de pixels, elles peuvent être copiées dans une autre variable de sortie qui utilise une sémantique de nuanceur de pixels.

Les variables globales sont affectées automatiquement aux registres par le compilateur. Les variables globales sont également appelées paramètres uniformes, car le contenu de la variable est le même pour tous les pixels traités chaque fois que le nuanceur est appelé. Les registres sont contenus dans la table constante, qui peut être lue à l’aide de l’interface ID3DXConstantTable .

La sémantique d’entrée pour les nuanceurs de pixels mappe les valeurs dans des registres matériels spécifiques pour le transport entre les nuanceurs de vertex et les nuanceurs de pixels. Chaque type de registre a des propriétés spécifiques. Étant donné qu’il n’existe actuellement que deux sémantiques pour les coordonnées de couleur et de texture, il est courant que la plupart des données soient marquées en tant que coordonnées de texture, même si elles ne le sont pas.

Notez que la structure de sortie du nuanceur de vertex a utilisé une entrée avec des données de position, qui n’est pas utilisée par le nuanceur de pixels. HLSL autorise les données de sortie valides d’un nuanceur de vertex qui ne sont pas des données d’entrée valides pour un nuanceur de pixels, à condition qu’elles ne soient pas référencées dans le nuanceur de pixels.

Les arguments d’entrée peuvent également être des tableaux. La sémantique est automatiquement incrémentée par le compilateur pour chaque élément du tableau. Pour instance, considérez la déclaration explicite suivante :

struct VS_OUTPUT
{
    float4 Position   : POSITION;
    float3 Diffuse    : COLOR0;
    float3 Specular   : COLOR1;               
    float3 HalfVector : TEXCOORD3;
    float3 Fresnel    : TEXCOORD2;               
    float3 Reflection : TEXCOORD0;               
    float3 NoiseCoord : TEXCOORD1;               
};

float4 Sparkle(VS_OUTPUT In) : COLOR

La déclaration explicite donnée ci-dessus équivaut à la déclaration suivante dont la sémantique sera automatiquement incrémentée par le compilateur :

float4 Sparkle(float4 Position : POSITION,
                 float3 Col[2] : COLOR0,
                 float3 Tex[4] : TEXCOORD0) : COLOR0
{
   // shader statements
   ...

Tout comme la sémantique d’entrée, la sémantique de sortie identifie l’utilisation des données pour les données de sortie du nuanceur de pixels. De nombreux nuanceurs de pixels écrivent dans une seule couleur de sortie. Les nuanceurs de pixels peuvent également écrire une valeur de profondeur dans une ou plusieurs cibles de rendu en même temps (jusqu’à quatre). Comme les nuanceurs de vertex, les nuanceurs de pixels utilisent une structure pour renvoyer plusieurs sorties. Ce nuanceur écrit 0 dans les composants de couleur, ainsi que dans le composant de profondeur.

struct PS_OUTPUT
{
    float4 Color[4] : COLOR0;
    float  Depth  : DEPTH;
};

PS_OUTPUT main(void)
{
    PS_OUTPUT out;

   // Shader statements
   ...

  // Write up to four pixel shader output colors
  out.Color[0] =  ...
  out.Color[1] =  ...
  out.Color[2] =  ...
  out.Color[3] =  ...

  // Write pixel depth 
  out.Depth =  ...

    return out;
}

Les couleurs de sortie du nuanceur de pixels doivent être de type float4. Lors de l’écriture de plusieurs couleurs, toutes les couleurs de sortie doivent être utilisées de manière contiguë. En d’autres termes, COLOR1 ne peut pas être une sortie, sauf si COLOR0 a déjà été écrit. La sortie de profondeur du nuanceur de pixels doit être de type float1.

Échantillonneurs et objets de texture

Un échantillonneur contient l’état de l’échantillonneur. L’état de l’échantillonneur spécifie la texture à échantillonner et contrôle le filtrage effectué pendant l’échantillonnage. Trois éléments sont nécessaires pour échantillonner une texture :

  • Texture
  • Un échantillonneur (avec l’état de l’échantillonneur)
  • Instruction d’échantillonnage

Les échantillonneurs peuvent être initialisés avec des textures et l’état de l’échantillonneur, comme illustré ici :

sampler s = sampler_state 
{ 
  texture = NULL; 
  mipfilter = LINEAR; 
};

Voici un exemple de code permettant d’échantillonner une texture 2D :

texture tex0;
sampler2D s_2D;

float2 sample_2D(float2 tex : TEXCOORD0) : COLOR
{
  return tex2D(s_2D, tex);
}

La texture est déclarée avec une variable de texture tex0.

Dans cet exemple, une variable d’échantillonnage nommée s_2D est déclarée. L’échantillonneur contient l’état de l’échantillonneur à l’intérieur des accolades. Cela inclut la texture qui sera échantillonnée et, éventuellement, l’état du filtre (c’est-à-dire les modes d’habillage, les modes de filtre, etc.). Si l’état de l’échantillonneur est omis, un état d’échantillonneur par défaut est appliqué en spécifiant un filtrage linéaire et un mode d’enveloppement pour les coordonnées de texture. La fonction sampler prend une coordonnée de texture à virgule flottante à deux composants et retourne une couleur à deux composants. Il est représenté avec le type de retour float2 et représente les données dans les composants rouge et vert.

Quatre types d’échantillonneurs sont définis (voir Mots clés) et les recherches de texture sont effectuées par les fonctions intrinsèques : tex1D(s, t) (DirectX HLSL),tex2D(s, t) (DirectX HLSL),tex3D(s, t) (DirectX HLSL),texCUBE(s, t) (DirectX HLSL). Voici un exemple d’échantillonnage 3D :

texture tex0;
sampler3D s_3D;

float3 sample_3D(float3 tex : TEXCOORD0) : COLOR
{
  return tex3D(s_3D, tex);
}

Cette déclaration d’échantillonneur utilise l’état d’échantillonneur par défaut pour les paramètres de filtre et le mode d’adresse.

Voici l’exemple d’échantillonnage de cube correspondant :

texture tex0;
samplerCUBE s_CUBE;

float3 sample_CUBE(float3 tex : TEXCOORD0) : COLOR
{
  return texCUBE(s_CUBE, tex);
}

Enfin, voici l’exemple d’échantillonnage 1D :

texture tex0;
sampler1D s_1D;

float sample_1D(float tex : TEXCOORD0) : COLOR
{
  return tex1D(s_1D, tex);
}

Étant donné que le runtime ne prend pas en charge les textures 1D, le compilateur utilise une texture 2D en sachant que la coordonnée y n’est pas importante. Étant donné que tex1D(s, t) (DirectX HLSL) est implémenté en tant que recherche de texture 2D, le compilateur est libre de choisir le composant y de manière efficace. Dans certains scénarios rares, le compilateur ne peut pas choisir un composant y efficace, auquel cas il émet un avertissement.

texture tex0;
sampler s_1D_float;

float4 main(float texCoords : TEXCOORD) : COLOR
{
    return tex1D(s_1D_float, texCoords);
}

Cet exemple particulier est inefficace, car le compilateur doit déplacer la coordonnée d’entrée dans un autre registre (car une recherche 1D est implémentée en tant que recherche 2D et la coordonnée de texture est déclarée en tant que float1). Si le code est réécrit à l’aide d’une entrée float2 au lieu d’un float1, le compilateur peut utiliser la coordonnée de texture d’entrée, car il sait que y est initialisé sur quelque chose.

texture tex0;
sampler s_1D_float2;

float4 main(float2 texCoords : TEXCOORD) : COLOR
{
    return tex1D(s_1D_float2, texCoords);
}

Toutes les recherches de texture peuvent être ajoutées avec « bias » ou « proj » (autrement dit, tex2Dbias (DirectX HLSL),texCUBEproj (DirectX HLSL)). Avec le suffixe « proj », la coordonnée de texture est divisée par le composant w. Avec « bias », le niveau mip est déplacé par le composant w. Ainsi, toutes les recherches de texture avec un suffixe prennent toujours une entrée float4. tex1D(s, t) (DirectX HLSL) et tex2D(s, t) (DirectX HLSL) ignorent respectivement les composants yz- et z.

Les échantillonneurs peuvent également être utilisés dans un tableau, bien qu’aucun back-end ne prenne actuellement en charge l’accès dynamique au tableau des échantillonneurs. Par conséquent, les éléments suivants sont valides, car ils peuvent être résolus au moment de la compilation :

tex2D(s[0],tex)

Toutefois, cet exemple n’est pas valide.

tex2D(s[a],tex)

L’accès dynamique des échantillonneurs est principalement utile pour écrire des programmes avec des boucles littérales. Le code suivant illustre l’accès au tableau de l’échantillonneur :

sampler sm[4];

float4 main(float4 tex[4] : TEXCOORD) : COLOR
{
    float4 retColor = 1;

    for(int i = 0; i < 4;i++)
    {
        retColor *= tex2D(sm[i],tex[i]);
    }

    return retColor;
}

Notes

L’utilisation du runtime de débogage Microsoft Direct3D peut vous aider à détecter les incompatibilités entre le nombre de composants dans une texture et un échantillonneur.

 

Écriture de fonctions

Les fonctions décomposent les tâches volumineuses en plus petites. Les petites tâches sont plus faciles à déboguer et peuvent être réutilisées, une fois éprouvées. Les fonctions peuvent être utilisées pour masquer les détails d’autres fonctions, ce qui facilite le suivi d’un programme composé de fonctions.

Les fonctions HLSL sont similaires aux fonctions C à plusieurs égards : elles contiennent une définition et un corps de fonction et déclarent des types de retour et des listes d’arguments. Comme pour les fonctions C, la validation HLSL vérifie les types de type sur les arguments, les types d’arguments et la valeur de retour pendant la compilation du nuanceur.

Contrairement aux fonctions C, les fonctions de point d’entrée HLSL utilisent la sémantique pour lier les arguments de fonction aux entrées et sorties du nuanceur (fonctions HLSL appelées ignorer la sémantique en interne). Cela facilite la liaison des données de mémoire tampon à un nuanceur et la liaison des sorties du nuanceur aux entrées du nuanceur.

Une fonction contient une déclaration et un corps, et la déclaration doit précéder le corps.

float4 VertexShader_Tutorial_1(float4 inPos : POSITION ) : POSITION
{
    return mul(inPos, WorldViewProj );
};

La déclaration de fonction inclut tout ce qui se trouve devant les accolades :

float4 VertexShader_Tutorial_1(float4 inPos : POSITION ) : POSITION

Une déclaration de fonction contient :

  • Type de retour
  • Nom d’une fonction
  • Liste d’arguments (facultatif)
  • Sémantique de sortie (facultatif)
  • Annotation (facultatif)

Le type de retour peut être l’un des types de données de base HLSL tels qu’un float4 :

float4 VertexShader_Tutorial_1(float4 inPos : POSITION ) : POSITION
{
   ...
}

Le type de retour peut être une structure qui a déjà été définie :

struct VS_OUTPUT
{
    float4  vPosition        : POSITION;
    float4  vDiffuse         : COLOR;
}; 

VS_OUTPUT VertexShader_Tutorial_1(float4 inPos : POSITION )
{
   ...
}

Si la fonction ne retourne pas de valeur, void peut être utilisé comme type de retour.

void VertexShader_Tutorial_1(float4 inPos : POSITION )
{
   ...
}

Le type de retour apparaît toujours en premier dans une déclaration de fonction.

float4 VertexShader_Tutorial_1(float4 inPos : POSITION ) : POSITION

Une liste d’arguments déclare les arguments d’entrée à une fonction. Il peut également déclarer des valeurs qui seront retournées. Certains arguments sont à la fois des arguments d’entrée et de sortie. Voici un exemple de nuanceur qui prend quatre arguments d’entrée.

float4 Light(float3 LightDir : TEXCOORD1, 
             uniform float4 LightColor,  
             float2 texcrd : TEXCOORD0, 
             uniform sampler samp) : COLOR 
{
    float3 Normal = tex2D(samp,texcrd);

    return dot((Normal*2 - 1), LightDir)*LightColor;
}

Cette fonction retourne une couleur finale, c’est-à-dire un mélange d’un échantillon de texture et de la couleur claire. La fonction prend quatre entrées. Deux entrées ont une sémantique : LightDir a la sémantique TEXCOORD1 et texcrd a la sémantique TEXCOORD0 . La sémantique signifie que les données de ces variables proviennent de la mémoire tampon de vertex. Même si la variable LightDir a une sémantique TEXCOORD1 , le paramètre n’est probablement pas une coordonnée de texture. Le type sémantique TEXCOORDn est souvent utilisé pour fournir une sémantique pour un type qui n’est pas prédéfini (il n’existe aucune sémantique d’entrée de nuanceur de vertex pour une direction de lumière).

Les deux autres entrées LightColor et samp sont étiquetées avec l’uniforme mot clé. Il s’agit de constantes uniformes qui ne changent pas entre les appels de dessin. Les valeurs de ces paramètres proviennent de variables globales du nuanceur.

Les arguments peuvent être étiquetés en tant qu’entrées avec le in mot clé et les arguments de sortie avec le mot clé out. Les arguments ne peuvent pas être passés par référence ; Toutefois, un argument peut être à la fois une entrée et une sortie s’il est déclaré avec le mot clé inout. Les arguments passés à une fonction qui sont marqués avec l’inout mot clé sont considérés comme des copies de l’original jusqu’à ce que la fonction retourne, et ils sont copiés à nouveau. Voici un exemple utilisant inout :

void Increment_ByVal(inout float A, inout float B) 
{ 
    A++; B++;
}

Cette fonction incrémente les valeurs dans A et B et les retourne.

Le corps de la fonction est l’ensemble du code après la déclaration de fonction.

float4 VertexShader_Tutorial_1(float4 inPos : POSITION ) : POSITION
{
    return mul(inPos, WorldViewProj );
};

Le corps se compose d’instructions qui sont entourées d’accolades. Le corps de la fonction implémente toutes les fonctionnalités à l’aide de variables, de littéraux, d’expressions et d’instructions.

Le corps du nuanceur effectue deux opérations : il effectue une multiplication de matrice et retourne un résultat float4. La multiplication de matrice est effectuée avec la fonction mul (DirectX HLSL), qui effectue une multiplication de matrice 4x4. Mul (DirectX HLSL) est appelée fonction intrinsèque, car elle est déjà intégrée à la bibliothèque de fonctions HLSL. Les fonctions intrinsèques seront traitées plus en détail dans la section suivante.

La multiplication de matrice combine un vecteur d’entrée Pos et une matrice composite WorldViewProj. Le résultat est les données de position transformées en espace d’écran. Il s’agit du traitement minimal du nuanceur de vertex que nous pouvons effectuer. Si nous utilisions le pipeline de fonction fixe au lieu d’un nuanceur de vertex, les données de vertex pourraient être dessinées après cette transformation.

La dernière instruction dans le corps d’une fonction est une instruction return. Tout comme C, cette instruction retourne le contrôle de la fonction à l’instruction qui a appelé la fonction.

Les types de retour de fonction peuvent être tous les types de données simples définis dans HLSL, y compris bool, int half, float et double. Les types de retour peuvent être l’un des types de données complexes tels que les vecteurs et les matrices. Les types HLSL qui font référence à des objets ne peuvent pas être utilisés comme types de retour. Cela inclut pixelshader, vertexshader, texture et sampler.

Voici un exemple de fonction qui utilise une structure pour un type de retour.

float4x4 WorldViewProj : WORLDVIEWPROJ;

struct VS_OUTPUT
{
    float4 Pos  : POSITION;
};

VS_OUTPUT VS_HLL_Example(float4 inPos : POSITION )
{
    VS_OUTPUT Out;

    Out.Pos = mul(inPos,  WorldViewProj );

    return Out;
};

Le type de retour float4 a été remplacé par la structure VS_OUTPUT, qui contient désormais un seul membre float4.

Une instruction return signale la fin d’une fonction. Il s’agit de l’instruction return la plus simple. Elle retourne le contrôle de la fonction au programme appelant. Elle ne retourne aucune valeur.

void main()
{
    return ;
}

Une instruction return peut retourner une ou plusieurs valeurs. Cet exemple retourne une valeur littérale :

float main( float input : COLOR0) : COLOR0
{
    return 0;
}

Cet exemple retourne le résultat scalaire d’une expression :

return  light.enabled;

Cet exemple retourne un float4 construit à partir d’une variable locale et d’un littéral :

return  float4(color.rgb, 1) ;

Cet exemple retourne un float4 construit à partir du résultat retourné à partir d’une fonction intrinsèque et de quelques valeurs littérales :

float4 func(float2 a: POSITION): COLOR
{
    return float4(sin(length(a) * 100.0) * 0.5 + 0.5, sin(a.y * 50.0), 0, 1);
}

Cet exemple retourne une structure qui contient un ou plusieurs membres :

float4x4 WorldViewProj;

struct VS_OUTPUT
{
    float4 Pos  : POSITION;
};

VS_OUTPUT VertexShader_Tutorial_1(float4 inPos : POSITION )
{
    VS_OUTPUT out;
    out.Pos = mul(inPos, WorldViewProj );
    return out;
};

Contrôle de flux

La plupart du matériel actuel de nuanceur de vertex et de pixels est conçu pour exécuter un nuanceur ligne par ligne, en exécutant chaque instruction une fois. HLSL prend en charge le contrôle de flux, qui inclut la création de branches statiques, les instructions prédicées, le bouclage statique, le branchement dynamique et le bouclage dynamique.

Auparavant, l’utilisation d’une instruction if a abouti à un code de nuanceur en langage assembly qui implémente à la fois le côté if et le côté else du flux de code. Voici un exemple du code HLSL qui a été compilé pour vs_1_1 :

if (Value > 0)
    oPos = Value1; 
else
    oPos = Value2; 

Et voici le code d’assembly résultant :

// Calculate linear interpolation value in r0.w
mov r1.w, c2.x               
slt r0.w, c3.x, r1.w         
// Linear interpolation between value1 and value2
mov r7, -c1                      
add r2, r7, c0                   
mad oPos, r0.w, r2, c1  

Certains matériels autorisent le bouclage statique ou dynamique, mais la plupart nécessitent une exécution linéaire. Sur les modèles qui ne prennent pas en charge le bouclage, toutes les boucles doivent être déployées. Par exemple, l’exemple DepthOfField utilise des boucles non roulées, même pour les nuanceurs ps_1_1.

HLSL prend désormais en charge chacun de ces types de contrôle de flux :

  • branchement statique
  • instructions prédicées
  • bouclage statique
  • branchement dynamique
  • bouclage dynamique

La création de branches statiques permet d’activer ou de désactiver des blocs de code de nuanceur en fonction d’une constante de nuanceur booléen. Il s’agit d’une méthode pratique pour activer ou désactiver les chemins de code en fonction du type d’objet en cours de rendu. Entre les appels de dessin, vous pouvez choisir les fonctionnalités que vous souhaitez prendre en charge avec le nuanceur actuel, puis définir les indicateurs booléens requis pour obtenir ce comportement. Toutes les instructions désactivées par une constante booléenne sont ignorées pendant l’exécution du nuanceur.

La prise en charge de la branchement la plus familière est la branche dynamique. Avec la branchement dynamique, la condition de comparaison réside dans une variable, ce qui signifie que la comparaison est effectuée pour chaque sommet ou chaque pixel au moment de l’exécution (par opposition à la comparaison qui se produit au moment de la compilation ou entre deux appels de dessin). La performance atteinte est le coût de la branche plus le coût des instructions du côté de la branche prise. La création de branches dynamiques est implémentée dans le modèle de nuanceur 3 ou ultérieur. L’optimisation des nuanceurs qui fonctionnent avec ces modèles est similaire à l’optimisation du code qui s’exécute sur un processeur.

Guide de programmation pour HLSL