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/09/2010

[XNA 4] Tutoriel 2 : Gestion des images (2D)

Filed under: .NET, Débutant, Windows Phone, XBOX 360, XNA — Étiquettes : , , , , , , , , — sebastiencourtois @ 16:34

Remarque : Ce tutorial fait suite au Tutorial 1 et reprend le projet utilisé dans ce dernier.

Nous allons voir dans ce tutorial, comment charger, afficher et déplacer des images (aussi appelé sprites). Vous trouverez ici les images utilisées pour ce tutorial.

  • Ajout des images au projet

Lorsque vous souhaitez utiliser une image dans un projet XNA (que ce soit en 2D ou en 3D), il est nécessaire de l’ajouter au projet Content associé au projet XNA (Tutorial2DContent dans notre exemple).

xna_tuto2_1

Clic droit sur le projet Tutorial2DContent > Ajouter > Elément existant.

xna_tuto2_2

On sélectionne les fichiers que l’on souhaite ajouter et on clique sur le bouton “Ajouter”. Une fois que les fichiers sont ajoutés, on peut voir leurs propriétés.

xna_tuto2_3

La propriété Asset Name est le nom de l’image que nous utiliserons au sein de l’application. Vous pouvez noter qu’il n’y a pas d’extension. Vous pouvez la modifier si vous le souhaitez.

  • Chargement des images

Une fois que nous avons ajouté les images au projet, nous allons les charger au démarrage de l’application. Cela se fait généralement dans la méthode LoadContent() de la classe Game.

protected override void LoadContent()
{
    spriteBatch = new SpriteBatch(GraphicsDevice);
    //Chargement des deux textures
    this.tank = Content.Load<Texture2D>("tank_body");
    this.canon = Content.Load<Texture2D>("tank_canon");
}

Pour charger des images, il suffit d’appeler la méthode Load de la propriété Game.Content. Il est nécessaire de lui donner le type de données que l’on souhaite charger. Dans notre cas, on souhaite charger une image qui est représenté, dans XNA, par la classe Texture2D. On passe en paramètre  l’Asset Name de l’image à charger (et non pas le nom du fichier).

Le résultat du chargement est stocké dans une propriété de type Texture2D.

//Stockage de la texture du corps du tank
Texture2D tank;
//Stockage de la texture du canon
Texture2D canon;

  • Libération de la mémoire prise par les images (Optionnel)

Bien que .NET soit pourvu d’un Garbage collector nettoyant régulièrement les zones mémoires non utilisés, il est possible de faire le ménage soit même. Ainsi, si l’on souhaite libérer la mémoire prise par ces deux textures, il suffit d’aller dans la méthode UnloadContent de Game et d’appeler les Dispose() sur chacune des textures.

protected override void UnloadContent()
{
   this.tank.Dispose();
   this.canon.Dispose();
}

Rappel : La méthode UnloadContent() est appelée lorsque l’on sort de la boucle de jeu courante.

  • Affichage des images

Maintenant que nous avons chargé nos images, nous allons les afficher. Cela se passe dans la méthode Draw() de la classe Game.

//Position du tank
 Vector2 TankPosition = new Vector2(30, 40);

 protected override void Draw(GameTime gameTime)
 {
     GraphicsDevice.Clear(Color.CornflowerBlue);
     spriteBatch.Begin();
     spriteBatch.Draw(this.tank, TankPosition, Color.White);
     spriteBatch.End();
     base.Draw(gameTime);
 }

On utilise spriteBatch pour dessiner les textures à l’écran. On doit “préparer” spriteBatch avant son utilisation en appelant la méthode Begin() (nous parlerons de ses paramètres optionnels plus loin). On utilise ensuite la méthode Draw() afin d’afficher l’image. Dans sa version la plus simple, Draw comprend 3 paramètres :

  1. L’image à afficher
  2. La position de l’image par rapport au coin haut gauche de l’écran. La position est définie dans une structure Vector2. Afin de simplifier le code, on a créé une variable TankPosition pour stocker et manipuler la position de l’image.
  3. La teinte de l’image. Si l’on souhaite avoir l’image originale sans modification, on met la couleur blanche.

Une fois que l’on a dessiné l’ensemble des images souhaités, on appelle la méthode End() du spriteBatch afin que celui-ci envoie l’ensemble des informations à la carte graphique (ce comportement dépend du mode de spriteBatch choisi… voir plus loin dans cet article …).

  • Déplacement des images

Si l’on souhaite déplacer l’image, il suffit de modifier les données de la variable TankPosition entre chaque appel de la méthode Draw(). Si vous vous rappelez de la structure d’une boucle de jeu XNA, une méthode est appelé avant chaque Draw() ==> Update(). Cette méthode est destinée à faire toutes les tâches non graphiques (qui n’interagisse pas directement avec la carte graphique). Ainsi si l’on souhaite déplacer notre tank d’un pixel vers la droite et d’un pixel vers le bas, on utilisera le code suivant :

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

   TankPosition.X += 1;
   TankPosition.Y += 1;
   base.Update(gameTime);
}
  • Système de coordonnées & Origines

