Tutorial Series - Introduction au développement d'application Direct3D avec XAML pour Windows Phone 8 : Création d’un mesh

Le dernier épisode nous a démontré qu’on pouvait se compliquer la vie pour afficher un fond bleu :) ; mais en contrepartie vous venez d’ouvrir la porte sur DirectX. Pour ce deuxième article, je vais donc tenter de vulgariser les concepts principaux de Direct3D pour vous aider à mettre le pied à l’étrier. Ces points ne sont toutefois pas spécifiques à Windows Phone, vous trouverez en bas de page des pointeurs sur des ressources additionnelles pour compléter ces informations.

Initialisation de Direct3D

Bonne nouvelle, vous n’avez pas à gérer cette partie de code, le contrôle DrawingSurfaceBackgroundGrid (ou DrawingSurface)prend en charge ce point pour vous, c’est cadeau ! Bien évidemment, si vous n’utilisez pas ces contrôles XAML et que vous souhaitez développer une application 100% native, vous devez effectuer cette initialisation … faut pas rêver non plus.

Concrètement, quelles sont les opérations prises en charge lors de cette initialisation ??

1. La création des objets device et device context

La communication avec la carte graphique (ou plus largement le périphérique)  s’effectue par l’intermédiaire de ces deux objets. Il s’agit des principaux points d’entrées pour utiliser Direct3D.

  • L’objet device (ID3D11Device) s’occupe principalement de créer les ressources graphiques (textures, vertex buffers, shaders, …).
  • L’objet device context (ID3D11DeviceContext) se charge d’effectuer les opérations de rendu en utilisant les ressources initilisés par l’objet device.      

 

2. La création de la swap chain

Le rendu n’est pas effectué directement à l’écran, il est réalisé dans un buffer mémoire (cad. un tableau de pixels de la dimension de l’écran) que l’on nomme back buffer. La swap chain se charge de copier le contenu du back buffer directement à l’écran. Pour information, la swap chain peut théoriquement utiliser plusieurs back buffers, mais Windows Phone limite son utilisation à un seul et unique buffer.

En complément, Direct3D permet d’accéder aux données du  back buffer (notre tableau de pixels) via l’objet render target view (ID3D11RenderTargetView).

Perdu ? Reprenons par exemple les lignes de code du post précédent. Le code de la méthode Direct3DBackground::Draw initialise en bleu notre écran. 

a/ La méthode ClearRenderTargetView est appelée depuis l’objet device context
b/On cible le back buffer via l’objet render target view. Ca devient plus clair ?

 HRESULT Direct3DBackground::Draw(_In_ ID3D11Device1* device, _In_ ID3D11DeviceContext1* context, _In_ ID3D11RenderTargetView* renderTargetView)
{
    // Clear the render target to default values.
    const float midnightBlue [] = { 0.098f, 0.098f, 0.439f, 1.000f };
    context->ClearRenderTargetView(
        renderTargetView,
        midnightBlue
        );

    RequestAdditionalFrame();

    return S_OK;
}

Pour compléter ces informations, je vous invite à consulter l’article Creating a Direct3D device and swap chain for Windows Phone 8.

Construction de notre objet 3D

Les sommets (déclaration des vertices)

Un objet 3D (ou mesh) se définie généralement par une liste de sommets (vertices) et de faces. Par exemple, huit sommets sont nécessaires pour décrire un cube. Et puisque nous sommes dans un repère à trois dimensions, chaque sommet comporte trois coordonnées : x, y et z. Si on place le centre du cube à l’origine de la scène avec des sommets définies entre les valeurs -0.5 et 0.5, on obtient donc le tableau suivant :

vertices

vertices 3D

Pour compléter notre exemple, on va associer une couleur à chaque sommet (pour visualiser les faces plus simplement lors du rendu). Cette couleur se compose également de trois valeurs : rouge, vert et bleu (format RGB). Un sommet sera donc défini par une position et une couleur associée de la manière suivante :

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

