Astuces DotNet (Sébastien Courtois)

16/05/2011

[XNA/DirectX/OpenGL] Les bases de la programmation 3D

Filed under: 3D, Débutant, DirectX, Intermediaire, Jeux vidéos, OpenGL, Windows Phone, XBOX 360, XNA — Étiquettes : , , , , , , , , — sebastiencourtois @ 09:00

Je n’ai pas été très bavard ces derniers temps (mon dernier post remonte à 5 mois… Confus). Une des mes excuses est mon départ de France pour travailler pour une société américaine à San Francisco. Je suis bien installé maintenant et je vais me remettre à bloguer un peu plus Sourire.

Cela fait quelques temps que je fais des posts sur la 3D mais je n’arrivais jamais à expliquer les bases de la programmation 3D de façon simple et claire. Au travers de mes recherches dans ce domaine, je suis tombé sur le site de Bobby Anguelov. Plutôt spécialisé en Intelligence Artificielle, il a écrit des cours très agréables à lire sur DirectX 10 en C++. Il a aussi été enseignant à l’université de Pretoria (Afrique du Sud). C’est un de ses cours que j’ai décidé de traduire et de publier ici.

Pourquoi ? :

  • Le cours est claire, simple et bien illustré. Pourquoi refaire quelque chose qui a déjà été bien fait par quelqu’un d’autre ? Sourire
  • Le cours est vraiment grand public/débutant. Il suffit d’aimer la 3D et de connaitre les bases de la programmation pour pouvoir lire ce cours.
  • Le cours est plutôt générique et les informations contenues dans ces documents peuvent aussi s’appliquer à toutes les technologies 3D (DirectX/OpenGL/XNA) ==> Il n’y a aucun code source dans ce cours.

Note : La traduction a été complexe car de nombreux termes sont les termes utilisés dans les blogs/forums … Par conséquent, j’ai essayé un maximum de conserver ces termes tout en fournissant les traductions françaises.

Le cours se découpe en 6 chapitres :

  1. Introduction et historique de la 3D (30 Slides)
  2. Mathématiques appliquées à la 3D (37 Slides)
  3. Le pipeline graphique (27 Slides)
  4. L’étape applicative du pipeline graphique (23 Slides)
  5. L’étape géométrique du pipeline graphique (24 Slides)
  6. L’étape rastérisation du pipeline graphique (84 Slides)

Le cours complet en francais est disponible en format zip.

Si vous préférez la version anglaise, vous pourrez trouver les cours à cette adresse : http://takinginitiative.net/computer-graphics-course-slides/

NOTE : Si vous comptez utiliser ces documents pour les publier ou les enseigner, merci de nous prévenir. Nous ne voulons pas de droit d’auteurs sur ces documents Sourire mais nous souhaitons juste savoir où sont utilisés ces informations (par fierté Sourire) et être cité (lien vers nos blogs …). Si vous souhaitez la version Powerpoint, contactez nous (commentaire sur ce blog ou celui de Bobby).

  • La suite … ?

J’ai pas mal d’idées de posts pour la suite. Les statistiques de ce blog montrent que XNA 4 est très populaire en ce moment et j’envisage deux tutoriaux sur le HLSL en XNA 4. Je vais continuer les posts sur DirectX. Toutefois, je vais me focaliser sur DirectX 11. Enfin, j’ai récupérer le SDK de PhysX 3 (sorite ce mois ci) et je vais faire des cours d’introduction à cette API physique très populaire dans les jeux vidéos. Je vais aussi voir pour des tutoriaux sur le Cry Engine 3 quand le SDK sera publique (Août aux dernières nouvelles).

En ce qui concerne XNA sur Silverlight 5…. Je ferais des tutoriaux lors de la sortie finale car l’API actuelle n’est pas encore complète (fin d’année je pense).

N’hésitez pas à mettre des commentaires sur ce cours et si vous avez des idées de tutoriaux/sujets que vous voudriez voir approfondis.

Publicités

26/11/2010

[DirectX 10/11] Tutoriel 2 : Création d’un device Direct3D

Filed under: 3D, C++, Débutant, DirectX, Intermediaire — Étiquettes : , , , , , — sebastiencourtois @ 17:26

Après un premier article d’introduction à DirectX, nous allons maintenant voir comment créer un device Direct3D (accès à la carte graphique). L’idée générale de cette série d’article est de réaliser un moteur de jeu gérant à la fois DirectX 10 et 11 afin de voir les évolutions des deux technologies.

  • Architecture générale du moteur

Avant de rentrer dans le vif du sujet, nous allons parler un peu architecture. Afin de simplifier la gestion des différentes versions de DirectX, nous allons créer une classe pour gérer chacune des versions de DirectX. Ces classes hériteront d’une super classe indiquant les méthodes obligatoires pour un gestionnaire.

dxt2-archi1

Les trois méthodes de DxManager sont :

  • Init(HWND* hWnd) : Initialisation du gestionnaire DirectX. Cette méthode va créer l’ensemble des ressources nécessaire pour l’utilisation de DirectX. Cette méthode ne sera appelé qu’une seule fois lors du lancement du programme. Il est nécessaire de lui fournir le handle de la fenêtre afin que DirectX puisse savoir où il doit afficher le rendu. Cette méthode renvoie ‘false’ en cas de problème lors de l’initialisation.
  • Render() : Méthode de rendu. Cette méthode sera appelée à chaque fois que l’on souhaite dessiner quelque chose à l’écran.
  • Error(LPCST msg) : Gestion des erreurs. Affichage de messages d’erreurs (MessageBox).

C’est le WinMain de l’application qui sera chargé de créer le gestionnaire directX approprié.

//DirectX 10/11 Manager
DXManager *dxManager;
//Activation/Désactivation de DirectX 11
bool DirectX11Enabled = true;

//Point d'entrée du programme
int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow )
{
    //Initialisation d'une fenêtre
    if(!InitWindow(hInstance,nCmdShow)) return 0;

    if(DirectX11Enabled)
        dxManager = new DX11Manager();
    else
        dxManager = new DX10Manager();

    if(!dxManager->Init(&hWnd)) return -1;

    // Boucle 
    MSG msg = {0};
    while (WM_QUIT != msg.message)
    {
        while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE) == TRUE)
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);            
        }    
        dxManager->Render();
    }
    return (int) msg.wParam;
}

  • Informations générales sur l’initialisation Direct3D

Dans ce tutorial, nous allons principalement parler de l’initialisation de Direct3D. Celle-ci se passe en 5 étapes :

  1. Création de la SwapChain
  2. Création du Device
  3. Récupération du Backbuffer
  4. Création du RenderTarget
  5. Création du ViewPort

L’étape la plus importante est la création du Device (Etape 2). Le Device est l’accès à la carte graphique. C’est à travers cet objet que nous allons pouvoir fournir les informations 3D que la carte graphique devra afficher.

Une autre notion importante est la notion de FrontBuffer et de BackBuffer. La carte graphique contient une zone mémoire où se trouve les données pour les pixels affichés à l’écran (FrontBuffer). Si l’on modifie l’un de ces pixels directement dans le FrontBuffer, le résultat sera instantanément affiché à l’écran. Le problème est que, si la chose que l’on souhaite dessiner à l’écran prend un peu trop de temps, on verra les morceaux du dessins apparaitre petit à petit ce qui peut être désagréable.Pour éviter ce problème, on utilise une deuxième zone mémoire nommé ‘BackBuffer’ qui permet de réaliser le dessin complet puis, une fois fini, d’envoyer l’ensemble des données dans le FrontBuffer pour affichage. Pour résumer, un buffer pour dessiner et un buffer pour afficher. Ce processus “d’échange” de buffer est réalisé par un élément de DirectX nommé SwapChain (Swap = échange en anglais).

IC412615

Fonctionnement de la SwapChain (tiré de MSDN). Comme vous pouvez le voir, il est possible d’avoir plusieurs BackBuffer.

Le RenderTarget défini l’endroit où l’on souhaite dessiner les données envoyés à la carte graphique. Dans notre cas, on souhaite dessiner sur le BackBuffer. Le renderTarget sera donc lié à notre BackBuffer.

Le Viewport défini la zone de l’écran sur laquelle on souhaite dessiner les pixels. On utilisera généralement l’ensemble de la fenêtre. Mais, dans certains cas, on peut souhaite afficher des choses différentes selon les parties de la fenêtre.

super_mario_kart-155809-1

Exemple d’utilisation de multiple Viewports avec un Viewport par joueur (Mario Kart SNES).

  • Gestionnaire Direct3D 10

Le gestionnaire DirectX 10 est constitué d’un fichier DX10Manager.h contenant la définition de la classe et une classe DX10Manager.cpp contenant son implémentation.

  1. Définition de la classe DX10Manager
#pragma once
#include <Windows.h>
#include <D3DX10.h>
#include "DXManager.h"

class DX10Manager : public DXManager
{
private:
    //Handle de la fenêtre
    HWND*                        hWnd;
    
    //Device (Lien avec la carte graphique
    ID3D10Device*                D3DDevice;
    //Swap Chain 
    IDXGISwapChain*                SwapChain;
    //Render Target
    ID3D10RenderTargetView*        RenderTargetView;
    //Viewport (zone d'affichage)
    D3D10_VIEWPORT                ViewPort;

public:
    DX10Manager(void);
    //Destructeur
    ~DX10Manager(void);
    //Initialisation DirectX
    virtual bool Init(HWND* hWnd);
    //Rendu 3D
    virtual void Render();

private:
    //Message d'erreur
    virtual void Error(LPCSTR Message);
};

Comme indiqué plus haut, la classe DX11 Manager hérite de la classe DXManager. Par conséquent, elle doit implémenter les méthodes Init,Render et Error décrite plus haut. Les méthodes supplémentaires de cette classe sont le constructeur et le destructeur (pour la libération des ressources). Au niveau des propriétés, on retrouve les propriétés décrites dans la section précédente.

2.     Implémentation de la classe DX10Manager

Dans cette partie, nous allons nous interesser à la méthode Init.

//Initialisation DirectX

bool DX10Manager::Init(HWND* hWnd)

