Partager via


Utilisation des nuanceurs et des ressources de nuanceur

Il est temps d’apprendre à utiliser des nuanceurs et des ressources de nuanceur pour développer votre jeu Microsoft DirectX pour Windows 8. Nous avons vu comment configurer l’appareil graphique et les ressources, et vous avez peut-être même commencé à modifier son pipeline. Examinons donc les nuanceurs de pixels et de vertex.

Si vous n’êtes pas familiarisé avec les langages de nuanceur, une explication rapide s’impose. Les nuanceurs sont de petits programmes de bas niveau compilés et exécutés à des étapes spécifiques dans le pipeline graphique. Ils sont spécialisés dans les opérations mathématiques à virgule flottante très rapides. Les programmes de nuanceur les plus courants sont les suivants :

  • Nuanceur de vertex : exécuté pour chaque vertex d’une scène. Ce nuanceur fonctionne sur les éléments de mémoire tampon de vertex fournis par l’application appelante, et génère un vecteur de position à 4 composants qui sera rastérisé en une position de pixel.
  • Nuanceur de pixels : exécuté pour chaque pixel dans une cible de rendu. Ce nuanceur reçoit des coordonnées rastérisées des étapes précédentes du nuanceur (dans les pipelines les plus simples, il s’agit du nuanceur de vertex) et retourne une couleur (ou une autre valeur de 4 composants) pour cette position de pixel, qui est ensuite écrite dans une cible de rendu.

Cet exemple inclut des nuanceurs de vertex et de pixels de base qui dessinent uniquement la géométrie et les nuanceurs plus complexes qui ajoutent des calculs d’éclairage de base.

Les programmes de nuanceur sont écrits en HLSL (Microsoft High Level Shader Language). La syntaxe HLSL ressemble beaucoup au langage C, mais sans les pointeurs. Les programmes de nuanceur doivent être très compacts et efficaces. Si la compilation de votre nuanceur génère trop d’instructions, son exécution est impossible et une erreur est retournée. (Notez que le nombre exact d’instructions autorisées dépend du Niveau de fonctionnalité Direct3D.)

Dans Direct3D, les nuanceurs ne sont pas compilés au moment de l’exécution. Ils sont compilés lorsque le reste du programme est compilé. Lorsque vous compilez votre application avec Microsoft Visual Studio 2013, les fichiers HLSL sont compilés en fichiers CSO (.cso) que votre application doit charger et placer en mémoire GPU avant de dessiner. Veillez à inclure ces fichiers CSO avec votre application lorsque vous créez son package ; il s’agit de ressources comme les maillages et les textures.

Comprendre la sémantique HLSL

Il est important de prendre un moment pour discuter de la sémantique HLSL avant de continuer, car il s’agit souvent d’un point de confusion pour les nouveaux développeurs Direct3D. HLSL contient des chaînes qui identifient une valeur transmise entre l’application et un programme de nuanceur. Bien qu’une variété de chaînes soient possibles, la meilleure pratique consiste à utiliser une chaîne comme POSITION ou COLOR qui indique l’utilisation. Vous attribuez ces sémantiques lorsque vous construisez une mise en mémoire tampon constante ou une disposition d’entrée. Vous pouvez également ajouter un nombre compris entre 0 et 7 à la sémantique afin d’utiliser des registres distincts pour des valeurs similaires. Par exemple : COLOR0, COLOR1, COLOR2...

Les sémantiques identifiées par le préfixe « SV_ » sont des sémantiques de valeur système modifiées par votre programme de nuanceur. Votre jeu (exécuté sur le processeur) ne peut pas les modifier. En règle générale, ces sémantiques contiennent des valeurs qui sont des entrées ou des sorties d’une autre étape de nuanceur dans le pipeline graphique, ou qui sont générées entièrement par le GPU.

Par ailleurs, les sémantiques SV_ se comportent différemment quand elles sont utilisées pour spécifier des entrées ou des sorties d’étape de nuanceur. Par exemple, SV_POSITION (sortie) contient les données de vertex transformées pendant l’étape du nuanceur de vertex, et SV_POSITION (entrée) contient les valeurs de position de pixels interpolées par le GPU pendant l’étape de rastérisation.