Pour des objets complexes, on chargera généralement les valeurs depuis un fichier (par exemple depuis un format Autodesk FBX qui fera l’objet d’un prochain post pour charger le vaisseau). Dans le cas présent, on va déclarer manuellement notre tableau de sommets, avec des couleurs définies arbitrairement :

 VertexPositionColor cubeVertices [] =
{
    { XMFLOAT3(-0.5f, -0.5f, -0.5f), XMFLOAT3(0.0f, 0.0f, 0.0f) }, // Vertice 0 at -0.5, -0.5, -0.5 // White
    { XMFLOAT3(-0.5f, -0.5f,  0.5f), XMFLOAT3(0.0f, 0.0f, 1.0f) }, // Vertice 1 at -0.5, -0.5,  0.5 // Blue
    { XMFLOAT3(-0.5f,  0.5f, -0.5f), XMFLOAT3(0.0f, 1.0f, 0.0f) }, // Vertice 2 at -0.5,  0.5, -0.5 // Green
    { XMFLOAT3(-0.5f,  0.5f,  0.5f), XMFLOAT3(0.0f, 1.0f, 1.0f) }, // Vertice 3 at -0.5,  0.5,  0.5 
    { XMFLOAT3( 0.5f, -0.5f, -0.5f), XMFLOAT3(1.0f, 0.0f, 0.0f) }, // Vertice 4 at  0.5, -0.5, -0.5 // Red
    { XMFLOAT3( 0.5f, -0.5f,  0.5f), XMFLOAT3(1.0f, 0.0f, 1.0f) }, // Vertice 5 at  0.5, -0.5,  0.5 
    { XMFLOAT3( 0.5f,  0.5f, -0.5f), XMFLOAT3(1.0f, 1.0f, 0.0f) }, // Vertice 6 at  0.5,  0.5, -0.5 
    { XMFLOAT3( 0.5f,  0.5f,  0.5f), XMFLOAT3(1.0f, 1.0f, 1.0f) }, // Vertice 7 at  0.5,  0.5,  0.5 // Black
};

Les faces (déclaration des indices)

Maintenant que nous avons nos sommets, il reste toutefois à définir les faces de notre cube pour compléter sa définition : celui-ci comporte 6 faces chacune composé de 4 sommets (un carré). Cependant, si vous raisonnez plus largement,  certains objets 3D peuvent se définir avec des faces non rectangulaires. Par exemple une pyramide à base carrée comporte 1 face de 4 sommets (la base) + 4 faces de 3 sommets (les côtés triangulaires).

Les algorithmes pour remplir les faces seraient particulièrement complexes si le nombre de sommets étaient variables. Direct3D (et plus généralement le monde de la 3D) utilise donc des triangles comme primitive pour définir des faces ; chaque carré pouvant se représenter par deux triangles.

Si on reprend l’exemple de notre cube, la première face se compose donc de deux triangles:

-
Triangle 1: {0, 2, 1}

-
Triangle 2: {1, 2, 3}

 

faces 3D

Et en extrapolant, on se retrouve donc avec 12 triangles pour une définition complète des faces. On va cataloguer/indexer ces triangles dans un tableau que l’on nomme liste d’index ou indices.

 unsigned short cubeIndices[] = 
{
    0,2,1, // -x
    1,2,3,

    4,5,6, // +x
    5,7,6,

    0,1,5, // -y
    0,5,4,

    2,6,7, // +y
    2,7,3,

    0,4,6, // -z
    0,6,2,

    1,3,7, // +z
    1,7,5,
};

Vertex buffer et Index buffer

On a désormais une définition de notre cube stockée dans deux tableaux (cubeVertices et cubeIndices) mais DirectX à besoin d’un coup de main supplémentaire pour consommer et interpréter les données précédentes.

En premier lieu, les deux listes [sommets (vertices) et faces (indices)] doivent être stockées dans des buffers prévus à cet effet, nommés sans trop d'originalité Vertex buffer et Index buffer : c’est le rôle de la méthode ID3D11Device::CreateBuffer.

Pour faire simple, ID3D11Device::CreateBuffer récupère en entrée …

-
Les données via la structure D3D11_SUBRESOURCE_DATA qui pointe sur nos tableaux cubeVertices ou cubeIndices.

-
Une description via D3D11_BUFFER_DESC pour indiquer l’usage de ces données : Vertex buffer, Index Buffer, …

 

… et nous retourne un objet qui implémente l’interface D3D11Buffer (notre buffer Direct3D). On obtient donc le code suivant pour instancier notre Vertex buffer :

 D3D11_SUBRESOURCE_DATA vertexBufferData = { 0 };
vertexBufferData.pSysMem = cubeVertices;
vertexBufferData.SysMemPitch = 0;
vertexBufferData.SysMemSlicePitch = 0;