{

    //Sauvegarde du handle de la fenêtre

  
this->hWnd = hWnd;

   
    //Récupération des dimensions de la fenêtre

  
RECT rc;

    GetClientRect( *hWnd, &rc );

    UINT width = rc.right – rc.left;

    UINT height = rc.bottom – rc.top;

Ce morceau de code ne fait que sauvegarder le handle de la fenêtre (car il sera réutilisé plus loin) puis de récupérer les dimensions de la fenêtre.

    //Création de la Swap Chain
    DXGI_SWAP_CHAIN_DESC swapChainDesc;
    ZeroMemory(&swapChainDesc, sizeof(DXGI_SWAP_CHAIN_DESC));
    
    //Création de deux buffers (un back buffer et un front buffer)
    swapChainDesc.BufferCount = 2;
    swapChainDesc.BufferDesc.Width = width;
    swapChainDesc.BufferDesc.Height = height;
    swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
    swapChainDesc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
    
    //Définition du nombre d'image par secondes maximun (60 images).
    swapChainDesc.BufferDesc.RefreshRate.Numerator = 60;
    swapChainDesc.BufferDesc.RefreshRate.Denominator = 1;
    
    //Multisampling setting
    swapChainDesc.SampleDesc.Quality = 0;
    swapChainDesc.SampleDesc.Count = 1;

    //Liaison de la swapchain et de le fenêtre
    swapChainDesc.OutputWindow = *hWnd;
    swapChainDesc.Windowed = true;    

On crée ensuite une structure décrivant la SwapChain. On indique tout d’abord le nombre de buffer que l’on souhaite utiliser (ici 2 car on compte utiliser FrontBuffer  et un BackBuffer). Il est nécessaire d’indiquer la taille des buffers (taille de la fenêtre) ainsi que le format des données pour les pixels (DXGI_FORMAT_R8G8B8A8_UNORM correspond à 8 bit pour chaque canal RGBA. La liste des formats de pixel est disponible sur le site MSDN.). La propriété BufferUsage indique que les buffers seront dessiné par un RenderTarget (Liste des BufferUsage disponible sur le site MSDN).La propriété RefreshRate permet de définir le nombre d’images par secondes maximum .La propriété SampleDesc permet de piloter le multisampling. Cette technique permet de réduire l’aliasing (effet de crénelage). Dans notre cas, nous allons la désactiver en mettant Quality à 0 et Count à 1 (en effet, ce domaine est vaste et nécessiterait un tutoriel à lui tout seul). Les deux dernières propriétés sont la définition de la fenêtre sur laquelle on souhaite faire apparaitre le résultat ainsi que si l’on souhaite que l’application fonctionne en plein écran ou non.

//Création du device
HRESULT codeErreur = D3D10CreateDeviceAndSwapChain( NULL, 
                                                    D3D10_DRIVER_TYPE_HARDWARE, 
                                                    NULL, 
                                                    D3D10_CREATE_DEVICE_DEBUG, 
                                                    D3D10_SDK_VERSION, 
                                                    &swapChainDesc, 
                                                    &this->SwapChain, 
                                                    &this->D3DDevice); 
if (FAILED(codeErreur)) 
{
    //Si erreur
    this->Error("Erreur lors de la création du device 3D.");
    return false;
}

On arrive dans la partie de création du Device avec la méthode D3D10CreateDeviceAndSwapChain. Le premier paramètre représente l’adapteur à utiliser. Cela correspond tout simplement aux GPU ou autre processeur vidéo que l’on souhaite utiliser. Si vous voulez utiliser l’adapter par défaut, il suffit de mettre NULL. Vous pouvez avoir la liste des adapters disponible grâce à la méthode EnumAdapters.

Le deuxième paramètre est le type de driver que vous souhaitez utiliser. Il y a deux valeurs qui sont utilisés principalement. D3D10_DRIVER_TYPE_HARDWARE indique que l’on souhaite que le rendu 3D soit réalisé par la carte graphique. Cela est le mode par défaut et le plus rapide. A l’inverse, le mode D3D10_DRIVER_TYPE_REFERENCE demande au CPU de prendre en charge le rendu 3D. Ce mode est très lent et est surtout utilisé si la carte graphique ne contient pas les instructions nécessaires pour réaliser le rendu (par exemple une rendu DX11 ne peut être fait sur une carte ne gérant que du DX10. On préfèrera dans ce cas utiliser le mode REFERENCE).

La troisième paramètre est le software rasterizer. Il s’agit d’un pointeur vers du code permettant de faire un rendu en plus de DirectX (sorte d’extension). Nous n’utiliserons jamais cela dont nous pouvons mettre NULL ici.

Le quatrième paramètre correspond aux attributs de création du device. Nous utiliserons un device de DEBUG afin de pouvoir avoir accès aux messages d’erreurs DirectX. Ce paramètre est un flag et peut être cumulés avec d’autres valeurs disponibles ici.

Le cinquième paramètre défini le numéro de version du SDK DirectX utilisé pour créer le programme.

La méthode utilisé ici crée le Device + la SwapChain. Afin de la créer, le sixième paramètre fourni la structure de description de cette SwapChain.

Les deux derniers paramètres sont des paramètres de sorties qui nous permettent de récupérer un pointeur sur le Device (D3DDevice) et sur la SwapChain.

La méthode D3D10CreateDeviceAndSwapChain renvoie une valeur HRESULT indiquant s’il y a eu une erreur ou non. La méthode FAILED() analyse le HRESULT et renvoie true s’il y a une erreur.

    //Création du rendertargetview
    ID3D10Texture2D* backbuffer;
    if(FAILED(this->SwapChain->GetBuffer(0,__uuidof(ID3D10Texture2D), (LPVOID*) &backbuffer)))
    {
        //Si erreur
        this->Error("Erreur lors de la récupération du backbuffer.");
        return false;
    }
    if(FAILED(this->D3DDevice->CreateRenderTargetView(backbuffer,NULL,&this->RenderTargetView)))
    {
        //Si erreur
        this->Error("Erreur lors de la création du render target view.");
        return false;
    }
    backbuffer->Release();
    this->D3DDevice->OMSetRenderTargets(1,&this->RenderTargetView,NULL);

Maintenant nous allons créer le RenderTarget qui va indiquer où l’on souhaite faire le dessin de la scène. Tout d’abord il est nécessaire de récupérer le backbuffer créé par la SwapChain gràce à la méthode GetBuffer de celle-ci. Le premier paramètre est l’index du backbuffer que l’on souhaite (0 = premier back buffer). Le deuxième paramètre indique le type de données que l’on souhaite récupérer. On utilisera ici un ID3D10Texture2D représentant une texture en 2 dimensions.

Une fois récupéré, on peut créer le RenderTarget en fournissant le backbuffer. Le deuxième paramètre de CreateRenderTargetView  permet fournir une structure de description pour le RenderTarget. Dans notre cas, nous utiliserons celui par défaut d’où la valeur NULL.

Une fois cela défini, on libère la ressource prise pour le backbuffer (on en avait besoin uniquement pour défini le RenderTarget). Puis on indique au device le RenderTarget à utiliser avec la méthode OMSetRenderTargets. Le premier paramètre indique le nombre de RenderTarget utilisé (ici 1). Le deuxième paramètre est un pointeur vers le RenderTarget choisi. Le dernier paramètre est un autre RenderTarget lié aux profondeur (DepthStencilView). Nous n’en aurons pas besoin ici => NULL.

    //Création du viewport
    ViewPort.Width = width;
    ViewPort.Height = height;
    ViewPort.MinDepth = 0.0f;
    ViewPort.MaxDepth = 1.0f;
    ViewPort.TopLeftX = 0;
    ViewPort.TopLeftY = 0;
    //Assignation du viewport
    this->D3DDevice->RSSetViewports(1,&this->ViewPort);

Dernière étape de l’initialisation : Le viewport.  Il suffit de remplir une structure D3D10_VIEWPORT et d’indiquer la taille (Width/Height), la position (TopLeftX/TopLeftY) ainsi que les profondeurs minimale et maximale (MinDepth/MaxDepth). Une fois défini,on indique ces information au Device gràce à la méthode RSSetViewports. Comme pour SetRenderTarget, le premier paramètre indique le nombre de viewport utilisé. Le deuxième est un pointeur vers la structure décrivant le viewport.

  • Gestionnaire Direct3D 11

Le gestionnaire DirectX 11 est constitué d’un fichier DX11Manager.h contenant la définition de la classe et une classe DX11Manager.cpp contenant son implémentation.

  1. Définition de la classe DX11Manager
#pragma once
#include <Windows.h>
#include <D3D11.h>
#include "DXManager.h"

class DX11Manager : public DXManager
{
private:
    //Handle de la fenêtre
    HWND*                        hWnd;
    
    //Device (Lien avec la carte graphique
    ID3D11Device*                D3DDevice;
    //Swap Chain 
    IDXGISwapChain*                SwapChain;
    //Render Target
    ID3D11RenderTargetView*        RenderTargetView;
    //Viewport (zone d'affichage)
    D3D11_VIEWPORT                ViewPort;
    //Context du device
    ID3D11DeviceContext* d3dImmediateContext;

public:
    DX11Manager(void);
    ~DX11Manager(void);
    //Initialisation DirectX
    virtual bool Init(HWND* hWnd);
    //Rendu 3D
    virtual void Render();

private:
    //Message d'erreur
    virtual void Error(LPCSTR Message);
};

Comme indiqué plus haut, la classe DX11Manager hérite de la classe DXManager. Par conséquent, elle doit implémenter les méthodes Init,Render et Error décrite plus haut. Les méthodes supplémentaires de cette classe sont le constructeur et le destructeur (pour la libération des ressources). Au niveau des propriétés, on retrouve les propriétés décrites dans la section précédente.

Remarque Différences DX10/DX11 : Vous pouvez voir une différence entre la version DirectX 10 et 11. En effet, une nouvelle propriété à été rajoutée (ID3D11DeviceContext). Cela est dû à une nouvelle architecture de DirectX facilitant le multithreading. En effet, jusqu’à DirectX 10, il était impossible d’utiliser plusieurs threads pour envoyer des données à la carte graphique. Avec DirectX 11, il est possible de faire des appels à la carte graphique depuis plusieurs threads à l’aide de contexte.

directx-opengl5-w-157028-13

Chaque thread a un DeviceContext lié au Device. Un thread donne la liste de ses données à afficher à son DeviceContext. Une fois l’ensemble des données fournis, celle-ci sont donné au Device principal qui réalise l’affichage.

2.     Implémentation de la classe DX11Manager

Dans cette partie, nous allons nous interesser à la méthode Init.

//Initialisation DirectX
bool DX11Manager::Init(HWND* hWnd)
{
    //Sauvegarde du handle de la fenêtre
    this->hWnd = hWnd;
    
    //Récupération des dimensions de la fenêtre
    RECT rc;
    GetClientRect( *hWnd, &rc );
    UINT width = rc.right - rc.left;
    UINT height = rc.bottom - rc.top;

Ce morceau de code ne fait que sauvegarder le handle de la fenêtre (car il sera réutilisé plus loin) puis de récupérer les dimensions de la fenêtre.

//Création de la Swap Chain
DXGI_SWAP_CHAIN_DESC swapChainDesc;
ZeroMemory(&swapChainDesc, sizeof(DXGI_SWAP_CHAIN_DESC));

//Création de deux buffers (un back buffer et un front buffer)
swapChainDesc.BufferCount = 2;
swapChainDesc.BufferDesc.Width = width;
swapChainDesc.BufferDesc.Height = height;
swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
swapChainDesc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;;

//Définition du nombre d'image par secondes maximun (60 images).
swapChainDesc.BufferDesc.RefreshRate.Numerator = 60;
swapChainDesc.BufferDesc.RefreshRate.Denominator = 1;

//Multisampling setting
swapChainDesc.SampleDesc.Quality = 0;
swapChainDesc.SampleDesc.Count = 1;

//Liaison de la swapchain et de le fenêtre
swapChainDesc.OutputWindow = *hWnd;
swapChainDesc.Windowed = true;  

On crée ensuite une structure décrivant la SwapChain. On indique tout d’abord le nombre de buffer que l’on souhaite utiliser (ici 2 car on compte utiliser FrontBuffer  et un BackBuffer). Il est nécessaire d’indiquer la taille des buffers (taille de la fenêtre) ainsi que le format des données pour les pixels (DXGI_FORMAT_R8G8B8A8_UNORM correspond à 8 bit pour chaque canal RGBA. La liste des formats de pixel est disponible sur le site MSDN.). La propriété BufferUsage indique que les buffers seront dessiné par un RenderTarget (Liste des BufferUsage disponible sur le site MSDN).La propriété RefreshRate permet de définir le nombre d’images par secondes maximum .La propriété SampleDesc permet de piloter le multisampling. Cette technique permet de réduire l’aliasing (effet de crénelage). Dans notre cas, nous allons la désactiver en mettant Quality à 0 et Count à 1 (en effet, ce domaine est vaste et nécessiterait un tutoriel à lui tout seul). Les deux dernières propriétés sont la définition de la fenêtre sur laquelle on souhaite faire apparaitre le résultat ainsi que si l’on souhaite que l’application fonctionne en plein écran ou non.

//Création du device
D3D_FEATURE_LEVEL FeatureLevels[] = {    
                                        D3D_FEATURE_LEVEL_11_0,
                                        D3D_FEATURE_LEVEL_10_1,
                                        D3D_FEATURE_LEVEL_10_0,
                                        D3D_FEATURE_LEVEL_9_3,
                                        D3D_FEATURE_LEVEL_9_2,
                                        D3D_FEATURE_LEVEL_9_1,
                                    };
int FeatureLevelCount = sizeof(FeatureLevels) / sizeof(D3D_FEATURE_LEVEL);
D3D_FEATURE_LEVEL SelectedFeatureLevel;
HRESULT codeErreur = D3D11CreateDeviceAndSwapChain(NULL, 
                                            D3D_DRIVER_TYPE_HARDWARE, 
                                            NULL, 
                                            D3D11_CREATE_DEVICE_DEBUG, 
                                            FeatureLevels, 
                                            FeatureLevelCount, 
                                            D3D11_SDK_VERSION, 
                                            &swapChainDesc, 
                                            &this->SwapChain, 
                                            &this->D3DDevice,
                                            &SelectedFeatureLevel,
                                            &this->d3dImmediateContext);
if (FAILED(codeErreur)) 
{
    //Si erreur
    this->Error("Erreur lors de la création du device 3D.");
    return false;
}

On arrive dans la partie de création du Device avec la méthode D3D11CreateDeviceAndSwapChain. Le premier paramètre représente l’adapter à utiliser. Cela correspond tout simplement aux GPU ou autre processeur vidéo que l’on souhaite utiliser. Si vous voulez utiliser l’adapter par défaut, il suffit de mettre NULL. Vous pouvez avoir la liste des adapters disponible grâce à la méthode EnumAdapters.

Le deuxième paramètre est le type de driver que vous souhaitez utiliser. Il y a deux valeurs qui sont utilisés principalement. D3D11_DRIVER_TYPE_HARDWARE indique que l’on souhaite que le rendu 3D soit réalisé par la carte graphique. Cela est le mode par défaut et le plus rapide. A l’inverse, le mode D3D11_DRIVER_TYPE_REFERENCE demande au CPU de prendre en charge le rendu 3D. Ce mode est très lent et est surtout utilisé si la carte graphique ne contient pas les instructions nécessaires pour réaliser le rendu (par exemple une rendu DX11 ne peut être fait sur une carte ne gérant que du DX10. On préfèrera dans ce cas utiliser le mode REFERENCE).

La troisième paramètre est le software rasterizer. Il s’agit d’un pointeur vers du code permettant de faire un rendu en plus de DirectX (sorte d’extension). Nous n’utiliserons jamais cela dont nous pouvons mettre NULL ici.

Le quatrième paramètre correspond aux attributs de création du device. Nous utiliserons un device de DEBUG afin de pouvoir avoir accès aux messages d’erreurs DirectX. Ce paramètre est un flag et peut être cumulés avec d’autres valeurs disponibles ici.

Le cinquième paramètre défini l’ensemble de FEATURE LEVEL à tester lors de la création du Device. Le paramètre suivant indique le nombre de FEATURE_LEVEL à tester. Les FEATURE_LEVEL sont des niveaux de fonctionnalités disponibles pour une carte. Ainsi, DirectX va tester si elle peut créer un device pour avec les fonctionnalités du premier FEATURE_LEVEL (dans notre cas DX11.0). S’il n’y arrive pas, il va tester avec le FEATURE_LEVEL suivant et ainsi de suite jusqu’à ce qu’il trouve un FEATURE_LEVEL compatible avec la carte.Il renverra l’info dans l’avant dernier paramètre (SelectedFeatureLevel).

La méthode utilisé ici crée le Device + la SwapChain. Afin de la créer, le septième paramètre fourni la structure de description de cette SwapChain.

Les deux suivants paramètres (8/9) sont des paramètres de sorties qui nous permettent de récupérer un pointeur sur le Device (D3DDevice) et sur la SwapChain.

Le dernier paramètre permet de récupérer le DeviceContext principal à utiliser. C’est au travers de celui ci que nous communiquerons avec la carte graphique (et non pas avec le D3DDevice comme en DX10).

La méthode D3D11CreateDeviceAndSwapChain renvoie une valeur HRESULT indiquant s’il y a eu une erreur ou non. La méthode FAILED() analyse le HRESULT et renvoie true s’il y a une erreur.

//Création du rendertargetview
ID3D11Texture2D* backbuffer;
if(FAILED(this->SwapChain->GetBuffer(0,__uuidof(ID3D11Texture2D), (LPVOID*) &backbuffer)))
{
    //Si erreur
    this->Error("Erreur lors de la récupération du backbuffer.");
    return false;
}
if(FAILED(this->D3DDevice->CreateRenderTargetView(backbuffer,NULL,&this->RenderTargetView)))
{
    //Si erreur
    this->Error("Erreur lors de la création du render target view.");
    return false;
}
backbuffer->Release();
this->d3dImmediateContext->OMSetRenderTargets(1,&this->RenderTargetView,NULL);

Maintenant nous allons créer le RenderTarget qui va indiqué où l’on souhaite faire le dessin de la scène. Tout d’abord il est nécessaire de récupérer le backbuffer créé par la SwapChain gràce à la méthode GetBuffer de celle-ci. Le premier paramètre est l’index du backbuffer que l’on souhaite (0 = premier back buffer). Le deuxième paramètre indique le type de données que l’on souhaite récupérer. On utilisera ici un ID3D10Texture2D représentant une texture en 2 dimensions.

Une fois récupérer, on peut créer le RenderTarget en fournissant le backbuffer. Le deuxième paramètre de CreateRenderTargetView  permet fournir une structure de description pour le RenderTarget. Dans notre cas, nous utiliserons celui par défaut d’où la valeur NULL.

Une fois cela défini, on libère la ressource prise pour le backbuffer (on en avait besoin uniquement pour défini le RenderTarget). Puis on indique au DeviceContext le RenderTarget à utiliser avec la méthode OMSetRenderTargets. Le premier paramètre indique le nombre de RenderTarget utilisé (ici 1). Le deuxième paramètre est un pointeur vers le RenderTarget choisi. Le dernier paramètre est un autre RenderTarget lié à la profondeur (DepthStencilView). Nous n’en aurons pas besoin ici => NULL.

//Création du viewport
ViewPort.Width = width;
ViewPort.Height = height;
ViewPort.MinDepth = 0.0f;
ViewPort.MaxDepth = 1.0f;
ViewPort.TopLeftX = 0;
ViewPort.TopLeftY = 0;
//Assignation du viewport
this->d3dImmediateContext->RSSetViewports(1,&this->ViewPort);

Dernière étape de l’initialisation : Le viewport.  Il suffit de remplir une structure D3D10_VIEWPORT et d’indiquer la taille (Width/Height), la position (TopLeftX/TopLeftY) ainsi que les profondeurs minimale et maximale (MinDepth/MaxDepth). Une fois défini,on indique ces information au Device grâce à la méthode RSSetViewports. Comme pour SetRenderTarget, le premier paramètre indique le nombre de viewport utilisé. Le deuxième est un pointeur vers la structure décrivant le Viewport.

  • La Méthode de rendu

L’objectif de ce tutoriel est surtout l’initialisation de Direct3D. Donc le rendu sera ici très sommaire. Nous allons uniquement vider le RenderTarget (donc le BackBuffer) en le remplissant d’une couleur (noire dans notre exemple) puis dire à la SwapChain d’afficher ce BackBuffer.

En DirectX 10, cela se fait comme suit :

//Rendu 3D
void DX10Manager::Render()
{
    this->D3DDevice->ClearRenderTargetView(this->RenderTargetView,D3DXCOLOR(0,0,0,0));
    
    //Dessin de la scène 3D

    this->SwapChain->Present(0,0);
}

En DirectX 11 :

//Rendu 3D
void DX11Manager::Render()
{
    float clearColor[] = {0,0,0,0};
    this->d3dImmediateContext->ClearRenderTargetView(this->RenderTargetView, clearColor);

    //Dessin de la scène 3D

    this->SwapChain->Present(0,0);
}
  • Libération des ressources

La création de device, buffer et ainsi … consomme de la mémoire et bloque des ressources. Il est nécessaire, lorsque l’on termine le programme, de libérer ces ressources. Nous allons nous servir du destructeur de la classe pour réaliser ces opérations.

En DX 10 :

//Destructeur
DX10Manager::~DX10Manager(void)
{
    //Libération des interfaces
    if(this->RenderTargetView) this->RenderTargetView->Release();
    if(this->SwapChain) this->SwapChain->Release();
    if(this->D3DDevice) this->D3DDevice->Release();
}

En DX 11 :

//Destructeur
DX11Manager::~DX11Manager(void)
{
    //Libération des interfaces
    if(this->RenderTargetView) this->RenderTargetView->Release();
    if(this->SwapChain) this->SwapChain->Release();
    if(this->d3dImmediateContext) this->d3dImmediateContext->Release();
    if(this->D3DDevice) this->D3DDevice->Release();
}

La méthode Release se charge de libérer les ressources. Le if permet de vérifier si ces ressources n’ont pas déjà été libérées.

  • Conclusion

Nous avons vu comment initialiser Direct3D en Direct 10 et 11. Le résultat à l’exécution, bien que très simpliste, est le suivant :

dxt2-final1

Code source du tutorial

Ce tutorial est encore un peu frustrant (beaucoup de code pour un écran noir). Dans le prochain tutorial, nous allons afficher un triangle à l’écran. Nous verrons par la même occasion un outil indispensable en DirectX 10 / 11 : Les Shaders.

07/10/2010

[XNA 4] Tutoriel 3D 3 : Affichage de textures sur un objet 3D

Filed under: .NET, 3D, C# 4, Débutant, Windows Phone, XBOX 360, XNA — Étiquettes : , , , , , , , — sebastiencourtois @ 14:24

Suite aux tutoriels précédents, nous avons appris à créer un cube coloré en 3D. La plupart des jeux utilisent des textures pour rendre plus réalistes leurs formes. Une texture n’est rien d’autre qu’un image que l’on plaque sur des faces 3d.

  • Mapping de texture sur une face

Prenons l’exemple d’une face carré créée par deux triangles. Nous avons une texture que l’on souhaite afficher sur cette face. Pour cela, il va falloir relier chaque vertex avec un point de la texture.

xna_tuto3d3_3 Image représentant les coordonnées d’une texture sur deux triangles (tiré du tutoriel XNA de Riemiers)

Ainsi le vertex en haut à gauche est lié au coin haut gauche de la texture. La coordonnées de la texture (aussi appelé UV Mapping) correspondant à ce coin est (0,0). Les coordonnées limites de chaque textures est 1. Ainsi si l’on définit les vertex pour un face, on obtient le code suivant :

new VertexPositionTexture( new Vector3(1 , 1 , -1)    ,new Vector2(1,0)),
new VertexPositionTexture( new Vector3(1 , -1 , -1)   ,new Vector2(1,1)),
new VertexPositionTexture( new Vector3(-1 , -1 , -1)  ,new Vector2(0,1)),
new VertexPositionTexture( new Vector3(-1 , 1 , -1)   ,new Vector2(0,0)),

On utilise maintenant une structure VertexPositionTexture pour définir nos vertex contenant la position 3d et la coordonnées de textures. Il suffira de fournir la texture avant l’appel du Draw pour que la carte graphique affiche la texture.

  • Texturer un cube avec 6 textures

Nous allons reprendre l’exemple du tutoriel précédent et nous allons appliquer une texture différente pour chaque face. Pour cela, il est nécessaire de rajouter 6 images dans le projet de contenu (pour l’exemple, nous avons pris des images dans le dossier Sample de Windows 7).

Texture2D[] faces;
protected override void LoadContent()
{
    spriteBatch = new SpriteBatch(GraphicsDevice);
    this.effect = new BasicEffect(this.GraphicsDevice);

    this.faces = new Texture2D[6];
    this.faces[0] = Content.Load<Texture2D>("Chrysanthemum");
    this.faces[1] = Content.Load<Texture2D>("Desert");
    this.faces[2] = Content.Load<Texture2D>("Hydrangeas");
    this.faces[3] = Content.Load<Texture2D>("Lighthouse");
    this.faces[4] = Content.Load<Texture2D>("Penguins");
    this.faces[5] = Content.Load<Texture2D>("Tulips");
   
}

Nous chargeons chacune des textures dans un tableau. Nous créons ensuite une méthode pour créer l’ensemble des données du cube.

VertexPositionTexture[] verticesTex;
private void CreateIndexedCubeTextured()
{
    this.verticesTex = new VertexPositionTexture[]
    {
        new VertexPositionTexture( new Vector3(1 , 1 , -1)    ,new Vector2(1,0)),
        new VertexPositionTexture( new Vector3(1 , -1 , -1)   ,new Vector2(1,1)),
        new VertexPositionTexture( new Vector3(-1 , -1 , -1)  ,new Vector2(0,1)),
        new VertexPositionTexture( new Vector3(-1 , 1 , -1)   ,new Vector2(0,0)),
        new VertexPositionTexture( new Vector3(1 , 1 , 1)     ,new Vector2(1,0)),
        new VertexPositionTexture( new Vector3(-1 , 1 , 1)    ,new Vector2(1,1)),
        new VertexPositionTexture( new Vector3(-1 , -1 , 1)   ,new Vector2(0,1)),
        new VertexPositionTexture( new Vector3(1 , -1 , 1)    ,new Vector2(0,0)),
        new VertexPositionTexture( new Vector3(1 , 1 , -1)    ,new Vector2(1,0)),
        new VertexPositionTexture( new Vector3(1 , 1 , 1)     ,new Vector2(1,1)),
        new VertexPositionTexture( new Vector3(1 , -1 , 1)    ,new Vector2(0,1)),
        new VertexPositionTexture( new Vector3(1 , -1 , -1)   ,new Vector2(0,0)),
        new VertexPositionTexture( new Vector3(1 , -1 , -1)   ,new Vector2(1,0)),
        new VertexPositionTexture( new Vector3(1 , -1 , 1)    ,new Vector2(1,1)),
        new VertexPositionTexture( new Vector3(-1 , -1 , 1)   ,new Vector2(0,1)),
        new VertexPositionTexture( new Vector3(-1 , -1 , -1)  ,new Vector2(0,0)),
        new VertexPositionTexture( new Vector3(-1 , -1 , -1)  ,new Vector2(1,0)),
        new VertexPositionTexture( new Vector3(-1 , -1 , 1)   ,new Vector2(1,1)),
        new VertexPositionTexture( new Vector3(-1 , 1 , 1)    ,new Vector2(0,1)),
        new VertexPositionTexture( new Vector3(-1 , 1 , -1)   ,new Vector2(0,0)),
        new VertexPositionTexture( new Vector3(1 , 1 , 1)     ,new Vector2(1,0)),
        new VertexPositionTexture( new Vector3(1 , 1 , -1)    ,new Vector2(1,1)),
        new VertexPositionTexture( new Vector3(-1 , 1 , -1)   ,new Vector2(0,1)),
        new VertexPositionTexture( new Vector3(-1 , 1 , 1)    ,new Vector2(0,0))
    };

    this.indices = new int[]
    {
        0, 3, 2, 0, 2, 1,
        4, 7, 6, 4,6, 5,
        8, 11, 10,8,10, 9,
        12, 15, 14, 12, 14, 13,
        16, 19, 18,16,18, 17,
        20, 23, 22,20,22, 21            
    };

    this.vb = new VertexBuffer(this.GraphicsDevice, typeof(VertexPositionTexture), this.verticesTex.Length, BufferUsage.WriteOnly);
    vb.SetData(this.verticesTex);
    this.ib = new IndexBuffer(this.GraphicsDevice, IndexElementSize.ThirtyTwoBits, this.indices.Length, BufferUsage.WriteOnly);
    this.ib.SetData(this.indices);

    this.GraphicsDevice.SetVertexBuffer(this.vb);

}

Remarque : Afin de simplifier, j’ai choisi de créer une méthode pour chaque type de façon de créer des cubes. De plus j’ai aussi choisi de créer une propriété verticesTex pour les données sur les vertices texturées.

La seule différence avec le tutoriel précédent est le remplacement des couleurs par les coordonnées de textures. Les différences principales se situent  au niveau de l’affichage.

protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.CornflowerBlue);

    effect.View = View;
    effect.Projection = Projection;
    effect.World = World * Matrix.CreateRotationY(RotateY) * Matrix.CreateRotationZ(RotateZ);
    effect.VertexColorEnabled = false;
    effect.TextureEnabled = true;           
    for (int i = 0; i < 6; i++)
    {
        effect.Texture = this.faces[i];
        foreach (EffectPass pass in effect.CurrentTechnique.Passes)
        {
            pass.Apply();
            this.GraphicsDevice.DrawUserIndexedPrimitives(PrimitiveType.TriangleList, this.verticesTex, 0, this.verticesTex.Length, this.indices, i * 6, 2);
        }
    }
    base.Draw(gameTime);
}

