Tutoriels Direct3D 10 : Bases

| Msdn | Romain Perruchon (Traducteur, relecteur) | Cyril Doillon (Traducteur, relecteur) |

Traduction d'un article original de la Library MSDN US

Les tutoriels suivant ont été créés afin d'apprendre à un programmeur les bases de la programmation de l'API Direct3D 10.

Ils couvrent la mise en place et les procédures basiques afin d'accomplir les taches fondamentales, comme dessiner et transformer.

Le public visé est un public familier avec la programmation, mais relativement nouveau dans la programmation 3D graphique. Les tutoriels de bases seront un premier pas sur Direct3D, servant à expliquer des concepts simple derrière le rendu 3D, comme les sommets, les transformations et les animations.

Les utilisateurs plus expérimentés migrant vers Direct3D 10 peuvent passer ces explications et aller voir les changements effectués dans cette itération de l'API.

Le SDK DirectX est téléchargeable Ici

- Titre : Tutoriels Direct3D 10 : Bases

- Auteurs : Msdn, Romain Perruchon (Traducteur, relecteur), Cyril Doillon (Traducteur, relecteur)

- Parution : 19 janvier 2009

- Mise à jour : 19 janvier 2009

- Licence : Cet article est une traduction d'un article original de Microsoft en anglais. Les éventuels problèmes résultant d'une mauvaise traduction ne sont pas imputables à Microsoft. Lire l'article original Ici.

Sommaire

- I. Tutoriels de base

- I-0. Tutoriel 0 : Les bases de Win32

- I-0-a. Résumé

- I-0-b. Source

- I-0-c. Préparation de la fenêtre

- I-1. Tutoriel 1 : L'essentiel de Direct3D 10

- I-1-a. Sommaire

- I-1-b. Source

- I-1-c. Création d'un device Direct3D 10

- I-1-d. En modifiant la boucle de messages

- I-1-e. Le code de l'affichage

- I-2. Tutoriel 2 : Dessiner un Triangle

- I-2-a. Résumé

- I-2-b. Source

- I-2-c. Eléments d'un Triangle

- I-2-d. Composition d'entrée

- I-2-e. Composition de sommets

- I-2-f. Création du vertex buffer

- I-2-g. Topologie des primitives

- I-2-h. Le dessin du triangle

- I-3. Tutoriel 3 : Shaders et système d'effets

- I-3-a. Résumé

- I-3-b. Source

- I-3-c. Le pipeline graphique

- I-3-d. Les shaders

- I-3-e. Les vertex shaders

- I-3-f. Les pixel shaders

- I-3-g. Système d'effets

- I-3-h. Créer l'effet et la technique d'effet

- I-3-i. Rassembler tout cela

- I-4. Tutoriel 4 : Espaces 3D

- I-4-a. Résumé

- I-4-b. Source

- I-4-c. Espaces 3D

- I-4-d. L'espace objet

- I-4-e. L'espace monde

- I-4-f. L'espace vue

- I-4-g. L'espace projection

- I-4-h. L'espace écran

- I-4-i. Transformation d'espace à espace

- I-4-j. Transformation monde

- I-4-k. Transformation vue

- I-4-l. Transformation de projection

- I-4-m. Utilisation des Transformations

- I-4-n. Modifier le Vertex Buffer

- I-4-o. Modifier le Vertex Shader

- I-4-p. Configurer les Matrices

- I-4-q. Mettre à jour les Buffers Constant

- I-5. Tutoriel 5 : Transformation 3D

- I-5-a. Résumé

- I-5-b. Source

- I-5-c. Transformation

- I-5-d. Translation

- I-5-e. Rotation

- I-5-f. Scaling

- I-5-g. Transformations multiples

- I-5-h. Créer l'orbit

- I-5-i. Le buffer de profondeur

- I-6. Tutoriel 6 : La lumière

- I-6-a. Résumé

- I-6-b. Source

- I-6-c. L'éclairage

- I-6-d. initialiser les lumières

- I-6-e. Rendu des lumières dans le Pixel Shader

- I-7. Tutoriel 7 : Mapping de texture et Constant Buffers

- I-7-a. Résumé

- I-7-b. Source

- I-7-c. Mapping de texture

- I-7-d. Creation d'un Shader Resource depuis une Texture

- I-7-e. Définir les coordonnées

- I-7-f. Attribuer une texture en tant que Shader Resource

- I-7-g. Appliquer la Texture (fx)

- I-7-h. Constant Buffers

I - Tutoriels de base

I-0 - Tutoriel 0 : Les bases de Win32

I-0-a - Résumé

Dans ce tutoriel préliminaire, nous allons passer en revue les éléments nécessaires pour créer une application Win32. Nous allons préparer une fenêtre vide pour l'utilisation de Direct3D 10.

I-0-b - Source

(SDK root)\Samples\C++\Direct3D10\Tutorials\Tutorial00

I-0-c - Préparation de la fenêtre

Chaque application Windows requiert au moins un objet fenêtre. Avant même de voir les spécificités de Direct3D 10, notre application doit avoir un objet fenêtre fonctionnel. 3 choses sont nécessaires :

1. Enregistrer la classe de la fenêtre

//
// Enregistrement de la classe de la fenêtre
//
WNDCLASSEX wcex;
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style          = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc    = WndProc;
wcex.cbClsExtra     = 0;
wcex.cbWndExtra     = 0;
wcex.hInstance      = hInstance;
wcex.hIcon          = LoadIcon(hInstance, (LPCTSTR)IDI_TUTORIAL1);
wcex.hCursor        = LoadCursor(NULL, IDC_ARROW);
wcex.hbrBackground  = (HBRUSH)(COLOR_WINDOW+1);
wcex.lpszMenuName   = NULL;
wcex.lpszClassName  = szWindowClass;
wcex.hIconSm        = LoadIcon(wcex.hInstance, (LPCTSTR)IDI_TUTORIAL1);
if( !RegisterClassEx(&wcex) )
return FALSE;

2. Créer un objet fenêtre

//
// Création de la fenêtre
//
g_hInst = hInstance; // Stock l'identifiant de l'instance de notre variable globale
RECT rc = { 0, 0, 640, 480 };
AdjustWindowRect( &rc, WS_OVERLAPPEDWINDOW, FALSE );
g_hWnd = CreateWindow( szWindowClass, L"Direct3D 10 Tutoriel 0 : Preparation de la fenetre", WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, rc.right - rc.left, rc.bottom - rc.top, NULL, NULL,
hInstance, NULL);

if( !g_hWnd )
return FALSE;

ShowWindow( g_hWnd, nCmdShow );

3. Récupérer et distribuer les messages de cette fenêtre

//
// Boucle principale des messages
//
MSG msg = {0};
while( GetMessage( &msg, NULL, 0, 0 ) )
{
TranslateMessage( &msg );
DispatchMessage( &msg );
}

LRESULT CALLBACK WndProc( HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam )
{
PAINTSTRUCT ps;
HDC hdc;

switch (message)
{
case WM_PAINT:
hdc = BeginPaint(hWnd, &ps);
EndPaint(hWnd, &ps);
break;

case WM_DESTROY:
PostQuitMessage(0);
break;

default:
return DefWindowProc(hWnd, message, wParam, lParam);
}

return 0;
}

Celles-ci sont les étapes nécessaires pour configurer un objet fenêtre qui est nécessaire pour chaque application Windows. Si nous compilons et démarrons ce code, nous verrons une fenêtre avec un fond blanc.

I-1 - Tutoriel 1 : L'essentiel de Direct3D 10

I-1-a - Sommaire

Dans ce premier tutoriel, nous allons examiner les éléments nécessaires pour créer une application minimale Direct3D 10. Toute application Direct3D 10 doit avoir ces éléments pour fonctionner correctement. Les éléments incluent la création d'une fenêtre et d'un objet device direct3D 10 puis l'affichage d'une couleur sur la fenêtre.

I-1-b - Source

(SDKracine)\Samples\C++\Direct3D10\Tutorials\Tutorial01

I-1-c - Création d'un device Direct3D 10

Maintenant que nous avons une fenêtre qui est affichée, nous pouvons continuer à établir un objet device Direct3D 10, lequel est nécessaire si nous voulons afficher des scènes 3D. La première chose à faire est de créer deux objets : un device et une chaîne d'échanges.

L'objet device est utilisé par l'application pour améliorer l'affichage sur un tampon. L'objet device Direct3D 10 contient aussi des méthodes pour créer des ressources

La chaîne d'échanges est alors chargée de lire le tampon dans lequel le device Direct3D 10 effectue le rendu et en affiche le contenu sur l'écran du moniteur. La chaîne d'échanges contient deux ou plusieurs tampons, principalement celui du devant et du fond. Ce sont des textures que le device Direct3D 10 dessine aussi pour l'affichage sur le moniteur. Le tampon du devant est celui que l'utilisateur regarde actuellement. Ce tampon est en lecture seule et ne peut être modifié. Le tampon du fond est la cible d'affichage sur laquelle le device va dessiner. Une fois que le dessin est terminé, la chaîne d'échange présentera le tampon du fond, en échangeant les deux tampons c'est-à-dire que le tampon du fond va devenir le tampon du devant et vice versa.

Pour créer la chaîne d'échanges, nous remplissons une structure DXGI_SWAPCHAIN_DESC qui décrit la chaîne d'échanges que nous sommes en train de créer. Quelques champs méritent d'être cités : BacktamponUsage est un drapeau qui dit à l'application comment le tampon du fond doit être utilisé. Dans ce cas, nous voulons afficher le tampon du fond, donc nous allons configurer la BackBufferUsage à DXGI_USAGE_RENDER_TARGET_OUTPUT. Le champ OutputWindow représente la fenêtre que la chaîne d'échanges va utiliser pour présenter des images sur l'écran. SampleDesc est utilisé pour permettre le multi-échantillonnage. Puisque ce tutoriel n'utilise pas de multi-échantillonnage, le compte de SampleDesc est configuré à 1 pour fonctionner et à 0 pour désactiver le multi-échantillonnage.

Une fois que la description a été remplie, nous pouvons appeler la fonction pour créer l'ensemble du device et de la chaîne d'échanges pour nous. Le code pour créer un device et une chaîne d'échange est listé ci-dessous :