CD3D11_BUFFER_DESC vertexBufferDesc(sizeof(cubeVertices), D3D11_BIND_VERTEX_BUFFER);
DX::ThrowIfFailed(
    m_d3dDevice->CreateBuffer(
    &vertexBufferDesc,
    &vertexBufferData,
    &m_vertexBuffer
    )
    );

Et le code quasi équivalent pour son adjoint l’Index Buffer :    

 D3D11_SUBRESOURCE_DATA indexBufferData = { 0 };
indexBufferData.pSysMem = cubeIndices;
indexBufferData.SysMemPitch = 0;
indexBufferData.SysMemSlicePitch = 0;

CD3D11_BUFFER_DESC indexBufferDesc(sizeof(cubeIndices), D3D11_BIND_INDEX_BUFFER);
DX::ThrowIfFailed(
    m_d3dDevice->CreateBuffer(
    &indexBufferDesc,
    &indexBufferData,
    &m_indexBuffer
    )
    );

 

Il y a toutefois une différence majeure pour DirectX entre ces deux buffers. Ils contiennent tous les deux des données mais, dans l’état, seul l’Index buffer est compréhensible par Direct3D. Pourquoi ?

Notre Vertex buffer contient la position des sommets, mais également des couleurs. Et à termes, vous allez renseigner d’autres informations associés à chaque sommet (notamment pour éclairer convenablement votre objet et le texturer) . Du coup, DirectX n’a aucune idée de la signification exacte des données que vous lui fournissez en entrée. Il n’est pas capable de les digérer sans un coup de pouce additionnel. Ce petit service est rendu par l’Input layout  (cf. section suivante).

Ce n’est pas le cas de l’Index buffer dont le seul travail est de d’indexer les sommets pour fournir une liste de triangles. Direct3D connait nativement la signification de ces données.

Input layout

Le vertex buffer contient pour le moment une suite de nombre flottants. Direct3D (et donc la carte graphique) va traiter ces données en les passant dans plusieurs moulinettes dont la principale se nomme vertex shader (qui fera l’objet du prochain post).

