Tutorial Series - Introduction au développement d'application Direct3D avec XAML pour Windows Phone 8 : Matrices

Matrices de transformation

On va consacrer ce post aux matrices. Si vous avez allégrement séché cette discipline mathématique en pensant que ça ne servait probablement à rien, pas de bol, dans le monde de la 3D c’est essentiel! :)

La seule explication simple (en tout cas à ma portée) c’est que chaque matrice se définie comme un tableau de nombres (4x4 dans notre cas) qui va nous permettre de nous simplifier largement la vie pour appliquer des transformations à notre cube (rotation, translation et mise à l’échelle). Comme je n’ai pas prétention à vous donner un cours d’algèbre dont mes notions sont somme toute assez limité, je vous invite à consulter les ressources en fin d’article (volé sur un post précédent de mon collègue David Rousset ; que je vous encourage à étudier au passage ^^):

Avant de déprimer, la bonne nouvelle c’est que Direct3D gère en grande partie la complexité inhérente aux matrices, le point important à comprendre c’est que les coordonnées de notre objet (les sommets dans un premier temps) vont transiter dans plusieurs référentiels.

  1. En premier lieu, on va pouvoir appliquer des transformations sur l’objet (model) pour le placer de manière appropriée dans la scène.
  2. Ensuite, on va regarder cet objet sous un certains angle de vue (view). Imaginez une caméra qui se place au-dessus de notre objet. On va appliquer les transformations adéquates pour voir l’objet dans ce nouveau référentiel.
  3. Et finalement la projection. On a beau être dans un monde en 3D, aux dernières nouvelles notre écran est une surface plane, l’emplacement de chaque pixel est représenté par 2 valeurs (x,y). On va donc basculer d’un référentiel 3D à un référentiel 2D.

Les transformations appliquées dans les référentiels précédents sont définies par des matrices différentes : model, view et projection. Nos données de départ (celles stockées dans le Vertex Buffer) vont donc être transformées par ces matrices pour obtenir les coordonnés finales de notre pixel à l’écran.

C’est en grande partie le rôle du Vertex Shader (encore un nouveau terme technique ^^) d’effectuer cette opération. Comment procède-t-il ? Il va simplement multiplier les coordonnées de départ par chacune des matrices model, view et projection, le tour est joué !

Constant Buffer

Avant de parler du Vertex Shader, un petit laïus additionnel sur le Constant Buffer, tout un programme pour vous faire rêver ami lecteur !

La carte graphique est notre amie, elle va vite (voir très vite) car elle a tout sous la main pour travailler. Son espace mémoire est dédiée et on lui a déjà fournie des informations à digérer avec nos buffers précédents (le Vertex Buffer et l’Index Buffer).

Il en va de même pour les matrices, on va les stocker dans un Constant Buffer. C’est un espace mémoire que le Vertex Shader va pouvoir consommerpour effectuer les calculs de projection. Avec cette dernière pièce à l’édifice, notre Vertex Shader est enfin prêt pour travailler sur nos données !

Ok, maintenant qu’on a introduit les concepts, un peu de code.

1. Décalaration du constant buffer

a. Dans SceneRenderer.cpp, nous allons commencer par déclarer la structure de notre Constant Buffer. Cette structure regroupe simplement nos 3 matrices 4x4 : model, view et projection.

 struct ModelViewProjectionConstantBuffer
{
    DirectX::XMFLOAT4X4 model;
    DirectX::XMFLOAT4X4 view;
    DirectX::XMFLOAT4X4 projection;
};

b. Et du coup, on en profite pour ajouter deux variables membres dans la classe SceneRenderer pour manipuler ces matrices.

 Microsoft::WRL::ComPtr<ID3D11Buffer> m_constantBuffer;
ModelViewProjectionConstantBuffer m_constantBufferData;
  • m_constantBuffer :c’est un pointeur sur notre buffer de données (au même titre que m_indexBuffer et m_vertexBuffer), il s’agit d’un espaces mémoire directement accessibles par la carte graphique (et du coup accessible par notre Vertex Shader)

  • m_constantBufferData : les matrices vont être préalablement stockées dans cette variable (comme pour cubeVertices et cubeIndices). En bref, on prépare nos trois matrices dans cette variable et on envoie le résultat à m_constantBuffer.

2. Initialisation du constant buffer

L’initialisation s’effectue dans SceneRenderer::CreateDeviceResources, on procède d‘une manière identique à l’Index/Vertex Buffer avec un appel à ID3D11Device::CreateBuffer. L’unique différence c’est que les données en entrée sont vides dans l’immédiat (mais on va résoudre ce problème dans un instant).

 CD3D11_BUFFER_DESC constantBufferDesc(sizeof(ModelViewProjectionConstantBuffer), D3D11_BIND_CONSTANT_BUFFER);