Voici quelques sémantiques HLSL courantes :

  • POSITION(n) pour les données de mémoire tampon de vertex. SV_POSITION fournit une position de pixel au nuanceur de pixels et ne peut pas être écrit par votre jeu.
  • NORMAL(n) pour les données normales fournies par la mémoire tampon de vertex.
  • TEXCOORD(n) pour les données de coordonnées UV de texture fournies à un nuanceur.
  • COLOR(n) pour les données de couleur RVBA fournies à un nuanceur. Notez qu’il est traité de façon identique pour coordonner les données, y compris l’interpolation de la valeur pendant la rastérisation. La sémantique vous aide simplement à identifier qu’il s’agit de données de couleur.
  • SV_Target[n] pour écrire à partir d’un nuanceur de pixels dans une texture cible ou une autre mémoire tampon de pixels.

Nous allons voir quelques exemples de sémantique HLSL lors du passage en revue de l’exemple.

Lecture à partir des mémoires tampons constantes

Tout nuanceur peut lire à partir d’une mémoire tampon constante si cette mémoire tampon est attachée à son étape en tant que ressource. Dans cet exemple, seul le nuanceur de vertex est attribué à une mémoire tampon constante.

La mémoire tampon constante est déclarée à deux emplacements : dans le code C++ et dans les fichiers HLSL correspondants qui y accèdent.

Voici comment la structure de mémoire tampon constante est déclarée dans le code C++.

typedef struct _constantBufferStruct {
    DirectX::XMFLOAT4X4 world;
    DirectX::XMFLOAT4X4 view;
    DirectX::XMFLOAT4X4 projection;
} ConstantBufferStruct;

Lors de la déclaration de la structure de la mémoire tampon constante dans votre code C++, vérifiez que toutes les données sont correctement alignées sur les limites de 16 octets. La méthode la plus simple consiste à utiliser des types DirectXMath, tels que XMFLOAT4 ou XMFLOAT4X4, comme indiqué dans l’exemple de code. Vous pouvez également éviter les mémoires tampons mal alignées en déclarant une assertion statique :

// Assert that the constant buffer remains 16-byte aligned.
static_assert((sizeof(ConstantBufferStruct) % 16) == 0, "Constant Buffer size must be 16-byte aligned");

Cette ligne de code génère une erreur au moment de la compilation si ConstantBufferStruct n’est pas aligné sur 16 octets. Pour plus d’informations sur l’alignement et l’emballage des mémoires tampons constantes, consultez Règles d’empaquetage des variables constantes.

À présent, voici comment la mémoire tampon constante est déclarée dans le code HLSL du nuanceur de vertex.

cbuffer ModelViewProjectionConstantBuffer : register(b0)
{
    matrix mWorld;      // world matrix for object
    matrix View;        // view matrix
    matrix Projection;  // projection matrix
};

Toutes les mémoires tampons (de constante, de texture, d’échantillonneur ou autre) doivent présenter un registre défini pour que le GPU puisse y accéder. Chaque étape du nuanceur permet jusqu’à 15 mémoires tampons constantes, et chaque mémoire tampon peut contenir jusqu’à 4 096 variables constantes. La syntaxe de déclaration d’utilisation de registre est la suivante :

  • b*#* : registre d’une mémoire tampon constante (cbuffer).
  • t*#* : registre d’une mémoire tampon de texture (tbuffer).
  • s*#* : registre d’un échantillonneur. (Un échantillonneur définit le comportement de recherche pour les texels dans la ressource de texture.)

Par exemple, le HLSL d’un nuanceur de pixels peut prendre une texture et un échantillonneur comme entrée avec une déclaration semblable à celle-ci.

Texture2D simpleTexture : register(t0);
SamplerState simpleSampler : register(s0);

Il vous appartient d’attribuer des mémoires tampons constantes aux registres : lorsque vous configurez le pipeline, vous attachez une mémoire tampon constante au même emplacement auquel vous l’avez attribué dans le fichier HLSL. Par exemple, dans la rubrique précédente, l’appel à VSSetConstantBuffers indique « 0 » pour le premier paramètre. Cela indique à Direct3D d’attacher la ressource de mémoire tampon constante pour inscrire 0, qui correspond à l’attribution de la mémoire tampon à register(b0) dans le fichier HLSL.

