Astuces DotNet (Sébastien Courtois)

17/02/2013

[SharpDX] Direct2D/Direct3D 10/11 Interoperability

Filed under: 3D, DirectX, Intermediaire, Jeux vidéos — Tags:, , , , , , , — 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.

01/02/2012

[KinectSDK] Affichage des flux vidéo couleur et profondeur de Kinect dans une fenêtre WPF avec MVVM

Filed under: .NET, C#, Débutant, Jeux vidéos, Kinect — Tags:, , , , , — sebastiencourtois @ 05:46

Prérequis pour ce tutoriel :

  • Kinect SDK doit être installé sur votre machine
  • Coding4Fun Kinect Toolkit (la dll utilisée est fournie dans le code exemple a la fin de cet article).
  • Kinect et un câble USB permettant de le connecter à un PC (seules les versions de Kinect achetée séparément de la console en ont un si ma mémoire est bonne. Si vous n’avez pas de câble Kinect/USB, vous devriez pouvoir en trouver sur le Microsoft Store).
  • Créer un projet Windows Application WPF et ajouter les références Microsoft.Research.Kinect (dans GAC) et Coding4Fun.Kinect.Wpf.

Suite à la présentation rapide de Kinect et de son SDK, nous allons maintenant rentrer dans le vif du sujet en créant une application WPF dans lequel nous allons afficher le flux couleur et profondeur de Kinect.

Afin de permettre la réutilisation de ce code, nous allons utiliser une architecture MVVM composée d’un ViewModel pour la fenêtre et d’un « ViewModel » Kinect afin de pouvoir réutiliser la partie Kinect indépendamment dans d’autres projets.

Les fichiers du projet sont :

  • MainWindow.xaml : Fichier xaml représentant la fenêtre.
  • MainWindowViewModel.cs : ViewModel de la fenêtre MainWindow
  • KinectVideoViewModel.cs : « ViewModel » pour acceder aux données Kinect. Une instance de cette classe est incluse dans MainWindowViewModel.

La fenêtre principale (MainWindow.xaml)

kinectdemoblog1

 

 

 

 

 

 

 

 

 

 

Screenshot de l’application finale

La fenêtre est une grille 2 lignes/2colonnes ou la première ligne contient les textes titres. La deuxième ligne contient des images contenant les flux vidéos (flux couleur à gauche et flux profondeur à droite).

Chacune des images voit leur source bindée sur des BitmapSource du KinectViewModel que nous verrons plus tard.

Le ViewModel est créé et lié au DataContext directement dans le fichier XAML.

1 <Window x:Class="KinectWpf.MainWindow" 2 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 3 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 4 xmlns:vm="clr-namespace:KinectWpf.ViewModel" 5 Title="Kinect Demo 1" Width="800" Height="600" > 6 <Window.DataContext> 7 <vm:MainWindowViewModel /> 8 </Window.DataContext> 9 <Grid> 10 <Grid.RowDefinitions> 11 <RowDefinition Height="Auto" /> 12 <RowDefinition Height="*" /> 13 </Grid.RowDefinitions> 14 <Grid.ColumnDefinitions> 15 <ColumnDefinition Width="*" /> 16 <ColumnDefinition Width="*" /> 17 </Grid.ColumnDefinitions> 18 <TextBlock Text="Color Stream" FontWeight="Bold" FontSize="15" VerticalAlignment="Center" Margin="5,0,0,0" /> 19 <TextBlock Text="Depth Stream" Grid.Column="1" FontWeight="Bold" FontSize="15" VerticalAlignment="Center" Margin="5,0,0,0" /> 20 <Image Grid.Row="1" Grid.Column="0" Source="{Binding Kinect.ColorStreamImageSource}" /> 21 <Image Grid.Row="1" Grid.Column="1" Source="{Binding Kinect.DepthStreamImageSource}"/> 22 </Grid> 23 </Window> 24

Le ViewModel de la fenêtre (MainWindowViewModel.cs)

 