DX::ThrowIfFailed(
    m_d3dDevice->CreateBuffer(
    &constantBufferDesc,
    nullptr,
    &m_constantBuffer
    )
    );

3. Initialisation des matrices model, view et projection

a. matrice model

On commence par s’attaquer à la matrice model. Dans cet exemple, je désire faire tourner notre cube : nous allons donc appliquer une matrice de rotation (sur l’axe des Y), et mettre à jour l’angle de rotation à chaque nouvelle frame.

C’est le but de la méthode SceneRenderer:Update,tout le travail est effectuée par la méthode XMatrixRotationY : elle prend simplement un angle comme argument d’entrée (en radian). Ce type de méthode existe pour l’ensemble des transformations, du coup le calcul matriciel devient une boite noire que vous pouvez utiliser les yeux fermés (ou presque).

 void SceneRenderer::Update(float timeTotal, float timeDelta)
{
    (void) timeDelta; // Unused parameter.
    XMStoreFloat4x4(&m_constantBufferData.model, XMMatrixTranspose(XMMatrixRotationY(timeTotal * XM_PIDIV4)));
}

Pour les matrices view et projection, l’initialisation est effectuée une fois pour toute au lancement de l’application dans la méthode SceneRenderer::CreateWindowSizeDependentResources.  

b. matrice view

La matrice view positionne la scène dans un nouveau référentiel, celui de notre caméra. Pour définir une caméra, nous avons toutefois besoin de 3 vecteurs:

-
eye définie la position de la camera

-
at nous indique la direction vers l’objet étudié

-
up positionne le haut de la caméra

 

La méthode XMatrixLookAtRH nous permet ensuite de repositionner la scène avec ces trois nouvelles coordonnées.

 XMVECTOR eye = XMVectorSet(0.0f, 0.7f, 1.5f, 0.0f);
XMVECTOR at = XMVectorSet(0.0f, -0.1f, 0.0f, 0.0f);
XMVECTOR up = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f);

XMStoreFloat4x4(&m_constantBufferData.view, XMMatrixTranspose(XMMatrixLookAtRH(eye, at, up)));

Note: Les vecteurs eye, at et up ne nécessitent que 3 valeurs (x, y, et z). On travaille toutefois avec 4 valeurs (la dernière étant 0.0f) car les matrices de transformations sont en 4x4.

c. matrice projection

Ça pique un chouïa plus, mais rien de violent ! Cette dernière matrice nous aide à basculer du monde 3D au référentiel 2D pour projeter notre scène à l’écran. On va utiliser également un petit cadeau de Direct3D pour nous simplifier la vie : XMMatrixPerspectiveFovRH. Cette fonction prend toutefois 4 arguments un peu austères au premier abord mais rien d’inabordable dans les faits :)

 

-
FovAngleY défine l’angle du champ de vision (field-of-view) sur l’axe des Y (de haut en bas). Cet angle est de 70 degrés dans notre cas. Et pour faire simple, cette valeur doit être préalablement convertie en radian :)

  • AspectRatio définie le rapport entre la largeur et la hauteur de l’écran (on divise simplement x par y)
  • NearZ indique le plan de clipping proche, tous les éléments devant ce plan sont ignorés.
  • FarZ indique le plan de clipping lointain, tous les éléments derrière ce plan sont ignorés.

 

Comme un dessin est probablement plus parlant, ca ressemble à ça :

fov (field of view) diagram

Et le tour est joué, on a notre troisième matrice !

 float aspectRatio = m_windowBounds.Width / m_windowBounds.Height;
float fovAngleY = 70.0f * XM_PI / 180.0f;  // convert in radian

if (aspectRatio < 1.0f)
{
    fovAngleY /= aspectRatio;
}

XMStoreFloat4x4(
    &m_constantBufferData.projection,
    XMMatrixTranspose(
        XMMatrixPerspectiveFovRH(
            fovAngleY,        // field-of-view
            aspectRatio,      // aspect ratio
            0.01f,            // near clipping plane
            100.0f            // far clipping plane
            )
        )
    );

Parfait, on a nos 3 matrices !  Mais à quel moment suis-je sensé copié les informations de la variable m_constantBufferData vers le buffer m_constantBuffer !?

C’est un peu tôt pour en parler, mais vous pouvez jeter un coup d’œil dans la méthode SceneRenderer::Render(), l’appel à UpdateSubresource effectue exactement ce travail. Promis on parle très prochainement en détail de ce point !

 

 m_d3dContext->UpdateSubresource(
    m_constantBuffer.Get(),
    0,
    NULL,
    &m_constantBufferData,
    0,
    0
    );

Ressources

 

 

Télécharger le Code