Premier changement, la désactivation des couleurs des vertex et l’activation des textures sur le BasicEffect. Cela indique qu’il faudra texturer les triangles que nous allons dessiner. Afin de pouvoir d’appliquer une texture par faces, il va être nécessaire de dessiner chacune des face séparément. Chacune des itérations de la boucle sera donc une face du cube comprenant 2 triangles. Pour chacune de ces faces, on définit la texture que l’on souhaite appliquer en remplissant la propriété Texture de Effect.

Remarque : J’utilise la méthode DrawUserIndexPrimitives ici sans raison particulière. La méthode DrawIndexPrimitives permet aussi l’affichage des textures :). Concernant l’avant dernier paramètre, le i * 6 correspond à la position de départ dans la table des index. I représente le numéro de la face et 6 représente le nombre d’indices par face (6 car 2 triangles à 3 indices). Le dernier paramètre indique que l’on souhaite afficher deux triangles. Pour plus d’informations sur les différents paramètres des méthodes Draw*(), voir le tutoriel précédent.

Remarque 2 : J’ai rajouté une deuxième variable de rotation afin de pouvoir visualiser plus de faces. Le code modifié est le suivant

float RotateZ = 0.0f;
float RotateY = 0.0f;

protected override void Update(GameTime gameTime)
{
    if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
        this.Exit();

    RotateZ += MathHelper.ToRadians(1.0f);
    RotateY += MathHelper.ToRadians(3.0f);

    base.Update(gameTime);
}

