Astuces DotNet (Sébastien Courtois)

17/02/2013

[SharpDX] Direct2D/Direct3D 10/11 Interoperability

Filed under: 3D, DirectX, Intermediaire, Jeux vidéos — Étiquettes : , , , , , , , — sebastiencourtois @ 22:48
Aparté de l’auteur de ce blog

Plus d’un an de silence sur ce blog depuis mon dernier post. Cela s’explique par beaucoup de changement. Tout d’abord, l’abandon de XNA par Microsoft (désormais officiel) m’ont fait me poser beaucoup de questions sur les technologies managées liées aux jeu vidéos que j’allais maintenant blogguer. Heureusement des initiatives comme SlimDX et SharpDX ont permis aux orphelins de XNA comme moi de trouver un alternative pour continuer a faire du jeu vidéo en .NET. De plus, cela fait plusieurs mois que je ne suis pas satisfait de la façon de présenter l’information sur ce blog et je cherche a une nouvelle alternative pour diffuser mes tutoriaux/exemples de code (si vous avez des idées, n’hésitez pas). Enfin, j’ai aussi changé de travail et de pays. Apres un an et demi a San Francisco, je suis maintenant a Montréal travaillant pour Ubisoft Montréal (Assassin’s Creed, Far Cry, Watch Dogs…) en tant que programmeur outils.

 

[EDIT 19/02/13]

Code fonctionnant en DirectX 10/11 Windows 7/8

Les explications techniques se trouvent dans l’article ci-dessous. Dans les différences avec les explications qui suivent se situe sur les classes utilise. Le render target 2d correspond au device context de Direct2d. Cet exemple fonctionne avec les dll SharpDX fournis par NuGet.

Constructeur :

_factory2D = new SharpDX.Direct2D1.Factory(SharpDX.Direct2D1.FactoryType.SingleThreaded, DebugLevel.Information);
Surface surface = swapChain.GetBackBuffer<Surface>(0);
RenderTargetProperties renderTargetProperties = new RenderTargetProperties
    {
        Usage = RenderTargetUsage.None,
        DpiX = 96,
        DpiY = 96,
        PixelFormat = new PixelFormat(Format.B8G8R8A8_UNorm,AlphaMode.Premultiplied),
        MinLevel = FeatureLevel.Level_10,
        Type = RenderTargetType.Hardware
    };
_renderTarget2D = new RenderTarget(_factory2D, surface, renderTargetProperties);
_factoryDW = new SharpDX.DirectWrite.Factory(FactoryType.Isolated);

Dessin :

_renderTarget2D.BeginDraw();
_renderTarget2D.DrawText(s, s.Length, _format, new RectangleF(950, 50, 1280, 300), _colorBrushYellow, DrawTextOptions.None, MeasuringMode.GdiClassic);
_renderTarget2D.EndDraw();

Prérequis pour ce tutorial

Afin de fonctionner, vous devez avoir :

  • Une machine sous Windows 8
  • Visual Studio 2012
  • SharpDX   (ATTENTION : NE PAS UTILISER LE PACKAGE Nuget. Pour un raison que je ne connais pas, il ne fournit pas les DLL Windows8. Celle ci se trouve dans le répertoire ‘Bin\Win8Desktop-net40’ ou vous aurez installer SharpDX.)
  • Les références a ajouter sont :
    • SharpDX
    • SharpDX.Direct2D1
    • SharpDX.Direct3D11
    • SharpDX.DXGI
  • Avoir un pipeline de rendu 3d DirectX 11 fonctionnant déjà de façon autonome (si vous n’en avez pas, vous pouvez utiliser mon pipeline de test).

Introduction

Direct2D et Direct3D sont deux technologies de DirectX permettant l’affichage de dessins 2D et de modèles 3D. Le problème est que Microsoft n’a jamais donné de façon simple  de les faire communiquer depuis la version DirectX 11. En effet, alors que Direct3d 11 était sorti, Direct2D devait toujours dépendre de DirectX 10.1 pour fonctionner ce qui rendait complexe une interaction entre les deux. Certains (comme Alexandre MUTEL – Createur du SharpDX) ont parfois trouvé des solutions de contournement mais cela restait complexe et lourd a mettre en place dans un architecture de moteur 3d déjà existante.

DirectX 11.1 permet de faire un pont entre Direct3d 11 et Direct2d1 en utilisant DXGI. DXGI (DirectX Graphics Infrastructure) est un modèle d’abstraction du driver de la carte graphique et se trouve être la base pour l’ensemble des technologies graphiques DirectX.

dxgi

L’idée va être d’utiliser le Device et la SwapChain créés par Direct3D et les “convertir” en Device/SwapChain DXGI pour être utilisé par Direct2d. Le but étant de faire écrire Direct2d dans le même backbuffer que Direct3d. Tout cela en étant le moins intrusif possible dans le code Direct3d existant.

 

Modification du code Direct3d

Comme indiqué précédemment, nous partons d’un pipeline Direct3d existant et fonctionnant de façon autonome. Si vous avez téléchargé mon pipeline de test, il s’agit de la classe Renderer3d.cs. Si vous lancez le programme, cela vous donne le cube tournant suivant.

cube

Première chose a faire est d’autoriser le support du device Direct3d avec le modèle BRGA utilisé par Direct2d. Pour cela, il suffit de rajouter le DeviceCreationFlags ‘BgraSupport’ lors de la création du Device 3d.