1 public class MainWindowViewModel : INotifyPropertyChanged 2 { 3 public KinectViewModel _kinect; 4 public KinectViewModel Kinect 5 { 6 get { return _kinect; } 7 set 8 { 9 this._kinect = value; 10 OnPropertyChanged("Kinect"); 11 } 12 } 13 14 public MainWindowViewModel() 15 { 16 Application.Current.Exit += (sender, args) => { Clean(); }; 17 Kinect = new KinectViewModel(); 18 Kinect.Start(RuntimeOptions.UseColor | RuntimeOptions.UseDepthAndPlayerIndex); 19 } 20 21 public void Clean() 22 { 23 Kinect.Release(); 24 } 25 26 #region INotifyPropertyChanged Interface 27 28 public event PropertyChangedEventHandler PropertyChanged; 29 public void OnPropertyChanged(string name) 30 { 31 if (this.PropertyChanged != null) 32 this.PropertyChanged(this, new PropertyChangedEventArgs(name)); 33 } 34 35 #endregion 36 }

Comme la plupart des ViewModel, cette classe implémente INotifyPropertyChanged afin de pouvoir remonter les changements au moteur de binding de WPF. Nous ajoutons une propriété KinectViewModel afin qu’il soit accessible directement par binding à la vue MainWindow.

Deux méthodes du KinectViewModel seront utilisées ici. La première est la méthode Start qui est chargée d’initialiser et de lancer la capture des flux vidéos (les paramètres de cette méthode seront détaillés dans la suite). La seconde est la méthode Release() charge de libérer les ressources utilisant Kinect.

Le ViewModel Kinect (KinectViewModel.cs)

 

1 public class KinectViewModel : INotifyPropertyChanged 2 { 3 private Runtime KinectRuntime; 4 5 public KinectViewModel(int id = 0) 6 { 7 if (Runtime.Kinects == null || Runtime.Kinects.Count <= id) 8 throw new InvalidOperationException("No kinect runtime found for id="+ id+"."); 9 this.KinectRuntime = Runtime.Kinects[id]; 10 } 11 [...] 12 }

Cette partie concerne l’initialisation du Runtime Kinect. La classe Runtime (provenant de Microsoft.Research.Kinect) référence, dans sa propriété Kinects sous la forme de Kinect[], l’ensemble des Kinects connectés à l’ordinateur. Notre ViewModel va permettre de sélectionner le Kinect souhaite grâce au paramètre id. Dans la plupart des cas, un seul Kinect est branché, on mettra une valeur par défaut id = 0. Une fois l’objet Kinect trouvé, on le conserve dans une variable KinectRuntime que nous utiliserons dorénavant.

 

1 public class KinectViewModel : INotifyPropertyChanged 2 { 3 [...] 4 public void Start(RuntimeOptions options) 5 { 6 this.KinectRuntime.Initialize(options); 7 8 if ((options & RuntimeOptions.UseColor) == RuntimeOptions.UseColor) 9 InitColorStream(); 10 if ((options & RuntimeOptions.UseDepth) == RuntimeOptions.UseDepth) 11 InitDepthStream(); 12 if ((options & RuntimeOptions.UseDepthAndPlayerIndex) == RuntimeOptions.UseDepthAndPlayerIndex) 13 InitDepthAndPlayerIndexStream(); 14 if ((options & RuntimeOptions.UseSkeletalTracking) == RuntimeOptions.UseSkeletalTracking) 15 InitSkeletonTracking(); 16 } 17 [...] 18 }

Une fois le KinectVIewModel crée, il faut initialiser puis démarrer les flux vidéos. Pour cela, il est nécessaire de fournir à Kinect le type des flux que l’on souhaite utiliser. On utilise pour ça l’énumération RuntimeOptions définie par le SDK Kinect comme suit :

 

1 namespace Microsoft.Research.Kinect.Nui 2 { 3 [Flags] 4 public enum RuntimeOptions 5 { 6 UseDepthAndPlayerIndex = 1, 7 UseColor = 2, 8 UseSkeletalTracking = 8, 9 UseDepth = 32, 10 } 11 }

On initialise le Kinect en appelant la méthode Initialize tout en fournissant cette énumération (cette énumération ayant un attribut [Flag], il est possible de les combiner en utilisant les opérateurs binaires). La série de conditions qui suivent la méthode Initialize vont scannes ces RuntimeOptions afin de déterminer les types de flux voulus. Cela va nous permettre d’appeler des méthodes d’initialisation spécifiques à ces flux.

 

1 public class KinectViewModel : INotifyPropertyChanged 2 { 3 [...] 4 #region Color Stream 5 6 public BitmapSource _colorStreamImageSource; 7 public BitmapSource ColorStreamImageSource 8 { 9 get { return _colorStreamImageSource; } 10 set 11 { 12 this._colorStreamImageSource = value; 13 OnPropertyChanged("ColorStreamImageSource"); 14 } 15 } 16 17 private void InitColorStream() 18 { 19 this.KinectRuntime.VideoFrameReady += KinectRuntime_VideoFrameReady; 20 this.KinectRuntime.VideoStream.Open(ImageStreamType.Video, 21 2, 22 ImageResolution.Resolution640x480, 23 ImageType.Color); 24 } 25 26 private void KinectRuntime_VideoFrameReady(object sender, ImageFrameReadyEventArgs e) 27 { 28 this.ColorStreamImageSource = e.ImageFrame.ToBitmapSource(); 29 } 30 31 #endregion 32 [...] 33 }

La partie gérant le flux vidéo couleur comprend 3 parties.

Une propriété bindable ColorStreamImageSource de type BitmapSource lie a un control Image de la MainWindow (<Image Grid.Row="1" Grid.Column="0" Source="{Binding Kinect.ColorStreamImageSource}" />).

Si la méthode Start, décrite ci-dessus, a une option RuntimeOptions.UseColor, alors la méthode InitColorStream ci-dessus sera appelée. Nous commençons par nous abonner à l’événement VideoFrameReady pour récupérer une image lorsque celle-ci est disponible. Une fois récupérer, il suffit de la passer a la méthode d’extension ToBitmapSource() (provenant du Coding4Fun Kinect Toolkit) pour créer le BitmapSource adéquat. Lorsque ce BitmapSource est assigné à la propriété ColorStreamImageSource, le databinding se déclenche pour mettre à jour l’image.

Le lancement du flux vidéo se fait grâce à la propriété VideoStream du KinectRuntime. En appelant la méthode Open avec les paramètres suivants, on lance la récupération des images.

Paramètres de la méthode Open()

Nom du parametre Type Description
streamType ImageStreamType Trois valeurs possibles sur ImageStreamType (Invalid,Video,Depth). Ce paramètre décrit le type de flux recuperer. Dans notre cas, on utilise la valeur Video.
poolSize int

Nombre d’images gardées en buffer par Kinect.

Les valeurs peuvent aller de 0 à 4.

2 est une valeur correcte pour la plupart des applications.

resolution ImageResolution

Résolution de l’image.

4 valeurs sont disponibles dans l’énumération ImageResolution, mais elles ne sont pas toutes disponibles pour tous les flux

Invalid

Resolution80x60 : capteur de profondeur uniquement. rarement utile.

Resolution320x240 : capteur de profondeur uniquement – 15 images/s.

Resolution640x480 : caméra vidéo uniquement – 30 images/s.

Resolution1280x1024 : caméra vidéo uniquement – 15 images/s – en réalité, il s’agit d’une résolution 1280×960.

En cas de valeurs erronées, la méthode Open() remonte une Exception avec le HRESULT=0x80070057

image ImageType

Type d’images/formatage des pixels de l’image.

DepthAndPlayerIndex : Formatage spécifique pour profondeur et player index (16 bits par pixel… 13 bits de profondeur/3 bits pour l’id du joueur. Nous en parlerons dans un prochain article.)

Color : Pixel couleur RGB sur 32 bits (l’ordre exact des couleurs pour chaque pixel est BGRX… 8 bits par couleur)

ColorYuv : Flux video 32 bits en YUV (voir “YUV Video” sur la MSDN pour plus d’informations)

ColorYuvRaw : Flux video 16 bits en YUV

Depth : Formatage spécifique pour la profondeur (16 bits par pixel)

 

1 public class KinectViewModel : INotifyPropertyChanged 2 { 3 [...] 4 #region Depth/PLayerIndex Stream 5 6 public BitmapSource _depthStreamImageSource; 7 public BitmapSource DepthStreamImageSource 8 { 9 get { return _depthStreamImageSource; } 10 set 11 { 12 this._depthStreamImageSource = value; 13 OnPropertyChanged("DepthStreamImageSource"); 14 } 15 } 16 17 private void InitDepthStream() 18 { 19 this.KinectRuntime.DepthFrameReady += KinectRuntime_DepthFrameReady; 20 this.KinectRuntime.DepthStream.Open(ImageStreamType.Depth, 21 2, 22 ImageResolution.Resolution320x240, 23 ImageType.Depth); 24 } 25 26 private void InitDepthAndPlayerIndexStream() 27 { 28 this.KinectRuntime.DepthFrameReady += KinectRuntime_DepthFrameReady; 29 this.KinectRuntime.DepthStream.Open(ImageStreamType.Depth, 30 2, 31 ImageResolution.Resolution320x240, 32 ImageType.DepthAndPlayerIndex); 33 } 34 35 private void KinectRuntime_DepthFrameReady(object sender, ImageFrameReadyEventArgs e) 36 { 37 this.DepthStreamImageSource = e.ImageFrame.ToBitmapSource(); 38 } 39 40 #endregion 41 [...] 42 }

On réalise la même opération pour la profondeur que pour la vidéo couleur. La seule différence résulte dans le fait que le flux vidéo peut renvoyer juste la profondeur ou la profondeur + le player Index. Chacun des deux modes s’initialisant de façon différente, on utilise deux méthodes pour lancer la récupération des images. Les deux s’abonnent au même événements et l’image, dans les deux cas, sera récupéré dans la méthode KinectRuntime_DepthFrameReady.

 

1 public class KinectViewModel : INotifyPropertyChanged 2 { 3 [...] 4 #region Skeleton tracking 5 6 private void InitSkeletonTracking() 7 { 8 throw new NotImplementedException(); 9 } 10 11 #endregion 12 13 #region Cleaning 14 15 public void Release() 16 { 17 this.KinectRuntime.Uninitialize(); 18 } 19 20 #endregion 21 22 #region INotifyPropertyChanged Interface 23 24 public event PropertyChangedEventHandler PropertyChanged; 25 public void OnPropertyChanged(string name) 26 { 27 if (this.PropertyChanged != null) 28 this.PropertyChanged(this, new PropertyChangedEventArgs(name)); 29 } 30 31 #endregion 32 33 }

La méthode InitSkeletonTracking() sera décrite dans un prochain article.

La méthode Release permet de libérer les ressources utilisées par Kinect. Pour cela, il est nécessaire, lors de la fermeture du programme ou lorsque l’on ne souhaite plus utiliser Kinect, il est nécessaire d’appeler la méthode Uninitialize() du KinectRuntime.

Code Source de l’article

Conclusion

Vous savez maintenant comment afficher des flux vidéos couleur et profondeurs provenant de Kinect ainsi que détecter si des joueurs sont visibles (dans ce cas, ils apparaissent colorés dans l’image du capteur de profondeur).

D’autres articles sont prévus sur le traitement d’images ainsi que le tracking des joueurs.

kinectappliTest

Exemple de l’application de test Kinect finale avec vidéo couleur (haut gauche)/profondeur (bas gauche)/traitement d’images (Détecteur de contour Sobel en haut à droite)/tracking de joueurs en bas à droite).

Un grand merci aux deux modèles (Gilles Peron/@GillesPeron et Kim Macrez). On se souviendra de votre sacrifice Smile.

28/01/2012

[Kinect SDK] Presentation de Kinect SDK

Filed under: C#, C++, Débutant, Jeux vidéos, Kinect — Tags:, , , , , , , — sebastiencourtois @ 08:44

Cet article est le premier d’une série d’articles sur le SDK de Kinect qui sort le 1er février. Ce SDK pour PC permet de dialoguer avec Kinect en .NET ainsi qu’en C++.

Au travers de cette série d’articles, vous apprendrez à récupérer les informations que fournissent les différents capteurs Kinect. Ce post d’introduction va présenter les différentes possibilités du SDK.

 

1. Kinect : Le Matériel

kinect-hardware

Kinect est composé de :

* une camera video RGB : Il s’agit d’une caméra couleur d’une résolution 640×480 (30 images/seconde) ou 1280×960 (15 images/seconde).

* une caméra de profondeur : Il s’agit d’un laser infrarouge (capteur de gauche) qui balaie la pièce de rayon infrarouge. Ces rayons sont visualisés par un capteur infrarouge (capteur de droite). La sensibilité de ce capteur s’ajuste automatiquement en fonction des conditions de la pièce (luminosité, obstacles…) Ce capteur de profondeur a une résolution de 320×240 ou 80×60 (30 images par secondes).

* 4 microphones chargés d’enregistrer le son et de réaliser des opérations de traitements du signal afin de supprimer les parasites (bruits extérieurs, éclos…). Ce système de microphone réparti sur l’ensemble du capteur permet de détecter la direction du son et ainsi reconnaitre le joueur qui a parlé.

* Un moteur pour incliner verticalement Kinect. Kinect peut aller de –27 degrés à + 27 degrés d’inclinaison. Cela permet au développeur d’adapter la vue si elle n’est pas adéquate. (Par exemple, si on voit le corps d’une personne, mais pas sa tête, le développeur peut incliner Kinect vers le haut afin d’avoir le corps en entier.)

 

2. Le SDK et la partie logicielle de Kinect

Le SDK Kinect est disponible sur le site : Kinect For Windows.

En plus des fonctions de bases de Kinect, le SDK embarque aussi :

* Le tracking de personnes (Skeleton Tracking) : Kinect peut indiquer si elle reconnait une personne et permet de tracker 20 points du corps en temps réel.

* En plus de la récupération d’un son sans parasites, le SDK fournit des aides pour la reconnaissance de la parole basée sur l’API Windows Speech (aussi utilise dans Windows Vista et 7 pour la reconnaissance de la parole).

kinect-voicelocator

Kinect Voice Locator : Un des samples du SDK

3. Les caractéristiques techniques et limitations de Kinect

Pour la caméra vidéo, Kinect fournit des images de 640×480 (30 images/seconde) ou 1280×960 (15images/seconde). Chaque pixel est codé en BGR sur 32 bits.

Pour le capteur de profondeur, Kinect fournit des images en 320×240 (30 images/seconde). Les données du capteur de profondeur sont codées sur 13 bits. Elle représente une distance en millimètre allant de 800 mm à 4000 mm. Lorsque le tracking des personnes est actif, les données de reconnaissance des personnes sont fusionnées avec les données de profondeur. Au final, chaque pixel correspond à 16 bits (13 pour la profondeur et 3 pour l’identification d’une personne). Il est possible de tracker 2 personnes en temps réel + 4 autres personnes « passives » (les personnes sont détectes, mais il n’y a pas de tracking de leur squelette).

L’une des difficultés est que les images venant de la caméra vidéo et du capteur de profondeur ne sont pas synchronisées par le SDK. Si le développeur souhaite faire du traitement d’images en utilisant les deux capteurs, il devra réaliser lui même la synchronisation. Cela peut s’avérer difficile, car les framerates entre les capteurs peuvent être différents et ne sont pas toujours constants.

Le moteur d’inclinaison de Kinect possède aussi ses limitations. En dehors de sa plage de valeurs (allant de –27 dégrées a +27 dégrées d’inclinaison verticale), il n’est pas possible d’envoyer des ordres a Kinect plus d’une fois par secondes ou plus de quinze fois toutes les 20 secondes. Cela est du a des limitations matériel, car le moteur n’est pas conçu pour bouger constamment. De plus, le fait de bouger la caméra prend du temps et peut perturber les capteurs Kinect.

La reconnaissance de la parole est entièrement basée sur des grammaires que le développeur doit fournir. Les grammaires correspondantes aux mots à reconnaitre. Cela peut être défini dans un fichier XML ou directement dans le code.

 

4. Conclusion

Nous avons fait un petit tour du propriétaire du composant Kinect et de ses possibilités. Dans les prochains articles Kinect, nous verrons comment récupérer les différentes informations (flux vidéo & profondeur, détections et tracking de joueurs…) et comment les utiliser (traitement d’images, dessin des squelettes…).

01/09/2011

[XNA 4] Tutoriel 6 : Sprites animés (Partie 1 : Une animation / frame size fixe)

Filed under: .NET, C#, Débutant, XNA — Tags:, , , , , — sebastiencourtois @ 21:27

Lors d’un tutoriel précédent, nous avons vu comment  afficher et déplacer des images. Nous allons maintenant voir comment afficher des animations pré-dessinées.

Tout commence par un fichier image (aussi appelé spritesheet) contenant l’ensemble des étapes de l’animation.

Jump

Exemple d’un personnage sautant (source : XNA App Hub )

explosion

Exemple d’animation d’explosion

Comme on peut le voir, l’animation est décompose en plusieurs étapes et il suffit de couper chacune des étapes (frames) puis de les afficher dans l’ordre a intervalle de temps régulier pour reproduire l’animation. Les frames sont de la même taille dans les exemples de ce post (ce ne sera pas toujours le cas comme nous le verrons dans des  prochains posts).

  • Présentation du projet de démonstration

Pour ce tutoriel, nous partirons d’un nouveau projet XNA 4 (Windows Game) auquel on va ajouter un autre projet utilitaire (Windows Game Library) nomme .Utils. Ce projet contiendra l’ensemble des classes nécessaires au chargement et a l’utilisation des images animes. Ce projet contiendra deux classes : SimpleAnimationDefinition et SimpleAnimationSprite.

Dans le même temps, on ajoute les différentes images (spritesheets) dans le projet Content de la solution. Vous pouvez trouver ces images ici : Tutoriel6a-SimpleAnimationContent.

Si tout se passe bien, votre solution devrait ressembler à ceci.

xnaT6a-projects

  • Définition d’une animation : Classe SimpleAnimationDefinition

Cet classe va permettre de paramétrer l’application en fournissant les informations nécessaires au gestion d’animation.

public class SimpleAnimationDefinition {
    public string AssetName { get; set; }
    public Point FrameSize  { get; set; }
    public Point NbFrames   { get; set; }
    public int FrameRate    { get; set; }
    public bool Loop        { get; set; }
}

La propriété AssetName indique le “mot clé” (AssetName défini dans le projet Content) de l’image contenant l’animation. Cela permettra de charger l’image en utilisant Content.Load<Texture2D>(…).

La propriété FrameSize définie la taille d’une frame de l’animation. Il faut souvent utiliser un logiciel de dessin type Paint pour connaitre la taille des frames (dans le cas du personnage qui saute, chaque frame fait 64 pixels sur 64 pixels. Pour l’explosion : 256 pixels sur 128 pixels). Nous verrons dans des posts suivants, comment gérer des animations dont les frames sont de tailles différentes.

La propriété FrameSize définie le nombre de frame de l’animation. Dans le cas du personnage, l’animation est compose de 11 frames. Pour cette propriété, on utilise un structure Point car certaines animations peuvent avoir des frames les uns en dessous des autres (comme c’est le cas pour l’explosion ou il y a 3 colonnes et 4 lignes de frames).

La propriété FrameRate définie la vitesse de l’animation. Par défaut, l’animation d’une application XNA est limite a 60 images par secondes mais nous verrons que, selon les fichiers, il est nécessaire de changer ce nombre d’images par secondes. Si on prend l’exemple du personnage contenant 11 frames, si l’on conserve un framerate de 60 images par secondes, l’animation complète prendra environ 16 ms.

La propriete Loop indique si l’on souhaite arrêter l’animation à la fin de celle-ci ou si l’on souhaite recommencer l’animation du début automatiquement.

new SimpleAnimationDefinition()
{
     AssetName = "explosion",
     FrameRate = 20,
     FrameSize = new Point(256,128),
     Loop = true,
     NbFrames = new Point(3,4)
}

Exemple de la configuration de l’animation explosion

  • Gestionnaire d’animation : Classe SimpleAnimationSprite

Cette classe va s’occuper de gérer une animation (charger l’image, initialiser les données, afficher les images …). Il sera initialisé grâce a un SimpleAnimationDefinition.

  • Proprietes de la classe :
public Point Position;
protected Game Game;
protected SimpleAnimationDefinition Definition;
protected SpriteBatch spriteBatch;
protected Texture2D sprite;
protected Point CurrentFrame;
protected bool FinishedAnimation = false;
protected double TimeBetweenFrame = 16; // 60 fps 
protected double lastFrameUpdatedTime = 0;

Position : Position de l’image dans l’écran

Game : Instance vers l’instance de la classe Game (utile pour charger l’image, créer un spriteBatch, avoir accès au GraphicsDevice…).

Definition : Définition de l’animation (voir paragraphe précédent)

spriteBatch : instance nécessaire pour afficher des images a l’écran (voir tutoriel 2).

sprite : Image

CurrentFrame : Position de la frame en cours d’affichage. Ce propriété sera utilisé pour calculer la partie de l’image/animation à afficher à l’écran.

FinishedAnimation : Indique si l’animation est terminé (cas d’une animation non boucle).

lastFrameUpdatedTime : Nombre de millisecondes depuis le dernier changement de frame

TimeBetweenFrame : Temps entre deux changements de frame (calculé grâce au Framerate).

Note : J’ai aussi rajouté deux propriétés pour gérer dynamiquement le framerate de l’animation. La première propriété ( private int _Framerate) contient juste la valeur du framerate. La deuxième est une propriété plus complexe défini comme suit :

private int _Framerate = 60;
public int Framerate
{
    get { return this._Framerate; }
    set {
       if (value <= 0)
           throw new ArgumentOutOfRangeException("Framerate can't be less or equal to 0");
       if (this._Framerate != value)
       {
          this._Framerate = value;
          this.TimeBetweenFrame = 1000.0d / (double)this._Framerate;
       }
   }
}

Cette propriété renvoie le framerate de l’animation mais permet aussi de définir ce framerate. Si on souhaite modifier ce framerate, la propriété va vérifier la valeur et modifier la propriété TimeBetweenFrame en consequence. J’utilise cette astuce afin d’éviter de faire la division 1000/Framerate a chaque Update pour connaitre d’éviter de temps entre deux frames.

  • Méthodes de la classe :

Voici le squelette de la classe.

public class SimpleAnimationSprite {
    /* Proprietes */ public SimpleAnimationSprite(Game game, SimpleAnimationDefinition definition)
    {
        /* Constructeur */ }

    public void Initialize()
    {
        /* Initialisation */ }

    public void LoadContent(SpriteBatch spritebatch = null)
    {
        /* Chargements des donnees */ }

    public void Reset()
    {
        /* Reinitialisation de l'animation */ }

    public void Update(GameTime time)
    {
        /* Mise a jour des donnees en vue de l'affichage */ }

    public void Draw(GameTime time, bool DoBeginEnd = true)
    {
        /* Affichage de l'animation */ }
}

Remarque : J’ai choisi de conserver une architecture proche de XNA pour construire la classe (Initialize,LoadContent,Update,Draw) afin de faciliter l’utilisation de celle-ci. Toutefois, certaines méthodes sont peut-être inutiles dans ce cas mais je pense que c’est une bonne manière de concevoir des classes réutilisables XNA. Je ne dérive pas de GameComponent car je veux avoir la liberté d’appeler mes classes comme je le souhaite (j’ai aussi lu des problèmes de performances possibles avec les GameComponent donc je privilégie la liberté d’utilisation et la performance).

Nous allons voir l’initialisation de la classe :

public SimpleAnimationSprite(Game game, SimpleAnimationDefinition definition)
{
    this.Game = game;
    this.Definition = definition;
    this.Position = new Point();
    this.CurrentFrame = new Point();
}

public void Initialize()
{
    this.Framerate = this.Definition.FrameRate;
}

public void Reset()
{
    this.CurrentFrame = new Point();
    this.FinishedAnimation = false;
    this.lastFrameUpdatedTime = 0;
}

L’idée est de définir le framerate et de déterminer la prochaine image à afficher comme étant la première du spritesheet (new Point() <=> X = 0,Y = 0) et en initialisant le framerate en fonction de la structure de définition.

public void LoadContent(SpriteBatch spritebatch = null)
{
    this.sprite = this.Game.Content.Load<Texture2D>(this.Definition.AssetName);
    if (spritebatch == null)
        this.spriteBatch = new SpriteBatch(this.Game.GraphicsDevice);
    else
        this.spriteBatch = spritebatch;
}

La méthode est assez triviale. On charge le spritesheet en tant que Texture2d et récupère ou crée  le SpriteBatch pour l’afficher dans la methode Draw. La raison pour laquelle j’autorise la création d’un SprtieBatch en dehors de la classe est par soucis d’optimisation et de réutilisation des ressources.

Les deux méthodes les plus intéressantes sont Update (qui va decider quel est l’image du spritesheet a afficher) et Draw pour l’afficher.

public void Update(GameTime time)
{
    if (FinishedAnimation) return;
    this.lastFrameUpdatedTime += time.ElapsedGameTime.Milliseconds;
    if (this.lastFrameUpdatedTime > this.TimeBetweenFrame)
    {
        this.lastFrameUpdatedTime = 0;
        if (this.Definition.Loop)
        {
            this.CurrentFrame.X++;
            if (this.CurrentFrame.X >= this.Definition.NbFrames.X)
            {
                this.CurrentFrame.X = 0;
                this.CurrentFrame.Y++;
                if (this.CurrentFrame.Y >= this.Definition.NbFrames.Y)
                    this.CurrentFrame.Y = 0;
            }
        }
        else
        {
            this.CurrentFrame.X++;
            if (this.CurrentFrame.X >= this.Definition.NbFrames.X)
            {
                this.CurrentFrame.X = 0;
                this.CurrentFrame.Y++;
                if (this.CurrentFrame.Y >= this.Definition.NbFrames.Y)
                {
                    this.CurrentFrame.X = this.Definition.NbFrames.X - 1;
                    this.CurrentFrame.Y = this.Definition.NbFrames.Y - 1;
                    this.FinishedAnimation = true;
                }
            }
        }
    }
}

Le choix de l’image à afficher se fait en utilisant le timer de XNA. On regarde si le temps depuis l’affichage de la dernière frame de l’animation est plus grand que le temps entre les frames (défini par le framerate). Si c’est le cas, on passe au frame de l’animation suivant. Le code parait complexe car on prend en compte les images “Carre” (i.e quand les frames sont stockes sur plusieurs lignes). Si l’on souhaite que l’animation tourne en boucle, on revient au premier frame une fois que l’animation est terminée.

public void Draw(GameTime time, bool DoBeginEnd = true)
{
    if(DoBeginEnd) this.spriteBatch.Begin();

    this.spriteBatch.Draw(this.sprite,
                          new Rectangle(this.Position.X, this.Position.Y, this.Definition.FrameSize.X, this.Definition.FrameSize.Y),
                          new Rectangle(this.CurrentFrame.X * this.Definition.FrameSize.X, this.CurrentFrame.Y * this.Definition.FrameSize.Y, this.Definition.FrameSize.X, this.Definition.FrameSize.Y),
                          Color.White);

    if (DoBeginEnd) this.spriteBatch.End();
}

L’affichage est relativement simple et se résumer a l’affichage d’une texture 2d par un spritebatch. La différence avec les tutoriaux précédents est que nous allons utiliser uniquement une partie de la texture (alors que d’habitude, on utilise l’ensemble de la texture). Pour cela, on utilise une méthode surchargée de Draw du SpriteBatch : SpriteBatch.Draw (Texture2D texture, Rectangle destinationRectangle, Nullable<Rectangle> sourceRectangle, Color color ). Ainsi, on joue sur le SourceRectangle pour choisir la partie de la texture que l’on souhaite afficher. Le rectangle de destination dépend de la position (point haut gauche) et de la taille de la frame.

Remarque : Le DoBeginEnd est un paramètre d’optimisation afin d’éviter d’utiliser la méthode Begin/End de multiple fois (==> perte de performances). Si DoBeginEnd = false, alors la SpriteBatch a déjà été Begin dans la méthode Draw de Game et ne doit pas être réouvert de nouveau. Cela permet de mutualiser les appels aux GPU (rappel : SpriteBatch est un Vertex/Pixel Shader).

  • Utilisation des animations dans le jeu

Nous allons maintenant utiliser la classe SimpleAnimationSprite dans la classe Game. On commence par ajouter la propriété qui va stocker les SimpleAnimationSprite. Notre exemple affichera 4 animationsmême temps donc celles-ci seront stockées dans un tableau.

public class Game1 : Microsoft.Xna.Framework.Game
{
   GraphicsDeviceManager graphics;
   SpriteBatch spriteBatch;
   SimpleAnimationSprite[] AnimManSprites;
   ....
}

La methode Initialize va creer et configurer les differents animations en utilisant les structures SimpleAnimationDefinition.

protected override void Initialize()
{
    this.AnimManSprites = new SimpleAnimationSprite[4];
    this.AnimManSprites[0] = new SimpleAnimationSprite(this, new SimpleAnimationDefinition()
    {
        AssetName = "Celebrate",
        FrameRate = 15,
        FrameSize = new Point(64, 64),
        Loop = true,
        NbFrames = new Point(11, 1)
    });
    this.AnimManSprites[0].Position.X = 250;
    this.AnimManSprites[0].Position.Y = 50;

    this.AnimManSprites[1] = new SimpleAnimationSprite(this, new SimpleAnimationDefinition()
    {
        AssetName = "Jump",
        FrameRate = 15,
        FrameSize = new Point(64, 64),
        Loop = true,
        NbFrames = new Point(11, 1)
    });
    this.AnimManSprites[1].Position.X = 300;
    this.AnimManSprites[1].Position.Y = 50;

    this.AnimManSprites[2] = new SimpleAnimationSprite(this, new SimpleAnimationDefinition()
    {
        AssetName = "Run",
        FrameRate = 15,
        FrameSize = new Point(64, 64),
        Loop = true,
        NbFrames = new Point(10, 1)
    });
    this.AnimManSprites[2].Position.X = 350;
    this.AnimManSprites[2].Position.Y = 50;

    this.AnimManSprites[3] = new SimpleAnimationSprite(this, new SimpleAnimationDefinition()
    {
        AssetName = "explosion",
        FrameRate = 20,
        FrameSize = new Point(256, 128),
        Loop = true,
        NbFrames = new Point(3, 4)
    });

    foreach (SimpleAnimationSprite anim in this.AnimManSprites)
        anim.Initialize();

    base.Initialize();
}

Remarque : Les valeurs des FrameSize et NbFrames dépendent des textures utilisées.

protected override void LoadContent()
{
    spriteBatch = new SpriteBatch(GraphicsDevice);
    foreach (SimpleAnimationSprite anim in this.AnimManSprites)
        anim.LoadContent(spriteBatch);
}

protected override void Update(GameTime gameTime)
{
    if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
        this.Exit();
    foreach (SimpleAnimationSprite anim in this.AnimManSprites)
        anim.Update(gameTime);
    base.Update(gameTime);
}

protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.CornflowerBlue);
    this.spriteBatch.Begin();
    foreach (SimpleAnimationSprite anim in this.AnimManSprites)
        anim.Draw(gameTime,false);
    this.spriteBatch.End();
    base.Draw(gameTime);
}

L’architecture de la classe SimpleAnimationSprite permet d’avoir des méthodes LoadContent,Update,Draw relativement simple. L’ensemble du code est fait dans Initialize (appelée une seule fois) pour réduire le code des méthodes Update/Draw (appelées 60 fois par secondes au moins). En jeu vidéo, tout est affaire d’optimisation.

Tutorial6aFinal

Résultat a la fin de ce tutorial (anime normalement Smile with tongue out)

  • Conclusion

Vous pouvez maintenant afficher et animer des images en utilisant des spritesheets dont  les frames ont la même taille. Nous verrons plus tard des cas plus complexe avec des spritesheet comme celle de Prince Of Persia 2.

Projet complet (Tutorial 6a)

16/05/2011

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

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.

11/12/2010

[XNA 4] Tutoriel 3D 4 : Modèles 3D et gestion de la lumière

Filed under: .NET, 3D, C#, Débutant, XBOX 360, XNA — Tags:, , , , , , — sebastiencourtois @ 01:20

Lors du dernier article sur la 3D dans XNA, nous avons vu comment charger des données graphiques pour les afficher (exemple du cube). Rentrer l’ensemble des coordonnées des points à la main est fastidieux rien que pour dessiner un cube alors je vous laisse imaginer pour une maison ou un personnage. C’est pourquoi il existe des modeleurs 3D permettant de dessiner les modèles 3D puis de générer des fichiers contenant l’ensemble des données pré-formatés pour être utilisable par la carte graphique.

Il existe de nombreux modeleurs 3D. L’un des plus utilisé est Blender. Il offre une bonne partie des fonctionnalités que propose les modeleurs pro come 3DSMAX ou Maya et il est gratuit. Son seul inconvénient est d’être parfois complexe à utiliser (beaucoup de raccourcis clavier à connaitre).

Les modeleurs 3D peuvent générer des fichiers contenant les informations 3D de la scène dessiné (position des vertex, indices, triangles, textures ….). Il existe un très grand nombre de format dans l’univers de la 3D (certains éditeurs de jeu crée parfois leur propre format pour un moteur de jeu défini). XNA prend en charge nativement au moins deux formats : le .X (format DirectX) et le format .fbx (Format Autodesk).

Dans ce tutoriel, nous utiliserons 3 modèles : Une sphère exporté depuis Blender (fait par moi même), un personnage texturé (venant du SDK DirectX) et un tank en 3D venant des démos XNA disponible sur le site officiel.

  • Chargement d’un modèle 3D

La première étape est de charger les modèles ainsi que leurs textures dans le projet ‘Content’ de la solution.

blog_model_1

(sphere.fbx correspond à la sphère / tiny.x et tiny_skin.dds correspond au personnage et tank.fbx,engine_diff_tex.tga et turret_alt_diff_tex.tga correspondent au tank)

Ensuite il suffit de charger les données dans le ContentManager de XNA. Un modèle 3D se charge dans la classe Model. Le chargement se réalise grâce à la méthode Load du ContentManager.

Model sphere;
Model tiny;
Model tank;

protected override void LoadContent()
{
    spriteBatch = new SpriteBatch(GraphicsDevice);

    this.sphere = Content.Load<Model>("sphere");
    this.tiny = Content.Load<Model>("tiny");
    this.tank = Content.Load<Model>("tank");

    this.font = Content.Load<SpriteFont>("font");
}
  • Dessin d’un modèle 3D

Une fois chargé, on souhaite afficher le modèle. Cela se réalise dans la méthode Draw de la classe Game. On peut faire afficher un modèle de plusieurs manières.

Tout d’abord, on peut afficher le modèle dans sa totalité en appelant la méthode Draw du Model :

this.sphere.Draw(Matrix.Identity, this.View, this.Projection);

La méthode Draw demande 3 paramètres correspondant aux 3 matrices principales que l’on à déjà vu dans les tutoriaux précédent (World, View, Projection). La plus importante des trois est la première (World) car elle permet d’indiquer la position, l’orientation et la taille de l’objet à afficher et varie donc généralement d’un modèle à l’autre.

Une autre méthode revient à afficher le modèle morceaux par morceaux. En effet, si vous regarder la classe Model, elle est composé de plusieurs ModelMesh eux mêmes composé de MeshPart. Dans chacun de ces objets se trouve un objet Effect (BasicEffect la plupart du temps) qui va interagir avec ces données. On peut donc afficher un modèle de la façon suivante :

foreach (ModelMesh mesh in this.tank.Meshes)
    foreach (BasicEffect effect in mesh.Effects)
    {
        effect.World = Matrix.Identity;
        effect.View = this.View;
        effect.Projection = this.Projection;
        mesh.Draw();
    }

Ce moyen peut paraitre plus long et compliqué mais il peut avoir son utilité notamment lors des animations de modèles que nous verrons dans un prochain tutoriel.

blog_model_2

  • La lumière : Les bases

Maintenant que vous savez charger des modèles 3D, nous allons nous intéresser à la lumière.

Si on se réfère à la définition physique Wikipedia, la lumière est une onde électromagnétique visible par l’œil humain. Sa longueur d’onde va de 380nm (violet) à 780 nm (rouge). La lumière a généralement une source qui peut être plus ou moins éloigné des objets éclairés. Lorsque la lumière part de cette source, elle se propage dans toutes les directions. Lorsqu’elle touche un objet, cet objet absorbe une partie des longueurs d’onde  et réfléchi le reste. La lumière réfléchie arrive dans notre œil. Si l’on regarde une balle rouge, cela veut dire que la balle a absorbé toute les autres longueurs d’onde que le rouge.

De plus chaque face d’un objet à une normale. Il s’agit d’un vecteur perpendiculaire à la  surface. Lorsqu’un rayon lumineux “touche” une face d’un objet selon un angle (angle d’incidence) par rapport à la normale de cette face, ce rayon est réfléchi avec le même angle à l’opposé de cette normale.

Afin d’avoir des rendus lumineux réalistes, XNA (et les API 3D en général) s’inspire très fortement des phénomènes physiques décrits ci-dessus.

  • Gestion de la lumière dans XNA : Pratique

Après ces rappels physiques, nous allons maintenant passer à l’utilisation de la lumière en XNA. Pour faire simple, nous utiliserons les lumières disponibles au travers de l’effet BasicEffect fourni par XNA. Les lumières sont gérés dans les effets car ce sont des opérations parfois couteuses en temps et que les effets sont exécutés par le GPU (donc plus rapidement). Nous verrons dans les tutoriaux sur les shaders comment créer nos propres types de lumières.

Par défaut, les lumières sont désactivés. On voit alors les modèles selon leurs couleurs originales (textures/couleurs définis directement dans le modèle). Pour l’activer, il suffit de passer la propriété LightingEnabled de BasicEffect à true.

blog_model_3

Notre modèle apparait alors en noir. Cela est normal car, bien que l’objet soit texturé, aucun rayon lumineux ne se réfléchi sur le tank donc notre camera (notre œil virtuel) ne reçoit aucune lumière donc il voit l’objet en noir.

  • Nous allons activer une première composante lumineuse : La composante ambiante.

Cette lumière est une lumière sans source lumineuse défini et dont la lumière est présente partout. Pour faire une analogie, on pourrait comparer cela à un jour nuageux au travers desquels ont ne voit pas la lumière. La lumière est pourtant présente car on parvient à voir les objets nous entourant. Par défaut, la lumière d’ambiante est noire. Il est possible de changer cette valeur grâce à la propriété AmbientLightColor.

blog_model_ambient

En terme de calcul, cela est très rapide car il suffit de rajouter la couleur ambiante sur les pixels du modèle sans tenir compte “des données physiques “(normales notamment). Le soucis est que cela rend le modèle plat.

  • Deuxième type de composante lumineuse: La composante diffuse

A l’instar de la lumière ambiante, la lumière diffuse ne provient d’aucune source définie. Toutefois, le calcul de la lumière prend en compte les normales de chacune des faces. Ainsi l’affichage fera apparaitre des ombres selon l’angle sous lequel l’objet est regardé. Il est possible de modifier la couleur de la lumière diffuse avec la propriété DiffuseColor de BasicEffect.

blog_model_diffuse

  • Troisième type de composante lumineuse : La composante spéculaire

La lumière spéculaire est le léger reflet que l’on voit dans un objet réfléchissant. Elle dépend de la position de la caméra, de la source lumineuse et différents vecteurs incidences/réflexions de la lumière. Il est possible de définir la force de la tache spéculaire ainsi que sa couleur avec les propriétés SpecularPower et SpecularColor.

blog_model_specular

Ici, la lumière est blanche (diffuse) et la lumière spéculaire est jaune.

  • Dernier type de composante lumineuse : La composante émissive

Cette composante va simuler le fait que le modèle émettent de la lumière. On peut définir la couleur d’émission avec la propriété EmissiveColor de BasicEffect.

blog_model_emissive

Exemple avec une lumière émissive bleu.

Si vous voulez plus d’informations sur les lumières en 3D, je vous conseille cet article.

  • Une lumière réaliste : La lumière directionnelle

BasicEffect permet la création de 3 lumières directionnelle. Une lumière directionnelle n’a pas de source et n’est défini que par ses composants et un vecteur directeur. On peut envisager cela comme étant une source lumineuse se trouvant à l’infini dont l’ensemble des rayons arrivent sur l’objet parallèle les uns aux autres selon le vecteur directeur.

blog_model_noDL blog_model_DL
Modèle sans lumières Modèle avec une lumière directionnelle blanche.

Le calcul de la lumière se faisant en fonction du vecteur directeur et des différentes normales des surfaces, on obtient un rendu plus réalistes avec des ombres et des tâches spéculaires.

Les propriétés des lumières directionnelles sont disponible avec la propriétés DirectionalLightX (avec X étant 0, 1 ou 2).

Exemple :

ef.DirectionalLight0.Enabled = DirectLightOn;
ef.DirectionalLight0.Direction = this.DirectLightPosition;
ef.DirectionalLight0.DiffuseColor = this.DirectLightColor.ToVector3();
ef.DirectionalLight0.SpecularColor = this.SpecularLight.ToVector3();
  • Méthode de calcul de la lumière : Par vertex ou par Pixel

Il est possible de calculer la lumière de deux façons. En calculant la lumière en fonction de chacun des vertex ou en faisant la calcul pour chacun des pixels. En général, le rendu par pixel est plus réaliste mais parfois plus lent que le traitement par vertex. Cela dépend de la définition des modèles. Plus il y aura de vertex sur le modèle, plus la qualité du traitement par vertex sera lourd et réaliste.

blog_model_noDL2 blog_model_DL2 blog_model_DL2_PL
Modèle normal Lumière calculée par vertex Lumière calculé par pixel
  • Brouillard

BasicEffect permet aussi de faire des sortes de brouillard. Les brouillards sont souvent des astuces dans les jeux vidéos pour réduire la profondeur de champ et de ne pas à avoir à afficher les objets 3D loin de la caméra. Le brouillard comporte 4 propriétés. La propriété FogEnabled permet d’activer ou désactiver le brouillard. FogColor permet de définir la couleur du brouillard. On peut définir la distance où commence et ou se termine le brouillard. Les propriétés  FogStart et FogEnd sont des valeurs numériques définissant la distance par rapport à la caméra.

blog_model_fog

  • La démo

J’ai réalisé une démo afin de pouvoir réaliser l’ensemble des screenshots pour ce tutoriel. Il est pilotable au clavier et permet de modifier l’ensemble des propriétés présentées ici.

La démo est disponible ici (11 Mo)

Raccourcis clavier :

Flèches directionnelles : Déplace la caméra autour de l’objet

Page Up/Page Down: Zoom +/-

A /Z/E: Changer le modèle (A : Sphère / Z : Personnage / E : Tank)

Q : Activation/Désactivation des lumières

S : Change le mode de calcul de la lumière (Vertex/Pixel)

D : Activation/Désactivation de la lumière directionnelle 0

Q : Change la couleur de la composante ambiante

H : Change la couleur de la composante diffuse

J : Change la couleur de la composante d’émission

R : Change la couleur de la composante spéculaire

T/Y : Augmente ou réduit la force de la composante spéculaire

W : Activation/Désactivation du brouillard

X : Change la couleur du brouillard

C/V : Change la valeur FogStart

B/N : Change la valeur de FogEnd

K/L/M/O : Déplacement de la lumière directionnelle 0 autour de l’objet

  • Conclusion

Nous avons fait un bon tour sur les lumières. Nous avons aussi exploré BasicEffect, l’effet de base fourni par XNA. Nous verrons, dans le prochain tutoriel, comment créer son propre effet en HLSL ainsi que la façon dont on peut réaliser des animations sur des modèles chargés dans XNA.

26/11/2010

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

Filed under: 3D, C++, Débutant, DirectX, Intermediaire — Tags:, , , , , — 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.

08/11/2010

[DirectX 10/11] Tutoriel 1 : Création d’une application Win32

Filed under: 3D, C++, Débutant, DirectX, Win32 — Tags:, , , , , , — sebastiencourtois @ 00:09

J’ai fait une petite coupure avec ce blog compte tenu des préparatifs de mon départ aux USA et de l’avancement de certains projets persos. Au passage, je voulais souligner la création d’un nouveau blog d’un ex-futur-collègue qui vient de rejoindre la communauté des blogueurs .NET : Gilles Peron.

J’ai aussi décidé de me lancer dans une nouvelle série d’articles sur la programmation en parallèle de celle sur XNA. Le sujet est donc DirectX 10/11. L’une des grosses nouveautés est que nous allons faire ça à l’ancienne : en C++ (oui j’ai dû réapprendre à faire du C++ pour vous Sourire).

  • Introduction à DirectX

DirectX est une boite à outil pour la création de jeu vidéo fournie par Microsoft et fonctionnant principalement sur Windows. DirectX est composé de nombreux catégories chacune étant liée à une partie précise d’un jeu (Direct3D pour le graphisme 3D, DirectInput pour la gestion clavier/souris/manette, DirectPlay pour le réseau …).

Nous en sommes aujourd’hui à la onzième version de DirectX (sorti en 2009). Toutefois, DirectX 11 ajoute des nouvelles fonctionnalités à DirectX 10 qui était une évolution majeure de l’API. C’est pourquoi nous étudierons les deux versions 10 et 11.


  • Installation et configuration du kit de développement DirectX

Pour développer en DirectX, il faut :

    • Un environnement de développement C++
    • Le kit de développement Direct X : Page MSDN DirectX SDK.

Une fois le SDK installé, il suffit de créer  un projet C++ vide et d’ajouter les dossiers includes et libs du DirectX SDK (par défaut dans c:\Program Files\) au projet.

blog_dx_t1_0 blog_dx_t1_1 blog_dx_t1_2
Création d’un nouveau projet C++ vide Ajout du répertoire libs DirectX au projet Ajout du répertoire include DirectX au projet
  • La base d’une application DirectX : La fenêtre

Vous êtes maintenant prêt développer en DirectX. Pour cela, il est nécessaire d’avoir une fenêtre dans laquelle nous allons afficher les informations de notre jeu. Pour cela, plusieurs choix sont possibles. Dans ce tutoriel, je ferais ce que recommande Microsoft dans son tutoriel, l’utilisation de l’API Win32. Cette API complexe existe depuis que Windows existe et gère tout ce qui est fenêtre et gestion des entrées/sortie. D’autres méthodes de gestion de fenêtres plus simple existent (notamment QT) mais elles ne sont pas toutes compatible avec DirectX 10/11.

L’objectif de ce tutoriel va donc être de créer une fenêtre que l’on pourra fermer en appuyant sur la croix de la barre de titre ou en appuyant sur Echap.

Remarque : L’API Win32 et DirectX sont des API distinctes. Win32 est une API qu’il n’est pas spécialement nécessaire de maitriser parfaitement si votre objectif est uniquement de faire de la 3D ou du jeu vidéo. Dans le texte qui suit, je vais essayer de décrire au maximum chacune des propriétés. Il n’est pas nécessaire de mémoriser toute ces informations (ou alors uniquement pour votre culture général).

  • Le point d’entrée de notre programme : WinMain

Tout programme commence toujours par un point d’entrée. En C/C++ classique, ce point d’entrée est écrit ainsi :

#include <stdio.h>
int main(int argc,char **argv)
{
    printf("Hello World !!");
    return 0;
}

La même chose, en Win32, s’écrit de la façon suivante :

#include <stdio.h>
#include <Windows.h>
int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow )
{
    MessageBox(NULL,"Hello World","Titre",MB_ICONERROR);
    return 0;
}