Lorsque vous exécutez le projet vous devriez trouver le résultat suivant :

xna_tuto3d3_4

  • Texturer un cube avec 1 texture

Une autre méthode pour texturer un objet 3D est d’utiliser une seule texture pour l’ensemble de l’objet. Cette texture fera l’office de patron comme cela se fait en couture ou en dessin. Pour notre exemple,  nous allons créer un dé. Pour cela on utilise une texture patron comme celle-ci.

cube_dice

Comme vous pouvez le remarquer, les 6 faces du dé sont représentées sur cette texture. C’est grâce aux coordonnées de textures de nos vertex que nous allons appliquer des morceaux de textures aux faces. Ainsi, si l’on prend la face contenant cinq rond, ses coordonnées de textures vont de (0.666;0.333) à (1.000,0.666). Connaissant cela, nous allons recréer une nouvelle fois nos vertices pour cette texture particulière.

private void CreateIndexedCube3DTextured()
{
    this.verticesTex = new VertexPositionTexture[]
    {
        new VertexPositionTexture( new Vector3(1 , 1 , -1)  , new Vector2(0.666f,0.333f)),
        new VertexPositionTexture( new Vector3(1 , -1 , -1) , new Vector2(0.666f,0.666f)),
        new VertexPositionTexture( new Vector3(-1 , -1 , -1), new Vector2(0.333f,0.666f)),
        new VertexPositionTexture( new Vector3(-1 , 1 , -1) , new Vector2(0.333f,0.333f)),

        new VertexPositionTexture( new Vector3(1 , 1 , 1)   , new Vector2(0.333f,0.333f)),
        new VertexPositionTexture( new Vector3(-1 , 1 , 1)  , new Vector2(0.333f,0.666f)),
        new VertexPositionTexture( new Vector3(-1 , -1 , 1) , new Vector2(0,0.666f)),
        new VertexPositionTexture( new Vector3(1 , -1 , 1)  , new Vector2(0,0.333f)),

        new VertexPositionTexture( new Vector3(1 , 1 , -1)  , new Vector2(0.666f,0.666f)),
        new VertexPositionTexture( new Vector3(1 , 1 , 1)   , new Vector2(0.666f,1)),
        new VertexPositionTexture( new Vector3(1 , -1 , 1)  , new Vector2(0.333f,1)),
        new VertexPositionTexture( new Vector3(1 , -1 , -1) , new Vector2(0.333f,0.666f)),

        new VertexPositionTexture( new Vector3(1 , -1 , -1) , new Vector2(0.666f,0)),
        new VertexPositionTexture( new Vector3(1 , -1 , 1)  , new Vector2(0.666f,0.333f)),
        new VertexPositionTexture( new Vector3(-1 , -1 , 1) , new Vector2(0.333f,0.333f)),
        new VertexPositionTexture( new Vector3(-1 , -1 , -1), new Vector2(0.333f,0)),

        new VertexPositionTexture( new Vector3(-1 , -1 , -1), new Vector2(1,0.333f)),
        new VertexPositionTexture( new Vector3(-1 , -1 , 1) , new Vector2(1,0.666f)),
        new VertexPositionTexture( new Vector3(-1 , 1 , 1)  , new Vector2(0.666f,0.666f)),
        new VertexPositionTexture( new Vector3(-1 , 1 , -1) , new Vector2(0.666f,0.333f)),

        new VertexPositionTexture( new Vector3(1 , 1 , 1)   , new Vector2(1,0.666f)),
        new VertexPositionTexture( new Vector3(1 , 1 , -1)  , new Vector2(1,1)),
        new VertexPositionTexture( new Vector3(-1 , 1 , -1) , new Vector2(0.666f,1)),
        new VertexPositionTexture( new Vector3(-1 , 1 , 1)  , new Vector2(0.666f,0.666f))
    };

    this.indices = new int[]
    {
        0, 3, 2, 0, 2, 1,       //1
        4, 7, 6, 4,6, 5,        //2
        8, 11, 10,8,10, 9,      //3
        12, 15, 14, 12, 14, 13, //4
        16, 19, 18,16,18, 17,   //5
        20, 23, 22,20,22, 21    //6         
    };

    this.vb = new VertexBuffer(this.GraphicsDevice, typeof(VertexPositionTexture), this.verticesTex.Length, BufferUsage.WriteOnly);
    vb.SetData(this.verticesTex);
    this.ib = new IndexBuffer(this.GraphicsDevice, IndexElementSize.ThirtyTwoBits, this.indices.Length, BufferUsage.WriteOnly);
    this.ib.SetData(this.indices);

    this.GraphicsDevice.SetVertexBuffer(this.vb);
}