SharpDX.Direct3D11.Device.CreateWithSwapChain(driverType,
                            DeviceCreationFlags.Debug | DeviceCreationFlags.BgraSupport,
                            levels,
                            desc,
                            out _device,
                            out _swapChain);

Ensuite, il est nécessaire de créer les DXGI Device et Swapchain depuis ceux de Direct3d. Cela se réalise en faisant un QueryInterface<>().

_dxgiDevice = _device.QueryInterface<SharpDX.DXGI.Device>();
_dxgiSwapChain = _swapChain.QueryInterface<SharpDX.DXGI.SwapChain>();

Avec ces informations, nous pouvons maintenant initialiser un device Direct2d a partir du pipeline Direct3d.

 

Initialisation de Direct2d

Comme Direct3d 11, Direct2d utilise un device et un device context pour communiquer avec la carte graphique. Nous allons ajouter en plus un lien entre la Surface Direct3d et le bitmap sur lequel nous allons écrire en Direct2d.

_device = new SharpDX.Direct2D1.Device(dxgiDevice);
_deviceContext = new DeviceContext(_device, DeviceContextOptions.None);
Surface surface = swapChain.GetBackBuffer<Surface>(0);
BitmapProperties1 bitmapProp = new BitmapProperties1(new PixelFormat(Format.B8G8R8A8_UNorm, AlphaMode.Premultiplied), 96, 96, BitmapOptions.Target | BitmapOptions.CannotDraw);
_target = new Bitmap1(_deviceContext, surface, bitmapProp);

Nous récupérons ainsi la Surface d’écriture du DXGI SwapChain de Direct3d (i.e le back buffer ou va écrire Direct3d). Cela nous permet de créer un Bitmap Direct2d qui va écrire directement dans le back buffer Direct3d.

Attention : Le BitmapProperties1 PixelFormat doit correspondre au PixelFormat utilisé lors de la création du Device/SwapChain Direct3d (SwapChainDescription.ModelDescription). Si ce n’est pas le cas, vous aurez un exception “The parameter is incorrect.” lors de la creation du bitmap.

 

Dessin de Direct2d

A partir du moment ou Direct2d a été initialisé, il suffit d’indiquer au Device Context Direct2d ou l’on souhaite dessiner avec la propriété Target. La suite reste du code Direct2d classique.

_deviceContext.Target = _target;
_deviceContext.BeginDraw();
_deviceContext.DrawText(s, s.Length, _format, new RectangleF(950, 50, 1280, 300), _colorBrushYellow, DrawTextOptions.None, MeasuringMode.GdiClassic);
_deviceContext.EndDraw();

Remarque : Le dessin Direct2D se fera par dessus le dessin présent dans le backbuffer.  Si vous souhaitez faire un HUD devant la scène 3d, il est nécessaire de dessiner la 2d après la 3d. Dans le cas contraire, votre dessin 2d sera caché par le dessin 3d par superposition. La méthode Present() de Direct3d (qui envoie le backbuffer a l’écran) doit être réalisé après lorsque l’ensemble des dessins sont terminés.

Si vous investissez un peu de temps, vous pouvez avoir une HUD de diagnostic comme dans la démo que j’ai réalisé pour ce tutorial.

cubefps

Vous pouvez telecharger le code du tutorial ici.

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.

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.

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

09/06/2010

[.NET 3.5] Attention lors de l’utilisation des lambdas/closures

Filed under: .NET, C#, C# 4, Hors Catégorie, Intermediaire — Étiquettes : , , , , — sebastiencourtois @ 13:05

Afin de commencer cette article, commençons par une petite devinette.

Quel sont les résultats de ces deux morceaux de code :

var actions = new List<Action>();
for (int i = 0; i < 10; i++)
    actions.Add(() => Console.WriteLine(i));

foreach (var action in actions)
    action();
var actions = new List<Action>();

for (int i = 0; i < 10; i++)
{
    int j = i;
    actions.Add(() => Console.WriteLine(j));
}

foreach (var action in actions)
    action();

La première réponse que vous avez du vous faire est que ces deux codes ont le même résultat : afficher les nombres de 0 à 9. En effet, on met des méthodes délégués dans une liste de délégués puis on parcourt cette liste afin d’exécuter le code de ces délégués. La seule différence entre les deux codes est l’utilisation d ‘une variable local j.

Et pourtant la réponse est :

  • Exemple 1 : 10 10 10 10 10 10 10 10 10 10
  • Exemple 2 : 0 1 2 3 4 5 6 7 8 9

Autre exemple dans le même genre :

var actions = new List<Action>();
string[] urls = 
{ 
   "http://www.url.com", 
   "http://www.someurl.com", 
   "http://www.someotherurl.com", 
   "http://www.yetanotherurl.com" 
};

for (int i = 0; i < urls.Length; i++)
{
    actions.Add(() => Console.WriteLine(urls[i]));
}
var actions = new List<Action>();
string[] urls = 
{ 
   "http://www.url.com", 
   "http://www.someurl.com", 
   "http://www.someotherurl.com", 
   "http://www.yetanotherurl.com" 
};