Par défaut, l’ensemble des entités 2D de XNA (fenêtres, sprites …) sont définies dans un système de coordonnées cartésienne orthonormé dont l’unité est le pixel.

xna_tuto2_4

Le coin haut gauche de l’entité est l’origine (0,0) et les x sont positif lorsque l’on va vers la droite. Les y sont positif quand on va vers le bas.

Il est important de noter que le point d’origine est le même que ce soit pour la position de l’entité comme pour d’autres transformations comme la rotation. Nous allons voir, dans le paragraphe suivant, qu’il est possible de modifier ce point d’origine.

  • Rotation des images

Nous allons maintenant faire tourner le tank sur lui-même. Pour cela nous allons rajouter une propriété permettant de conserver la rotation

//Rotation en degré du tank
float TankRotation = 30.0f;

Attention : La rotation 0 est l’image tel qu’importée dans XNA. De plus la rotation se fait selon l’axe du point d’origine. Si l’on souhaite faire tourner une image sur elle-même, il est nécessaire de déplacer le point d’origine au centre de l’image (Width/2, Height/2).

Pour réaliser la rotation,nous utilisons une version plus avancée de la méthode Draw du spritebatch :

protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.CornflowerBlue);
    spriteBatch.Begin();
    spriteBatch.Draw(this.tank,                                  // Texture (Image)
                     TankPosition,                               // Position de l'image
                     null,                                       // Zone de l'image à afficher
                     Color.White,                                // Teinte
                     MathHelper.ToRadians(TankRotation),         // Rotation (en rad)
                     new Vector2(tank.Width/2,  tank.Height/2),  // Origine
                     1.0f,                                       // Echelle
                     SpriteEffects.None,                         // Effet
                     0);                                         // Profondeur
    spriteBatch.End();
    base.Draw(gameTime);
}

Les deux premiers et le quatrième paramètres ont déjà été décrit précédemment. Intéressons-nous aux autres paramètres :

    • Zone de l’image : Il est possible de ne copier qu’une partie de la texture à l’écran. Dans ce cas, on indique un rectangle indiquant la partie de la texture à afficher. Si l’on souhaite afficher la texture en entier, on passe la valeur null.
    • Rotation : Dans ce paramètre, on indique la rotation que l’on souhaite appliquer à la texture. Attention, l’angle de rotation doit être donné en RADIANS. Pour vous aider,  XNA fournir une classe MathHelper avec une méthode statique convertissant les degrés en radians.
    • Origine : Ce paramètre permet d’indiquer le point d’origine de la texture pour l’ensemble des transformations (déplacement, rotation…). Par défaut, il est situé en haut à gauche mais il est possible de le modifier comme indiqué dans notre exemple (le point d’origine de notre exemple se trouve au centre de la texture afin que celle-ci tourne sur elle-même.).
    • Echelle : Il est possible d’agrandir et de réduire la taille de votre texture. La valeur 1 permet de conserver la taille originale.
    • Effet : Ce paramètre permet l’application d’un effet de renversement (renversement vertical ou horizontal).Si vous ne souhaitez pas utiliser ces effets, mettre à SpriteEffects.None.
    • Profondeur : Le dernier paramètre permet d’indiquer la profondeur à laquelle vous souhaitez mettre votre texture. Cela permet de gérer les cas de chevauchement de texture (voir paragraphe suivant).

L’ensemble des méthodes Draw() de spriteBatch sont décrites en détail dans la MSDN.

  • Superposition et transparence

Si nous faisons la même chose avec le canon du tank, on obtient le code suivant :

//Position du tank
Vector2 TankPosition = new Vector2(30, 40);
//Rotation en degré du canon
float CanonRotation = 40.0f;

protected override void Draw(GameTime gameTime)
{
   GraphicsDevice.Clear(Color.CornflowerBlue);
   spriteBatch.Begin();
   spriteBatch.Draw(this.canon, TankPosition, null, Color.White, MathHelper.ToRadians(CanonRotation), new Vector2(canon.Width / 2 - 4.0f, canon.Height / 2), 1.0f, SpriteEffects.None, 0);
   spriteBatch.Draw(this.tank, TankPosition, null, Color.White, MathHelper.ToRadians(TankRotation), new Vector2(tank.Width / 2, tank.Height / 2), 1.0f, SpriteEffects.None, 0);
   spriteBatch.End();
   base.Draw(gameTime);
}

Remarque : Le point d’origine pour le canon contient un –4.0f pour X. Cela provient du décalage entre le centre de la texture du tank et celle du canon. Afin d’avoir un affichage un peu plus réaliste, j’ai décalé légèrement le point d’origine du canon.