Nous allons aussi charger la texture avec le code suivant (cube_dice étant le nom de la texture ajouté dans le dossier contenu) :

Texture2D[] faces;
Texture2D TexDice;
protected override void LoadContent()
{
    spriteBatch = new SpriteBatch(GraphicsDevice);
    this.effect = new BasicEffect(this.GraphicsDevice);

    this.faces = new Texture2D[6];
    this.faces[0] = Content.Load<Texture2D>("Chrysanthemum");
    this.faces[1] = Content.Load<Texture2D>("Desert");
    this.faces[2] = Content.Load<Texture2D>("Hydrangeas");
    this.faces[3] = Content.Load<Texture2D>("Lighthouse");
    this.faces[4] = Content.Load<Texture2D>("Penguins");
    this.faces[5] = Content.Load<Texture2D>("Tulips");
    this.TexDice = Content.Load<Texture2D>("cube_dice");          
}

C’est encore au niveau de la fonction d’affichage que les choses vont changer :

protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.CornflowerBlue);

    effect.View = View;
    effect.Projection = Projection;
    effect.World = World * Matrix.CreateRotationZ(RotateZ) * Matrix.CreateRotationY(RotateY);
    effect.VertexColorEnabled = false;
    effect.TextureEnabled = true;
    effect.Texture = this.TexDice;
    foreach (EffectPass pass in effect.CurrentTechnique.Passes)
    {
        pass.Apply();
        this.GraphicsDevice.DrawUserIndexedPrimitives(PrimitiveType.TriangleList, this.verticesTex, 0, this.verticesTex.Length, this.indices, 0, this.indices.Length / 3);
    }
    base.Draw(gameTime);
}

Nous n’avons plus besoin de séparer chacun des faces car nous n’allons utiliser qu’une seule texture et que ce sont les coordonnées de textures des vertices qui vont sélectionner les morceaux de textures pour chaque face. Il suffit d’affecter la texture au BasicEffect et appelé la méthode Draw comme d’habitude. Le résultat est le suivant :

xna_tuto3d3_5

  • Pourquoi utiliser une texture “3D” plutôt que 6 textures ?

La première raison peut être la taille. En effet, 6 textures sont souvent plus grosses qu’une seule. De plus la création de 6 texture2D va faire augmenter la mémoire en RAM. L’exemple du patron du dé n’est pas un bon exemple car il y a de nombreux blancs dans ce patron ce qui peut la rendre plus grosse que 6 textures. Mais dans les jeux un peu plus poussés les textures ressemblent parfois à ça :

xna_tuto3d3_6a

xna_tuto3d3_6b

Texture “3D”

Modèle 3d + Texture 3d

Remarque : ces images sont tirées du blog : http://alexismigdalski.wordpress.com/category/dernieres-publications/3d/. Il est à noter que ce type de texture peut être générer par un modeleur 3D après que vous ayez dessiné vous-même sur votre modèle 3D.

L’autre avantage de la texture “3d” est qu’il n’y a qu’un seul appel à la méthode Draw. Cela évite donc les dialogues entre GPU/CPU. Cela permet souvent de gagner en performances.

  • Conclusion

Vous savez maintenant utiliser des textures pour vos modèles 3D. Nous verrons dans le prochain tutoriel comment charger un modèle 3D provenant d’un modeleur 3d et comment utiliser les lumières fournis par BasicEffect.

Source Code de ce tutoriel

06/10/2010

[XNA 4] Tutoriel 3D 2 : Cube / VertexBuffer / IndexBuffer

Après avoir joué avec un triangle lors du tutoriel précédent, nous allons maintenant voir comment faire un cube coloré. Nous allons aussi étudier la gestion des vertices au sein de la carte graphique et comment optimiser l’affichage et le rendu 3d.

Nous reprendrons le résultat du dernier tutoriel pour commencer celui-ci.

  • Au commencement était le cube…

Nous allons commencer par un petit rappel sur la définition mathématique d’un cube. Un cube est une forme géométrique à 3 dimensions comprenant 6 faces, 12 arêtes et 8 sommets.

180px-Hexahedron

Comme vous pouvez le voir sur l’image ci-dessus, les faces mathématiques sont des carrés. Or XNA ne permet de définir que des triangles. Il faudra compter donc 2 triangles par face pour notre cube. Cela donnera quelque chose comme cela.

xna_tuto3d2_1

Comme indiqué lors de l’introduction de cet article, nous souhaitons réaliser un cube coloré. Chaque face ayant une couleur uni et différente des autres faces or, en XNA, chaque sommet a une position et une couleur unique. Un sommet d’un cube est commun à trois faces. Mais on ne peut pas, avec un seul sommet, assigné trois couleurs différentes. il sera donc nécessaire de créer, pour un même sommet mathématique, 3 vertices (un pour chaque face auquel il est relié). On passe donc de 8 sommets mathématiques à 36 vertices.

Nous allons créer une nouvelle méthode CreateCubeColored afin de créer l’ensemble des vertices.

private void CreateCubeColored()
{
    this.vertices = new VertexPositionColor[]
    {
        new VertexPositionColor( new Vector3(1 , 1 , -1)    ,Color.Red), 
        new VertexPositionColor( new Vector3(-1 , 1 , -1)   ,Color.Red), 
        new VertexPositionColor( new Vector3(-1 , -1 , -1)  ,Color.Red), 
        new VertexPositionColor( new Vector3(1 , 1 , -1)    ,Color.Red), 
        new VertexPositionColor( new Vector3(-1 , -1 , -1)  ,Color.Red), 
        new VertexPositionColor( new Vector3(1 , -1 , -1)   ,Color.Red), 
        new VertexPositionColor( new Vector3(1 , 1 , 1)     ,Color.Green), 
        new VertexPositionColor( new Vector3(1 , -1 , 1)    ,Color.Green), 
        new VertexPositionColor( new Vector3(-1 , -1 , 1)   ,Color.Green), 
        new VertexPositionColor( new Vector3(1 , 1 , 1)     ,Color.Green), 
        new VertexPositionColor( new Vector3(-1 , -1 , 1)   ,Color.Green), 
        new VertexPositionColor( new Vector3(-1 , 1 , 1)    ,Color.Green), 
        new VertexPositionColor( new Vector3(1 , 1 , -1)    ,Color.Blue), 
        new VertexPositionColor( new Vector3(1 , -1 , -1)   ,Color.Blue), 
        new VertexPositionColor( new Vector3(1 , -1 , 1)    ,Color.Blue), 
        new VertexPositionColor( new Vector3(1 , 1 , -1)    ,Color.Blue), 
        new VertexPositionColor( new Vector3(1 , -1 , 1)    ,Color.Blue), 
        new VertexPositionColor( new Vector3(1 , 1 , 1)     ,Color.Blue), 
        new VertexPositionColor( new Vector3(1 , -1 , -1)   ,Color.Orange), 
        new VertexPositionColor( new Vector3(-1 , -1 , -1)  ,Color.Orange), 
        new VertexPositionColor( new Vector3(-1 , -1 , 1)   ,Color.Orange), 
        new VertexPositionColor( new Vector3(1 , -1 , -1)   ,Color.Orange), 
        new VertexPositionColor( new Vector3(-1 , -1 , 1)   ,Color.Orange), 
        new VertexPositionColor( new Vector3(1 , -1 , 1)    ,Color.Orange), 
        new VertexPositionColor( new Vector3(-1 , -1 , -1)  ,Color.Purple), 
        new VertexPositionColor( new Vector3(-1 , 1 , -1)   ,Color.Purple), 
        new VertexPositionColor( new Vector3(-1 , 1 , 1)    ,Color.Purple), 
        new VertexPositionColor( new Vector3(-1 , -1 , -1)  ,Color.Purple), 
        new VertexPositionColor( new Vector3(-1 , 1 , 1)    ,Color.Purple), 
        new VertexPositionColor( new Vector3(-1 , -1 , 1)   ,Color.Purple), 
        new VertexPositionColor( new Vector3(1 , 1 , 1)     ,Color.Yellow), 
        new VertexPositionColor( new Vector3(-1 , 1 , 1)    ,Color.Yellow), 
        new VertexPositionColor( new Vector3(-1 , 1 , -1)   ,Color.Yellow), 
        new VertexPositionColor( new Vector3(1 , 1 , 1)     ,Color.Yellow), 
        new VertexPositionColor( new Vector3(-1 , 1 , -1)   ,Color.Yellow), 
        new VertexPositionColor( new Vector3(1 , 1 , -1)    ,Color.Yellow), 
    };
}

Remarque : l’ordre des vertices est important car dépendant du backface culling expliqué dans le tutoriel précédent. L’écriture de ces définitions de vertices peut être fastidieux à la longue. Il est à noter que cela est rarement utilisé. On préfère utiliser des modeleurs 3D (type Blender, Maya ou 3DSMAX) pour réaliser des modèles fournissant directement ces données.

On modifie maintenant la méthode Initialize pour appeler la méthode CreateCubeColored.

protected override void Initialize()
{
    //this.CreateTriangle();
    this.CreateCubeColored();
    this.CreateCamera();

    base.Initialize();
}

Puis on réalise l’affichage du cube de la même façon que pour le triangle (sans changer une ligne de code).

protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.CornflowerBlue);

    effect.View = View;
    effect.Projection = Projection;
    effect.World = World * Matrix.CreateRotationY(RotateZ);
    effect.VertexColorEnabled = true;

    foreach(EffectPass pass in effect.CurrentTechnique.Passes)
    {
        pass.Apply();
        this.GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList, vertices, 0, vertices.Length / 3);
    }
    base.Draw(gameTime);
}

Vous obtenez un Cube coloré qui tourne sur lui-même.

xna_tuto3d2_2

  • Gestion de la mémoire : VertexBuffer

Lorsque nous créons un tableau de vertices, celui-ci est stocké au niveau de la RAM du microprocesseur (CPU). Lors de l’appel à la méthode DrawUserPrimitives, nous fournissons le tableau de vertices. La méthode va chercher en RAM les données pour les copier sur la mémoire de la carte graphique puis faire le rendu adéquat. Or, vu que la méthode DrawUserPrimitives est appelée 60 fois par secondes, cette copie à lieu un grand nombre de fois inutilement car les données n’ont pas changé.

Pour pallier ce problème, il existe une classe VertexBuffer chargé de copier une fois pour toute les données des vertices sur la mémoire de la carte graphique et de les réutiliser directement.

Pour réaliser créer le VertexBuffer, il suffit de modifier CreateCubeColored.

private VertexBuffer vb;

private void CreateCubeColored()
{
    this.vertices = new VertexPositionColor[] { /* Données du cube */ };
    this.vb = new VertexBuffer(this.GraphicsDevice, typeof(VertexPositionColor), this.vertices.Length, BufferUsage.WriteOnly);
    vb.SetData(this.vertices);
    this.GraphicsDevice.SetVertexBuffer(this.vb);
}

Le constructeur du VertexBuffer prend en paramètre le lien vers la carte graphique, le type de vertex utilisé, le nombre de vertices à stocker ainsi que l’utilisation que l’on compte faire du VertexBuffer. Ce dernier paramètre peut avoir deux valeurs : None (par défaut) ou WriteOnly (optimisé pour l’écriture et la modification des données). Une fois créé, on remplit le VertexBuffer avec le tableau vertices puis on fournit le VertexBuffer à la carte graphique grâce à la méthode SetVertexBuffer en vue de la copie des données.

Au niveau de l’affichage, on va remplacer la méthode DrawUserPrimitives par DrawPrimitives.

protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.CornflowerBlue);

    effect.View = View;
    effect.Projection = Projection;
    effect.World = World * Matrix.CreateRotationY(RotateZ);
    effect.VertexColorEnabled = true;

    foreach (EffectPass pass in effect.CurrentTechnique.Passes)
    {
        pass.Apply();
        //this.GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList, vertices, 0, vertices.Length / 3);
        this.GraphicsDevice.DrawPrimitives      (PrimitiveType.TriangleList,           0, this.vertices.Length / 3);
    }
    base.Draw(gameTime);
}

Les deux méthodes ont une seule différence dans leur paramètre. Le deuxième paramètre de DrawUserPrimitives n’existe plus dans DrawPrimitives. Ce paramètre servait à fournir le tableau de vertices. Cela n’est plus nécessaire car DrawPrimitives va chercher les données sur les vertices dans le VertexBuffer défini précédemment.

On obtient le même résultat que précédemment si on exécute l’application.

  • IndexBuffer : Optimisation mémoire des vertices

Si vous regarder la liste des vertices décrivant le cube, vous remarquerez que certains sont redondant (le [0] et le [3], le [2] et le [4] …). XNA fournit un moyen de réduire ces redondances en ne fournissant qu’un seul vertex qui sera réutilisé par plusieurs faces. Pour cela, on a besoin de deux tableaux : un tableau de vertices “uniques” et un tableau d’index qui va permettre de lié les vertices et les faces.

int[] indices;

private void CreateIndexedCubeColored()
{
    this.vertices = new VertexPositionColor[]
    {
        new VertexPositionColor( new Vector3(1 , 1 , -1)    ,Color.Red),
        new VertexPositionColor( new Vector3(1 , -1 , -1)   ,Color.Red),
        new VertexPositionColor( new Vector3(-1 , -1 , -1)  ,Color.Red),
        new VertexPositionColor( new Vector3(-1 , 1 , -1)   ,Color.Red),
        new VertexPositionColor( new Vector3(1 , 1 , 1)     ,Color.Green),
        new VertexPositionColor( new Vector3(-1 , 1 , 1)    ,Color.Green),
        new VertexPositionColor( new Vector3(-1 , -1 , 1)   ,Color.Green),
        new VertexPositionColor( new Vector3(1 , -1 , 1)    ,Color.Green),
        new VertexPositionColor( new Vector3(1 , 1 , -1)    ,Color.Blue),
        new VertexPositionColor( new Vector3(1 , 1 , 1)     ,Color.Blue),
        new VertexPositionColor( new Vector3(1 , -1 , 1)    ,Color.Blue),
        new VertexPositionColor( new Vector3(1 , -1 , -1)   ,Color.Blue),
        new VertexPositionColor( new Vector3(1 , -1 , -1)   ,Color.Orange),
        new VertexPositionColor( new Vector3(1 , -1 , 1)    ,Color.Orange),
        new VertexPositionColor( new Vector3(-1 , -1 , 1)   ,Color.Orange),
        new VertexPositionColor( new Vector3(-1 , -1 , -1)  ,Color.Orange),
        new VertexPositionColor( new Vector3(-1 , -1 , -1)  ,Color.Purple),
        new VertexPositionColor( new Vector3(-1 , -1 , 1)   ,Color.Purple),
        new VertexPositionColor( new Vector3(-1 , 1 , 1)    ,Color.Purple),
        new VertexPositionColor( new Vector3(-1 , 1 , -1)   ,Color.Purple),
        new VertexPositionColor( new Vector3(1 , 1 , 1)     ,Color.Yellow),
        new VertexPositionColor( new Vector3(1 , 1 , -1)    ,Color.Yellow),
        new VertexPositionColor( new Vector3(-1 , 1 , -1)   ,Color.Yellow),
        new VertexPositionColor( new Vector3(-1 , 1 , 1)    ,Color.Yellow)
    };

    this.indices = new int[]
    {
        0, 3, 2, 0, 2, 1,
        4, 7, 6, 4,6, 5,
        8, 11, 10,8,10, 9,
        12, 15, 14, 12, 14, 13,
        16, 19, 18,16,18, 17,
        20, 23, 22,20,22, 21            
    };

}

La nouvelle méthode CreateIndexedCubeColored créé un tableau de 24 vertices uniques et un tableau d’indices liant les triangles aux vertices. Ainsi le premier triangle sera les trois premières valeurs du tableau d’indices (0,3,2). Cela permet de réduire les redondances et d’alléger la mémoire.

Méthode sans IndexBuffer : 36 vertices de 16 octets => 576 octets

Méthode avec IndexBuffer : 24 vertices de 16 octets + 144 octets d’indices => 528 octets (ou 24*16+72 => 456 octets si on utilise des short à la place de int pour les indexs).

Même si la différence ne parait pas énorme sur un cube, il faut imaginer qu’elle est très grande quand les vertices/modèles deviennent très complexe.

Pour l’affichage de données indexés, il est nécessaire comme pour les vertex d’utiliser un VertexBuffer et un IndexBuffer.

VertexBuffer vb;
IndexBuffer ib;
int[] indices;

private void CreateIndexedCubeColored()
{
    this.vertices = new VertexPositionColor[]  {  /*Vertices*/ };

    this.indices = new int[]  { /*Indices*/ };

    this.vb = new VertexBuffer(this.GraphicsDevice, typeof(VertexPositionColor), this.vertices.Length, BufferUsage.WriteOnly);
    vb.SetData(this.vertices);
    this.GraphicsDevice.SetVertexBuffer(this.vb);

    this.ib = new IndexBuffer(this.GraphicsDevice, IndexElementSize.ThirtyTwoBits, this.indices.Length, BufferUsage.WriteOnly);
    this.ib.SetData(this.indices);
    this.GraphicsDevice.Indices = this.ib;

}

L’indexBuffer se crée de la même façon que le VertexBuffer. La seule différence se situe sur le deuxième paramètre où il est nécessaire d’indiquer le type de données utilisée pour les index. Deux possibilités : 32 bits (int) ou 16 bits (short). Le choix est ouvert et dépend du nombre de vertices utilisés (short est compris entre –32768 à +32768.) et de la technologie sous-jacente. Le Windows Phone, par exemple, ne gère que des indexs 16 bits.

Une fois les VertexBuffer et IndexBuffer configurés et liés à la carte graphique, il ne reste plus qu’à afficher le nouveau cube.

protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.CornflowerBlue);

    effect.View = View;
    effect.Projection = Projection;
    effect.World = World * Matrix.CreateRotationY(RotateZ);
    effect.VertexColorEnabled = true;

    foreach (EffectPass pass in effect.CurrentTechnique.Passes)
    {
        pass.Apply();
        //this.GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList, vertices, 0, vertices.Length / 3);
        //this.GraphicsDevice.DrawPrimitives      (PrimitiveType.TriangleList,           0, this.vertices.Length / 3);
        this.GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, this.vertices.Length, 0, this.indices.Length / 3);
    }
    base.Draw(gameTime);
}

On utilise cette fois la méthode DrawIndexedPrimitives. Le premier paramètre reste le type de primitives à dessiner. Le dernier paramètre correspond toujours au nombre de primitives (triangles) à dessiner.

Les 4 paramètres intermédiaires se nomment baseVertex,minVertexIndex, numVertices et startIndex et leur fonctionnement est un peu plus complexe.

Le paramètre baseVertex permet de décaler la valeur des indices d’une constante. Ainsi si on a un tableau d’indices {0,2,3} et que l’on met baseVertex à 30; durant le temps de l’appel DrawIndexPrimitives, le tableau d’indices va devenir {30,32,33}. Dans notre cube, on laisse cette valeur à 0 car nos indices sont déjà aux bonnes valeurs.

fig4

Exemple de l’affichage de deux triangles à partir d’un index Buffer (tiré  du blog de John Steed).

Prenons l’exemple ci-dessus d’un index buffer contenant deux triangles. Si l’on souhaite n’afficher que le deuxième, on mettra la valeur de startIndex à 3 (startIndex représentant la position de début du triangle dans l’indexbuffer). MinIndex représente la valeur d’index minimale utilisé (ici les valeurs d’index pour le triangle sont 3,2,0 => Minimun : 0). Enfin la plage d’index pour ce triangle est 0=>3. Par conséquent le nombre de vertices qui seront potentiellement utilisable est de 4. En effet, on prend aussi les vertices contenues dans la plage même s’ils ne sont pas utile (ici, le vertex 1 n’est pas utile pour le dessin du triangle 2).

Pour une explication peut être plus claire et en anglais sur ces paramètres, je vous conseille ce post.

  • Et les performances dans tout ça…

J’ai rajouté deux compteurs de FPS afin d’analyser les différences de performances entre les différentes méthodes. Les résultats pour le cube ont été sensiblement équivalents pour l’ensemble des méthodes. Toutefois, il ne faut pas en conclure que les VertexBuffer/IndexBuffer sont inutiles. En effet, dans le cas de modèles importants, le temps et la mémoire gagnés par ces mécanismes permet une amélioration sensible des performances.

