Partager via


Cet article a fait l'objet d'une traduction automatique.

Facteur DirectX

Nuanceurs de sommets et transformations

Charles Petzold

Téléchargez l'exemple de Code

Charles PetzoldEn 1975, l'artiste Richard Haas a peint le côté plat d'un immeuble dans le quartier de Soho de Manhattan pour ressembler à la façade d'un bâtiment en fonte classique, y compris des options comme un chat dans une fenêtre. Temps n'a pas été tendre avec cette peinture, mais à ses débuts, il avait l'air très réel et trompé beaucoup de gens.

Ces peintures — appelé trompe-l'œil, ce qui signifie « trompent le œil » — utiliser les ombres et les ombres pour faire ressortir une troisième dimension sur une surface plane à deux dimensions. Nous avons tendance à être chatouillé par ces illusions, mais en même temps désireux de nous assurer que nous pouvons discerner l'astuce. Une méthodes les plus simples consiste à essayer d'observer la peinture sous des angles différents pour voir si elle a la même apparence.

Un processus mental semblable se produit lorsqu'un programme d'ordinateur affiche des graphiques qui semblent à cheval sur la ligne entre 2D et 3D. Est-ce vraiment 3D ? Ou juste 2D avec quelques astucieux qui se chevauchent et l'ombrage ? Nous ne pouvons pas déplacer nos têtes du côté à côté pour établir les faits, mais nous pourrions être en mesure de persuader le programme pour faire pivoter le graphique pour voir ce qui se passe.

L'image en Figure 1 ressemble beaucoup à l'écran affiché par le programme de ThreeTriangles dans l'article précédent de cette colonne (msdn.microsoft.com/magazine/dn768854). Mais le programme téléchargeable pour cette colonne, appelée ThreeRotatingTriangles, tourne en effet cet assemblage de trois triangles. L'effet est visuellement très intéressant, et comme les trois triangles se déplacent en fonction à l'autre, le programme établit qu'il n'y a en effet certains traitement de graphiques 3D en cours. Toutefois, si vous regardez le code, vous découvrirez que le programme est toujours écrit entièrement avec Direct2D plutôt que Direct3D, à l'aide de la fonctionnalité puissante de Direct2D effets.

l'affichage du programme ThreeRotatingTriangles
Figure 1 l'affichage du programme ThreeRotatingTriangles

Entrer des données dans un effet