Au niveau des changements entre les deux codes, on voit tout d’abord l’ajout d’un fichier windows.h. Ce fichier contient les fonctions principales de l’API Win32.

La signature du point d’entrée est plus complexe. Cette méthode retourne un nombre au système d’exploitation. La valeur 0 correspond à un programme qui s’est terminé correctement. Le mot APIENTRY signifie que la méthode est une méthode qui peut être appelé (CALLBACK). Le nom de la méthode WinMain est le nom du point d’entrée.Ce nom est défini dans windows.h et est obligatoire pour être reconnu comme un point d’entrée valide. Les deux paramètres suivants correspondent aux instances du programme. La première est l’instance pour le programme que le WinMain va lancer. La deuxième correspond à l’instance du programme qui appel ce WinMain. Dans notre cas, hPrevInstance sera NULL car nous lançons directement l’application (nous ne passons pas par un autre programme pour la lancer.). Le paramètre lpCmdLine correspond aux paramètres qui sont ajouté en arguement au lancement de l’application (équivalent du paramètre argv d’un main C/C++ classique).Pour récupérer la commande complète, vous pouvez aussi utiliser la méthode GetCommandLine. Enfin le dernier paramètre indique comment la fenêtre devra être affiché à son lancement (minimisé, plein écran …). Si vous voulez plus d’informations sur le WinMain, vous trouverez les informations voulues sur la MSDN.