Pour savoir la méthode qui convient le mieux à votre projet, vous n’avez pas d’autres moyens que de tester et mesurer les performances pour chacune des méthodes.

  • Conclusion

Après le triangle, nous avons vu comment créer un cube en optimisant les données fournies à la carte graphique. Dans le prochain tutoriel, nous allons créer un dé en texturant notre cube.

Code Source de ce tutoriel

05/10/2010

[XNA 4] Tutoriel 3D 1 : Hello World 3D

Filed under: .NET, 3D, C# 4, Débutant, Intermediaire, XBOX 360, XNA — Étiquettes : , , , , , , , , — sebastiencourtois @ 16:31

Après une série de tutoriels sur la programmation 2D en XNA, nous allons passer à une partie plus intéressante et surtout plus complexe … la 3D. Qui n’a pas rêvé de recréer soi-même un Crysis ou encore un Gran Tourismo.

new-crysis-dx10-screenshot-20061110001326035 gran_turismo_5
Copie d’écran de Crysis Copie d’écran de Gran Tourismo 5

Il faut bien se rendre compte que ces jeux, magnifique graphiquement, ont été réalisés par des équipes composés de dizaines de programmeurs, artistes 2D/3D  qui ont travaillé pendant des mois pour arriver à un tel réalisme. Même si être ambitieux/optimiste  aide dans ce métier, il faut se rendre à l’évidence que vous ne pourrez pas seul et avant de nombreuses années arriver à un tel résultat.

Toutefois, si cela peut vous consoler, nous allons utiliser des outils/concepts proches de ce que les développeurs de ces jeux utilisent et nous seront confrontés (à notre niveau) aux mêmes types de problèmes inhérent à la création de jeu vidéo.

  • Un peu de vocabulaire/concept

Le monde de la 3d est un monde qui parait assez obscur et mathématiques avec son propre vocabulaire et ses propres concepts. Toutefois, ceux-ci peuvent être rattachés à des éléments du quotidien (on vit dans un univers en 3d, non :)).

Un Vertex (pluriel : vertices) : Un vertex est un point dans un univers 3d. C’est l’élément de base dans la construction de formes 3D primitives (lignes, triangle). Il est généralement décrit par sa position dans l’univers 3d ainsi que sa couleur, normals et d’autres informations nécessaires au rendu 3d que nous verrons plus tard.

Un ligne : Primitive 3D composé de deux vertices

Un triangle : Primitive 3d composé de 3 vertices. C’est la primitive la plus évolué qu’une carte graphique peut dessiner. Les modèles 3d que vous pouvez voir dans les jeux vidéo ne sont généralement qu’une accumulation de triangles mis cote à cote.

eames_wireframe Remarque : Les cartes graphiques peuvent aussi dessiner des primitives à 4 vertices mais cette fonctionnalité n’est pas disponible dans XNA 4.

Graphics Device : Ce terme représente l’accès à la carte graphique. Toute les opérations graphiques passeront par des appels au graphics Device (notamment la partie de rendu 3d).

  • Premier projet 3D en XNA

Dans ce tutoriel, nous allons créer un triangle.

Pour commencer, il faut créer un nouveau projet XNA (Windows Game). XNA nous crée déjà le graphics Device nécessaire à la communication avec la carte graphique ainsi que les outils pour la gestion du contenu (ContentManager).

Pour une scène 3d, il est nécessaire d’avoir au minimum 2 choses :

    • Un modèle 3D

Dans notre exemple, notre modèle 3D va être relativement simple car il s’agira d’un triangle. Pour cela, il nous suffit de créer un tableau de 3 vertices. Chaque vertex aura une position et une couleur. Dans notre classe Game, nous allons créer un tableau pour contenir les vertices ainsi qu’une méthode créant les données du triangle.

VertexPositionColor[] vertices;
//Création du triangle
private void CreateTriangle()
{
   this.vertices = new VertexPositionColor[]
   {
       new VertexPositionColor( new Vector3(-1,-1,0), Color.Red),
       new VertexPositionColor( new Vector3(0,1,0), Color.Green),
       new VertexPositionColor( new Vector3(1,-1,0), Color.Blue),
   };
}

Pour contenir les données d’un vertex, XNA fournit une série de classe de base permettant de stocker correctement ces données. Dans notre cas, nous souhaitons définir seulement la position et la couleur des points. Nous utiliserons donc la structure VertexPositionColor prenant en paramètre une position dans un univers 3D (sous la forme d’une structure Vector3) et une couleur. Nous stockons le tout dans un tableau défini au préalable dans notre classe Game.

Remarque : D’autres types de structures de stockage de vertices existent dans XNA (VertexPositionColor / VertexPositionColorTexture / VertexPositionNormalTexture / VertexPositionTexture). Vous pouvez créer votre propre classe en implémentant l’interface IVertexType. Nous verrons dans un  prochain tutoriel la création de notre propre structure de stockage.

    • Une visualisation (ou caméra)

Une fois le modèle 3D défini, il est nécessaire d’indiquer comment on souhaite le voir. Pour cela, il est nécessaire de fournir la position de l’observateur, l’endroit qu’il regarde et d’autres informations comme l’angle de vision. Ces informations seront ensuite stockées sous forme de matrices mathématiques qui serviront à la carte graphique pour le rendu 3D.

Matrix View;
Matrix Projection;
Matrix World;

Vector3 CameraPosition = new Vector3(0.0f, 0.0f, 5.0f);
Vector3 CameraTarget = Vector3.Zero;
Vector3 CameraUp = Vector3.Up;

//Création de la caméra
private void CreateCamera()
{
    View = Matrix.CreateLookAt(CameraPosition, CameraTarget, CameraUp);
    Projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, this.GraphicsDevice.Viewport.Width / this.GraphicsDevice.Viewport.Height, 0.01f, 1000.0f);
    World = Matrix.Identity;
}

La première matrice est la matrice View (dite de “Visualisation”). Elle sert à positionner la caméra et la façon dont celle-ci regarde la scène. On utilise une méthode d’aide fourni par XNA (CreateLookAt) qui prend en premier paramètre la position de l’utilisateur dans l’univers 3D (sous la forme d’un Vector3). Le paramètre suivant est l’endroit que regarde l’utilisateur. Le troisième paramètre indique “l’orientation” de la caméra. Dans notre exemple, nous souhaitons que le dessus de la camera soit orienté vers le haut (coordonnées {0,1,0} ce qui équivaut au Vector3.Up).

La deuxième matrice est la matrice de projection. Celle-ci indique les caractéristique de la caméra (lentille, distance focale). Une méthode d’aide XNA permet de créer cette matrice facilement (CreatePerspectiveFieldOfView). Le premier paramètre est l’angle d’ouverture (ou angle de vision) de la caméra. Par exemple, un humain a un angle de vision allant de 1° (angle d’attention) jusqu’à 180 ° (angle de perception).Cela veut dire qu’il peut, sans bouger la tête voir tout ce qui l’entoure à 180 °. Arbitrairement, nous allons décider d’utiliser un angle de vision de 45° ou PI/4 (car les angles sont décrits en radians en XNA). Le paramètre suivant est le ratio d’aspect. Cela indique le format de la caméra (16/9 comme au cinéma, 4/3). Cela correspond tout simplement à la largeur de la zone visible divisé par la hauteur. Les deux derniers paramètres correspondent à la distance minimale et maximale d’affichage. Ainsi tous les objets trop proche ou trop loin de la caméra ne seront pas affichés.

La troisième matrice nous permettra de placer les objets dans l’univers 3d. Pour l’instant, nous lui assignons la valeur de base (matrice identité).

    • Méthode d’initialisation

Nous avons maintenant tous les éléments nécessaire pour réaliser un scène 3d. Nous allons maintenant appelé les deux méthodes décrites plus haut dans la méthode Initialize().

protected override void Initialize()
{
   this.CreateTriangle();
   this.CreateCamera();
   base.Initialize();
}
  • Affichage d’un triangle

Afin de réaliser l’affichage, nous allons utiliser un shader. Un shader est un programme créé spécifiquement pour être exécuté sur la carte graphique. En effet, la carte graphique permet la réalisation de calcul mathématique très rapidement. La création de shader sera l’objet de plusieurs tutoriels mais, dans ce premier article sur la 3d, nous allons utilisé celui fournit par XNA : BasicEffect.

BasicEffect effect;

protected override void LoadContent()
{
  spriteBatch = new SpriteBatch(GraphicsDevice);
  this.effect = new BasicEffect(this.GraphicsDevice);
}

BasicEffect se crée en fournissant uniquement le graphicsDevice et il sera utilisé lors de la partie affichage (méthode Draw).

protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.CornflowerBlue);

    effect.View = View;
    effect.Projection = Projection;
    effect.World = World;
    effect.VertexColorEnabled = true;

    foreach(EffectPass pass in effect.CurrentTechnique.Passes)
    {
        pass.Apply();
        this.GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList, vertices, 0, vertices.Length / 3);
    }

    base.Draw(gameTime);
}

Première étape dans la méthode Draw. Nettoyer l’écran avec une couleur de fond de la même façon que pour la 2D. On fournit ensuite les trois matrices de projections au shader BasicEffect. On active aussi le booléen VertexColorEnabled pour indiquer que l’on souhaite que les couleurs définis sur les vertices soient prises en compte. On utilise ensuite la propriété CurrentTechnique pour utiliser les différentes passes du shaders (le fonctionnement exacte des shaders sera expliqué dans un autre tutoriel et sa compréhension n’est pas indispensable pour celui-ci).

La méthode la plus importante de ce morceau de code est le DrawUserPrimitivers qui va prendre en paramètre le type de primitive. Il s’agit de la façon dont nous allons associer les vertices.

Initialising_Article_triangles

Dans notre exemple, nous utiliserons TriangleList. Cette primitive prend 3 vertex pour en faire un triangle puis les 3 vertex suivants pour un autre triangle et ainsi de suite.

Le deuxième paramètre de DrawUserPrimitives est le tableau de vertices proprement dit. Le paramètre suivant est l’index de départ du tableau de vertices. Dans notre cas, on souhaite utiliser tous les vertices donc on indique que le premier vertex est en position 0. Le dernier paramètre indiquent le nombre de primitives (ici triangle) que l’on souhaite dessiner. On prend le nombre total de vertices de notre tableau (3) et on divise par le nombre de vertex nécessaire pour faire un triangle (3). On indique que l’on souhaite dessiner un seul triangle.

Si vous lancer le projet, vous devriez obtenir l’écran suivant :

xna_tuto3d1_1

On remarque que le fond est bleu ce qui correspond à la couleur d’effacement au début de notre méthode Draw (Clear(Color.CornFlowerBlue)). Le triangle est visible mais on remarque que les couleurs, correctes aux sommets, fusionnent entre elles en s’écartant des sommets. Il s’agit du comportement de base de DirectX. Si l’on souhaite faire un triangle uni, il faut que les sommets aient la même couleur.

  • Un peu de mouvement

Nous allons introduire une rotation sur ce triangle afin de prouver que nous sommes bien dans un univers 3D. Pour cela, on va rajouter une variable de rotation que nous allons incrémenter dans la méthode Update.