Dans l’état, la carte graphique ne sait pas la structure de ces données. Direct3D a besoin qu’on déblaie le passage en précisant le format de chaque élément pour que le vertex shader puisse les interpréter. L’input layout sert exactement à ca ! Dans notre cas, on obtient donc la définition suivante:

 const D3D11_INPUT_ELEMENT_DESC vertexDesc [] =
{
    { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
    { "COLOR", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};

DX::ThrowIfFailed(
    m_d3dDevice->CreateInputLayout(
    vertexDesc,
    ARRAYSIZE(vertexDesc),
    fileData->Data,
    fileData->Length,
    &m_inputLayout
    )
    );
  • POSITION : Exprime la position de nos sommets sous la forme des 3 flottants (32bits) : x, y et z.

  • COLOR : Exprime la couleur de nos sommets sous la forme de 3 flottants également (32bits): r, g et b.

 

Vous pouvez constater qu’on utilise le format DXGI_FORMAT_R32G32B32_FLOAT dans les deux cas, qui s’apparente à l’utilisation d’une couleur. Ce n’est pas une erreur, comme il s’agit de 3 flottants dans les deux cas, pourquoi ne pas utiliser le même format?

De manière plus imagé, notre tableau de flottants est donc désormais interprétable de la manière suivante par Direct3D:

input layout

 

Bon, je vous laisse digérer tout ca ^^.

 

Vous pouvez d’ores et déjà télécharger le code ici (qui ressemble pas mal au template par défaut dans l’état; mais ça ne va pas tarder à changer) et consulter la portion de code SceneRenderer::CreateDeviceResources() qui reprend (hormis le chargement des shaders) l’ensemble du code de cet article.

 

SceneRenderer::CreateDeviceResources

 void SceneRenderer::CreateDeviceResources()
{
    auto loadVSTask = DX::ReadDataAsync("SimpleVertexShader.cso");
    auto loadPSTask = DX::ReadDataAsync("SimplePixelShader.cso");

    auto createVSTask = loadVSTask.then([this](Platform::Array<byte>^ fileData) {
        DX::ThrowIfFailed(
            m_d3dDevice->CreateVertexShader(
            fileData->Data,
            fileData->Length,
            nullptr,
            &m_vertexShader
            )
            );

        const D3D11_INPUT_ELEMENT_DESC vertexDesc [] =
        {
            { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
            { "COLOR", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
        };

        DX::ThrowIfFailed(
            m_d3dDevice->CreateInputLayout(
            vertexDesc,
            ARRAYSIZE(vertexDesc),
            fileData->Data,
            fileData->Length,
            &m_inputLayout
            )
            );
    });

    auto createPSTask = loadPSTask.then([this](Platform::Array<byte>^ fileData) {
        DX::ThrowIfFailed(
            m_d3dDevice->CreatePixelShader(
            fileData->Data,
            fileData->Length,
            nullptr,
            &m_pixelShader
            )
            );

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

    auto createCubeTask = (createPSTask && createVSTask).then([this]() {
        VertexPositionColor cubeVertices [] =
        {
            { XMFLOAT3(-0.5f, -0.5f, -0.5f), XMFLOAT3(0.0f, 0.0f, 0.0f) }, // Vertice 0 at -0.5, -0.5, -0.5 // White
            { XMFLOAT3(-0.5f, -0.5f,  0.5f), XMFLOAT3(0.0f, 0.0f, 1.0f) }, // Vertice 1 at -0.5, -0.5,  0.5 // Blue
            { XMFLOAT3(-0.5f,  0.5f, -0.5f), XMFLOAT3(0.0f, 1.0f, 0.0f) }, // Vertice 2 at -0.5,  0.5, -0.5 // Green
            { XMFLOAT3(-0.5f,  0.5f,  0.5f), XMFLOAT3(0.0f, 1.0f, 1.0f) }, // Vertice 3 at -0.5,  0.5,  0.5 
            { XMFLOAT3( 0.5f, -0.5f, -0.5f), XMFLOAT3(1.0f, 0.0f, 0.0f) }, // Vertice 4 at  0.5, -0.5, -0.5 // Red
            { XMFLOAT3( 0.5f, -0.5f,  0.5f), XMFLOAT3(1.0f, 0.0f, 1.0f) }, // Vertice 5 at  0.5, -0.5,  0.5 
            { XMFLOAT3( 0.5f,  0.5f, -0.5f), XMFLOAT3(1.0f, 1.0f, 0.0f) }, // Vertice 6 at  0.5,  0.5, -0.5 
            { XMFLOAT3( 0.5f,  0.5f,  0.5f), XMFLOAT3(1.0f, 1.0f, 1.0f) }, // Vertice 7 at  0.5,  0.5,  0.5 // Black
        };

        D3D11_SUBRESOURCE_DATA vertexBufferData = { 0 };
        vertexBufferData.pSysMem = cubeVertices;
        vertexBufferData.SysMemPitch = 0;
        vertexBufferData.SysMemSlicePitch = 0;
        CD3D11_BUFFER_DESC vertexBufferDesc(sizeof(cubeVertices), D3D11_BIND_VERTEX_BUFFER);
        DX::ThrowIfFailed(
            m_d3dDevice->CreateBuffer(
            &vertexBufferDesc,
            &vertexBufferData,
            &m_vertexBuffer
            )
            );

        unsigned short cubeIndices [] =
        {
            0, 2, 1, // -x
            1, 2, 3,

            4, 5, 6, // +x
            5, 7, 6,

            0, 1, 5, // -y
            0, 5, 4,

            2, 6, 7, // +y
            2, 7, 3,

            0, 4, 6, // -z
            0, 6, 2,

            1, 3, 7, // +z
            1, 7, 5,
        };

        m_indexCount = ARRAYSIZE(cubeIndices);

        D3D11_SUBRESOURCE_DATA indexBufferData = { 0 };
        indexBufferData.pSysMem = cubeIndices;
        indexBufferData.SysMemPitch = 0;
        indexBufferData.SysMemSlicePitch = 0;
        CD3D11_BUFFER_DESC indexBufferDesc(sizeof(cubeIndices), D3D11_BIND_INDEX_BUFFER);
        DX::ThrowIfFailed(
            m_d3dDevice->CreateBuffer(
            &indexBufferDesc,
            &indexBufferData,
            &m_indexBuffer
            )
            );
    });

    createCubeTask.then([this]() {
        m_loadingComplete = true;
    });
}

Prochain épisode prochainement sur les shaders et les matrices, …. keep posted ! :)

Ressources

 

Télécharger le Code