Afin de tester que tout fonctionne correctement, nous allons demander l’affichage d’une boite de dialogue d’erreur. Celle ci est disponible gràce à la méthode MessageBox. Cette méthode prend en premier paramètre le handle (sorte de pointeur) sur la fenêtre qui l’a crée. Dans notre exemple, nous n’avons pas de fenêtre donc nous utiliserons la valeur NULL pour ce paramètre. Ensuite, il est nécessaire de fournir le texte puis le titre que l’on souhaite afficher. Enfin, on peut customiser les icones,boutons de la boite de dialogues avec le dernier paramètre. La liste et l’utilisation de ce paramètre est décrit en détail sur la MSDN.

Lorsque l’on lance ce morceau de code, on obtient le résultat suivant :

blog_dx_t1_3

  • Création d’une fenêtre de base

Nous allons maintenant chercher à créer une fenêtre normale. Pour cela nous allons rajouter 3 variables globales : Un Handle sur la fenêtre, deux nombre représentant la largeur et la hauteur de la fenêtre.

//Handle de la fenêtre
HWND hWnd;        
//Largeur de la fenêtre
int windowWidth = 800;    
//Hauteur de la fenêtre
int windowHeight = 600;

On modifie la méthode WinMain afin de créer et gérer un fenêtre.

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

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