Cela semble correct mais lorsque l’on affiche le résultat :

xna_tuto2_5a

xna_tuto2_5b

Canon et Tank correctement placé

Lors de la rotation du canon, une partie de celui ci disparait sous le tank lui même.

Ce problème est du à la façon dont spriteBatch affiche les sprites. Cela peut être défini dans la méthode Begin() grâce au premier paramètre qui est l’énumération SpriteSortMode

Valeur

Description

Deferred

Valeur par défaut et la plus utilisée. Envoi les opérations à la carte graphique lors de l’appel de End(). Cela permet d’utiliser plusieurs spritesBatchs sans risquer d’avoir de conflit au niveau de la carte graphique. Dépend du paramètre profondeur de la méthode Draw.Le premier plan étant 1 et le fond 0.

BackToFront

Affichage des sprites selon le paramètre de profondeur passé par  la méthode Draw. Le premier plan étant 1 et le fond 0.

FrontToBack

Affichage des sprites selon le paramètre de profondeur passé par  la méthode Draw. Le premier plan étant 0 et le fond 1.

Immediate

Affiche les images en écrasant les pixels déjà présent. Ce mode est le plus rapide mais celui qui gère le moins bien les transparences et supperposition ainsi que l’utilisation de multiples spritesBatch.

Texture

Affiche les images dans l’ordre des textures. Ainsi, si plusieurs appels à Draw se font avec la même texture, l’ensemble des appels sera fait en même temps pour gagner du temps en transfert de données vers la carte graphiques.

Pour la documentation originale sur SpriteSortMode : La page MSDN.

Begin peut prendre aussi un autre paramètre pour indiquer comment il doit gérer les cas d’écrasement de pixel (vouloir dessiner un pixel qui a déjà été dessiné par une autre texture). Ce paramètre est de type BlendState et comprend 4 valeurs possibles.

Valeur

Description

AlphaBlend

Valeur par défaut. Fusion du pixel source/destination en tenant compte du canal alpha (transparence).

Additive

Ajoute les canaux RGBA entre les deux pixels

NonPremultiplied

Ajoute les canaux RGB entre les deux pixels sans tenir compte de la transparence

Opaque

La couleur du nouveau pixel écrase celle du tampon.

Pour la documentation originale sur BlendState: La page MSDN.

Pour résoudre le problème que nous avons rencontré, il y a donc deux solutions :

    • Intervertir les deux méthodes Draw()
protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.CornflowerBlue);
    spriteBatch.Begin();
    spriteBatch.Draw(this.tank, TankPosition, null, Color.White, MathHelper.ToRadians(TankRotation), new Vector2(tank.Width / 2, tank.Height / 2), 3.0f, SpriteEffects.None, 0);            
    spriteBatch.Draw(this.canon, TankPosition, null, Color.White, MathHelper.ToRadians(CanonRotation), new Vector2(canon.Width / 2 - 4.0f, canon.Height / 2), 3.0f, SpriteEffects.None, 0);
    spriteBatch.End();
    base.Draw(gameTime);
}

    • Changer la méthode Begin
protected override void Draw(GameTime gameTime)
{
     GraphicsDevice.Clear(Color.CornflowerBlue);
     spriteBatch.Begin(SpriteSortMode.FrontToBack,null);
     spriteBatch.Draw(this.canon, TankPosition, null, Color.White, MathHelper.ToRadians(CanonRotation), new Vector2(canon.Width / 2 - 4.0f, canon.Height / 2), 3.0f, SpriteEffects.None, 0);
     spriteBatch.Draw(this.tank, TankPosition, null, Color.White, MathHelper.ToRadians(TankRotation), new Vector2(tank.Width / 2, tank.Height / 2), 3.0f, SpriteEffects.None, 0);
     spriteBatch.End();
     base.Draw(gameTime);
}

On obtient un affichage “correct” :

xna_tuto2_6

Remarque : Les petites zones bleues proviennent de mon placement tank/canon qui n’est pas parfait sur pour ces images.

  • Conclusion

Vous savez maintenant charger ,afficher et déplacement un image dans un univers 2D XNA. Dans un prochain post, nous verrons comment déplacer ces images en fonction des entrées de l’utilisateurs (souris,clavier,Manette XBOX 360, Touch Windows Phone). Le prochain post est consacré à l’affichage de texte dans la fenêtre.

Téléchargement du résultat final

  • Exercices pour tester ses compétences
  1. Faire se déplacer le char dans l’écran en le faisant rebondir sur les coins de l’écran.
    • Indice 1 : Attention au point d’origine
    • Indice 2 : Les dimensions de l’écran sont disponibles au travers de graphics.GraphicsDevice.Viewport
    • Indice 3 : Attention au pixel transparent autour du tank
  2. Faire le même exercices en gérant le couple char+canon.

Solution possible

Propulsé par WordPress.com.

%d blogueurs aiment cette page :