L'organisation globale du programme ThreeTriangles antérieure sauvegardée dans ThreeRotatingTriangles. La classe qui fournit l'implémentation de Direct2D effet porte le nom de rotation­TriangleEffect plutôt que SimpleTriangleEffect, mais il continue à mettre en œuvre de la ID2D1EffectImpl (« application de l'effet") et interfaces ID2D1DrawTransform.

Le SimpleTriangleEffect antérieure n'était pas du tout polyvalent. Il contenait des sommets codé en dur pour afficher les trois triangles qui se chevauchent. Permet de RotatingTriangleEffect les sommets à être défini en dehors de la classe, et tant la mise en œuvre de l'effet et le vertex shader ont été améliorés pour tenir compte des transformations de matrice.

En général, une implémentation de l'effet comme RotatingTriangle­effet contient une méthode statique qui s'inscrit en appelant RegisterEffectFromString et en s'associant avec un ID de classe. Dans le programme ThreeRotatingTriangles, la ThreeRotating­TrianglesRenderer classe appelle cette méthode statique dans son constructeur d'enregistrer l'effet.

ThreeRotatingTrianglesRenderer définit également un objet de type ID2D1Effect comme un champ privé :

Microsoft::WRL::ComPtr<ID2D1Effect>
            m_rotatingTriangleEffect;

Pour utiliser l'effet, le programme doit créer cet objet en référençant les ID de classe de l'effet dans un appel à CreateEffect. Dans le ThreeRotating­TrianglesRenderer classe, cela se produit dans le CreateDeviceDependent­méthode de ressources :

d2dContext->CreateEffect(
  CLSID_RotatingTriangleEffect, &m_rotatingTriangleEffect);

L'effet peut être joué avec un appel à la méthode DrawImage. Voilà comment ThreeRotatingTrianglesRenderer a fait l'appel dans sa méthode Render :

d2dContext->DrawImage(m_rotatingTriangleEffect.Get());

Mais il n'y a d'autres qui peut être fait entre ces deux appels. ID2D1Effect dérive de ID2D1Properties, qui a des méthodes nommées SetValue et GetValue qui permettent à un programme définir des propriétés sur l'effet. Ces propriétés peuvent aller de la simple effet options exprimées comme Boolean drapeaux aux grands tampons de données. Cependant, SetValue et GetValue ne sont pas souvent utilisés. Dont ils ont besoin d'identifier le bien donné par index, et une plus grande clarté de programme est obtenue en utilisant plutôt les méthodes SetValueByName et GetValueByName.

N'oubliez pas que ces méthodes SetValueByName et GetValueByName font partie de l'objet ID2D1Effect, qui est l'objet retourné par l'appel de CreateEffect. L'objet de ID2D1Effect transmet les valeurs de ces propriétés à la classe d'implémentation effet vous avez écrit — la classe qui implémente les interfaces ID2D1EffectImpl et ID2D1DrawTransform. Cela semble un peu détournée, mais il a fait de cette façon donc effets enregistrés peuvent être utilisés sans accès aux classes qui implémentent l'effet.

Mais cela signifie qu'une implémentation de l'effet par exemple la classe RotatingTriangleEffect doit lui-même indiquent qu'il peut accepter les propriétés de divers types, et il doit fournir des méthodes pour définir et obtenir les propriétés.

Cette information est fournie par l'implémentation de l'effet quand il enregistre lui-même à l'aide de RegisterEffectFromString. Dans cet appel il faut certains XML contenant les noms et les types des propriétés diverses les supports de mise en œuvre d'effet. RotatingTriangle­effet prend en charge quatre propriétés, avec les types de données et les noms suivants :

  • VertexData du blob de type, c'est-à-dire un mémoire tampon référencé par un pointeur d'octet.
  • ModelMatrix de type matrix4x4.
  • ViewMatrix de type matrix4x4.
  • ProjectionMatrix de type matrix4x4.

Les noms des types de données sont spécifiques aux effets Direct2D. Lorsqu'un effet qui prend en charge les propriétés est enregistré, l'application de l'effet doit également fournir un tableau d'objets D2D1_VALUE_TYPE_BINDING. Chacun des objets dans ce tableau associe une propriété nommée, par exemple VertexData, avec deux méthodes dans l'application de l'effet que définir et obtenir les données. Pour VertexData, les deux méthodes sont nommés SetVertexData et GetVertexData. (Lorsque vous définissez ces méthodes Get, assurez-vous d'inclure le mot clé const, ou vous obtiendrez un de ces erreurs du modèle bizarre qui seront complètement déconcertants.)

De même, la classe RotatingTriangleEffect définit les méthodes nommées SetModelMatrix et GetModelMatrix, et ainsi de suite. Ces méthodes ne sont pas appelées par n'importe quel programme d'application — en effet, ils sont privés de RotatingTriangleEffect. Au lieu de cela, le programme appelle les méthodes SetValueByName et GetValueByName sur l'objet ID2D1Effect, qui appelle ensuite les méthodes Set et Get dans la mise en œuvre de l'effet.

La mémoire tampon de Vertex

La classe ThreeRotatingTrianglesRenderer enregistre le tournant­TrianglesEffect dans son constructeur et restitue l'effet dans sa méthode Render. Mais entre ces deux appels, la classe de convertisseur appelle SetValueByName sur l'objet ID2D1Effect pour passer des données dans l'application de l'effet.

Le premier quatre effet propriétés répertoriées précédemment est VertexData, c'est-à-dire un ensemble de sommets utilisés pour définir un mémoire tampon de vertex. Pour les effets de Direct2D, le nombre d'éléments dans un mémoire tampon de vertex doivent être un multiple de trois, regroupés en triangles. Le programme de ThreeRotatingTriangles affiche seulement trois triangles avec trois sommets de chaque, mais l'effet peut gérer une mémoire tampon plus importante.

Le format de la mémoire tampon de vertex attendu par RotatingTriangle­effet est défini dans une structure en RotatingTriangleEffect.h :

struct PositionColorVertex
{
  DirectX::XMFLOAT3 position;
  DirectX::XMFLOAT3 color;
};

Il s'agit du format utilisé par SimpleTriangleEffect, mais un peu différemment défini. La figure 2 montre comment la méthode CreateDeviceDependentResources dans ThreeRotatingTrianglesRenderer transfère le tableau vertex à l'effet après que l'effet a été créé.

Figure 2 création de l'effet et réglage de la mémoire tampon de Vertex

void ThreeRotatingTrianglesRenderer::CreateDeviceDependentResources()
{
  ID2D1DeviceContext1* d2dContext =
    m_deviceResources->GetD2DDeviceContext();
  // Create the effect
  DX::ThrowIfFailed(d2dContext->CreateEffect(
                  CLSID_RotatingTriangleEffect,
                  &m_rotatingTriangleEffect)
    );
  // Set the vertices
  std::vector<PositionColorVertex> vertices =
  {
    // Triangle 1
    { XMFLOAT3(0, -1000, -1000), XMFLOAT3(1, 0, 0) },
    { XMFLOAT3(985, -174, 0), XMFLOAT3(0, 1, 0) },
    { XMFLOAT3(342, 940, 1000), XMFLOAT3(0, 0, 1) },
    // Triangle 2
    { XMFLOAT3(866, 500, -1000), XMFLOAT3(1, 0, 0) },
    { XMFLOAT3(-342, 940, 0), XMFLOAT3(0, 1, 0) },
    { XMFLOAT3(-985, -174, 1000), XMFLOAT3(0, 0, 1) },
    // Triangle 3
    { XMFLOAT3(-866, 500, -1000), XMFLOAT3(1, 0, 0) },
    { XMFLOAT3(-643, -766, 0), XMFLOAT3(0, 1, 0) },
    { XMFLOAT3(643, -766, 1000), XMFLOAT3(0, 0, 1) }
  };
  DX::ThrowIfFailed(
    m_rotatingTriangleEffect->SetValueByName(L"VertexData",
      (byte *) &vertices)
    );
  // Ready to render!
  m_readyToRender = true;
}

Les coordonnées X et Y sont basées sur les sinus et cosinus des angles en tranches de 40 degrés avec un rayon de 1 000. Le Z coordonnées vont de ­–1,000 pour le premier plan à 1 000 pour le fond. Dans le SimpleTriangleEffect plus tôt, j'ai été très prudent de mettre Z entre 0 et 1 en raison des conventions utilisées pour sortie 3D clip. Comme vous le verrez, ce n'est pas nécessaire ici parce que la caméra de réelles transformations s'appliquera aux sommets.

L'appel de SetValueByName avec un nom de VertexData provoque l'objet ID2D1Effect appeler la méthode SetVertexBuffer dans RotatingTriangleEffect pour transmettre les données. Cette méthode effectue un cast du pointeur d'octet vers son type original et les appels CreateVertexBuffer pour stocker les informations d'une manière qui lui permet d'être passé au nuanceur vertex.

Appliquer les transformations

Figure 3 présente la méthode de mise à jour en ThreeRotatingTrianglesRenderer trois transformations de matrice de calcul et des trois appels à SetValueByName. Le code a été simplifié légèrement afin de supprimer les contrôles pour retourne HRESULT errant.

Figure 3 définissant les trois transformer Matrices

void ThreeRotatingTrianglesRenderer::Update(DX::StepTimer const& timer)
{
  if (!m_readyToRender)
      return;
  // Apply model matrix to rotate vertices
  float angle = float(XM_PIDIV4 * timer.GetTotalSeconds());
  XMMATRIX matrix = XMMatrixRotationY(angle);
  XMFLOAT4X4 float4x4;
  XMStoreFloat4x4(&float4x4, XMMatrixTranspose(matrix));
  m_rotatingTriangleEffect->SetValueByName(L"ModelMatrix", float4x4);
  // Apply view matrix
  matrix = XMMatrixLookAtRH(XMVectorSet(0, 0, -2000, 0),
                            XMVectorSet(0, 0, 0, 0),
                            XMVectorSet(0, 1, 0, 0));
  XMStoreFloat4x4(&float4x4, XMMatrixTranspose(matrix));
  m_rotatingTriangleEffect->SetValueByName(L"ViewMatrix", float4x4);
  // Base view width and height on coordinates of model
  float width = 2000;
  float height = 2000;
  // Adjust width and height for landscape and portrait modes
  Windows::Foundation::Size logicalSize = 
    m_deviceResources->GetLogicalSize();
  if (logicalSize.Width > logicalSize.Height)
      width *= logicalSize.Width / logicalSize.Height;
  else
      height *= logicalSize.Height / logicalSize.Width;
  // Apply projection matrix   
  matrix = XMMatrixOrthographicRH(width, height, 500, 4000);
  XMStoreFloat4x4(&float4x4, XMMatrixTranspose(matrix));
  m_rotatingTriangleEffect->SetValueByName(L"ProjectionMatrix", float4x4);
}

Comme d'habitude, la mise à jour est appelée à la cadence de l'affichage vidéo. La première matrice qu'il calcule est appliquée sur les sommets de leur tourner autour de l'axe des Y. La deuxième matrice est une transformation d'affichage de caméra standard qui se traduit par le déplacement de la scène afin que le spectateur est sur l'origine du système de coordonnées en trois dimensions et regardant tout droit le long de l'axe Z. La troisième est une matrice de projection standard, qui se traduit en X et Y coordonnent étant normalisés à des valeurs comprises entre ­-1 et 1 et les coordonnées Z entre 0 et 1.

Ces matrices doivent être appliquées aux coordonnées du vertex. Alors pourquoi ne le programme suffit de les multiplier par le tableau des sommets définis dans la méthode CreateDeviceDependentResources et définissez ensuite une nouvelle mémoire tampon de vertex dans le RotatingTrianglesEffect ?

Il est certainement possible de définir un tampon de vertex dynamique qui change avec la fréquence d'images de l'affichage vidéo, et dans certains cas, il est nécessaire. Mais si les sommets doivent uniquement être modifiées par les transformations de matrice, un tampon de vertex dynamique n'est pas aussi efficace que maintenir le même tampon tout au long du programme et en appliquant les transformations plus tard dans le pipeline — plus précisément, dans le nuanceur de sommets qui s'exécute sur le GPU vidéo.

Cela signifie que le nuanceur vertex a besoin de nouvelles transformations de matrice pour chaque image de l'écran vidéo, et qui soulève une autre question : Comment une implémentation effet obtient-il des données dans le nuanceur de sommets ?

Le tampon constante de Shader

Données sont transférées du code d'application dans un shader grâce à un mécanisme appelé un constant buffer. Don' t laisser le nom vous tromper en pensant que son contenu reste constante tout au long du programme. Ce n'est certainement pas le cas. Très souvent les changements constants de tampon avec chaque image de l'écran vidéo. Toutefois, le contenu de la mémoire tampon constante est constant pour tous les sommets dans chaque image, et le format de la mémoire tampon constant est fixé au moment de la compilation par le programme.

Le format de la mémoire tampon de vertex shader constante est défini dans deux endroits : dans le code C++ et dans le nuanceur de sommets elle-même. Dans l'application de l'effet, il ressemble à ceci :

struct VertexShaderConstantBuffer
{
  DirectX::XMFLOAT4X4 modelMatrix;
  DirectX::XMFLOAT4X4 viewMatrix;
  DirectX::XMFLOAT4X4 projectionMatrix;
} m_vertexShaderConstantBuffer;

Lorsque la méthode Update dans le moteur de rendu appelle SetValueByName pour définir une des matrices, l'objet de ID2D1Effect appelle la méthode Set appropriée dans la classe RotatingTrianglesEffect. Ces méthodes sont nommées SetModelMatrix, SetViewMatrix et SetProjection­Matrix et ils ont simplement transfèrent la matrice au champ approprié dans l'objet m_vertexShaderConstantBuffer.

L'objet ID2D1Effect du principe que tout appel à SetValueByName entraîne un changement selon laquelle implique probablement un changement correspondant à la sortie graphique, donc la PrepareForRender méthode est appelée dans l'implémentation de l'effet. C'est là l'effet de la mise en œuvre peut profiter de l'occasion pour appeler SetVertexShaderConstantBuffer pour transférer le contenu de la VertexShaderConstantBuffer dans le nuanceur de sommets.

Le nouveau Vertex Shader

Maintenant, enfin, vous pouvez regarder le code de Shader langage HLSL (High Level) qui fait une grande partie de l'ouvrage en faisant pivoter les sommets de ces trois triangles et leur orientation dans l'espace 3D. Il s'agit du nuanceur de sommets de nouveaux et améliorés, montré dans son intégralité dans Figure 4.

Figure 4 le nuanceur de sommets pour l'effet Triangle tournant

// Per-vertex data input to the vertex shader
struct VertexShaderInput
{
  float3 position : MESH_POSITION;
  float3 color : COLOR0;
};
// Per-vertex data output from the vertex shader
struct VertexShaderOutput
{
  float4 clipSpaceOutput : SV_POSITION;
  float4 sceneSpaceOutput : SCENE_POSITION;
  float3 color : COLOR0;
};
// Constant buffer provided by effect.
cbuffer VertexShaderConstantBuffer : register(b1)
{
  float4x4 modelMatrix;
  float4x4 viewMatrix;
  float4x4 projectionMatrix;
};
// Called for each vertex.
VertexShaderOutput main(VertexShaderInput input)
{
  // Output structure
  VertexShaderOutput output;
  // Get the input vertex, and include a W coordinates
  float4 pos = float4(input.position.xyz, 1.0f);
  // Pass through the resultant scene space output value
  output.sceneSpaceOutput = pos;
  // Apply transforms to that vertex
  pos = mul(pos, modelMatrix);
  pos = mul(pos, viewMatrix);
  pos = mul(pos, projectionMatrix);
  // The result is clip space output
  output.clipSpaceOutput = pos;
  // Transfer the color
  output.color = input.color;
  return output;
}

Remarquez comment les structures sont définies : Le VertexShaderInput dans le shader est le même format que la structure de PositionColorVertex, définie dans le fichier d'en-tête C++. Le VertexShaderConstantBuffer est le même format que la structure portant le même nom dans le code C++. La structure de VertexShaderOutput correspond à la structure de PixelShaderInput dans le nuanceur de pixels.

Le nuanceur vertex associées à la SimpleTriangleEffect dans l'article du mois dernier, un tampon de ClipSpaceTransforms ont été fournies automatiquement pour convertir depuis l'espace (les coordonnées en pixels utilisées pour les sommets des triangles) de la scène en clip-space, qui implique normalisé des coordonnées X et Y, comprise entre – 1 et 1, et Z coordonnées qui vont de 0 à 1.

Ce n'est plus nécessaire, alors j'ai enlevé sans aucune conséquence fâcheuse. Au lieu de cela, la matrice de projection fait le travail équivalent. Comme vous pouvez le voir, la fonction principale applique les trois matrices au poste d'entrée de vertex et affecte le résultat au champ clipSpaceOuput de la structure de sortie.

Ce champ clipSpaceOutput est obligatoire. Il s'agit de comment le tampon de profondeur est réussi, et comment les résultats sont mappés sur la surface exposée. Toutefois, le champ sceneSpaceOutput de la structure VertexShaderOutput n'est pas obligatoire. Si vous supprimez ce domaine — et aussi supprimer le champ de la structure PixelShaderInput de nuanceur de pixels — le programme fonctionnera de la même chose.

Rang de Major et Major de la colonne

Le shader effectue trois multiplications des positions de matrices :

pos = mul(pos, modelMatrix);
pos = mul(pos, viewMatrix);
pos = mul(pos, projectionMatrix);

En notation mathématique, ces multiplications ressemblent à ceci :

m11   m12    m13   m14
m21   m22    m23   m24
m31   m32    m33   m34
m41   m42    m43   m44
|x   y   z   w| ×

Lorsque cette multiplication est effectuée, les quatre chiffres qui décrivent le point (x, y, z, w) sont multipliés par les quatre chiffres dans la première colonne de la matrice (m11, m21, m31, m41) et les quatre produits sont additionnées et ensuite le processus se poursuit avec la deuxième, troisième et quatrième colonnes.

Le vecteur (x, y, z, w) se compose de quatre numéros mémorisés adjacents. Examiner le matériel qui implémente le traitement parallèle de la multiplication de matrices. Ne pensez-vous pas qu'il pourrait être plus rapide pour exécuter ces quatre multiplications en parallèle si les chiffres dans chaque colonne sont également stockées dans mémoire adjacent ? Cela semble très probable, qui implique que le meilleur moyen pour stocker les valeurs de la matrice en mémoire est la commande m11, m21, m31, m41, m12, m22 et ainsi de suite.

Qui est connu comme l'ordre de colonne-major. Le bloc de mémoire commence avec la première colonne de la matrice, puis la deuxième, troisième et quatrième. Et c'est ce qui shaders vertex suppose que l'Organisation des matrices en mémoire lors de l'exécution de ces multiplications.

Cependant, ce n'est pas la façon que DirectX normalement stocke les matrices en mémoire. Les structures XMMATRIX et XMFLOAT4X4 dans la bibliothèque DirectX Math stockent des matrices dans l'ordre ligne-champ : M11, m12, m13, m14, m21, m22 et ainsi de suite. Cela ressemble à un ordre plus naturel pour beaucoup d'entre nous, car il est similaire à l'ordre que nous avons lu les lignes de texte — horizontalement puis verticalement.

Malgré tout, il y a une incompatibilité entre DirectX et shader code, et c'est pourquoi vous remarquerez dans le code de Figure 3 que toute matrice est soumis à un appel de XMMatrixTranspose avant d'être expulsé vers le nuanceur vertex. La fonction XMMatrixTranspose convertit des matrices ligne champ à matrices colonne-major et retour à nouveau si vous avez besoin qui.

C'est la solution la plus courante de ce problème, mais ce n'est pas la seule solution. Vous pouvez également spécifier un drapeau pour compiler le shader pour ordre ligne-champ, ou vous pouvez laisser l'interrupteur de matrices non transposée et juste autour de l'ordre de la matrice et le vecteur dans les multiplications :

pos = mul(viewMatrix, pos);

L'étape finale

L'ombrage des trois triangles montre certainement que les couleurs de vertex sont interpolées sur la surface de chaque triangle, mais le résultat est assez sommaire. Sûrement, je peux faire mieux en utilisant cet outil puissant ombrage pour imiter la réflexion de la lumière. Ce sera la dernière étape de cette plongée dans le monde versatile de Direct2D.


Charles Petzold est un contributeur de longue date à MSDN Magazine et l'auteur de « Programmation Windows, 6e édition » (Microsoft Press, 2013), un livre sur l'écriture d'applications pour Windows 8. Son site Web est charlespetzold.com.

Grâce à l'expert technique Microsoft suivant d'avoir relu cet article : Doug Erickson