Une première étape va être de créer et d’afficher la fenêtre. Cela va être fait par la méthode InitWindow que nous allons créer dans un moment.

Pour l’instant, nous allons nous concentrer sur la boucle. Windows gère ses fenêtres grâce à un système de message. Ainsi lorsqu’un ordre est donné à une fenêtre (minimiser la fenêtre, la fermer …), le système envoie un message à la fenêtre. Cette boucle regarde si des messages sont disponibles avec la méthode PeekMessage puis analyse le message et agit en conséquence (TranslateMessage) puis dispatch le message à d’autres fenêtres (les fenêtres enfants par exemples). On sort de la boucle lorsque le message de fermeture de l’application (Message WM_QUIT) est reçu par a fenêtre.

La valeur retour du WinMain dépend du wParam du dernier message. En effet, les messages peuvent aussi remonter des codes erreurs. Par conséquent, on utilise cette valeur plutôt que donner la valeur 0 en dur.

Concentrons-nous maintenant sur la création de la fenêtre proprement dite :

//Initialisation de la fenêtre Win32
bool InitWindow(HWND hWnd,HINSTANCE hInstance,int nCmdShow)
{
    //Structure d'information sur une fenêtre
    WNDCLASSEX wndStruct;
    ZeroMemory(&wndStruct,sizeof(WNDCLASSEX));
    wndStruct.cbSize        = sizeof(WNDCLASSEX);
    wndStruct.style            = CS_HREDRAW | CS_VREDRAW;
    wndStruct.lpfnWndProc    = (WNDPROC)wndProc;
    wndStruct.hInstance        = hInstance;
    wndStruct.hCursor        = LoadCursor(NULL, IDC_ARROW);
    wndStruct.hbrBackground    = (HBRUSH)COLOR_WINDOW;
    wndStruct.lpszClassName    = TEXT("TutorialWindow");
    RegisterClassEx(&wndStruct);

    hWnd = CreateWindow( "TutorialWindow", 
                         "Tutorial 1 : Win32", 
                         WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX,
                         CW_USEDEFAULT, 
                         CW_USEDEFAULT, 
                         windowWidth, 
                         windowHeight, 
                         NULL, 
                         NULL, 
                         hInstance, 
                         NULL);

    if(!hWnd) return false;

    ShowWindow(hWnd,nCmdShow);
    UpdateWindow(hWnd);

    return true;
}