DXGI_SWAP_CHAIN_DESC sd;
ZeroMemory( &sd, sizeof(sd) );
sd.tamponCount = 1;
sd.tamponDesc.Width = 640;
sd.tamponDesc.Height = 480;
sd.tamponDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
sd.tamponDesc.RefreshRate.Numerator = 60;
sd.tamponDesc.RefreshRate.Denominator = 1;
sd.tamponUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
sd.OutputWindow = g_hWnd;
sd.SampleDesc.Count = 1;
sd.SampleDesc.Quality = 0;
sd.Windowed = TRUE;

if( FAILED( D3D10CreateDeviceAndSwapChain( NULL, D3D10_DRIVER_TYPE_REFERENCE, NULL,
0, D3D10_SDK_VERSION, &sd, &g_pSwapChain, &g_pd3dDevice ) ) )
{
return FALSE;
}

La prochaine chose que nous avons besoin de faire est de créer une vue cible d'affichage. Une vue cible d'affichage est un type de ressources au niveau vue dans Direct3D 10. Une ressource au niveau vue permet à une ressource d'être reliée à la pipeline graphique lors d'une phase particulière. Pensez que la « ressource au niveau vue*» est comme un type cast en langage C. Un bout de mémoire brute en C peut être casté en n'importe quel type de données. Nous pouvons caster ce bout de mémoire en un tableau d'entiers, un tableau de flottants, une structure, un tableau de structures etc. La mémoire brute elle-même n'est pas trop utile pour nous si nous ne connaissons pas ce type. Les ressources au niveau vue de Direct3D 10 fonctionnent d'une manière similaire. Par exemple une texture 2D, analogue à un bout de mémoire brute est une ressource brute sous-jacente. Une fois que nous avons cette ressource nous pouvons créer différentes ressources au niveau vue pour relier cette texture aux différentes phases dans le pipeline graphique avec différents formats : une cible d'affichage qui doit dessiner, un tampon de gabarit étendu qui recevra une information étendue ou une ressource de texture. Les types cast en C permett à un bout de mémoire d'être utilisé de différentes façons, les ressources au niveau vue de Direct3D 10 font de même.

Nous avons besoin de créer une vue de cible d'affichage car nous aimerions lier le tampon du fond de notre chaîne d'échange comme étant une cible d'affichage, de telle façon que Direct3D 10 puisse l'afficher dedans. Nous appelons d'abord GetBuffer() pour obtenir l'objet du tampon du fond. Facultativement, nous pouvons remplir la structure D3D10_RENDERTARGETVIEW_DESC qui décrit la vue cible d'affichage qui va être créée. Cette description est normalement le second paramètre de CreateRenderTarget. Cependant, pour ces tutoriels, la vue cible d'affichage par défaut suffira. La vue cible d'affichage par défaut peut être obtenue en passant NULL comme second paramètre. Une fois que nous avons créé la vue cible d'affichage, nous pouvons appeler OMSetRenderTargets() pour la lier au pipeline de façon à ce qu'à la sortie, cette dernière affiche ce qu'elle va écrire dans le tampon du fond. Le code pour créer et configurer la vue cible d'affichage est écrit ci-dessous :

// creation d'une vue de cible d'affichage
ID3D10Texture2D *pBacktampon;
if( FAILED( g_pSwapChain->Gettampon( 0, __uuidof( ID3D10Texture2D ), (LPVOID*)&pBacktampon ) ) )
return FALSE;
hr = g_pd3dDevice->CreateRenderTargetView( pBacktampon, NULL, &g_pRenderTargetView );
pBacktampon->Release();
if( FAILED( hr ) )
return FALSE;
g_pd3dDevice->OMSetRenderTargets( 1, &g_pRenderTargetView, NULL );

La dernière chose que nous avons besoin de faire avant que Direct3D 10 puisse afficher est d'initialiser la zone d'affichage. Les coordonnées de l'espace cadré du plan de la zone d'affichage, qui va de -1 à 1 pour x et y, et de 0 à 1 pour z, affiche l'espace cible. Ce dernier est aussi connu sous le nom de "l'espace à pixel". Dans Direct3D 9, une zone d'affichage par défaut est créée pour être de la même taille que la cible d'affichage si l'application n'en crée pas d'autres. Dans Direct3D 10, il n'y a pas de zone d'affichage créée par défaut. Par conséquent, nous devons en créer une avant que nous puissions voir quelque chose sur l'écran. Puisque nous aimerions utiliser la cible d'affichage entière pour la sortie, nous allons situer le point du sommet gauche à (0, 0) et la largeur et la hauteur sera identique à la taille de la cible d'affichage. Le code qui illustre ceci est présenté ci-dessous :

D3D10_VIEWPORT vp;
vp.Width = 640;
vp.Height = 480;
vp.MinDepth = 0.0f;
vp.MaxDepth = 1.0f;
vp.TopLeftX = 0;
vp.TopLeftY = 0;
g_pd3dDevice->RSSetViewports( 1, &vp );

I-1-d - En modifiant la boucle de messages

Nous avons créée la fenêtre et le device Direct3D 10, et nous sommes prêts à afficher. Cependant, il y a toujours un problème avec notre boucle de messages : cette dernière utilise GetMessage() pour obtenir des messages. Le problème avec GetMessage() est que s'il n'y a pas de message dans la file pour la fenêtre applicatrice, GetMessage() bloque et ne retourne pas son résultat jusqu'à ce qu'un message soit libre. Par conséquent, au lieu d'afficher, notre application bloque à l'intérieur de GetMessage() quand la file de message est vide. Nous pouvons résoudre ce problème en utilisant PeekMessage() à la place de GetMessage(). PeekMessage() peut récupérer un message comme GetMessage() le fait, mais quand il n'y a pas de message qui attend, PeekMessage() retourne immédiatement son résultat au lieu de bloquer. Nous pouvons alors afficher quelque chose. La boucle de messages modifiée, qui utilise PeekMessage(), ressemble a ceci :

MSG msg = {0};
while( WM_QUIT != msg.message )
{
if( PeekMessage( &msg, NULL, 0, 0, PM_REMOVE ) )
{
TranslateMessage( &msg );
DispatchMessage( &msg );
}
else
{
Render();  // fait l'affichage
}
}

I-1-e - Le code de l'affichage

L'affichage est fait dans la fonction Render(). Dans ce tutoriel, nous afficherons la scène la plus simple possible, qui est de remplir l'écran avec une seule couleur. Dans Direct3D 10, une façon simple de remplir la cible d'affichage avec une seule couleur est d'utiliser la méthode ClearRenderTargetView() du device. Nous définissons d'abord une structure D3D10_COLOR qui décrit la couleur avec laquelle nous voudrions remplir l'écran, puis on transmet cette structure à ClearRenderTargetView(). Dans cet exemple, un shader de bleu est choisi. Une fois que nous avons rempli notre tampon du fond, nous appelons la méthode Present() de la chaîne d'échanges pour compléter l'affichage. Present() est responsable de l'affichage du contenu du tampon de la chaîne d'échanges sur l'écran de sorte que l'utilisateur puisse le voir. La fonction Render() ressemble à ceci :

void Render()
{
//
// nettoie le tampon du fond
//
float ClearColor[4] = { 0.0f, 0.125f, 0.6f, 1.0f }; // RGBA
g_pd3dDevice->ClearRenderTargetView( g_pRenderTargetView, ClearColor );

g_pSwapChain->Present( 0, 0 );
}

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.0f, 0.5f, 0.5f ),
D3DXVECTOR3( 0.5f, -0.5f, 0.5f ),
D3DXVECTOR3( -0.5f, -0.5f, 0.5f ),
};
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 );
}

I-3 - Tutoriel 3 : Shaders et système d'effets

I-3-a - Résumé

Dans le tutoriel précédent, nous avons configuré un vertex buffer et envoyé un triangle au GPU. Maintenant, nous allons en fait passer par étapes au travers du pipeline graphique et regarder comment chaque niveau fonctionne. Le concept de shaders et du système d'effets sera expliqué.

Notez que ce tutoriel partage le même code source que le précédent, mais correspondera à différentes sections.

I-3-b - Source

(SDK root)\Samples\C++\Direct3D10\Tutorials\Tutorial03

I-3-c - Le pipeline graphique

Dans le tutoriel précédent, nous avons configuré le vertex buffer, et ensuite, nous avons associé une composition de sommets avec un objet technique. Maintenant, nous allons expliquer l'objet technique et les shaders qui le composent. Pour comprendre pleinement chacun des shaders, nous allons faire un pas en arrière et regarder l'intégralité du pipeline graphique.

Dans le tutoriel 2, quand nous avons appelé Apply depuis la technique, nous avons en fait limité notre shader à un niveau du pipeline. Ensuite, quand nous avons appelé Draw, nous avons commencé à traiter les données des sommets passées dans le pipeline graphique. Les sections suivantes décrivent en détail ce qu'il se produit après la commande Draw.

I-3-d - Les shaders

Dans Direct3D 10, les shaders résident dans différents niveaux du pipeline graphique. Ce sont de courts programmes qui, exécutés par le GPU, prennent certaines données d'entrées, traitent ces données, et ensuite, produisent le résultat vers le niveau suivant du pipeline. Direct3D 10 supporte 3 types de shaders : le vertex shader, le geometry shader, et le pixel shader. Un vertex shader prend un sommet comme entrée. Il est utilisé une seule fois pour tous les sommets passés au GPU via des vertex buffers. Un geometry shader prend une primitive comme entrée, et est utilisé une seule fois pour toutes les primitives passées au GPU. Une primitive est un point, une ligne, ou un triangle. Un pixel shader prend un pixel (ou parfois appelé un fragment) comme entrée, et est utilisé une seule fois pour chaque pixel d'une primitive que nous souhaitons afficher. Ensemble, les vertex, geometry, et pixel shaders se situent où l'essentiel de l'action se produit. Lors de l'affichage avec Direct3D 10, le GPU doit avoir un vertex shader et un pixel shader valide. Le geometry shader est une fonctionnalité avancée dans Direct3D 10 et est optionnel, donc nous ne discuterons pas des geometry shaders dans ce tutoriel.

I-3-e - Les vertex shaders

Les vertex shaders sont de courts programmes qui sont executés par le GPU sur des sommets. Pensez aux vertex shaders comme des fonctions C qui prennent chaque sommet comme entrée, traitent l'entrée, et ensuite produisent le sommet modifié. Après que l'application ait passé les données des sommets au GPU sous la forme d'un vertex buffer, le GPU itère au travers les sommets du vertex buffer, et exécute le vertex shader actif une fois pour chaque sommet, en passant les données des sommets au vertex shader en tant que paramètres d'entrées.