for (int i = 0; i < urls.Length; i++)
{
    int j = i;
    actions.Add(() => Console.WriteLine(urls[j]));
}
Résultat : IndexOutOfRangeException (Index was outside the

bounds of the array
Résultat : les liens du tableaux sont affichés
  • Les lambdas peuvent être nuire gravement à la santé … mentale des développeurs 

Etrange non ? Décompilons le tout première exemple afin devoir ce qui est effectivement exécuté.

Note : Pour une raison inconnue, je n’ai pas réussi à obtenir ceci depuis Reflector. J’ai donc récupérer le code de l’article original.

[CompilerGenerated]
private sealed class <>c__DisplayClass2
{
    // Fields
    public int i;

    // Methods
    public void <Main>b__0()
    {
        Console.WriteLine(this.i);
    }
}
[...]
var actions = new List<Action>();

<>c__DisplayClass2 localFrame = new <>c__DisplayClass2();
for (localFrame.i = 0; localFrame.i < 10; localFrame.i++)
{
	actions.Add(localFrame.<Main>b__0);
}

foreach (var action in actions)
{
	action();
}

Comme on peut le voir, le fait de créer une lambda en utilisant une variable extérieure à celle ci à entrainer la génération d’une classe (<>c__DisplayClass2") qui est utilisé pour stocker la valeur de cette variable (dans la varaible i) et une méthode (<Main>b__0) qui contient le code de la lambda.  Si on regarde le code du premier for, on s’aperçoit que cette boucle incrémente la variable i de la classe DisplayClass2. Donc, à la fin de la première boucle, la classe DisplayClass2 contient une variable i = 10 et la liste actions contient une référence vers la méthode de cette classe. Ainsi lors de la boucle d’appel actions, on se retrouve a appeler le code Console.WriteLine(i) <=> Console.WriteLine(10) !!!!

Si on résonne de la même façon sur l’exemple 2, on obtient un appel Console.WriteLine(urls[4]) ==> Exception.

  • Pourquoi l’utilisation d’une variable locale résout le problème

Si on regarde le code ci dessous, on s’aperçoit que le problème vient du fait que la classe DisplayClass2 est créé en dehors de la boucle for et que celle ci utilise la variable i comme compteur. Le fait d’utiliser une variable temporaire à l’intérieur de la boucle for, oblige le compilateur à créer plusieurs instance de DisplayClass (une par itération). On obtient ainsi quelque chose comme le code suivant.

for (int idx = 0; idx < 10; idx++)

{

    <>c__DisplayClass2 localFrame = new <>c__DisplayClass2();

    localFrame.i = idx;

    actions.Add(localFrame.<Main>b__0);

}

Remarque : On pourrait penser que chaque instance est dispose lors de la sortie du scope. Cela n’’est pas vrai car la liste actiosn conserve une référence sur l’objet (en l’occurence sur une de ses méthodes) donc le garbage collector ne prend pas cet objet.

  • Conclusion

Les lambdas sont aujourd’hui utilisé dans de nombreuses technologies .NET (LINQ,PFX…) et sont une bonne alternative aux délégués classiques. Toutefois, on peut voir que cela peut engendré une certaine confusion voire des bugs lorsqu’on les utilise.

Je vous mets le lien vers l’article dont je me suis fortement inspiré pour cet article : Lambdas – Know your closures

10/05/2010

[Nouveautés C# .NET 4] Code Contracts : Mettre ses contrats sur des interfaces

Filed under: .NET, C# 4, DevLabs, Hors Catégorie, Intermediaire — Étiquettes : , , , , , — sebastiencourtois @ 15:30

Ce post fait suite à une introduction sur les Code Contracts publié précedement.

Les codes contracts permettent de spécifier et contrôler des règles au sein de votre code. Dans l’optique de la création d’API, il est interessant d’utiliser les Code Contract au sein de classe mais il est encore plus judicieux de les utiliser sur des interfaces (notamment pour les tests unitaires / IoC …). Nous allons donc voir comment réaliser des “code contracts” sur des interfaces.

  • Pour cela mettons nous en situation …

public interfaceIPerson
{
    void SetAge(int age);
}

public interfaceICustomer : IPerson
{
    void SetId(int Id);
}

public classPerson: IPerson
{
    protected int Age { get; set; }

    public void SetAge(int age)
    {
        this.Age = age;
    }
}

public classCustomer: Person,ICustomer
{
    protected int Id { get; set; }
    public void SetId(int id)
    {
        this.Id = id;
    }
}

Nous créons deux interfaces IPerson pouvant modifier l’age d’une personne et ICustomer dérivant de IPerson (donc on pourra modifier son age aussi) où l’on pourra modifier son Identifiant client. Ces deux interfaces ont été implémenté dans deux classes Person et Customer. Si on prend le scénario de la création d’une API, le créateur de l’API aura défini et manipulera les interfaces et le développeur utilisant l’API, aura implémenté les deux classes.

  • Comment le créateur de l’API peut définir des spécifications sur les paramètres de ses interfaces ?

Le but pour le créateur de l’API est de fournir un modèle de développement. Il peut ainsi indiquer les méthodes et les types à utiliser mais ne peut pas avoir plus de précision quand au contenu des paramètres. On a vu, qu’avec Code contract, il peut définir ces informations au sein des méthodes. Cela reviendrait à créer une classe abstrait (pas toujours pertinent surtout dans un langage où il n’y a pas d’héritage multiple comme le C#).

Pour définir un code contract sur une interface, quatre étapes sont nécessaires :

  1. Créer une classe contrat héritant el ‘interface sur laquelle on souhaite faire le contrat.
  2. Implémenter les méthodes de l’interface dans la classe de contrat en fournissant les code contracts.
  3. Décorer l’interface avec l’attribut ContractClass en fournissant le type de la classe de contract
  4. Décorer la classe avec l’attribut ContractClassFor en fournissant le nom des interface qui sont régit par le contrat
[ContractClass(typeof(PersonContracts))]
public interface IPerson
{
    void SetAge(int age);
}

[ContractClassFor(typeof(IPerson))]
public class PersonContracts : IPerson
{
    public void SetAge(int age)
    {
        Contract.Requires(age > 0 && age < 120, "L'age doit être compris entre 1 et 120 ans.");
    }
}

Le tour est joué. Ainsi pour le code exemple devient :

[ContractClass(typeof(PersonContracts))]
public interface IPerson
{
    void SetAge(int age);
}

[ContractClassFor(typeof(IPerson))]
public class PersonContracts : IPerson
{
    public void SetAge(int age)
    {
        Contract.Requires(age > 0 && age < 120, "L'age doit être compris entre 1 et 120 ans.");
    }
}

[ContractClass(typeof(CustomerContracts))]
public interface ICustomer : IPerson
{
    void SetId(int Id);
}

[ContractClassFor(typeof(ICustomer))]
public class CustomerContracts : PersonContracts,ICustomer
{
    public void SetId(int Id)
    {
        Contract.Requires(Id > 0, "L'identifiant doit être supérieur à 0.");
    }
}

Le code ci dessus est fait par le créateur de l’API.

public class Person : IPerson
   {
       protected int Age { get; set; }

       public void SetAge(int age)
       {
           this.Age = age;
       }
   }

   public class Customer : Person,ICustomer
   {
       protected int Id { get; set; }
       public void SetId(int id)
       {
           this.Id = id;
       }
   }

Le code ci dessus est fait par l’utilisateur de l’API. Il n’a pas à se préoccuper des contrats définis dans l’API et peut définir ses propres contracts.

03/05/2010

[.NET] Immutabilité et chaines de caractères en .NET

Filed under: .NET, C#, Débutant, Hors Catégorie, Intermediaire, Optimisation — Étiquettes : , , , , , , , — sebastiencourtois @ 15:31

Nous avons vu brièvement lors d’un post précédent un objet “immutable” (en anglais, immuable en vrai français) : Le Tuple”. Toutefois, nous ne sommes pas rentrés dans les détails de ce type d’objet et nous n’avons pas étudié le plus utilisé de tous : La classe String.

  • Définition de l’immutabilité

Un objet “immutable” est une objet dont les propriétés sont définies à la création de l’objet puis ne peuvent plus changer durant la vie de l’objet.

Une des raisons de la création de ce type d’objet (c’est d’ailleurs aussi un design pattern) : la gestion de la mémoire. En effet, vu que les données sont créées à la création de l’objet, il suffit d’allouer l’ensemble de la mémoire d’un coup et de ne plus y toucher jusqu’à la destruction de l’objet.

Un autre avantages est le multithreading. En effet, si l’objet n’évolue pas au cours de sa vie, tous les threads peuvent y accéder quand ils veulent sans avoir besoin de se synchroniser. Cela permet une programmation plus simple et un programme plus rapide.

  • Exemple concret d’immutabilité : String

Un objet “immutable” que nous utilisons tous les jours sans le savoir spécialement : la classe string.

Prenons l’exemple C/C++ suivant :

#include <stdio.h>
#include <string.h>

int main(int argc,char **argv)
{
    char *test = new char[7];
    strcpy(test,"Bonjour");
    printf("%c",test[2]);
    test[2] = 'N';
    printf("%c",test[2]);
}

Le programme copie une chaine (“bonjour”) dans un tableau de caractère puis affiche le 3ème caractères (‘n’) puis décide de modifier ce troisième caractères et de le réafficher. Tout cela se passe normalement, on obtient “BoNjour” à la fin de l’éxécution du programme.

Prenons le même type de programme en C# avec une string.

static void Main(string[] args)
{
    string test = "Bonjour le monde !!!";
    Console.WriteLine(test[2]);
    test[2] = 'N';
    Console.WriteLine(test[2]);
}

Rien ne laisse présager un problème, on utilise System.String comme une tableau de caractère et on modifie sa troisième valeur. Et pourtant on obtient l’erreur suivante :

immutable1

Un petit tour sur le code de la classe System.String et on voit en effet que l’indexer est uniquement en GET :

// Summary:
//     Represents text as a series of Unicode characters.
[Serializable]
[ComVisible(true)]
…….
    // Summary:
    //     Gets the character at a specified character position in the current System.String
    //     object.
    //
    // Parameters:
    //   index:
    //     A character position in the current string.
    //
    // Returns:
    //     A Unicode character.
    //
    // Exceptions:
    //   System.IndexOutOfRangeException:
    //     index is greater than or equal to the length of this object or less than
    //     zero.
    public char this[int index] { get; }

On peut donc voir que la classe String est bien immutable car sa valeur est fixé à sa création et après la chaine devient read only.

  • Les méthodes de la classe System.String

Vous pourriez vous dire, à raison, que la classe string contient des méthodes permettant modifier la chaine après création (ToUpper/ToLower). Or si vous remarquez bien, l’ensemble de ces méthodes retournent un type string. La chaine en paramètre est la chaine d’entrée et restera toujours à la même valeur alors que la valeur de retour sera une nouvelle chaine traité à partir de la première.

Un petit exemple tiré de la MSDN démontrant cela :

class Program
{
  static void Main(string[] args)
  {
     string testString = "A TEST STRING"; Console.WriteLine("testString: " + testString + Environment.NewLine);
     Console.WriteLine("Performing 'testString.ToLower();'");
     // Does not alter the string. Returns a new lowercase string, but           
     // we do not assign it to anything, so it is discarded            
    testString.ToLower();
    Console.WriteLine("testString: " + testString + Environment.NewLine);
    Console.WriteLine("Performing 'string lowerString = " + "testString.ToLower();'");
    // Assign the returned string to a new variable           
    string lowerString = testString.ToLower();
    Console.WriteLine("lowerString: {0}{1}testString: {2}{1}",
    lowerString, Environment.NewLine, testString);
    // Finally convert the test string to lowercase by assigning the             
    // result of ToLower back to itself. Note that this still doesn't            
    // result in the original testString object changing; instead, the            
    // old testString is discarded and the variable is set to the newly-            
    // created lowercase string object           
    Console.WriteLine("Performing 'testString = testString.ToLower();'");
    testString = testString.ToLower();
    Console.WriteLine("testString: " + testString);
    Console.ReadLine();
  }
}

  • Cas de la concaténation de chaines

L’utilisation du caractères ‘+’ pour la concaténation de chaines s’est énormément répandu depuis la sortie du .NET. Toutefois, il faut savoir que cela peut être désastreux pour les performance. En effet, chaque utilisation de ‘+’ entraine la création d’une nouvelle chaine.  Ainsi l’ajout de 1000 caractères un à un avec le caractère ‘+’ va entrainer la création de 1000 chaines de caractères allant de 0 à 999 caractères (999! caractères pour les matheux).

Dans ce cas, on préconise l’utilisation de la version “mutable” (comprendre : modifiable) de System.String : StringBuilder. On peut voir l’intérêt dans le test suivant :

static void Main(string[] args)
{
    for (int nb = 1; nb < 1000000; nb *= 10)
    {
        Stopwatch sw = Stopwatch.StartNew();
        string s = "";
        for (int i = 0; i < nb; i++)
            s += "T";
        sw.Stop();
        Console.WriteLine("Temps string \t {0} \t: {1}", nb, sw.Elapsed);

        Stopwatch sw2 = Stopwatch.StartNew();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < nb; i++)
            sb.Append("T");
        sw2.Stop();
        Console.WriteLine("Temps SB \t {0} \t: {1}", nb, sw2.Elapsed);
    }
    Console.ReadLine();
}

Résultat du test de performance :

Nombre Concaténation Temps String Temps StringBuilder
1,00 00:00:00.0000015 00:00:00.0000034
10,00 00:00:00.0000076 00:00:00.0000007
100,00 00:00:00.0000138 00:00:00.0000026
1 000,00 00:00:00.0011658 00:00:00.0000211
10 000,00 00:00:00.0388231 00:00:00.0001182
100 000,00 00:00:05.4556546 00:00:00.0011412

Le résultat est sans appel en faveur de StringBuilder qui est jusqu’à 500x plus rapide que la concaténation avec le caractère +.

  • Conclusion

Il faut donc faire attention lorsque l’on utilise des objets du framework afin de savoir s’ils sont “mutables” ou non car, bien qu’un objet “immutable” soit là pour permettre des meilleurs performances, cela peut s’avérer le contraire lorsqu’il est mal utilisé (exemple string VS StringBuilder).

19/04/2010

[Nouveautés .NET 4] Dynamic VS Réflexion VS Code classique : Performances

Filed under: .NET, C# 4, Débutant, Intermediaire, Optimisation — Étiquettes : , , , , , , — sebastiencourtois @ 10:28

Suite à des discussions sur le sujet des performances avec les Dynamics, j’ai décidé de monter le petit test suivant :

class Program
   {
       static void Main(string[] args)
       {
           Test t = new Test();
           dynamic d = t;
           //Premier appel
           Console.WriteLine("Premier appel");
           
           //Appel direct
           Stopwatch sw = Stopwatch.StartNew();
           t.TotoNoParam();
           sw.Stop();
           Console.WriteLine("Appel direct : {0}",sw.Elapsed);
           //Appel reflexion
           sw.Reset();
           sw.Start();
           Test.CallMethod<Test>(t, "TotoNoParam", null);
           sw.Stop();
           Console.WriteLine("Appel Reflection : {0}", sw.Elapsed);
           //Appel dynamique
           sw.Reset();
           sw.Start();
           d.TotoNoParam();
           sw.Stop();
           Console.WriteLine("Appel Dynamique : {0}", sw.Elapsed);

           //Second appel
           Console.WriteLine("Second appel");

           //Appel direct
           sw.Reset();
           sw.Start();
           t.TotoNoParam();
           sw.Stop();
           Console.WriteLine("Appel direct : {0}", sw.Elapsed);
           //Appel reflexion
           sw.Reset();
           sw.Start();
           Test.CallMethod<Test>(t, "TotoNoParam", null);
           sw.Stop();
           Console.WriteLine("Appel Reflection : {0}", sw.Elapsed);
           //Appel dynamique
           sw.Reset();
           sw.Start();
           d.TotoNoParam();
           sw.Stop();
           Console.WriteLine("Appel Dynamique : {0}", sw.Elapsed);

           //Appel chainés
           //Appel direct
           for (int i = 1; i < 10000000; i *= 10)
           {
               sw.Reset();
               sw.Start();
               for (int j = 0; j < i; j++)
                   t.TotoNoParam();
               sw.Stop();
               Console.WriteLine("Appel direct x {0} \t: {1}",i, sw.Elapsed);
           }

           //Appel reflection
           for (int i = 1; i < 10000000; i *= 10)
           {
               sw.Reset();
               sw.Start();
               for (int j = 0; j < i; j++)
                   Test.CallMethod<Test>(t, "TotoNoParam", null);
               sw.Stop();
               Console.WriteLine("Appel reflection x {0} \t: {1}", i, sw.Elapsed);
           }

           //Appel dynamic
           for (int i = 1; i < 10000000; i *= 10)
           {
               sw.Reset();
               sw.Start();
               for (int j = 0; j < i; j++)
                   d.TotoNoParam();
               sw.Stop();
               Console.WriteLine("Appel dynamic x {0} \t: {1}", i, sw.Elapsed);
           }

           Console.WriteLine("Fini");
           Console.ReadLine();
       }

   }

La classe Test est la même que le poste précédent. J’ai d’abord voulu tester les performances pour un appel d’une méthode sans paramètres avec 3 méthodes :

  • Appel direct (classique depuis .NET 1)
  • Appel en utilisant la réflexion (MethodInfo.Invoke)
  • le mot clé dynamic.

Voici les tableaux de résultats ( les graphiques ne rendant pas correctement les valeurs, j’ai choisi de ne pas en mettre).

Sur un appel (en ms) Direct Réflexion Dynamics
Premier appel 0,0622 0,4020 26,0880
Second appel 0,0007 0,0076 0,7007
  88x 53x 37x

Premier constat, un deuxième appel est toujours plus rapide que le premier quelque soit la méthode. Cela est dû à une optimisation de .NET qui compile le code à la volée et le met en cache. Le premier appel subit donc la compilation + mise en cache. Cela ajoute un temps (non négligeable) supplémentaire pour l’exécution de la méthode.

Deuxième constant, on remarque que Direct est 6 à 10 x plus rapide que la réflexion et 400 à 1000x fois plus rapide que Dynamics.

Si l’on regarde les apples multiples, on obtient le tableau suivant (en secondes) :

Appels Multiples Direct Réflexion Dynamics Dynamics/Direct Réflexion / Direct Réflexion/Dynamics
1,00 0,0000003 0,0000069 0,0007376 2 458,67 23,00 0,01
10,00 0,0000003 0,0000238 0,0000015 5,00 79,33 15,87
100,00 0,0000011 0,0002062 0,0000072 6,55 187,45 28,64
1 000,00 0,0000103 0,0020013 0,0000691 6,71 194,30 28,96
10 000,00 0,0000979 0,0211307 0,0006662 6,80 215,84 31,72
100 000,00 0,0009323 0,1582175 0,0067310 7,22 169,71 23,51
1 000 000,00 0,0096497 1,5509957 0,0872035 9,04 160,73 17,79

Remarque : Afin de clarifier les choses, ces tableaux a été généré plusieurs fois afin d’être sur de la validité des résultats.

On peut voir que l’appel classque reste, de loin, la méthode la plus rapide. Direct reste environ 5 à 10 x plus rapide que dynamic et 80 à 200 x plus rapide que la réflexion. On remarque aussi que la reflexion est plus lente (15 à 30x) que Dynamic (ce qui est en contradiction avec le premier tableau). De plus, le premier appel multiple pour Dynamic est étonnamment élevé car il intervient APRES deux appels à cette méthode issus de l’expérience précédente donc il ne devrait plus y avoir de compilation/mise en cache.

Je n’ai pas encore trouvé les raisons de ces “incohérences”. Si vous avez des explications, n’hésitez pas à commenter ce post.

Concernant les appels de méthodes avec paramètres, on se retrouve avec les mêmes plages de valeurs (Direct 5 à 10x plus rapide que reflexion et 500 à 1200 x plus rapide que dynamic). On retrouve les mêmes incohérences du deuxième tableau.

Conclusion :

L’utilisation du mot clé dynamic peut avoir des conséquences dramatiques sur les performances d’un code C# et ne doit donc être utilisé que dans les cas vraiment spécifiques comme l’interopérabilité avec un langages dynamique.

EDIT : 28 / 07 / 2010 : Suite à un commentaire, je publie ici le code ainsi qu’une optimisation proposé pour le test :

class Program
    {
        static void Main(string[] args)
        {
            Test t = new Test();
            dynamic d = t;

            Console.WriteLine("Test Appel Direct");
            Stopwatch sw = new Stopwatch();
            for (int i = 1; i < 10000000; i *= 10)
            {
                sw.Reset();
                sw.Start();
                for (int j = 0; j < i; j++)
                    t.TotoNoParam();
                sw.Stop();
                Console.WriteLine("Appel direct x {0} \t: {1}", i, sw.Elapsed);
            }

            Console.WriteLine("Test Appel Reflection");
            for (int i = 1; i < 10000000; i *= 10)
            {
                sw.Reset();
                sw.Start();
                for (int j = 0; j < i; j++)
                    Program.CallMethod<Test>(t, "TotoNoParam");
                sw.Stop();
                Console.WriteLine("Appel Reflection x {0} \t: {1}", i, sw.Elapsed);
            }

            Console.WriteLine("Test Appel Reflection (optimized)");
            Type type = t.GetType();
            MethodInfo mi = type.GetMethod("TotoNoParam");
            for (int i = 1; i < 10000000; i *= 10)
            {
                sw.Reset();
                sw.Start();
                for (int j = 0; j < i; j++)
                    mi.Invoke(t,null);
                sw.Stop();
                Console.WriteLine("Appel Reflection (optimized) x {0} \t: {1}", i, sw.Elapsed);
            }

            Console.WriteLine("Test Appel Dynamic");
            for (int i = 1; i < 10000000; i *= 10)
            {
                sw.Reset();
                sw.Start();
                for (int j = 0; j < i; j++)
                    d.TotoNoParam();
                sw.Stop();
                Console.WriteLine("Appel Dynamic x {0} \t: {1}", i, sw.Elapsed);
            }
            Console.ReadLine();
        }

        public static void CallMethod<T>(T instance, string methodName) where T : class
        {
            Type t = instance.GetType();
            MethodInfo mi = t.GetMethod("TotoNoParam");
            if (mi == null)
                throw new InvalidOperationException(string.Format("La méthode {0} n'existe pas pour le type {1}", methodName, t.Name));
            mi.Invoke(instance, new object[] { });
        }
    }

    public class Test
    {
        public string ValA { get; set; }
        public int ValB;
        public dynamic ValC;

        public void TotoNoParam() { }
        public void Toto(int a) { }
        public string Tata(dynamic b) { return string.Empty; }
    }

Les résultats :

Test Appel Direct

Appel direct x 1        : 00:00:00.0000614

Appel direct x 10       : 00:00:00.0000003

Appel direct x 100      : 00:00:00.0000011

Appel direct x 1000     : 00:00:00.0000076

Appel direct x 10000    : 00:00:00.0000702

Appel direct x 100000   : 00:00:00.0007034

Appel direct x 1000000  : 00:00:00.0072613

Test Appel Reflection

Appel Reflection x 1    : 00:00:00.0005199

Appel Reflection x 10   : 00:00:00.0000188

Appel Reflection x 100  : 00:00:00.0001620

Appel Reflection x 1000         : 00:00:00.0016719

Appel Reflection x 10000        : 00:00:00.0155864

Appel Reflection x 100000       : 00:00:00.1943837

Appel Reflection x 1000000      : 00:00:01.8779117

Test Appel Reflection (optimized)

Appel Reflection (optimized) x 1        : 00:00:00.0000069

Appel Reflection (optimized) x 10       : 00:00:00.0000184

Appel Reflection (optimized) x 100      : 00:00:00.0001509

Appel Reflection (optimized) x 1000     : 00:00:00.0014998

Appel Reflection (optimized) x 10000    : 00:00:00.0151252

Appel Reflection (optimized) x 100000   : 00:00:00.1431094

Appel Reflection (optimized) x 1000000  : 00:00:01.3252002

Test Appel Dynamic

Appel Dynamic x 1       : 00:00:00.0346741

Appel Dynamic x 10      : 00:00:00.0000053

Appel Dynamic x 100     : 00:00:00.0000107

Appel Dynamic x 1000    : 00:00:00.0000921

Appel Dynamic x 10000   : 00:00:00.0009150

Appel Dynamic x 100000  : 00:00:00.0088054

Appel Dynamic x 1000000         : 00:00:00.0764506

Apparement même en sortant le MethodInfo de l’appel, on obitent encore des résultats plus rapide avec Dynamic qu’avec Reflection.

14/04/2010

[Nouveautés C# .NET 4] Introduction à la Covariance / Contravariance

Filed under: .NET, Débutant, Intermediaire — Étiquettes : , , , , , — sebastiencourtois @ 14:44

Pour la sortie de .NET 4, je vais faire quelques articles sur les nouveautés du langages (en attendant de pouvoir jouer avec SL4). Aujourd’hui, nous allons voir la notion de Covariance / Contravariance.

EDIT (08/05/2010) : La Covariance/Contravariance n’est pas disponible pour Silverlight 4

  • Définitions

La covariance et la contravariance dixit Wikipedia :

Within the type system of a programming language, a typing rule or a type conversion operator is:

  • covariant if it preserves the ordering, ≤, of types, which orders types from more specific to more generic;
  • contravariant if it reverses this ordering, which orders types from more generic to more specific;
  • invariant if neither of these apply.

Bon, je suis d’accord qu’avec cette définition, on est pas plus avancé. On voit que cela parle de conversion de type et de généricité mais ça reste très abstrait.

Nous allons donc voir des exemples puis revenir sur ces définitions.

  • Exemples sur la covariance 

Nous prendrons une architecture simple pour ces exemples :

public abstract class Person { }
public class Employe : Person { }
public class Customer : Person { }
public class Manager : Employe { }

Nous allons maintenant jouer avec des tableaux de ces objets :

Person[] MaSociete = new Manager[10];  // .NET 3.5 / 4 : OK 
Employe[] MaSociete2 = new Manager[10]; // .NET 3.5 / 4 : OK 

Rien de transcendant là dessus. On a un tableau de Manager. Un manager étant une personne (ça dépend dans quel société …:)), il n’y a pas de problème à mettre les données d’un tableau de manager dans un tableau de personnes. De même pour mettre nos données manager dans le tableau d’employés (Manager étant un employé).

Faison la même chose avec des listes

Manager[] MaSociete3 = new Manager[10];
IEnumerable<Manager> im = MaSociete3.Where(p => p.ToString() == "Manager"); // .NET 3.5 / 4 : OK 
IEnumerable<Manager> im2 = new List<Manager>(); // .NET 3.5 / 4 : OK 

Le requête LINQ renvoie un IEnumerable<Manager> que je peux donc mettre dans im. De même pour une liste de manager car List<T> implémente IEnumerable<T>.

IEnumerable<Person> ip = im;  // .NET 3.5 : Erreur de compilation  / .NET 4 : OK

Sur cette ligne, on obtient une erreur de compilation en .NET 3.5 (Cannot Convert IEnumerable<Manager> to IEnumerable<Person>). Pourtant cela devrait marcher comme un tableau (un manager est une personne donc un tableau de personne peut contenir des données venant d’un tableau de manager). Cela n’est pas possible car, en .NET 3.5, IEnumerable<T> n’est pas covariant (à l’inverse de Array qui lui est covariant). C’est pour corriger ce manque que Microsoft a autorisé la covariance des interfaces et des délégués en .NET 4.

En .NET 4, je peux ainsi écrire :

IEnumerable<Person> ipl = new List<Manager>();  // .NET 3.5 : Erreur de compilation  / .NET 4 : OK
Func<string> funcString = () => { return "Bonjour le monde"; };
Func<object> funcObject = funcString;         // .NET 3.5 : Erreur de compilation  / .NET 4 : OK

Pour résumer, on peut dire que la covariance permet de caster un type générique A<T> dans un type générique A<K> si T hérite directement ou indirectement de K et si le type T est un paramètre de sortie (nous reviendrons sur cette notion plus loin).

Remarque :  La covariance (ainsi que la contravariance) fonctionne uniquement avec des interfaces et des délégués. Les arguments génériques doivent être des types références. Il n’est pas possible de faire les lignes suivantes :

List<Person> lo = new List<Manager>();         // .NET 3.5 / 4 : Erreur de compilation 
IEnumerable<ValueType> lo = new List<int>();    // .NET 3.5 / 4 : Erreur de compilation 

  • Exemples sur la contravariance 

Dans certains cas, on aimerait que l’inverse soit possible.

  Action<object> actObject = (object o) => { };
  Action<string> actString = actObject;          // .NET 3.5 : Erreur de compilation  / .NET 4 : OK

Dans cet exemple, je souhaite pouvoir caster mon délégués avec une valeur de retour de type string ou un de ses parents. En effet,lorsque je récupère une donnée, je peux la caster dans une variable d’un type dont elle hérite. C’est le principe de la contravariance introduite dans .NET 4.

La contravariance est la possiblité de caster un type générique A<T> dans un type générique A<K> si T est parent direct ou indirect de K et si T est un paramètre d’entrée (nous reviendrons sur cette notion plus loin).

  • Les interfaces / Délégués déja covariant et contravariant

Voici la liste des interfaces/délégués ayant été mis à jour au sein du framework .net 4 pour tenir compte de cette nouvelle possibilité.

 

  • Exemple pratique : La création d’une interface covariant et contravariant

Prenons l’exemple d’une classe réalisant de la transformations de données.

public interface IDataTransformer<T, K>
{
    K Transform(T data);
}

public class DataTransformer<T, K> : IDataTransformer<T, K>
{
    public K Transform(T data)
    {
        return default(K);
    }
}

Nous avons donc une interface IDataTransformer comprenant deux paramètres génériques T et K et une méthode qui va transformer une variable de type T en type K. Nous allons créer un type implémentant cette interface puis l’utiliser.

IDataTransformer<Manager, string> dataTransformer = new DataTransformer<Manager, string>(); // .NET 3.5 / 4 : OK
IDataTransformer<Person, string> dataTransformer2 = new DataTransformer<Manager, string>(); // .NET 3.5 / 4 : Erreur de compilation 
IDataTransformer<Manager, object> dataTransformer3 = new DataTransformer<Manager, string>(); // .NET 3.5 / 4 : Erreur de compilation 

Si le premier cas fonctionne logiquement, les deux cas suivants sont  moins logique. Dans le cas de dataTransfomer2, si on crée un DataTransformer prenant un Manager en entrée, on devrait aussi pouvoir prendre une personne (rappelons le, les managers sont des personnes).Pourtant le compilateur n’est pas de cette avis car il n’arrive pas à convertir DataTransformer<Manager,string> en IDataTransformer<Person,string>. De même pour dataTransformer3. Si on récupère en sortie une chaine de caractère, on pourrait très bien récupérer un object car string dérive de object.

Voila les limites de .NET 3.5 et voyons comment faire pour corriger cela en  .NET 4. Pour cela nous allons créer une nouvelle interface et une nouvelle classe Variant.

public interface IDataTransformerVariant<in T,out K>
{
    K Transform(T data);
}

  
public class DataTransformerVariant<T, K> : IDataTransformerVariant<T, K>
{
    public K Transform(T data)
    {
        return default(K);
    }
}

La différence se situe uniquement dans la déclaration de l’interface IDataTransformerVariant. On a rajouté un mot clé in devant le type d’entrée (T) et un out devant le type de sortie (K). En procédant ainsi, on active la covariance et la contravariance et on oblige le paramètre générique T a toujours être en paramètres de méthodes et le paramètre générique K a toujours être en valeur retour d’une méthode. Ne pas suivre cette règle entraine l’erreur de compilation suivante :

CoVarImg1

  • Conclusion

Pour conclure, je reprendrais les phrases importantes de cet article :

la covariance permet de caster un type générique A<T> dans un type générique A<K> si T hérite directement ou indirectement de K et si le type T est un paramètre de sortie.

La contravariance est la possiblité de caster un type générique A<T> dans un type générique A<K> si T est parent direct ou indirect de K et si T est un paramètre d’entrée.

Le fait que un paramètre générique soit d’entrée ou de sortie dépend de son placement dans les méthodes interfaces ou des délégués.

A titre d’information, ce concept est disponible depuis .NET 2.0 en IL (comme l’à montré Simon Ferquel dans son blog) mais aucun mot clé n’étant fourni en .NET, on ne pouvait pas l’utliser en C#.

Merci à Alexis Pera (MSP) pour la relecture et correction de cet article.

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.

Older Posts »

Créez un site Web ou un blog gratuitement sur WordPress.com.