Afin de créer une fenêtre, il est nécessaire de remplir une structure qui va décrire la fenêtre. Cette structure est WNDCLASSEX. Elle contient un grand nombre de propriété mais nous n’en utiliserons que 7 pour notre exemple.

cbSize Taille de la structure (en octets)
style Indique le comportement de la fenêtre. Dans notre cas, nous demandons à redessiner le contenu de la fenêtre lors qu’il y a  un changement vertical ou horizontal de la fenêtre. Les valeurs possibles sont disponibles ici.
lpfnWndProc Callback permettant de récupérer les évènements de la fenêtre (souris, clavier …). Nous allons créer cette méthode (wndProc) dans le paragraphe suivant.
hInstance Instance de l’application (celle fourni par le WinMain)
hCursor Curseur de souris lorsque celui ci passe sur la fenêtre (IDC_Arrow correspond au curseur de base).
hbrBackground Couleur de fond de la fenêtre
lpszClassName Nom pour la fenêtre. A ne pas confondre avec le titre de la fenêtre. Ce nom de fenêtre sera utilisé lors de la création de fenêtre pour identifier la structure WNDCLASSEX que l’on souhaite utiliser.

Plus d’informations sur la structure WNDCLASSEX sur la MSDN.

Une fois la structure de la fenêtre est définie, il est nécessaire de l’enregistrer dans le système afin de pouvoir la réutiliser. Cela se fait avec la méthode RegisterClassEx.