Lecture à partir des mémoires tampons de vertex

La mémoire tampon de vertex fournit les données de triangle des objets de scène aux nuanceurs de vertex. Comme avec la mémoire tampon constante, la structure de mémoire tampon de vertex est déclarée dans le code C++, à l’aide de règles d’empaquetage similaires.

typedef struct _vertexPositionColor
{
    DirectX::XMFLOAT3 pos;
    DirectX::XMFLOAT3 color;
} VertexPositionColor;

Il n’existe aucun format standard pour les données de vertex dans Direct3D 11. Au lieu de cela, nous définissons notre propre disposition de données de vertex à l’aide d’un descripteur. Les champs de données sont définis à l’aide d’un tableau de structures D3D11_INPUT_ELEMENT_DESC. Ici, nous affichons une disposition d’entrée simple qui décrit le même format de vertex que la structure précédente :

D3D11_INPUT_ELEMENT_DESC iaDesc [] =
{
    { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT,
    0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },

    { "COLOR", 0, DXGI_FORMAT_R32G32B32_FLOAT,
    0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};

hr = device->CreateInputLayout(
    iaDesc,
    ARRAYSIZE(iaDesc),
    bytes,
    bytesRead,
    &m_pInputLayout
    );

Si vous ajoutez des données au format de vertex lors de la modification de l’exemple de code, veillez également à mettre à jour la disposition d’entrée, ou le nuanceur ne pourra pas l’interpréter. Vous pouvez modifier la disposition de vertex comme suit :

typedef struct _vertexPositionColorTangent
{
    DirectX::XMFLOAT3 pos;
    DirectX::XMFLOAT3 normal;
    DirectX::XMFLOAT3 tangent;
} VertexPositionColorTangent;

Dans ce cas, vous devez modifier la définition de disposition d’entrée comme suit.

D3D11_INPUT_ELEMENT_DESC iaDescExtended[] =
{
    { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT,
    0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },

    { "NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT,
    0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },

    { "TANGENT", 0, DXGI_FORMAT_R32G32B32_FLOAT,
    0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};

hr = device->CreateInputLayout(
    iaDesc,
    ARRAYSIZE(iaDesc),
    bytes,
    bytesRead,
    &m_pInputLayoutExtended
    );

Chacune des définitions d’éléments de disposition d’entrée est précédée d’une chaîne, telle que « POSITION » ou « NORMAL », c’est-à-dire la sémantique que nous avons abordée précédemment dans cette rubrique. Il s’agit d’un handle qui aide le GPU à identifier cet élément lors du traitement du vertex. Choisissez des noms communs et explicites pour vos éléments de vertex.

Tout comme avec la mémoire tampon constante, le nuanceur de vertex a une définition de mémoire tampon correspondante pour les éléments de vertex entrants. (C’est pourquoi nous avons fourni une référence à la ressource de nuanceur de vertex lors de la création de la disposition d’entrée. Direct3D valide la disposition de données par vertex avec la structure d’entrée du nuanceur.) Notez que la sémantique correspond entre la définition de disposition d’entrée et cette déclaration de mémoire tampon HLSL. Toutefois, un « 0 » est ajouté à COLOR. Il n’est pas nécessaire d’ajouter le 0 si vous n’avez qu’un seul élément COLOR déclaré dans la disposition, mais il est recommandé de l’ajouter au cas où vous choisissez d’ajouter d’autres éléments de couleur à l’avenir.

struct VS_INPUT
{
    float3 vPos   : POSITION;
    float3 vColor : COLOR0;
};

Transmission de données entre nuanceurs

Les nuanceurs acceptent les types d’entrée et retournent les types de sortie de leurs fonctions principales lors de l’exécution. Pour le nuanceur de vertex défini dans la section précédente, le type d’entrée était la structure VS_INPUT et nous avons défini une disposition d’entrée correspondante et une structure C++. Un tableau de cette structure est utilisé pour créer une mémoire tampon de vertex dans la méthode CreateCube.

Le nuanceur de vertex retourne une structure PS_INPUT, qui doit contenir au minimum la position de vertex finale de 4 composants (float4). Cette valeur de position doit avoir la sémantique de valeur système, SV_POSITION, déclarée pour que le GPU dispose des données dont il a besoin pour effectuer l’étape de dessin suivante. Notez qu’il n’existe pas de correspondance de 1 à 1 entre la sortie du nuanceur de vertex et l’entrée du nuanceur de pixels. Le nuanceur de vertex retourne une structure pour chaque vertex donné, mais le nuanceur de pixels s’exécute une fois pour chaque pixel. Cela est dû au fait que les données par vertex passent d’abord par l’étape de rastérisation. Cette étape détermine quels pixels « couvrent » la géométrie que vous dessinez, calcule les données interpolées par vertex pour chaque pixel, puis appelle le nuanceur de pixels une fois pour chacun de ces pixels. L’interpolation est le comportement par défaut lors de la rastérisation des valeurs de sortie, et est essentielle en particulier pour le traitement correct des données vectorielles de sortie (vecteurs légers, normales par vertex et tangentes, etc.).

struct PS_INPUT
{
    float4 Position : SV_POSITION;  // interpolated vertex position (system value)
    float4 Color    : COLOR0;       // interpolated diffuse color
};

Passage en revue du nuanceur de vertex

L’exemple de nuanceur de vertex est très simple : prenez un vertex (position et couleur), transformez la position de coordonnées de modèle en coordonnées projetées de perspective et retournez-la (ainsi que la couleur) au rastériseur. Notez que la valeur de couleur est interpolée avec les données de position, en fournissant une valeur différente pour chaque pixel, même si le nuanceur de vertex n’a effectué aucun calcul sur la valeur de couleur.

VS_OUTPUT main(VS_INPUT input) // main is the default function name
{
    VS_OUTPUT Output;

    float4 pos = float4(input.vPos, 1.0f);

    // Transform the position from object space to homogeneous projection space
    pos = mul(pos, mWorld);
    pos = mul(pos, View);
    pos = mul(pos, Projection);
    Output.Position = pos;

    // Just pass through the color data
    Output.Color = float4(input.vColor, 1.0f);

    return Output;
}

Un nuanceur de vertex plus complexe, tel qu’un nuanceur qui configure les vertex d’un objet pour l’ombrage Phong, peut ressembler plus à ceci. Dans ce cas, nous profitons du fait que les vecteurs et les normales sont interpolés pour une surface approximativement lisse.

// A constant buffer that stores the three basic column-major matrices for composing geometry.
cbuffer ModelViewProjectionConstantBuffer : register(b0)
{
    matrix model;
    matrix view;
    matrix projection;
};

cbuffer LightConstantBuffer : register(b1)
{
    float4 lightPos;
};

struct VertexShaderInput
{
    float3 pos : POSITION;
    float3 normal : NORMAL;
};

// Per-pixel color data passed through the pixel shader.

struct PixelShaderInput
{
    float4 position : SV_POSITION; 
    float3 outVec : POSITION0;
    float3 outNormal : NORMAL0;
    float3 outLightVec : POSITION1;
};

PixelShaderInput main(VertexShaderInput input)
{
    // Inefficient -- doing this only for instruction. Normally, you would
 // premultiply them on the CPU and place them in the cbuffer.
    matrix mvMatrix = mul(model, view);
    matrix mvpMatrix = mul(mvMatrix, projection);

    PixelShaderInput output;

    float4 pos = float4(input.pos, 1.0f);
    float4 normal = float4(input.normal, 1.0f);
    float4 light = float4(lightPos.xyz, 1.0f);

    // 
    float4 eye = float4(0.0f, 0.0f, -2.0f, 1.0f);

    // Transform the vertex position into projected space.
    output.gl_Position = mul(pos, mvpMatrix);
    output.outNormal = mul(normal, mvMatrix).xyz;
    output.outVec = -(eye - mul(pos, mvMatrix)).xyz;
    output.outLightVec = mul(light, mvMatrix).xyz;

    return output;
}

Passage en revue du nuanceur de pixels

Le nuanceur de pixels dans cet exemple est probablement la quantité minimale absolue de code que peut présenter un nuanceur de pixels. Il prend les données de couleur de pixel interpolées générées lors de la rastérisation et la retourne comme sortie, où elle est écrite dans une cible de rendu. Ce n’est pas très intéressant.

PS_OUTPUT main(PS_INPUT In)
{
    PS_OUTPUT Output;

    Output.RGBColor = In.Color;

    return Output;
}

La partie importante est la sémantique de valeur système SV_TARGET sur la valeur de retour. Il indique que la sortie doit être écrite dans la cible de rendu primaire, qui est la mémoire tampon de texture fournie à la chaîne d’échange pour l’affichage. Cela est requis pour les nuanceurs de pixels. Sans les données de couleur du nuanceur de pixels, Direct3D n’aurait rien à afficher.

Un exemple de nuanceur de pixels plus complexe pour effectuer l’ombrage Phong peut ressembler à ceci. Étant donné que les vecteurs et les normales ont été interpolés, nous n’avons pas besoin de les calculer sur une base par pixel. Toutefois, nous devons les renormaliser à cause du fonctionnement de l’interpolation. Conceptuellement, nous devons progressivement « tourner » le vecteur dirigé vers le vortex A pour le diriger vers le vortex B, en conservant sa longueur, tandis que l’interpolation coupe plutôt sur une ligne droite entre les deux points de terminaison vectoriels.

cbuffer MaterialConstantBuffer : register(b2)
{
    float4 lightColor;
    float4 Ka;
    float4 Kd;
    float4 Ks;
    float4 shininess;
};

struct PixelShaderInput
{
    float4 position : SV_POSITION;
    float3 outVec : POSITION0;
    float3 normal : NORMAL0;
    float3 light : POSITION1;
};

float4 main(PixelShaderInput input) : SV_TARGET
{
    float3 L = normalize(input.light);
    float3 V = normalize(input.outVec);
    float3 R = normalize(reflect(L, input.normal));

    float4 diffuse = Ka + (lightColor * Kd * max(dot(input.normal, L), 0.0f));
    diffuse = saturate(diffuse);

    float4 specular = Ks * pow(max(dot(R, V), 0.0f), shininess.x - 50.0f);
    specular = saturate(specular);

    float4 finalColor = diffuse + specular;

    return finalColor;
}

Dans un autre exemple, le nuanceur de pixels prend ses propres mémoires tampons constantes qui contiennent des informations légères et essentielles. La disposition d’entrée dans le nuanceur de vertex serait développée pour inclure des données normales, et la sortie de ce nuanceur de vertex devrait inclure des vecteurs transformés pour le vertex, la lumière et la normale du vertex dans le système de coordonnées de vue.

Si vous avez des mémoires tampons de texture et des échantillonneurs avec des registres attribués (t et s, respectivement), vous pouvez également y accéder dans le nuanceur de pixels.

Texture2D simpleTexture : register(t0);
SamplerState simpleSampler : register(s0);

struct PixelShaderInput
{
    float4 pos : SV_POSITION;
    float3 norm : NORMAL;
    float2 tex : TEXCOORD0;
};

float4 SimplePixelShader(PixelShaderInput input) : SV_TARGET
{
    float3 lightDirection = normalize(float3(1, -1, 0));
    float4 texelColor = simpleTexture.Sample(simpleSampler, input.tex);
    float lightMagnitude = 0.8f * saturate(dot(input.norm, -lightDirection)) + 0.2f;
    return texelColor * lightMagnitude;
}

Les nuanceurs sont des outils très puissants qui peuvent être utilisés pour générer des ressources procédurales telles que des mappages d’ombres ou des textures de bruit. En fait, les techniques avancées nécessitent que vous considériez les textures plus abstraitement, pas comme des éléments visuels, mais comme des mémoires tampons. Elles contiennent des données telles que des informations de hauteur, ou d’autres données qui peuvent être échantillonnées dans la passe finale du nuanceur de pixels ou dans ce cadre particulier dans le cadre d’une passe d’effets à plusieurs étapes. L’échantillonnage multiple est un outil puissant et l’épine dorsale de nombreux effets visuels modernes.

Étapes suivantes

Nous espérons que vous êtes maintenant à l’aise avec DirectX 11 et prêt à travailler sur votre projet. Voici quelques liens pour répondre à d’autres questions que vous pouvez avoir sur le développement avec DirectX et C++ :

Utilisation des ressources d’appareil DirectX

Comprendre le pipeline de rendu Direct3D 11