Tant qu'un vertex shader peut être utilisé pour effectuer plusieurs tâches, le travail le plus important d'un vertex shader est la transformation. La transformation est le procédé de conversion des vecteurs d'un système de coordonnée vers un autre. Par exemple, un triangle dans une scène 3D peut avoir ses sommets aux positions (0, 0, 0) (1, 0, 0) (0, 1, 0). Quand le triangle est dessiné sur un buffer de texture 2D, le GPU doit connaître les coordonnées 2D des points du buffer où les sommets doivent être dessinés. C'est la transformation qui nous aide à accomplir celà. La transformation sera traitée en détails dans le tutoriel suivant. Pour ce tutoriel, nous utiliserons un vertex shader simple, qui ne fait rien excepté passer les données d'entrée en tant que sortie.

Dans les tutoriels Direct3D 10, nous écrirons nos shaders en High-Level Shading Language (HLSL), et les applications utiliseront ces shaders avec le système d'effets. Rappelez vous que nos données sommets ont une position 3D, et le vertex shader ne traitera pas du tout l'entrée. Le vertex shader résultant ressemblera à ceci :

float4 VS( float4 Pos : POSITION ) : SV_POSITION
{
return Pos;
}

Ce vertex shader ressemble fortement à une fonction C. HLSL utilise une syntaxe ressemblant au C pour rendre l'apprentissage plus simple pour les programmeurs C/C++. Nous pouvons voir que ce vertex shader, nommé VS, prend un paramètre de type float4 et retourne une valeur float4. Dans HLSL, un float4 est un vecteur à 4 composants, où chaque composant est un nombre à virgule flottante. Les deux points définissent les sémantiques du paramètre ainsi que la valeur retournée. Comme mentionné plus haut, les sémantiques dans HLSL décrivent la nature des données. Dans notre shader précédent, nous choisissons POSITION comme sémantique du paramètre d'entrée Pos car ce paramètre contiendra la position du sommet. Les sémantiques de la valeur retournée, SV_POSITION, sont des sémantiques prédéfines avec des significations spéciales. Ces sémantiques disent au pipeline graphique que les données associées aux sémantiques définissent la position dans l'espace cadré. Cette position est requise par le GPU afin de dessiner les pixels à l'écran. (On discutera de l'espace cadré dans le tutoriel suivant.) Dans notre shader, on rend la donnée de position d'input et d'output de la même manière dans le pipeline.

I-3-f - Les pixel shaders

Les moniteurs d'ordinateurs modernes sont habituellement des affichages matriciels, ce qui signifie que l'écran est en fait une grille à deux dimensions de petits points appelés pixels. Chaque pixel contient une couleur indépendante des autres pixels. Quand nous affichons un triangle à l'écran, nous n'affichont pas réellement un triangle en tant qu'une seule entité. Mais plutôt, nous allumons un groupe de pixels qui sont couverts par la zone du triangle. L'image 2 montre une illustration de ceci.

A Gauche : ce que nous souhaitons dessiner. A Droite : ce qui se trouve réellement sur l'écran.

Le procédé de conversion d'un triangle défini par 3 sommets vers un ensemble de pixels couverts par le triangle est appelé "rasterisation". Le GPU détermine d'abord quels pixels sont couverts par le triangle qui est dessiné. Ensuite, il invoque le pixel shader actif pour chacun de ces pixels. Le but premier d'un pixel shader est de calculer la couleur que chaque pixel doit avoir. Le shader prend certaines entrées du pixel qui va être colorié, calcule la couleur du pixel, ensuite retourne cette couleur vers le pipeline. Les entrées qu'il prend proviennent du geometry shader actif, ou, si un geometry shader n'est pas présent, comme dans ce tutoriel, l'input provident directement du vertex shader.

Le vertex shader que nous avons créé précédemment produit un float4 avec les sémantiques SV_POSITION. Cela sera l'entrée de notre pixel shader. Comme les pixel shaders produisent des valeurs de couleur, la sortie de notre pixel shader sera un float4. On donne à la sortie les sémantiques SV_TARGET pour signifier la sortie vers le format de l'affichage ciblé. Le pixel shader ressemble à ceci :

float4 PS( float4 Pos : SV_POSITION ) : SV_Target
{
return float4( 1.0f, 1.0f, 0.0f, 1.0f );  // Jaune, avec Alpha = 1
}

I-3-g - Système d'effets

Notre fichier d'effet se compose de deux shaders, un vertex shader et un pixel shader, et la définition de la technique. La définition de la technique définira les shaders correspondants pour chaque section. En plus, il y a aussi la semantique pour compiler le shader. Notez que le geometry shader est laissé à NULL car il n'est pas nécessaire et sera traité plus tard.

// Définition de la technique
technique10 Render
{
pass P0
{
SetVertexShader( CompileShader( vs_4_0, VS() ) );
SetGeometryShader( NULL );
SetPixelShader( CompileShader( ps_4_0, PS() ) );
}
}

I-3-h - Créer l'effet et la technique d'effet

Dans le code de l'application, nous devrons créer un objet effet. Cet objet effet représente notre fichier d'effet, et est créé en appelant D3D10CreateEffectFromFile(). Une fois que nous avons créé l'objet d'effet, nous pouvons appeler la méthode ID3D10Effect::GetTechniqueByName(), en passant "Render" en tant que nom, pour obtenir l'objet technique qui sera utilisé pour faire l'affichage actuel. Le code est démontré ci-dessous :

// Crée l'effet
if( FAILED( D3DX10CreateEffectFromFile( L"Tutorial03.fx", NULL, NULL, D3D10_SHADER_ENABLE_STRICTNESS, 0, g_pd3dDevice, NULL, NULL, &g_pEffect, NULL ) ) )
return FALSE;

// Obtient la technique
g_pTechnique = g_pEffect->GetTechniqueByName( "Render" );

I-3-i - Rassembler tout cela

Après avoir parcouru le pipeline graphique, nous pouvons commencer à comprendre le procédé d'affichage du triangle que nous avons créé au début du tutoriel 2. La création des applications Direct3D requiert deux étapes distinctes. La première serait de créer la source des données dans les données de sommets, comme nous avons fait dans le tutoriel 2; le second niveau sera de créer les shaders qui transformeront ces données pour l'affichage, qui a été montré dans ce tutoriel.

I-4 - Tutoriel 4 : Espaces 3D

I-4-a - Résumé

Dans le tutoriel précédent, nous avons réussi à afficher un triangle au centre de la fenêtre de notre application. Nous n'avons pas prêté beaucoup d'attention aux positions des sommets que nous avons pris dans notre vertex buffer. Dans ce tutoriel, nous allons rentrer dans le détail des positions 3D et des transformations.

L'objectif de ce tutoriel sera d'avoir un objet 3D affiché à l'écran. Là où le précédent tutoriel se concentrait sur l'affichage d'objet 2D dans un monde 3D, ici nous afficherons un objet 3D.

I-4-b - Source

(SDK root)\Samples\C++\Direct3D10\Tutorials\Tutorial04

I-4-c - Espaces 3D

Dans le tutoriel précédent, les sommets du triangle étaient placés stratégiquement pour s'aligner parfaitement sur l'écran. Cependant, ce ne sera pas toujours le cas, et donc, nous avons besoin d'un système pour décrire les objets dans l'espace 3D et un système pour les afficher.

Dans le monde réel, les objets existent dans un espace 3D. Cela signifie que pour placer un objet dans une position particulière dans le monde, nous aurons besoin d'utiliser un système de coordonnées et de définir 3 coordonnées qui correspondent à la position. Dans le graphisme par ordinateur, les espaces 3D sont communément dans un système de coordonnées Cartésien. Dans ce système de coordonnées, 3 axes, X, Y, et Z, perpendiculaires les uns envers les autres, dictant les coordonnées que chaque point dans l'espace possède. Ce système de coordonnées est de plus divisé en un système main gauche et main droite. Dans un système main gauche, lorsque l'axe X pointe vers la droite et l'axe Y pointe vers le haut, et l'axe Z pointe vers l'avant. Dans un système en main droite, avec les mêmes axes X et Y, l'axe Z pointe vers l'arrière.

Figure 1 : Système de coordonnées main gauche contre main droite

Maintenant que nous avons abordé le système de coordonnées, considérons des espaces 3D. Un point a différentes coordonnées dans différents espaces. Dans un exemple en 1D, supposons que nous ayons une règle et que nous notons le point P, à la marque 5 cm de la règle. Maintenant, si nous déplaçons la règle d'1 cm vers la droite, le même point se trouve à la marque 4 cm. En déplaçant la règle, l'image de référence a changé, et ainsi, bien que le point n'ait pas bougé, il possède une nouvelle coordonnée.

Figure 2 : Illustration de l'espace 1D

En 3D, un espace est typiquement défini par une origine et trois axes uniques depuis l'origine : X, Y et Z. Il y a plusieurs espaces habituellement utilisés dans le graphisme par ordinateur : l'espace objet, l'espace monde, l'espace vue, l'espace de projection et l'espace écran.

Figure 3 : Un cube défini dans l'espace objet

I-4-d - L'espace objet

Remarquez que le cube est centré sur l'origine. L'espace objet, également appelé l'espace du modèle, correspond à l'espace utilisé par les artistes quand ils créent les modèles 3D. Habituellement, les artistes créent des modèles qui sont centrés autour de l'origine afin qu'il soit plus facile d'effectuer des transformations, telles que la rotation des modèles, comme nous le verrons quand nous nous interresserons aux transformations. Les 8 sommets ont les coordonnées suivantes :

(-1, 1, -1)
( 1, 1, -1)
(-1, -1, -1)
( 1, -1, -1)
(-1, 1, 1)
( 1, 1, 1)
(-1, -1, 1)
( 1, -1, 1)

Parce que l'espace objet est ce que les artistes utilisent habituellement quand ils dessinent et créent des modèles, les modèles qui sont stockés sur le disque sont également dans l'espace objet. Une application peut créer un vertex buffer pour représenter un tel modèle et initialiser le buffer avec les données du modèle. Ainsi, les sommets dans le vertex buffer seront habituellement aussi dans l'espace objet. Cela signifie aussi que le vertex shader reçoit en entrée les données des sommets dans l'espace objet.

I-4-e - L'espace monde

L'espace Monde (World space) est un espace partagé par chaque objet dans la scène. Il est utilisé pour définir la relation spatiale entre les objets que l'on souhaite afficher (render). Pour visualiser l'espace Monde, on pourrait imaginer que nous nous trouvons dans le coin sudouest d'une pièce rectangulaire qui fait face au nord. On définit le coin où nos pieds se trouvent comme étant l'origine (0, 0, 0). L'axe X se trouve à notre droite; l'axe Y vers le haut; et l'axe Z vers l'avant, la même direction que celle dont nous faisons face. Quand on effectue ceci, chaque position dans la pièce peut être identifiée avec une série de coordonnées XYZ. Par exemple, il pourrait y avoir une chaise à 5 pieds en face et 2 pieds à notre droite. Il pourrait y avoir une lampe de 8 pieds de haut sur le plafond directement haut-dessus de la chaise. On peut se référer à la position de la chaise qui est (2, 0, 5) et la position de la lampe qui est (2, 8, 5). Comme nous pouvons le voir, l'espace monde est appelé ainsi car il nous dit où les objets sont en relation les uns les autres dans le monde.

I-4-f - L'espace vue

L'espace vue, parfois appelé espace caméra, est similaire à l'espace monde en cela qu'il est typiquement utilisé pour la scène entière. Cependant, dans l'espace vue, l'origine se trouve sur le spectateur ou la caméra. La direction de la vue (où le spectateur regarde) défini l'axe Z positif. Une direction "haut" définie par l'application devient l'axe Y positif comme expliqué ci-dessous.

Figure 4 : Le même objet en espace monde (gauche) et en espace vue (droite)

L'image de gauche montre une scène qui se compose d'un objet ressemblant à un humain et un spectateur (caméra) regardant l'objet. L'origine et les axes qui sont utilisés par l'espace monde sont indiqués en rouge. L'image de droite montre l'espace vue en relation avec l'espace monde. Les axes de l'espace vue sont indiqués en bleu. Pour éclaircir l'illustration, l'espace vue ne possède pas la même orientation que l'espace monde sur l'image de gauche pour les lecteurs. Notez que dans l'espace vue, le spectateur regarde dans la direction Z.

I-4-g - L'espace projection

L'espace projection réfère à l‘espace après l'application de la transformation de projection depuis l'espace vue. Dans cet espace, le contenu visible a les coordonnées X et Y compris entre -1 et 1, et la coordonnée Z allant de 0 à 1.

I-4-h - L'espace écran

L'espace écran est parfois utilisé pour se référer aux localisations dans le frame buffer. Comme le frame buffer est habituellement une texture 2D, l'espace écran est un espace 2D. Le coin supérieur gauche est l'origine avec les coordonnées (0, 0). Le X positif va vers la droite et le Y positif pointe vers le bas. Pour un tampon qui est de w pixels de large et h pixels de haut, le pixel le plus bas sur la droite a les coordonnées (w - 1, h - 1).

I-4-i - Transformation d'espace à espace

Les transformations sont les plus habituellement utilisées pour convertir les sommets d'un espace vers un autre. En graphisme 3D par ordinateur, il y a logiquement 3 telles transformations dans le pipeline : transformation monde, vue, et projection. Les opérations de transformations individuelles telles que la translation, la rotation, et la mise à l'échelle sont étudiées dans le tutoriel suivant.

I-4-j - Transformation monde

La transformation monde, comme son nom le suggère, convertit les sommets depuis l'espace objet vers l'espace monde. Il se compose habituellement d'une ou plusieurs mise à l'échelle, rotations et translations, basées sur la taille, l'orientation et la position que nous voulons donner à l'objet. Chaque objet dans la scène possède sa propre matrice de transformation monde. C'est parce que chaque objet a sa propre taille, orientation, et position.

I-4-k - Transformation vue

Après que les sommets soient convertis dans l'espace monde, la transformation vue convertit ces sommets depuis l'espace monde vers l'espace vue. En se souvenant de la conversation plus haut, disant que l'espace vue est ce que le monde paraît depuis la vue du spectateur (ou la caméra). En espace vue, le spectateur est localisé à l'origine, regardant le long de l'axe Z positif.

Il faut noter que bien que l'espace vue soit le monde depuis l'image de référence du spectateur, la matrice de transformation vue est appliquée aux sommets, pas au spectateur. D'où, la matrice de vue doit effectuer la transformation inverse que nous appliquons à notre spectateur ou caméra. Par exemple, si nous voulons déplacer la caméra de 5 unités vers la direction -Z, nous aurions besoin de calculer une matrice de vue qui déplace les sommets de 5 unités le long de la direction +Z. Bien que la caméra ait reculé, les sommets, depuis le point de vue de la caméra, ont avancé. Dans Direct3D, une API appelée D3DXMatrixLookAtLH() est souvent utilisée pour calculer une matrice de vue. Nous aurons simplement besoin de lui dire où le spectateur se trouve, où il regarde, et la direction représentant le haut du spectateur, également appelée le vecteur haut, pour obtenir une matrice de vue correspondante.

I-4-l - Transformation de projection

La transformation de projection convertit les sommets depuis les espaces 3D comme les espaces monde et vue vers l'espace projection. Dans l'espace projection, les coordonnées X et Y d'un sommet sont obtenues depuis les ratios X/Z et Y/Z de ce sommet dans l'espace 3D.

Figure 5 : Projection

Dans l'espace 3D, les choses apparaissent en perspective. Ainsi, plus un objet est proche, plus grand il apparaît. Comme il est montré que le somemt d'un arbre à h unités de haut et à d unités de l'oeil du spectateur, apparaîtra au même point que le sommet d'un autre arbre de 2h d'unités de haut et 2d unités de distance. De cela, où un sommet apparaît sur un écran 2D est directement affecté vers ses ratios X/Z et Y/Z.

Un des paramètres qui définit un espace 3D est appelé champ de vision (field-of-view en anglais - FOV). Le FOV dénote quels objets sont visibles depuis une position particulière, pendant que l'on regarde dans une direction particulière. Les humains ont un champ de vision qui est vers l'avant (on ne peut pas voir ce qui se passe derrière nous) et on ne peut pas voir les objets qui sont trop proches ou trop loin. En graphisme par ordinateur, le champ de vision est contenu dans un frustum de vue. Le frustum de vue est défini par 6 plans en 3D. Deux de ces plans sont parallèles au plan XY. Ceux-ci sont appelés le plans Z proche et le plan Z éloigné. Les 4 autres plans sont définis par le champ de vision horizontal du spectateur et le champ de vision vertical. Le FOV le plus large est où le volume frustrum est le plus large et où le spectateur voit le plus d'objets.

Le GPU filtre les objets qui sont en dehors du frustum de vue afin qu'il n'ait pas à dépenser du temps à dessiner quelque chose qui ne sera pas affiché. Ce procédé est appelé clipping. Le frustum de vue est une pyramide à 4 côtés avec un toit découpé. Le clipping de ce volume est compliqué car pour clipper par rapport à un plan du frustrum de vue, le GPU doit comparer chaque sommet à l'équation du plan. A la place, le GPU effectue généralement d'abord la transformation de projection, et ensuite clippe sur le volume du frustrum de vue. L'effet de transformation de projection sur le frustum de vue est que la forme du frustrum de vue pyramidale devient une boîte dans l'espace projection. C'est parce que, comme mentionné plus haut, dans l'espace projection, les coordonnées X et Y sont basées sur les X/Z et Y/Z dans l'espace 3D. Donc, le point a et le point b auront la même coordonnée X et Y dans l'espace de projection, c'est pourquoi le frustum de vue devient une boîte.

Figure 6 : Cône de vue

Supposez que le sommet de deux arbres se trouve exactement sur le haut une arrête du frustum de vue. De plus, supposons que d = 2h. La coordonnée Y le long de l'arrête supérieure dans l'espace projection sera ensuite 0.5 (car h/d = 0.5). Donc, les valeurs Y après projection qui sont plus grandes que 0.5 seront clippées par le GPU. Le problème ici est que 0.5 est déterminé par le champ de vision vertical choisi par le programme, et différentes valeurs de FOV produisent différentes valeurs que le GPU doit clipper à nouveau. Pour rendre le procédé plus convenable, les programmes 3D mettent généralement à l'échelle les valeurs projetées de X et Y des sommets afin que les valeurs X et Y visibles aient une portée de -1 à 1. En d'autres mots, tout ce qui a des coordonnées X et Y qui se trouvent en dehors de la portée [-1 1] sera clippé. Pour que ce schéma de clipping fonctionne, la matrice de projection doit mettre à l'échelle les coordonnées X et Y des sommets projetés par l'inverse de h/d, ou d/h. d/h est également la co-tangeante de la moitié du FOV. Avec la mise à l'échelle, le haut du view frustum devient h/d * d/h = 1. Tout ce qui est plus grand que 1 sera clippé par le GPU. C'est ce que nous voulons.

Un ajustement similaire est généralement effectué pour la coordonnée Z dans l'espace projection. On souhaiterait que le plan Z proche et le plan Z éloigné soit respectivement à 0 et 1 dans l'espace projection. Quand Z = la valeur Z proche dans l'espace 3D, Z doit être 0 dans l'espace projection; quand Z = la valeur Z éloigné dans l'espace 3D, Z doit être 1 dans l'espace projection. Après que ceci soit effectué, les valeurs Z en dehors de [0 1] seront clippées par le GPU.

Dans Direct3D 10, la manière la plus simple d'obtenir une matrice de projection est d'appeler la méthode D3DXMatrixPerspectiveFovLH(). On fournit simplement 4 paramètres-- FOVy, Aspect, Zn, et Zf -- et elle retourne une matrice qui effectue tout ce qui est nécessaire comme mentionné ci-dessus. FOVy est le FOV dans la direction Y. Aspect est le ratio d'aspect, qui est le ratio de l'espace vue de la largeur sur la hauteur. A partir de FOVy et Aspect, FOVx peut être calculé. Ce ratio d'aspect est habituellement obtenu depuis le ratio de la largeur sur la hauteur de l'affichage ciblé. Zn et Zf sont respectivement les valeurs Z proche et Z éloigné dans l'espace vue.

I-4-m - Utilisation des Transformations

Dans le tutoriel précédent, nous avons écrit un programme qui affiche un seul triangle à l'écran. Quand on crée le vertex buffer, les positions des sommets que nous utilisons, sont directement dans l'espace projection afin que nous n'ayons pas à effectuer de transformation. Maintenant que nous avons une compréhension de l'espace 3D et des transformations, nous allons modifier le programme afin que le vertex buffer soit défini dans l'espace objet, comme il doit l'être. Ensuite, nous modifierons notre vertex shader pour transformer les sommets depuis l'espace objet vers l'espace projection.

I-4-n - Modifier le Vertex Buffer

Comme nous avons commencé à représenter les choses en 3 dimensions, nous avons changé le triangle plat du tutoriel précédent en un cube. Cela nous permettra de démontrer ces concepts plus clairement.

SimpleVertex vertices[] =
{
{ D3DXVECTOR3( -1.0f, 1.0f, -1.0f ), D3DXVECTOR4( 0.0f, 0.0f, 1.0f, 0.0f ) },
{ D3DXVECTOR3( 1.0f, 1.0f, -1.0f ), D3DXVECTOR4( 0.0f, 1.0f, 0.0f, 0.0f ) },
{ D3DXVECTOR3( 1.0f, 1.0f, 1.0f ), D3DXVECTOR4( 0.0f, 1.0f, 1.0f, 0.0f ) },
{ D3DXVECTOR3( -1.0f, 1.0f, 1.0f ), D3DXVECTOR4( 1.0f, 0.0f, 0.0f, 0.0f ) },
{ D3DXVECTOR3( -1.0f, -1.0f, -1.0f ), D3DXVECTOR4( 1.0f, 0.0f, 1.0f, 0.0f ) },
{ D3DXVECTOR3( 1.0f, -1.0f, -1.0f ), D3DXVECTOR4( 1.0f, 1.0f, 0.0f, 0.0f ) },
{ D3DXVECTOR3( 1.0f, -1.0f, 1.0f ), D3DXVECTOR4( 1.0f, 1.0f, 1.0f, 0.0f ) },
{ D3DXVECTOR3( -1.0f, -1.0f, 1.0f ), D3DXVECTOR4( 0.0f, 0.0f, 0.0f, 0.0f ) },
};

Si vous le remarquez, tout ce que nous avons fait est de spécifier les huit points du cube, mais nous n'avons actuellement pas décrit individuellement les triangles. Si nous avions passé ceci tel quel, la sortie ne serait pas ce que l'on attend. Nous devons spécifier les triangles qui forment le cube au travers de ces huit points.

Pour un cube, beaucoup de triangles partageront le même sommet et ce serait une perte de mémoire de les redéfinir encore et encore. Ainsi, il y a une méthode pour spécifier juste les huit points et ensuite laisser Direct3D savoir quels points prendre pour un triangle. Cela est effectué au travers d'un index buffer. Un index buffer contiendra une liste, qui réfèrera à l'indice des sommets dans le buffer, pour spécifier quels points utiliser dans chaque triangle. Le code ci-dessous montre quels points compose chacun de nos triangles.

// Créé un index buffer
DWORD indices[] =
{
3,1,0,
2,1,3,
0,5,4,
1,5,0,
3,4,7,
0,4,3,
1,6,5,
2,6,1,
2,7,6,
3,7,2,
6,4,5,
7,4,6,
};

Comme vous pouvez le voir, le premier triangle est défini par les points 3, 1, et 0. Cela signifie que le premier triangle a ses sommets respectivement à : ( -1.0f, 1.0f, 1.0f ),( 1.0f, 1.0f, -1.0f ), et ( -1.0f, 1.0f, -1.0f ). Il y a six faces pour le cube, et chaque face est composée de deux triangles, donc, vous voyez 12 triangles au total ici défini.

Comme chaque sommet est explicitement listé, et qu'aucun des deux triangles ne partage des arrêtes (au moins, de la façon dont ils ont été définis), ceci est considéré comme une liste de triangles. Au total, pour les 12 triangles dans la liste de triangle, nous devrons avoir au total 36 sommets.

La création de l'index buffer est très similaire au vertex buffer, où nous spécifions les paramètres tels que la taille et le type dans une structure, et on a appelé CreateBuffer. Le type est D3D10_BIND_INDEX_BUFFER, et comme nous avons declaré notre tableau en utilisant des DWORD, on utilisera sizeof(DWORD).

bd.Usage = D3D10_USAGE_DEFAULT;
bd.ByteWidth = sizeof( DWORD ) * 36; // 36 sommets nécessaires pour 12 triangles dans une triangle list
bd.BindFlags = D3D10_BIND_INDEX_BUFFER;
bd.CPUAccessFlags = 0;
bd.MiscFlags = 0;
InitData.pSysMem = indices;
if( FAILED( g_pd3dDevice->CreateBuffer( &bd, &InitData, &g_pIndexBuffer ) ) )
return FALSE;

Une fois que l'on a créé ce buffer, nous devrons le définir afin que Direct3D sache se référer à cet index buffer lors de la génération des triangles. On spécifie le pointeur vers le buffer, le format, et le décalage dans le buffer pour commencer le référencement.

// Défini l'index buffer
g_pd3dDevice->IASetIndexBuffer( g_pIndexBuffer, DXGI_FORMAT_R32_UINT, 0 );

I-4-o - Modifier le Vertex Shader

Dans notre vertex shader du tutoriel précédent, on a pris la même position d'entrée et de sortie de sommet sans aucune modification. On peut faire cela car la position d'entrée du sommet est déjà définie dans l'espace projection. Maintenant, comme la position d'entrée du sommet est définie dans l'espace objet, nous devons la transformer avant de la sortir du vertex shader. On effectue ceci en 3 étapes : on transforme depuis l'espace objet vers l'espace monde, on transforme de l'espace monde vers l'espace vue, et on transforme de l'espace vue vers l'espace projection. La première chose que nous devons faire est de déclarer 3 variables de buffer constants. Les buffers constants sont utilisés pour stocker des données que l'application a besoin de passer aux shaders. Avant l'affichage, l'application écrit habituellement des données importantes dans les buffers constants, et ensuite durant l'affichage, les données peuvent être lues depuis l'intérieur des shaders. Dans un fichier FX, des variables de buffer constants sont déclarées comme des variables globales en C++. Les 3 variables que nous utiliserons sont les matrices de transformation monde, vue et projection de type HLSL "matrix".

Une fois que nous avons déclaré les matrices dont nous aurons besoin, on met à jour notre vertex shader pour transformer la position d'entrée en utilisant les matrices. Un vecteur est transformé en multipliant le vecteur par une matrice. Dans HLSL, ceci est effectué en utilisant la fonction intrinsèque mul(). Notre déclaration de variable et le nouveau vertex shader sont montrés ci-dessous :

matrix World;
matrix View;
matrix Projection;
//
// Vertex Shader
//
VS_OUTPUT VS( float4 Pos : POSITION, float4 Color : COLOR )
{
VS_OUTPUT output = (VS_OUTPUT)0;
output.Pos = mul( Pos, World );
output.Pos = mul( output.Pos, View );
output.Pos = mul( output.Pos, Projection );
output.Color = Color;
return output;
}

Dans le vertex shader, chaque mul() applique une transformation à la position d'entrée. Les transformations monde, vue, projection sont appliquées dans cet ordre séquentiellement. Ceci est nécessaire car la multiplication de vecteurs et de matrices n'est pas commutative.

I-4-p - Configurer les Matrices

Nous avons mis à jour notre vertex shader pour transformer en utilisant les matrices, mais nous avons aussi besoin de définir 3 matrices dans notre programme. Ces trois matrices stockeront la transformation pour être utilisées lors de l'affichage. Avant l'affichage, on copie les valeurs de ces matrices dans le buffer constant de shader. Ensuite, quand on débute l'affichage en appelant Draw(), notre vertex shader lit les matrices stockées dans le buffer constant. En plus des matrices, nous avons aussi besoin de définir 3 pointeurs ID3D10EffectMatrixVariable. ID3D10EffectMatrixVariable est un objet qui représente une matrice dans le buffer constant. Nous pouvons utiliser ces 3 objets pour copier les matrices vers le buffer constant. Ainsi, nos variables globales possèdent ceci en plus :

ID3D10EffectMatrixVariable* g_pWorldVariable = NULL;
ID3D10EffectMatrixVariable* g_pViewVariable = NULL;
ID3D10EffectMatrixVariable* g_pProjectionVariable = NULL;
D3DXMATRIX g_World;
D3DXMATRIX g_View;
D3DXMATRIX g_Projection;

Pour initialiser les 3 objets ID3D10EffectMatrixVariable, on appelle ID3D10Effect::GetVariableByName() après la création de l'effet, en passant le nom de la variable qui nous intéresse. GetVariableByName() retourne une interface ID3D10EffectVariable, même si l'objet sous-jacent est spécifique au type de variable définie dans le fichier FX. Dans ce cas, les objets sous jacents sont ID3D10EffectMatrixVariable. Pour obtenir une interface ID3D10EffectMatrixVariable depuis un ID3D10EffectVariable, on peut appeler sa méthode AsMatrix() :

g_pWorldVariable = g_pEffect->GetVariableByName( "World" )->AsMatrix();
g_pViewVariable = g_pEffect->GetVariableByName( "View" )->AsMatrix();
g_pProjectionVariable = g_pEffect->GetVariableByName( "Projection" )->AsMatrix();

La chose suivante que nous avons besoin de faire est de pointer avec les 3 matrices que nous utiliserons pour faire la transformation. Nous voulons que le triangle soit à l'origine, parallèle au plan XY. Voici exactement comment cela est stocké dans le vertex buffer dans l'espace objet. Ainsi, la transformation monde ne doit rien faire, et nous initialisons la matrice monde avec une matrice identité. Nous souhaitons configurer notre caméra afin qu'elle soit située à [0 1 -5], regardant au point [0 1 0]. On peut appeler D3DXMatrixLookAtLH() pour calculer correctement une matrice de vue pour nous en utilisant le vecteur haut [0 1 0] car nous souhaitons toujours que la direction Y+ reste toujours vers le haut. Finalement, pour avoir une matrice de projection, on appelle D3DXMatrixPerspectiveFovLH(), avec un FOV vertical de 90 degrés (pi/2), un ratio d'aspect de 640/480 qui apporte la taille de notre back buffer, et les Z proche et éloigné respectivement à 0.1 et 100. Cela signifie que tout ce qui est plus proche que 0.1 ou plus loin que 100 ne sera pas visible à l'écran. Ces trois matrices sont stockées dans les variables globales g_World, g_View, et g_Projection.

I-4-q - Mettre à jour les Buffers Constant

Nous avons les matrices, et maintenant nous devons les écrire dans le buffer constant quand nous dessinons afin que le GPU puisse les lire. Les buffers constants sont étudiés en profondeur dans le prochain tutoriel. Pour l'instant, considérez-les comme les conteneurs pour les constantes passées aux shaders. Car nous écrivons les matrices dans le buffer constant, on appelle la méthode de SetMatrix() de ID3D10EffectMatrixVariable, en lui passant la matrice à écrire dans le buffer. Ceci doit être effectué avant que l'on fasse l'appel à Draw().

//
// Met à jour les variables
//
g_pWorldVariable->SetMatrix( (float*)&g_World );
g_pViewVariable->SetMatrix( (float*)&g_View );
g_pProjectionVariable->SetMatrix( (float*)&g_Projection );

I-5 - Tutoriel 5 : Transformation 3D

I-5-a - Résumé

Dans le tutoriel précédent, on a dessiné un cube depuis l'espace modèle à l'écran. Dans ce tutoriel, nous allons étendre le concept de transformation et montrer une simple animation qui peut être faite avec ces transformations.

Au final de ce tutoriel, nous aurons un objet qui tourne autour d'un autre. Il sera utile de décrire les transformations et comment elles peuvent être combinées pour obtenir l'effet désiré. Les tutoriaux futurs seront construits sur cette fondation quand nous introduirons de nouveaux concepts.

I-5-b - Source

(SDK root)\Samples\C++\Direct3D10\Tutorials\Tutorial05

I-5-c - Transformation

En graphisme 3D, la transformation est souvent utilisée pour opérer sur des sommets et des vecteurs. C'est aussi utilisé pour les convertir d'un espace vers un autre. La transformation est effectuée via multiplication avec une matrice. Il y a typiquement 3 types de transformation primitive qui peuvent être effectuées sur des sommets : la translation, la rotation , et le scaling (sa distance depuis l'origine). En plus de celles-ci, la transformation de projection est utilisée pour passer de l'espace vue à l'espace projection. La bibliothèque D3DX contient des API qui peuvent construire commodément une matrice pour beaucoup de buts tels que la translation, la rotation, le scaling, la transformation monde vers vue, la transformation vue vers projection, etc. Une application peut ensuite utiliser ces matrices pour transformer les sommets dans sa scène. Une compréhension basique des transformations de matrices est nécessaire. On va brièvement examiner ci-dessous quelques exemples.

I-5-d - Translation

La translation se réfère au mouvement ou déplacement d'une certaine distance dans l'espace. En 3D, la matrice utilisée pour la translation a la forme :

1  0  0  0
0  1  0  0
0  0  1  0
a  b  c  1

Où (a, b, c) est le vecteur qui définit la direction et la distance de déplacement. Par exemple, pour déplacer un sommet de -5 unités le long de l'axe X (direction X négative), on peut la multiplier avec cette matrice :

1  0  0  0
0  1  0  0
0  0  1  0
-5  0  0  1

Si nous appliquons ceci à un objet cube centré à l'origine, le résultat est que la boîte est déplacée de 5 unités vers l'avant sur l'axe X négatif, comme indiqué sur la figure 1, après que la translation soit appliquée.

Figure 1 : effet de la translation

En 3D, un espace est typiquement défini par un point d'origine et trois axes uniques depuis l'origine : X, Y et Z. Il y a plusieurs espaces habituellement utilisés en graphisme sur ordinateur : l'espace objet, l'espace monde, l'espace vue, l'espace projection et l'espace écran.

Figure 2 : un cube défini dans l'espace objet

I-5-e - Rotation

La rotation se réfère à la rotation des sommets sur un axe en partant de l'origine. Ces 3 axes sont les axes X, Y, et Z dans l'espace. Un exemple en 2D ferait pivoter le vecteur [1 0] de 90 degrés dans le sens contraire des aiguilles d'une montre. Le résultat de la rotation est le vecteur [0 1]. La matrice utilisée pour la rotation ? dans le sens des aiguilles d'une montre sur l'axe Y ressemble à ceci :

cos?  0   -sin?  0
0    1     0    0
sin?  0    cos?  0
0    0     0    1

La figure 3 montre l'effet de rotation d'un cube, centrée à l'origine, de 45 degrés sur l'axe Y.

Figure 3 : l'effet d'une rotation sur l'axe Y

I-5-f - Scaling

Le scaling se réfère à l'agrandissement ou la diminution de la taille de vecteurs composants le long des axes. Par exemple, un vecteur peut être agrandi dans toutes les directions ou réduit seulement sur l'axe X. Pour faire le scaling, on applique habituellement la matrice ci-dessous :

p  0  0  0
0  q  0  0
0  0  r  0
0  0  0  1

Où p, q, et r sont les facteurs de scaling le long des directions X, Y et Z, respectivement. La figure 4 montre l'effet de scaling par 2 le long de l'axe X et le scaling par 0.5 le long de l'axe Y.

Figure 4 : l'effet du scaling

I-5-g - Transformations multiples

Pour appliquer de multiples transformations à un vecteur, on peut simplement multiplier le vecteur par la première matrice de transformation et, ensuite, multiplier le vecteur résultant par la seconde matrice de transformation, et ainsi de suite. La multiplication des vecteurs et des matrices étant associative, on peut aussi multiplier toutes les matrices d'abord et ensuite multiplier les vecteurs par la matrice du produit et obtenir un résultat identique. La figure 5 montre comment le cube serait si nous combinions ensemble une rotation et une translation.

Figure 5 : l'effet d'une translation et d'une rotation

I-5-h - Créer l'orbit

Dans ce tutoriel, on va transformer deux cubes. Le premier tournera sur place, pendant que le second tournera autour du premier, et tournera sur son propre axe. Les deux cubes auront leur propre matrice de transformation de monde qui leur est associée, et cette matrice leur sera réappliquée à chaque frame dessinée.

Il y a des fonctions dans D3DX qui nous assisterons dans la création de la matrice de rotation, de translation et de scaling.

- Les rotations effectuées autour des axes X, Y et Z sont accomplies avec les fonctions D3DXMatrixRotationX, D3DXMatrixRotationY, et D3DXMatrixRotationZ respectivement. Elles créent des matrices de rotations basiques qui tournent autour des axes primaires. Les rotations complexes autour d'autres axes peuvent être effectuées en multipliant ensemble plusieurs d'entre elles.

- Les translations peuvent être effectuées en invoquant la fonction D3DXMatrixTranslation. Cette fonction créera une matrice qui va translater les points spécifiés par les paramètres.

- Le scaling est effectué avec D3DXMatrixScaling. Elle fait le scaling seulement le long des axes primaires. Si le scaling le long d'axes arbitraires est souhaité, alors la matrice de scaling peut être multipliée avec une matrice de rotation appropriée pour accomplir l'effet.

Le premier cube tournera sur place et agira comme le centre de l'orbite. Le cube a une rotation le long de l'axe Y appliquée à la matrice monde associée. C'est effectué en appelant la fonction D3DXMatrixRotationY montrée ci-dessous. Le cube est pivoté par un certain montant à chaque frame. Comme les cubes sont supposés tourner continuellement, la valeur par laquelle la matrice de rotation est basée est incrémentée avec chaque frame.

// 1er Cube : tourne autour de l'origine
D3DXMatrixRotationY( &g_World1, t );

Le second cube orbitera autour du premier. Pour montrer de multiples transformations, un facteur de scaling, et son propre axe de rotation seront ajoutés. La formule utilisée est montrée juste en dessous du code (en commentaire). D'abord le cube sera réduit à l'échelle d'une taille de 30%, et ensuite sera pivoté autour de son propre axe de rotation (l'axe Z dans ce cas). Pour simuler l'orbite, il sera translaté depuis l'origine, et ensuite, pivoté autour de l'axe Y. L'effet désiré peut être obtenu en utilisant 4 matrices séparées avec leur transformation individuelle (mScale, mSpin, mTranslate, mOrbit), puis, multipliées ensemble.

// 2ème Cube : tourne autour de l'origine
D3DXMATRIX mTranslate;
D3DXMATRIX mOrbit;
D3DXMATRIX mSpin;
D3DXMATRIX mScale;
D3DXMatrixRotationZ( &mSpin, -t );
D3DXMatrixRotationY( &mOrbit, -t*2.0f );
D3DXMatrixTranslation( &mTranslate, -4.0f, 0.0f, 0.0f );
D3DXMatrixScaling( &mScale, 0.3f, 0.3f, 0.3f );

D3DXMatrixMultiply( &g_World2, &mScale, &mSpin );
D3DXMatrixMultiply( &g_World2, &g_World2, &mTranslate );
D3DXMatrixMultiply( &g_World2, &g_World2, &mOrbit );
//g_World2 = mScale * mSpin * mTranslate * mOrbit;

Un point important à noter est que ces opérations ne sont pas commutatives. L'ordre dans lequel les transformations sont appliquées est important. Expérimentez avec l'ordre de transformation et observez les résultats.

Comme toutes les fonctions de transformation vont créer une nouvelle matrice depuis les paramètres, le montant avec lequel elles pivotent doit être incrémenté. Ceci est effectué en mettant à jour la variable "time".

// Met à jour le temps (time)
t += D3DX_PI * 0.0125f;

Avant que les appels d'affichage ne soient effectués, la technique doit collecter les variables pour les shaders. Ici, la matrice monde, la matrice vue et la matrice de projection sont attribuées vers la technique. Notez que la matrice monde est propre à chaque cube, et donc, change pour chaque objet qui passe par elle/lui.

//
// Affiche le 1er cube
//
D3D10_TECHNIQUE_DESC techDesc;
g_pTechnique->GetDesc( &techDesc );
for( UINT p = 0; p < techDesc.Passes; ++p )
{
g_pTechnique->GetPassByIndex( p )->Apply(0);
g_pd3dDevice->DrawIndexed( 36, 0, 0 );
}

//
// Met à jour les variables pour le second cube
//
g_pWorldVariable->SetMatrix( (float*)&g_World2 );
g_pViewVariable->SetMatrix( (float*)&g_View );
g_pProjectionVariable->SetMatrix( (float*)&g_Projection );

//
// Affiche le second cube
//
for( UINT p = 0; p < techDesc.Passes; ++p )
{
g_pTechnique->GetPassByIndex( p )->Apply(0);
g_pd3dDevice->DrawIndexed( 36, 0, 0 );
}

I-5-i - Le buffer de profondeur

Il y a un ajout important dans ce tutoriel, qui est le buffer de profondeur (depth buffer). Sans lui, le cube le plus petit en orbite serait toujours dessiné au dessus du cube plus grand au centre quand il se trouverait à l'arrière du dernier. Le buffer de profondeur permet à Direct3D de garder des traces de la profondeur de chaque pixel dessiné à l'écran. Le comportement par défaut du buffer de profondeur dans Direct3D 10 est de vérifier chaque pixel dessiné à l'écran par rapport à la valeur stockée dans le buffer de profondeur pour ce pixel dans l'espace écran. Si la profondeur du pixel affiché est moindre ou égale à la valeur déjà dans le buffer de profondeur, le pixel est dessiné et la valeur dans le buffer de profondeur est mise à jour par la profondeur du pixel nouvellement dessiné. D'autre part, si le pixel dessiné a une profondeur plus grande que la valeur déjà dans le buffer de profondeur, le pixel est ignoré et la valeur de profondeur dans le buffer de profondeur reste inchangée.

Le code suivant dans l'exemple crée un buffer de profondeur (une texture DepthStencil). Il crée aussi une DepthStencilView du buffer de profondeur afin que Direct3D 10 sache l'utiliser en tant que texture Depth Stencil.

// Crée une texture depth stencil
D3D10_TEXTURE2D_DESC descDepth;
descDepth.Width = width;
descDepth.Height = height;
descDepth.MipLevels = 1;
descDepth.ArraySize = 1;
descDepth.Format = DXGI_FORMAT_D32_FLOAT;
descDepth.SampleDesc.Count = 1;
descDepth.SampleDesc.Quality = 0;
descDepth.Usage = D3D10_USAGE_DEFAULT;
descDepth.BindFlags = D3D10_BIND_DEPTH_STENCIL;
descDepth.CPUAccessFlags = 0;
descDepth.MiscFlags = 0;
hr = g_pd3dDevice->CreateTexture2D( &descDepth, NULL, &g_pDepthStencil );
if( FAILED(hr) )
return hr;

// Crée la depth stencil view
D3D10_DEPTH_STENCIL_VIEW_DESC descDSV;
descDSV.Format = descDepth.Format;
descDSV.ViewDimension = D3D10_DSV_DIMENSION_TEXTURE2D;
descDSV.Texture2D.MipSlice = 0;
hr = g_pd3dDevice->CreateDepthStencilView( g_pDepthStencil, &descDSV, &g_pDepthStencilView );
if( FAILED(hr) )
return hr;

Afin d'utiliser ce buffer depth stencil nouvellement créé, le tutoriel doit l'attribuer au device. C'est effectué en passant la vue depth stencil au troisième paramètre de la fonction OMSetRenderTargets.

g_pd3dDevice->OMSetRenderTargets( 1, &g_pRenderTargetView, g_pDepthStencilView );

Comme avec la cible affichée, nous devons aussi vider le buffer de profondeur avant l'affichage. Cela assure que les valeurs de profondeur des frames précédentes n'ignorent pas incorrectement les pixels dans la frame actuelle. Dans ces tutoriaux le buffer de profondeur est configuré pour avoir un nombre maximum de (1.0).

//
// Vide le depth buffer à 1.0 (profondeur max)
//
g_pd3dDevice->ClearDepthStencilView( g_pDepthStencilView, D3D10_CLEAR_DEPTH, 1.0f, 0 );

I-6 - Tutoriel 6 : La lumière

I-6-a - Résumé

Dans les tutoriaux précédents, le monde semblait ennuyant car tous les objets été éclairés de la même façon. Ce tutoriel va introduire le concept d'éclairage simple et comment il peut être appliqué. La technique utilisée sera l'éclairage lambertien.

Le résultat de ce tutoriel modifiera l'exemple précédent pour inclure une source de lumière. Cette source de lumière sera attachée au cube en orbite. Les effets de lumière pourront être vu sur le cube centré.

I-6-b - Source

(SDK root)\Samples\C++\Direct3D10\Tutorials\Tutorial06

I-6-c - L'éclairage

Dans ce tutoriel, le type le plus basique d'éclairage sera introduit : l'éclairage lambertien. L'éclairage lambertien a une intensité uniforme irrespectueuse de la distance par rapport à la lumière. Quand la lumière touche la surface, le montant de lumière réfléchie est calculé par l'angle d'incidence que la lumière a sur la surface. Quand une lumière brille directement sur une surface, elle est montrée pour refletter toute la lumière, avec une intensité maximum. Cependant, comme l'angle de la lumière augmente, l'intensité de la lumière s'estompera.

Pour calculer l'intensité qu'une lumière a sur une surface, l'angle entre la direction de la lumière et la normale de la surface doit être calculé. La normale pour une surface est définie en tant qu'un vecteur qui est perpendiculaire à la surface. Le calcul de l'angle peut être effectué avec un simple produit en croix, lequel retournera la projection de la direction du vecteur de la lumière sur la normale. Plus large est l'angle, plus petite sera la projection. Donc, cela nous donne la fonction correcte pour moduler la lumière diffusée.

La source de lumière utilisée dans ce tutoriel est une approximation de l'éclairage directionnel. Le vecteur qui décrit la source de lumière détermine la direction de la lumière. Comme c'est une approximation, peu importe où se trouve un objet, la direction dans laquelle la lumière brille est la même. Un exemple de cette source de lumière est le soleil; le soleil est toujours vu comme brillant dans la même direction pour tous les objets dans la scène. En plus, l'intensité de la lumière sur des objets individuels n'est pas prise en considération.

Les autres types de lumières incluent les points lumineux (point lights), qui rayonnent uniformément la lumière depuis son centre, et les spots lumineux (spot lights), qui sont directionnels mais pas uniformes au travers de tous les objets.

I-6-d - initialiser les lumières

Dans ce tutoriel, il y aura deux sources de lumières. Une sera placée statiquement au dessus et derrière le cube, et une autre sera en orbite autour du cube centré. Notez que le cube en orbite dans le tutoriel précédent a été remplacé avec cette source de lumière.

Comme l'éclairage est calculé par les shaders, les variables devront être déclarées et ensuite attribuées dans les variables à l'intérieur de la technique. Dans cet exemple, nous avons juste besoin de la direction de la source de lumière, ainsi que de sa couleur. La première lumière est grise et immobile, et la seconde est une lumière rouge en orbite.

// Configure nos paramètres d'éclairage
D3DXVECTOR4 vLightDirs[2] =
{
D3DXVECTOR4( -0.577f, 0.577f, -0.577f, 1.0f ),
D3DXVECTOR4( 0.0f, 0.0f, -1.0f, 1.0f ),
};
D3DXVECTOR4 vLightColors[2] =
{
D3DXVECTOR4( 0.5f, 0.5f, 0.5f, 0.0f ),
D3DXVECTOR4( 0.5f, 0.0f, 0.0f, 0.0f )
};

La lumière en orbite tourne juste comme le cube dans le dernier tutoriel. La matrice de rotation appliquée changera la direction de la lumière, pour afficher l'effet qui brille toujours face au centre. Notez que la fonction D3DXVec3Transform est utilisée pour multiplier une matrice avec un vecteur. Dans le tutoriel précédent, nous avons juste multiplié les matrices de transformation dans la matrice monde, nous sommes passée ensuite dans le shader pour transformation; mais dans ce cas, nous allons faire la transformation monde de la lumière par le CPU, pour une question de simplicité.

//fait tourner la seconde lumière autour de l'origine
D3DXMATRIX mRotate;
D3DXVECTOR4 vOutDir;
D3DXMatrixRotationY( &mRotate, -2.0f*t );
D3DXVec3Transform( &vLightDirs[1], (D3DXVECTOR3*)&vLightDirs[1], &mRotate );

La direction de la lumière et la couleur sont toutes les deux passées dans le shader juste comme des matrices. La variable associée est appelée pour affecter, et le paramètre y est passé.

//
// Met à jour les variables d'éclairage
//
g_pLightDirVariable->SetFloatVectorArray( (float*)vLightDirs, 0,    2 );
g_pLightColorVariable->SetFloatVectorArray( (float*)vLightColors, 0,    2 );

I-6-e - Rendu des lumières dans le Pixel Shader

Une fois que nous avons toutes les données configurées et le shader correctement fourni avec les données, on peut calculer le terme de l'éclairage lambertien sur chaque pixel depuis les sources de lumière. Nous utiliserons la règle du produit en croix discutée plus haut.

Une fois que l'on a pris le produit en croix de la lumière par rapport à la normale, celui-ci peut être multiplié avec la couleur de la lumière pour calculer l'effet de cette lumière. Cette valeur est passée au travers de la fonction saturate, qui converti la portée vers [0, 1]. Finalement, le résultat depuis les deux éclairages séparés sont additionnés ensemble pour créer la couleur pixel finale.

Considérez que le matériau de la surface n'est pas factorisé dans le calcul de la lumière; la couleur finale sur la surface est le résultat des couleurs des lumières.

//
// Pixel Shader
//
float4 PS( PS_INPUT input) : SV_Target
{
float4 finalColor = 0;

//do NdotL lighting for 2 lights
for(int i=0; i<2; i++)
{
finalColor += saturate( dot( (float3)vLightDir[i],input.Norm) * vLightColor[i] );
}
return finalColor;
}

Une fois au travers du pixel shader, les pixels auront été modulés par les lumières et vous pouvez voir l'effet de chaque lumière sur la surface du cube. Notez que la lumière dans ce cas semble plate car les pixels sur la même surface auront la même normale. La lumière diffuse est un modèle d'éclairage très simple et très facile à calculer. Des modèles d'éclairage plus complexes peuvent être utilisés pour accomplir des matériaux plus riches et plus réalistes.

I-7 - Tutoriel 7 : Mapping de texture et Constant Buffers

I-7-a - Résumé

Dans le tutoriel précédent nous avons introduit l'éclairage à notre projet; maintenant nous allons construire sur tout cela en ajoutant des textures à notre cube. En plus, nous allons introduire le concept des constant buffers et comment ils peuvent être utilisés pour accélérer le traitement en minimisant l'usage de la bande passante.

Le résultat de ce tutoriel modifiera le cube central pour qu'il y ait une texture mappée sur lui.

Ce tutoriel conclut l'introduction des concepts basiques dans Direct3D 10. Les tutoriaux suivants seront construits à partir de ces concepts en introduisant DXUT, le chargement de mesh, ainsi qu'un exemple de chaque shader.

I-7-b - Source

(SDK root)\Samples\C++\Direct3D10\Tutorials\Tutorial07

I-7-c - Mapping de texture

Le mapping de texture se réfere à la projection d'une image 2D sur une géométrie 3D. On peut penser à ceci comme un emballage cadeau, en plaçant un papier décoratif par-dessus une boîte fade. Pour faire ceci, nous devons spécifier comment les points sur la surface de la géométrie correspondent avec l'image 2D.

Le soucis est de correctement aligner les coordonnées du modèle avec la texture. Pour des modèles complexes, il est difficile de déterminer les coordonnées pour les textures à la main. Donc, les programmes de modélisation 3D vont généralement exporter les modèles avec les coordonnées de textures correspondantes. Comme notre exemple est un cube, il est facile de déterminer les coordonnées nécessaires pour correspondre à la texture. Les coordonnées de texture sont définies aux vertices et interpolées pour les pixels individuels sur la surface.

I-7-d - Creation d'un Shader Resource depuis une Texture

La texture est une image 2D qui est obtenue depuis un fichier et utilisée pour créer une vue shader-resource, afin qu'elle puisse être lue depuis un shader.

hr = D3DX10CreateShaderResourceViewFromFile( g_pd3dDevice, L"seafloor.dds", NULL, NULL,
&g_pTextureRV, NULL );

I-7-e - Définir les coordonnées

Avant que l'on puisse mapper l'image sur notre cube, nous devons d'abord définir les coordonnées de textures sur chacun des vertices du cube. Comme les images peuvent être de plusieurs tailles, le système de coordonnées utilisé a été normalisé à [0, 1]. Le coin supérieur gauche de la texture correspond à (0,0) et le coin inférieur droit mappe à (1,1).

Dans cet exemple, nous avons l'entièreté de la texture qui s'étend sur chaque côté du cube. Cela simplifie la définition des coordonnées, sans confusion. Cependant, il est entièrement possible de spécifier la texture pour s'étendre sur toutes les 6 faces, bien qu'il soit plus difficile de définir les points, et cela apparaîtra étiré et distorsionné.

D'abord, on met à jour la structure utilisée pour définir nos vertices pour inclure les coordonnées de texture.

struct SimpleVertex
{
D3DXVECTOR3 Pos; // Position
D3DXVECTOR2 Tex; // Texture Coordinate
};

Ensuite, on met à jour l'input layout des shaders pour également inclure ces coordonnées.

// Défini l'input layout
D3D10_INPUT_ELEMENT_DESC layout[] =
{
{ L"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0,
D3D10_INPUT_PER_VERTEX_DATA, 0 },
{ L"TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 12,
D3D10_INPUT_PER_VERTEX_DATA, 0 },
};

Comme l'input layout a changé, l'input vertex shader correspondant doit aussi être modifié pour correspondre à l'ajout.

struct VS_INPUT
{
float4 Pos : POSITION;
float2 Tex : TEXCOORD;
};

Finalement, nous sommes prêts à inclure les coordonnées de texture dans nos sommets que nous avons definies dans le tutoriel 4. Notez que le second paramètre d'input est un D3DXVECTOR2 contenant les coordonnées de texture. Chaque sommet du cube correspondra à un coin de la texture. Ceci crée un mapping simple où chaque sommet reçoit (0,0) (0,1) (1,0) ou (1,1) comme coordonnée.

// Crée le vertex buffer
SimpleVertex vertices[] =
{
{ D3DXVECTOR3( -1.0f,  1.0f, -1.0f ), D3DXVECTOR2( 0.0f, 0.0f ) },
{ D3DXVECTOR3(  1.0f,  1.0f, -1.0f ), D3DXVECTOR2( 1.0f, 0.0f ) },
{ D3DXVECTOR3(  1.0f,  1.0f,  1.0f ), D3DXVECTOR2( 1.0f, 1.0f ) },
{ D3DXVECTOR3( -1.0f,  1.0f,  1.0f ), D3DXVECTOR2( 0.0f, 1.0f ) },

{ D3DXVECTOR3( -1.0f, -1.0f, -1.0f ), D3DXVECTOR2( 0.0f, 0.0f ) },
{ D3DXVECTOR3(  1.0f, -1.0f, -1.0f ), D3DXVECTOR2( 1.0f, 0.0f ) },
{ D3DXVECTOR3(  1.0f, -1.0f,  1.0f ), D3DXVECTOR2( 1.0f, 1.0f ) },
{ D3DXVECTOR3( -1.0f, -1.0f,  1.0f ), D3DXVECTOR2( 0.0f, 1.0f ) },

{ D3DXVECTOR3( -1.0f, -1.0f,  1.0f ), D3DXVECTOR2( 0.0f, 0.0f ) },
{ D3DXVECTOR3( -1.0f, -1.0f, -1.0f ), D3DXVECTOR2( 1.0f, 0.0f ) },
{ D3DXVECTOR3( -1.0f,  1.0f, -1.0f ), D3DXVECTOR2( 1.0f, 1.0f ) },
{ D3DXVECTOR3( -1.0f,  1.0f,  1.0f ), D3DXVECTOR2( 0.0f, 1.0f ) },

{ D3DXVECTOR3( 1.0f, -1.0f,  1.0f ), D3DXVECTOR2( 0.0f, 0.0f ) },
{ D3DXVECTOR3( 1.0f, -1.0f, -1.0f ), D3DXVECTOR2( 1.0f, 0.0f ) },
{ D3DXVECTOR3( 1.0f,  1.0f, -1.0f ), D3DXVECTOR2( 1.0f, 1.0f ) },
{ D3DXVECTOR3( 1.0f,  1.0f,  1.0f ), D3DXVECTOR2( 0.0f, 1.0f ) },

{ D3DXVECTOR3( -1.0f, -1.0f, -1.0f ), D3DXVECTOR2( 0.0f, 0.0f ) },
{ D3DXVECTOR3(  1.0f, -1.0f, -1.0f ), D3DXVECTOR2( 1.0f, 0.0f ) },
{ D3DXVECTOR3(  1.0f,  1.0f, -1.0f ), D3DXVECTOR2( 1.0f, 1.0f ) },
{ D3DXVECTOR3( -1.0f,  1.0f, -1.0f ), D3DXVECTOR2( 0.0f, 1.0f ) },

{ D3DXVECTOR3( -1.0f, -1.0f, 1.0f ), D3DXVECTOR2( 0.0f, 0.0f ) },
{ D3DXVECTOR3(  1.0f, -1.0f, 1.0f ), D3DXVECTOR2( 1.0f, 0.0f ) },
{ D3DXVECTOR3(  1.0f,  1.0f, 1.0f ), D3DXVECTOR2( 1.0f, 1.0f ) },
{ D3DXVECTOR3( -1.0f,  1.0f, 1.0f ), D3DXVECTOR2( 0.0f, 1.0f ) },
};

Quand on découpe la texture, nous devons la moduler avec une couleur de materiau pour la géométrie se trouvant en dessous.

I-7-f - Attribuer une texture en tant que Shader Resource

Une texture est un objet comme les matrices et les vecteurs que nous avons vu dans les tutoriaux précédents. Avant qu'ils ne puissent être utilisés par le shader, ils ont besoin d'être définis dans l'effet. Ceci peut être effectué en obtenant un pointeur vers la variable, et ensuite, le définir en tant que shader resource.

g_pDiffuseVariable =
g_pEffect->GetVariableByName("txDiffuse")->AsShaderResource();

Après que le pointeur ressource ait été obtenu, il peut être utilisé pour accrocher la vue ressource texture 2D que nous avons initialisée plus tôt.

g_pDiffuseVariable->SetResource( g_pTextureRV );

On continue, maintenant nous sommes prêts à utiliser la texture à l'intérieur du shader.

I-7-g - Appliquer la Texture (fx)

Pour actuellement mapper la texture au dessus de la géométrie, on appellera une fonction de recherche de texture à l'intérieur du pixel shader. La fonction, Sample effectuera un recherche de texture d'une texture 2D pour ensuite retourner la couleur découpée. Le pixel shader montré ci-dessous appelle cette fonction et multiplie par la couleur mesh soulignée (ou la couleur matériau) et ensuite produit la couleur finale.

- txDiffuse est l'objet stockant notre texture que l'on a passée depuis le code plus haut, quand on lui a attribué la vue resource g_pTextureRV

- samLinear sera décrit plus bas, ce sont les spécifications découpées pour la recherche de texture.

- input.Tex sont les coordonnées de la texture que nous avons spécifiées dans la source

// Pixel Shader
float4 PS( PS_INPUT input) : SV_Target
{
return txDiffuse.Sample( samLinear, input.Tex ) * vMeshColor;
}

La variable samLinear est une structure qui contient les informations pour dire au pixel shader comment découper la texture fournie. Dans notre cas, on a un filtre linéaire, et nos deux adresses de synthèse. Ces configurations seront utiles pour de simples textures, et une explication des filtres dépasse le but de ce tutoriel.

SamplerState samLinear
{
Filter = MIN_MAG_MIP_LINEAR;
AddressU = Wrap;
AddressV = Wrap;
};

Une autre chose que nous devons nous rappeler de faire est de passer les coordonnées de texture dans le vertex shader, ou sinon les données sont perdues quand elles atteignent le pixel shader. . Ici, on copie juste les coordonnées d'input vers l'output, et on laisse l'hardware gérer le reste.

// Vertex Shader
PS_INPUT VS( VS_INPUT input )
{
PS_INPUT output = (PS_INPUT)0;
output.Pos = mul( input.Pos, World );
output.Pos = mul( output.Pos, View );
output.Pos = mul( output.Pos, Projection );
output.Tex = input.Tex;

return output;
}

I-7-h - Constant Buffers

En commençant avec Direct3D 10, une application peut utiliser un constant buffer pour définir les shader constants (shader variables). Les constant buffers sont déclarés en utilisant une syntaxe similaire aux structures de style C.

La meilleure façon d'utiliser les constant buffers est d'organiser les shader variables dans des constant buffers basés sur leur fréquence de mise à jour. Cela permet à une application de minimiser la bande passante nécessaire pour mettre à jour les shader constants. Comme exemple, ce tutoriel groupe les constantes dans 3 structures : une pour les variables qui changent à chaque frame, une pour les variables qui changent seulement quand une taille de fenêtre est changée, et une pour les variables qui sont définies une seule fois et ensuite plus modifiées.

cbuffer cbNeverChanges
{
matrix View;
};

cbuffer cbChangeOnResize
{
matrix Projection;
};

cbuffer cbChangesEveryFrame
{
matrix World;
float4 vMeshColor;
};