L’étape suivante est la création du handle de la fenêtre. On utilise pour cela la méthode CreateWindow. Le premier paramètre correspond au nom de la fenêtre comme décrit dans la structure WNDCLASSEX. Le deuxième paramètre est le titre de la fenêtre. Ensuite, on peut choisir le style de la fenêtre (barre de titre, bouton de fermeture …). La liste complète des possibilités se trouve ici. Les quatre paramètres suivants sont la position x,y du coin supérieur gauche de la fenêtre puis sa largeur et hauteur. La constante CW_USEDEFAULT est utilisé ici afin de laisser au système d’exploitation le choix de la position de la fenêtre. Le paramètre suivant indique si cette fenêtre doit être lié à une  autre fenêtre (ce n’est pas notre cas de figure donc NULL). Le NULL suivant correspond au handle d’un menu. Vu que nous ne souhaitons pas de menu, nous mettons une nouvelle fois NULL. Le paramètre qui suit n’est autre que l’instance à laquelle sera lié la fenêtre. Le dernier paramètre n’est pas utile dans notre cas (cas des fenêtres multiples MDI).

La description complète de cette méthode se trouve ici.

Une fois créé, on récupère un handle pour notre fenêtre. Si ce handle est NULL, c’est que la fenêtre n’a pas été créée. Dans le cas contraire, on peut l’afficher avec la méthode ShowWindow auquel on passe le paramètre nCmdShow reçu par le WinMain qui indique comment s’affiche la fenêtre (plein écran, minimisé…).

