I-2. Tutoriel 2 : Dessiner un Triangle▲
I-2-a. Résumé▲
Dans le tutoriel précédent, nous avons construit l'armature minimale d'une application Direct3D 10 qui produit une seule couleur dans la fenêtre. Dans ce tutoriel, nous allons étendre l'application pour dessiner un triangle à l'écran. Nous allons parcourir le processus pour définir les structures de données associées à un triangle.
Le produit de ce tutoriel est une fenêtre avec un triangle dessiné en son centre.
I-2-b. Source▲
(SDK root)\Samples\C++\Direct3D10\Tutorials\Tutorial02
I-2-c. Eléments d'un Triangle▲
Un triangle est défini par ses trois points, aussi appelés sommets. Un ensemble de 3 sommets avec des positions uniques définie un triangle unique. Afin que le GPU dessine un triangle, nous devons lui donner la position de 3 sommets. Pour un exemple 2D, disons que nous souhaitons dessiner un triangle comme sur l'image 1. Nous passons les 3 sommets avec les positions (0, 0) (0, 1) et (1, 0) au GPU, alors le GPU a assez d'informations pour dessiner le triangle que nous voulons.
Donc maintenant, nous savons que nous devons passer 3 positions au GPU afin de dessiner un triangle. Comment passe-t-on ces informations au GPU ? Avec Direct3D 10, les informations des sommets, comme la position, sont stockées dans un tampon de ressources. Un tampon qui est utilisé pour stocker les informations des sommets est appelé le "vertex buffer". Nous devons créer un vertex buffer suffisamment grand pour 3 sommets et le remplir avec les positions des sommets. Dans Direct3D 10, l'application doit spécifier une taille de tampon en octets lorsque l'on crée un tampon de ressources. Nous savons que le buffer doit être assez grand pour 3 sommets, mais de combien d'octets a besoin chaque sommet ? Pour répondre à cette question, il est nécessaire de comprendre la composition des sommets.
I-2-d. Composition d'entrée▲
Un sommet a une position. Plus souvent que jamais, il a aussi d'autres attributs, comme une normale, une ou plusieurs couleurs, des coordonnées de textures (utilisées pour le placement de textures), et ainsi de suite. La composition de sommets défini comment ces attributs se situent en mémoire : quel type de données utilise chaque attribut, quelle taille à chaque attribut, et l'ordre des attributs en mémoire. Parce que les attributs ont habituellement différents types, similaires aux champs dans une structure C, un sommet est habituellement représenté par une structure. La taille du sommet est facilement obtenue à partir de la taille de la structure.
Dans ce tutoriel, nous travaillons seulement avec la position des sommets. Donc, nous définissons notre structure de sommets avec un seul champ de type D3DXVECTOR3. Ce type est un vecteur de 3 composants à virgules flottantes, qui est typiquement le type de données utilisé pour une position en 3D.
struct
SimpleVertex
{
D3DXVECTOR3 Pos; // Position
}
;
Nous avons maintenant une structure qui représente notre sommet. Celle-ci prend soin de stocker les informations de sommets dans la mémoire système de notre application. Cependant, quand nous fournissons au GPU le vertex buffer contenant nos sommets, nous lui fournissons juste un morceau de mémoire. Le GPU doit aussi connaître la composition de sommet afin d'extraire les attributs corrects du tampon. Pour accomplir ceci, l'utilisation de la composition d'entrée sera requise.
Dans Direct3D 10, une composition d'entrée est un objet Direct3D qui décrit la structure des sommets d'une façon qui peut être comprise par le GPU. Chaque attribut de sommet peut être décrit avec la structure D3D10_INPUT_ELEMENT_DESC. Une application définie une ou plusieurs D3D10_INPUT_ELEMENT_DESC, ensuite utilise ce tableau pour créer l'objet de composition d'entrée qui décrit complètement les sommets. Nous allons maintenant regarder en détails les champs de D3D10_INPUT_ELEMENT_DESC.
SemanticName | SemanticName est une chaîne de caractères contenant un mot qui décrit la nature ou le but (ou sémantiques) de cet élément. Le mot peut être dans une des formes qu'un identifieur C peut avoir, et peut être tout ce que nous choisissons. Par exemple, un bon nom sémantique pour la position des sommets est POSITION. Les noms sémantiques ne sont pas sensibles à la casse. |
SemanticIndex | Les SemanticIndex complètent les noms sémantiques. Un sommet peut avoir de multiples attributs de même nature. Par exemple, il peut avoir 2 séries de coordonnées de texture ou 2 séries de couleurs. Au lieu d'utiliser des noms sémantiques qui possèdent des nombres, tels que "COLOR0" et "COLOR1", les deux éléments peuvent partager un seul nom sémantique, "COLOR", avec des indices différents 0 et 1. |
Format | Le format définie le type de données qui sera utilisé pour cet élément. Par exemple, un format DXGI_FORMAT_R32G32B32_FLOAT a trois nombres à virgules flottantes de 32 bits, ce qui fait que l'élément utilise 12 octets. Un format DXGI_FORMAT_R16G16B16A16_UINT a quatre entiers non signés de 16 bits, ce qui fait que l'élément utilise 8 octets. |
InputSlot | Comme mentionné précédemment, une application Direct3D 10 passe les données de sommet au GPU par l'utilisation de vertex buffer. Dans Direct3D 10, plusieurs vertex buffer peuvent être fournis au GPU simultanément, 16 pour être exact. Chaque vertex buffer est assigné à un numéro d'emplacement d'entrée compris entre 0 et 15. Le champ InputSlot dit au GPU quel vertex buffer il doit aller chercher pour cet élément. |
AlignedByteOffset | Un sommet est stocké dans un vertex buffer, qui est simplement un morceau de mémoire. Le champ AlignedByteOffset dit au GPU l'emplacement mémoire pour commencer à aller chercher les données pour cet élément. |
InputSlotClass | Ce champ a habituellement la valeur D3D10_INPUT_PER_VERTEX_DATA. Quand une application utilise l'instanciation, elle peut définir un InputSlotClass de la composition d'entrée à D3D10_INPUT_PER_INSTANCE_DATA pour travailler avec le vertex buffer contenant les données d'instance. L'instanciation est un sujet avancé de Direct3D et ne sera pas discutée ici. Pour ce tutoriel, nous utiliserons exclusivement D3D10_INPUT_PER_VERTEX_DATA. |
InstanceDataStepRate | Ce champ est utilisé pour l'instanciation. Comme nous n'utilisons pas l'instanciation, ce champ n'est pas utilisé et doit être défini à 0. |
Maintenant, nous pouvons définir notre tableau D3D10_INPUT_ELEMENT_DESC et créer la composition d'entrée :
// Défini la composition d'entrée
D3D10_INPUT_ELEMENT_DESC layout[] =
{
{
L"POSITION"
, 0
, DXGI_FORMAT_R32G32B32_FLOAT, 0
, 0
, D3D10_INPUT_PER_VERTEX_DATA, 0
}
,
}
;
UINT numElements =
sizeof
(layout)/
sizeof
(layout[0
]);
I-2-e. Composition de sommets▲
Dans le prochain tutoriel, nous expliquerons l'objet "technique" et les shaders associés. Pour l'instant, nous nous concentrerons juste sur la création de l'objet de composition de sommets Direct3D 10 pour la technique. Cependant, nous apprendrons que la technique et les shaders sont étroitement couplés avec cette composition de sommets. La raison est que la création d'un objet de composition de sommet requiert la signature d'entrée du shader de sommets. Nous appelons tout d'abord la méthode de la technique GetPassByIndex() pour obtenir un objet d'effet de passage qui réprésente le premier passage de la technique. Ensuite, nous appellons la méthode de l'objet de passage GetDesc() pour obtenir une structure de description du passage. Dans cette structure, il y a un champ nommé pIAInputSignature qui renvoie vers la donnée binaire que représente la signature d'entrée du vertex shader utilisé dans ce passage. Une fois que nous avons cette donnée, nous pouvons appeler ID3D10Device::CreateInputLayout() pour créer un objet de composition de sommets, et ID3D10Device::IASetInputLayout() pour le définir en tant que composition de sommets active. Le code pour effectuer tout cela est montré ci-dessous :
// Crée la composition d'entrée
D3D10_PASS_DESC PassDesc;
g_pTechnique->
GetPassByIndex( 0
)->
GetDesc( &
PassDesc );
if
( FAILED( g_pd3dDevice->
CreateInputLayout( layout, numElements, PassDesc.pIAInputSignature,
PassDesc.IAInputSignatureSize, &
g_pVertexLayout ) ) )
return
FALSE;
// Défini la composition d'entrée
g_pd3dDevice->
IASetInputLayout( g_pVertexLayout );
I-2-f. Création du vertex buffer▲
Une chose que nous aurons aussi besoin de faire durant l'initialisation est de créer le vertex buffer qui contiendra les données des sommets. Pour créer un vertex buffer dans Direct3D 10, nous remplissons deux structures, D3D10_BUFFER_DESC et D3D10_SUBRESOURCE_DATA, et ensuite, nous appelons ID3D10Device::CreateBuffer(). D3D10_BUFFER_DESC décrit l'objet vertex buffer qui sera créé, et D3D10_SUBRESOURCE_DATA décrit les données actuelles qui seront copiées dans le vertex buffer durant la création. La création et l'initialisation du vertex buffer sont effectuées en une seule fois de sorte que nous n'ayons pas besoin d'initialiser le buffer plus tard. Les données qui seront copiées dans le vertex buffer sont les sommets, un tableau de 3 SimpleVertex. Les coordonnées dans le tableau de sommets sont choisies afin que nous puissions voir un triangle au milieu de la fenêtre de notre application lors de l'affichage avec nos shaders. Après que le vertex buffer soit créé, nous pouvons appeler ID3D10Device::IASetVertexBuffers() pour l'attribuer vers le device. Le code complet est montré ici :
// Crée le vertex buffer
SimpleVertex vertices[] =
{
D3DXVECTOR3( 0.0
f, 0.5
f, 0.5
f ),
D3DXVECTOR3( 0.5
f, -
0.5
f, 0.5
f ),
D3DXVECTOR3( -
0.5
f, -
0.5
f, 0.5
f ),
}
;
D3D10_BUFFER_DESC bd;
bd.Usage =
D3D10_USAGE_DEFAULT;
bd.ByteWidth =
sizeof
( SimpleVertex ) *
3
;
bd.BindFlags =
D3D10_BIND_VERTEX_BUFFER;
bd.CPUAccessFlags =
0
;
bd.MiscFlags =
0
;
D3D10_SUBRESOURCE_DATA InitData;
InitData.pSysMem =
vertices;
if
( FAILED( g_pd3dDevice->
CreateBuffer( &
bd, &
InitData, &
g_pVertexBuffer ) ) )
return
FALSE;
// Défini le vertex buffer
UINT stride =
sizeof
( SimpleVertex );
UINT offset =
0
;
g_pd3dDevice->
IASetVertexBuffers( 0
, 1
, &
g_pVertexBuffer, &
stride, &
offset );
I-2-g. Topologie des primitives▲
La topologie des primitives identifie comment le GPU obtient les trois sommets dont il a besoin pour dessiner le triangle. Nous avons discuté ci-dessus qu'afin d'afficher un seul triangle, l'application a besoin d'envoyer 3 sommets au GPU. Ainsi, le vertex buffer comporte 3 sommets. Que se passe t-il si nous voulons dessiner deux triangles ? Une solution est d'envoyer 6 sommets au GPU. Les 3 premiers sommets définissent le premier triangle et les 3 autres sommets définissent le second triangle. Cette topologie est appelée une liste de triangles. Les listes de triangles ont l'avantage d'être facile à comprendre, mais dans certains cas, ils sont très inefficaces. De tels cas se produisent quand vous affichez successivement des triangles qui partagent des sommets. Par exemple, l'image 3a nous montre un carré composé de 2 triangles : A B C et C B D. (Par convention, les triangles sont typiquement définis en listant leurs sommets dans le sens des aiguilles d'une montre.) Si nous envoyons ces deux triangles vers le GPU en utilisant une liste de triangles, notre vertex buffer ressemblerait à ceci :
A B C C B D
Notez que B et C apparaissent deux fois dans le vertex buffer car ils sont partagés par les deux triangles.
L'image 3a contient un carré composé de 2 triangles; l'image 3b contient une forme pentagonale composée de 3 triangles.
Nous pouvons faire un vertex buffer plus petit si nous pouvons dire au GPU que lors de l'affichage du second triangle, au lieu d'aller chercher les 3 sommets depuis le vertex buffer, d'utiliser 2 des sommets du triangle précédent et d'aller chercher seulement 1 sommet depuis le vertex buffer. Il s'avère que cela est supporté par Direct3D, et la topologie est appelée "bande de triangles". Lors du dessin d'une bande de triangles, le tout premier triangle est défini par les trois premiers sommets du vertex buffer. Le triangle suivant est défini par les deux derniers sommets du triangle précédent plus le sommet suivant dans le vertex buffer. En prenant le carré sur l'image 3a comme exemple, en utilisant une bande de triangle, le vertex buffer ressemblerait à ceci :
A B C D
Les trois premiers sommets, A B C, définissent le premier triangle. Le second triangle est défini par B et C, les deux derniers sommets du premier triangle, plus D. Donc, en utilisant la topologie de bande de triangles, la taille vertex buffer est passée de 6 sommets à 4 sommets. Similairement, pour trois triangles comme sur l'image 3b, l'utilisation d'une liste de triangles nécessiterait un vertex buffer comme ceci :
A B C C B D C D E
En utilisant une bande de triangles, la taille du vertex buffer est dramatiquement réduite :
A B C D E
Vous avez pu noter que dans l'exemple de la bande de triangles, le second triangle est défini par B C D. Ces 3 sommets ne forment pas un ordre dans le sens des aiguilles d'une montre. C'est un phénomène naturel de l'utilisation des bandes de triangles. Pour contourner ceci, le GPU échange automatiquement l'ordre des 2 vertices provenant du triangle précédent. Il effectue seulement ceci pour le second triangle, le quatrième triangle, le sixième triangle, le huitième triangle, et ainsi de suite. Cela assure que tous les triangles sont définis par des sommets dans un ordre correct (sens des aiguilles d'une montre, dans ce cas). A côté des listes de triangles et des bandes de triangles, Direct3D 10 supporte plusieurs autres types de topologies de primitives. Nous ne discuterons pas de ceux-ci dans ce tutoriel.
Dans notre code, nous avons un triangle, donc ce que nous spécifions importe peu. Cependant, nous devons spécifier quelque chose, donc nous optons pour une liste de triangle.
// Défini la topologie de primitive
g_pd3dDevice->
IASetPrimitiveTopology( D3D10_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
I-2-h. Le dessin du triangle▲
L'élément final manquant est le code qui effectue l'affichage actuel du triangle. Comme mentionné précédemment, ce tutoriel utilisera le système d'effets. Nous commençons en appelant ID3D10EffectTechnique::GetDesc() sur l'objet technique obtenu plus tôt pour recevoir une structure D3D10FX_TECHNIQUE_DESC qui décrit la technique. Un des membres de D3D10FX_TECHNIQUE_DESC, Passes, indique le nombre de passage que la technique contient. Pour dessiner correctement en utilisant cette technique, l'application doit boucler le même nombre de fois qu'il y a de passages. Dans la boucle, nous devons d'abord appeler la méthode GetPassByIndex() de la technique pour obtenir l'objet de passage, ensuite appeler sa méthode Apply() pour avoir le système d'effet attribué aux shaders associés et les états d'affichages vers le pipeline graphique. La chose suivante que nous faisons est d'appeler ID3D10Device::Draw(), laquelle commande au GPU de dessiner en utilisant le vertex buffer courant, la composition de sommets, et la topologie de primitives. Le premier paramètre de Draw() est le nombre de sommets à envoyer au GPU, et le second paramètre est l'indice du premier sommet pour commencer à envoyer. Comme nous dessinons un triangle et que nous dessinons depuis le début du vertex buffer, nous utilisons 3 et 0, respectivement, pour les deux paramètres. Le code entier d'affichage du triangle ressemble à ceci :
// Affichage d'un triangle
D3D10_TECHNIQUE_DESC techDesc;
g_pTechnique->
GetDesc( &
techDesc );
for
( UINT p =
0
; p <
techDesc.Passes; ++
p )
{
g_pTechnique->
GetPassByIndex( p )->
Apply(0
);
g_pd3dDevice->
Draw( 3
, 0
);
}