float RotateZ = 0.0f;
protected override void Update(GameTime gameTime)
{
    if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
        this.Exit();

    RotateZ += MathHelper.ToRadians(2.0f);

    base.Update(gameTime);
}

Nous incrémentons donc la variable de 2° par appel à la méthode Update (60 x par secondes par défaut sur XNA). Il est à noter que la valeur est toujours à stocker en Radians. MathHelper fournit des méthodes d’aides pour traduire les angles de degré en radians et inversement.

Une fois la variable modifié, il suffit de modifier la matrice World qui est chargé de placer le triangle dans l’espace.

protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.CornflowerBlue);

    effect.View = View;
    effect.Projection = Projection;
    effect.World = World * Matrix.CreateRotationY(RotateZ);
    effect.VertexColorEnabled = true;

    foreach(EffectPass pass in effect.CurrentTechnique.Passes)
    {
        pass.Apply();
        this.GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList, vertices, 0, vertices.Length / 3);
    }

    base.Draw(gameTime);
}

Il suffit de multiplier la matrice World avec une matrice de rotation créé à partir de l’angle RotateZ pour réaliser cette effet. Si vous relancer l’application le triangle tourne…

  • Mon triangle disparait puis réapparait sans arrêt.

Lorsque vous lancez l’application le triangle commence à tourner mais il disparait un moment au bout d’un demi-tour puis réapparait. Cela est dû à une optimisation de la carte graphique nommé Backface Culling. En effet, les cartes graphiques n’affiche pas les faces (triangles) ne leur faisant pas face. Vous trouverez une explication plus complète sur le fonctionnement  ici. Il est possible de désactiver cette optimisation en modifiant la propriété RasterisationState du graphicsDevice. Cela se réalise généralement dans la méthode Initialize().

this.GraphicsDevice.RasterizerState = new RasterizerState()
{
   CullMode = CullMode.None,
   FillMode = FillMode.Solid
};

Remarque : Vous pouvez aussi régler par la propriété FillMode si vous souhaitez une vision des objets “solides” ou en fil de fer.

Vous pouvez remarquer que CullMode à 3 valeurs possibles : CullClockwiseFace ,CullCounterClockwiseFace, None. Par défaut, le CullMode est à CullClockwise ce qui veut dire que les vertex doivent être définis dans l’ordre des aiguilles d’une montre pour que la face soit visible.

TriangleAll
Représentation d’un triangle dont les vertices sont dans le sens des aiguilles d’une montre (tiré d’un tutoriel anglais XNA : http://www.toymaker.info/Games/XNA/html/xna_simple_triangle.html).

Lorsque le triangle a fait son premier demi-tour, les vertex passent dans le sens inverses. N’étant plus dans le sens des aiguilles d’une montre, le triangle n’est plus affiché.

Une autre solution, qui permet de conserver le Backface Culling, serait de créer un deuxième triangle avec les vertices dans l’ordre inverse. Ainsi, si on modifie le tableau de vertices comme suit  :

//Création du triangle
private void CreateTriangle()
{
    this.vertices = new VertexPositionColor[]
    {
        //Triangle 1
        new VertexPositionColor( new Vector3(-1,-1,0), Color.Red),
        new VertexPositionColor( new Vector3(0,1,0), Color.Green),
        new VertexPositionColor( new Vector3(1,-1,0), Color.Blue),
        //Triangle 2
        new VertexPositionColor( new Vector3(1,-1,0), Color.Blue),
        new VertexPositionColor( new Vector3(0,1,0), Color.Green),
        new VertexPositionColor( new Vector3(-1,-1,0), Color.Red),

    };
}

On obtient un triangle (en fait deux triangles qui s’interchange) qui tourne correctement.

  • Conclusion

Après ce tutoriel un peu long et complexe, je pense que vous aurez compris que la route est longue avant de pouvoir faire une jeu aussi techniquement abouti que les jeux présentés plus haut. Vous savez maintenant faire un triangle ce qui est la base de toutes les applications 3D. Dans les prochains tutoriels, nous verrons comment afficher un cube, le texturer puis se déplacer dans un univers 3d. Nous verrons aussi quelques astuces pour optimiser l’affichage.

N’hésitez pas à donner vos commentaires et poser vos questions sur ce tutoriel.

Lien vers la solution

01/02/2010

[Silverlight 3/4] Comment faire des contrôles avec deux faces différentes ?

Filed under: Intermediaire, Silverlight, Silverlight 3, Silverlight 4, XAML — Étiquettes : , , , , — sebastiencourtois @ 21:23

Au commencement Microsoft créa Silverlight. Silverlight, la technologie RIA nouvelle génération pour créer des applications vectorielles entièrement en code managé. Microsoft vit que Silverlight était bon… mais qu’il lui manquait un petit truc pour rivaliser avec son rival. Alors Microsoft dit : “Que la 3D soit” et la 3D fut intégrée dans Silverlight … enfin pas tout à fait.

Tout d’abord, la « 3D » dans Silverlight n’est qu’un moteur de perspective qui ne permet que de faire des rotations/translations de contrôles. Point de moteur ou de monde 3D ici. On parle uniquement ici que de pouvoir faire des jolis effets en alliant animations et perspectives.

Ensuite cette perspective a une feature (« not a bug ») assez ennuyeuse à mon sens : Si on prend l’exemple d’un contrôle contenant une image, lorsque l’on retourne ce contrôle, on se retrouve avec … la même image inversée.

Cap3

Personnellement j’aurais aimé avoir accès à une deuxième face pour pouvoir ajouter des nouveaux contrôles et, comme on n’est jamais mieux servi que par soi-même, j’ai donc cherché un moyen de réaliser ce contrôle. J’ai appelé ce contrôle : FacesPanel.

Fonctionnement de base du contrôle FacesPanel

En parcourant les forums, la pratique général est d’avoir deux contrôles sur un même conteneur (un Grid par exemple) et de rendre visible l’un ou l’autre selon la propriété RotationX/RotationY d’un PlaneProjection. L’algorithme est le suivant :

double Roty = Math.Abs(((PlaneProjection)this.Projection).RotationY);
double Rotx = Math.Abs(((PlaneProjection)this.Projection).RotationX);
if ((Roty % 360 > 90 && Roty % 360 < 270) || (Rotx % 360 > 90 && Rotx % 360 < 270))
{
    //Back Face
}
else
{
    //Front Face
}

Remarque : Vous vous demander peut être pourquoi on ne trouve pas de RotationZ. La raison est qu’une Rotation sur l’axe Z se fera toujours “face” à l’utilisateur. Par conséquent, aucun cas de changement de face. Une autre interrogation pourrait être au sujet des Math.Abs et des %. Cela est simplement dû au fait que les rotations peuvent être négatives comme supérieure à 360°.

Cette méthode a deux inconvénients qu’il va falloir gérer :

1°) Lors du retournement du contrôle, le fait de changer la visibilité des contrôles ne résout pas le problème de l’inversion graphique car c’est le container qui tourne donc le contrôles interne suit le mouvement. Pour gérer ce problème, on utilise un ScaleTransform afin de remettre le contrôle dans le bon sens.

2°) Je n’ai pas réussi à récupérer les valeurs RotationX/RotationY au cours d’une animation lorsque je me binde sur ces propriétés. Cela est problématique pour avoir un comportement naturel au cours d’une animation (le changement de face se faisant uniquement à la fin de l’animation). Pour parer ce problème, j’ai utilisé une solution peu élégant/optimisé mais qui à le mérite de fonctionner (si vous avez mieux, n’hésitez pas à commenter). J’utilise un DispatcherTimer afin que, périodiquement, je regarde le valeurs de RotationX/RotationY pour voir si un changement de face est nécessaire.

Propriétés/Methodes de FacesPanel :

Voila pour la base du fonctionnement. Afin de rendre le contrôle exploitable, j’ai rajouté les propriétés suivantes :

  • FrontFace (type : UIElement) : Contrôle pour la face principale
  • BackFace (type : UIElement) : Contrôle pour la face caché
  • AnimationDirection (type: TurnDirection ) : Permet de définir le sens du retournement de la face. Se base sur l’énum TurnDirection défini dans le même fichier .cs. 5 valeurs (NONE,LEFT,RIGHT,UP,BOTTOM)
  • AnimationDuration (type : int) : Durée de l’animation de retournement
  • AnimationEasing (Type : IEasingFunction) : Animation Easing pour l’animation de  retournement.
  • GoToFrontFace() : Montre la face principale.
  • GoToBackFace() : Montre la face “caché”.
  • TurnFace() : Change de face

Cas d’utilisation : Photo Bucket

Afin de faire un cas d’utilisation, j’ai choisi de reprendre une démonstration Silverlight 4 sur glisser/déposer faite par Audrey Petit. Ce que je souhaitait faire était de pouvoir glisser déposer des images puis, par click de souris sur une image, la retourner pour marquer des commentaires au dos. J’ai donc utilisé l’exemple d’Audrey en rajoutant un contrôle spécifique pour gérer les images.

PictureControl.xaml :

<UserControl x:Class="PhotoBucket.PictureControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:PhotoBucket"
    mc:Ignorable="d" Width="150" Height="150">
    <local:FacesPanel x:Name="FacesPanelControl" AnimationDirection="LEFT" AnimationDuration="3" MouseLeftButtonDown="FacesPanelControl_MouseLeftButtonDown">
        <local:FacesPanel.AnimationEasing>
            <ElasticEase />
        </local:FacesPanel.AnimationEasing>
        <local:FacesPanel.FrontFace>
            <Border Background="White" BorderBrush="Black" BorderThickness="1">
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="120" />
                        <RowDefinition Height="30" />
                    </Grid.RowDefinitions>
                    <Image x:Name="ImageZone"  Margin="10,5,10,0" />
                    <TextBlock Text="{Binding ElementName=txtTitle,Path=Text}" FontSize="10" FontStyle="Italic" 
                               Grid.Row="1" HorizontalAlignment="Center" VerticalAlignment="Center" />
                </Grid>
            </Border>
        </local:FacesPanel.FrontFace>
        <local:FacesPanel.BackFace>
            <Border Background="White" Margin="5" BorderBrush="Black" BorderThickness="1">
                <StackPanel>
                    <TextBlock Text="Titre de la photo" Margin="2" />
                    <TextBox x:Name="txtTitle"  Text="Test" Margin="2" />
                </StackPanel>
            </Border>
        </local:FacesPanel.BackFace>
    </local:FacesPanel>
</UserControl>

Picture.xaml.cs :

    public partial class PictureControl : UserControl
    {
        public PictureControl()
        {
            InitializeComponent();
        }

        private void FacesPanelControl_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            ((FacesPanel)sender).TurnFace();
        }
    }

Et voici le résultat en image :

Cap1 Cap2

Et en code

  • Lien du source de la démo du contrôle sur VS 2008 / SL3 (pas d’application photo car pas de drag / drop en SL3)
  • Lien du source de la démo VS  (Appli Photo Bucket)

Si vous avez des commentaires pour améliorer ce contrôle, n’hésitez pas.

Propulsé par WordPress.com.

%d blogueurs aiment cette page :