On appelle enfin la méthode UpdateWindow afin de forcer le rafraichissement de la fenêtre.

  • Réponses aux évènements de la fenêtre

Une fenêtre reçoit des évènements venant des périphériques (clavier,souris). Ce type d’évènements est à prendre en charge dans une méthode indiqué dans la propriété lpfnWndProc de la structure WNDCLASSEX (voir plus haut). Dans notre cas, elle s’appelle wndProc et à toujours la même signature.

LRESULT CALLBACK wndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message) 
    {
        case WM_KEYDOWN    :    
            if(wParam == VK_ESCAPE)
                PostQuitMessage(0);
        break;        
        case WM_DESTROY    :    
            PostQuitMessage(0);
        break;
    }
    
    return DefWindowProc(hWnd, message, wParam, lParam);
}

Cela fonctionne sur un système de message comme pour la boucle principale sauf que celle ci est spécifique à l’application. Lorsqu’un nouveau message pour la fenêtre arrive, la méthode wndProc est appelée. Nous gérons ici deux messages.

WN_DESTROY est reçu lorsque la fenêtre va être détruite. Cela peut provenir d’un clic sur la croix de fermeture de la fenêtre mais cela peut aussi venir d’autres facteurs comme le fermeture de l’application dans le gestionnaire de tâches ou encore l’extinction de l’ordinateur.

WM_KEYDOWN correspond à une touche du clavier qui a été enfoncée. Le code de la touche est disponible grâce au paramètre wParam.

Vous pouvez trouver ici la liste des messages pour wndProc.

On appelle la méthode PostQuitMessage avec un code retour 0 (aucun problème) pour envoyer un message de fermeture de l’application (donc de la fenêtre). Cela enverra un message WM_Quit à la boucle du WinMain qui sortira du programme.

  • La récompense

Nous sommes arrivés au bout de notre périple. Si vous lancez l’application, vous devriez vous retrouver avec la fenêtre suivante.

blog_dx_t1_4

Si ce n’est pas le cas, je vous donne mon projet de solution (VS 2010).

  • Conclusion

J’imagine votre première réaction à la fin de ce tutoriel : “Tout ça pour ….. ça Triste !!!“. Effectivement le code Win32 est assez indigeste, complexe (j’en connais qui aime ça Sourire) pour un résultat très médiocre. Il s’agit pourtant de la base de tout jeu vidéo DirectX et vous êtes obligé de passer par là pour commencer un projet. La bonne nouvelle est qu’une fois que c’est fait, on n’a plus à s’en préoccuper et on peut réutiliser le même code sans problème. Je vous conseille donc de  garder votre projet sous la main car vous pourriez en avoir besoin très bientôt.

07/10/2010

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

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

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

  • Mapping de texture sur une face

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

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

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

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

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

  • Texturer un cube avec 6 textures

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

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

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

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

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

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

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

    this.GraphicsDevice.SetVertexBuffer(this.vb);

}

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

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

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

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

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

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

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

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

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

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

    base.Update(gameTime);
}

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

xna_tuto3d3_4

  • Texturer un cube avec 1 texture

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

cube_dice

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

xna_tuto3d3_5

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

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

xna_tuto3d3_6a

xna_tuto3d3_6b

Texture “3D”

Modèle 3d + Texture 3d

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

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

  • Conclusion

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

Source Code de ce tutoriel

06/10/2010

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

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

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

  • Au commencement était le cube…

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

180px-Hexahedron

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

xna_tuto3d2_1

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

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

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

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

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

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

    base.Initialize();
}

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

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

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

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

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

xna_tuto3d2_2

  • Gestion de la mémoire : VertexBuffer

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

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

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

private VertexBuffer vb;

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

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

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

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

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

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

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

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

  • IndexBuffer : Optimisation mémoire des vertices

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

int[] indices;

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

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

}

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

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

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

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

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

VertexBuffer vb;
IndexBuffer ib;
int[] indices;

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

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

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

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

}

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

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

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

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

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

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

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

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

fig4

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

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

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

  • Et les performances dans tout ça…

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

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

  • Conclusion

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

Code Source de ce tutoriel

Older Posts »

Thème Silver is the New Black. Créez un site Web ou un blog gratuitement sur WordPress.com.

Suivre

Recevez les nouvelles publications par mail.

%d bloggers like this: