Astuces DotNet (Sébastien Courtois)

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 — Étiquettes : , , , , , — 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.

Publicités

28/01/2012

[Kinect SDK] Presentation de Kinect SDK

Filed under: C#, C++, Débutant, Jeux vidéos, Kinect — Étiquettes : , , , , , , , — 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 — Étiquettes : , , , , , — 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)

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 — Étiquettes : , , , , , , — 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.

09/06/2010

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

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

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

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

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

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

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

foreach (var action in actions)
    action();

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

Et pourtant la réponse est :

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

Autre exemple dans le même genre :

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

{

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

    localFrame.i = idx;

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

}

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

  • Conclusion

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

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

03/05/2010

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

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

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

  • Définition de l’immutabilité

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

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

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

  • Exemple concret d’immutabilité : String

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

Prenons l’exemple C/C++ suivant :

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

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

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

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

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

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

immutable1

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

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

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

  • Les méthodes de la classe System.String

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

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

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

  • Cas de la concaténation de chaines

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

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

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

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

Résultat du test de performance :

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

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

  • Conclusion

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

Propulsé par WordPress.com.

%d blogueurs aiment cette page :