Astuces DotNet (Sébastien Courtois)

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.

08/05/2010

[MS Research / DevLabs] Accelerator v2 : Exploiter vos GPU facilement en .NET

Filed under: .NET, DevLabs, Hors Catégorie — Étiquettes : , , , , , , , , — sebastiencourtois @ 17:47

Suite à ma visite sur les sites Internet de Microsoft Research et DevLabs, j’ai décidé de vous parler de ces projets de Microsoft qui seront dans les prochaines versions de .NET (ou pas). Dans les anciens project MS Research que l’on retrouve aujourd’hui au grand public, on pourrait cité WPF/Silverlight,Surface,PFX…

De ce post, nous allons parler d’une technologie dont le nom de code est “Accelerator”.

  • Présentation du contexte

De nos jours, les fabricants d’ordinateur nous fournisse des processeurs toujours plus puissants que ce soit en fréquence d’horloge qu’en nombre de cœurs. Ces nouvelles possibilités commencent à être exploiter correctement (notamment grâce à PFX de .NET 4).

Mais ce qui est moins exploiter est la puissance de calcul des cartes graphiques (GPU). Il faut savoir qu’une carte graphique peut avoir plusieurs processeurs dont chacun peut avoir de nombreux coeurs (jusqu’à 500 pour les plus récentes). Tout ce potentiel n’est utilisé que pour les jeux afin de faire des rendus et des calculs mathématiques afin de rendre des scènes 3D.

Le gros avantages des GPU est sa rapidité de calcul et sa gestion optimisé du parallélisme.

En revanche, les défauts des GPU par rapport aux CPU :

  • Les instructions sont limités aux opérations mathématiques 3D alors que le CPU peut avoir des opérations plus génériques
  • Les instructions dépendent beaucoup du constructeurs de la carte graphique (beaucoup plus que pour un CPU)
  • Il y a très peu d’API permettant d’accéder aux GPU directement (sans passer des API 3D).
  • Qu’est ce qu’Accelerator ?

L’idée du projet Accelerator est de fournir une API (Managé et C++) permettant d’envoyer du code sur ces GPU qu’il l’exécute.

D’autres ont déjà eu l’idée avant Microsoft :

  • Nvidia avec CUDA
  • ATI avec ATI Stream
  • Appel avec OpenCL
  • “Pixel Shaders”

Bien que ces API existent (encore au stade BETA pour la plupart), Microsoft a décidé de créé le projet Accelerator afin de fournir une API de développement pour les GPU indépendant de la plateforme et extensible.

  • Comment cela fonctionne ?

Accelerator se compose de deux gros blogs : Accelerator Library et Target Runtime.

image Structure général d’une application Accelerator

L’”Accelerator Library” est une API de développement pour préparer les données afin d’être exécutés sur le GPU. Cela se base sur un système abstrait à base d’arbre d’expression (nous le verrons plus loin). Cette API est composé d’une partie Native (C++) et d’une partie Managé (wrapper léger sur l’API native).

Le “Target Runtime” est une sorte de driver permettant de faire communiquer l’API avec les couches matériels (GPU … mais aussi CPU Multicore). Ce driver lit les arbres d’expressions d’”Accelarator Library” et envoie les instructions correspondantes aux GPU. Le driver est aussi en charge de la gestion de la charge sur les processeurs/cœurs. Microsoft fournit deux target runtime par défaut : Un DirectX9 (permettant d’accéder indépendamment à l’ensemble des carte graphiques du marché) et un pour les multi-coeurs CPU 64bits. Si vous souhaitez utilisé Accelerator sur d’autres types de processeurs, il suffit de développer un target runtime pour l’architecture cible.

Afin de comprendre le fonctionnement de Accelerator Library, prenons un exemple. Prenons deux tableaux contenant des valeurs à virgules flottantes (float). On souhaite additionner les cases une à une dans un troisième tableaux. Sur un CPU, on aurait une boucle for qui ferait les opération dans l’ordre des cellules et une cellule à la fois.

acceleratorv2 Exécution d’une addition sur un tableau (Sequential Program = CPU / Data Parallel Program = GPU)

Sur le GPU, le tableau est divisé et chaque cœurs à son propre lot de données à exécutés. Après exécution, le tableau est réassemblé à partir des résultats des tableaux de chacun des cœurs.

Afin que les opérations soient abstraites par rapport aux matériels sous jacent, Accelerator fournit un système d’arbre permettant de faciliter la création des instructions GPU. Par exemple, le graph suivant représente l’addition case par case de deux tableau puis la division de chacun des cases résultat par 2.

image Arbre d’expression Accelerator

  • Comment installer Accelerator ?

Accelerator v2 est actuellement disponible sur https://connect.microsoft.com/acceleratorv2.

Une fois installé, vous avez accès à  un document d’introduction et un document contenant l’ensemble des classes du projet. Je vous conseille le document d’introduction qui est très bien fait (j’en reprend un partie dans ce post).

Afin de développer, vous devez ajouter en référence l’assembly Microsoft.Accelerator.dll (contenu dans le répertoire C:\Program Files\Microsoft\Accelerator v2\bin\Managed).

Attention : Pour l’exécuter, il est nécessaire de placer la dll Accelerator.dll (se trouvant dans le répertoire C:\Program Files\Microsoft\Accelerator v2\bin\x86 ou C:\Program Files\Microsoft\Accelerator v2\bin\x64 selon votre architecture ) dans le répertoire où s’exécute l’application sous peine d’avoir l’exception : “DllNotFoundException : Unable to load DLL ‘Accelerator.dll’: Le module spécifié est introuvable. (Exception from HRESULT: 0x8007007E)”

  • Un Hello world “Accelrator”

En reprenant l’exemple d’addition case à case puis d’une division par 2, on obtient un code C# suivant (le reste du code a été enlevé afin de pas alourdir le code).

On commence par simplifier le code en utilisant des using.

using FPA = Microsoft.ParallelArrays.FloatParallelArray;
using PA = Microsoft.ParallelArrays.ParallelArrays;

FloatParallelArray représente un tableau de float sur la cible (carte graphique dans notre cas).. ParallelArrays regroupe les instruction disponible pour la cible.

DX9Target evalTarget = new DX9Target();

On crée le driver qui sera utilisé pour exécuter les opérations. Afin de faire simple, nous utiliserons le driver DirectX9 afin d’accéder aux GPU.

float[] inputArray1 = new float[arrayLength];
float[] inputArray2 = new float[arrayLength];
float[] stackedArray = new float[arrayLength];
/* Ajout de données dans les tableaux  inputArray1/inputArray2 */

On crée trois tableaux. Deux tableaux contenant les données d’entrée (inputArray1,2) que l’on remplit de valeurs diverses et un tableau pour les résultats.

FPA fpInput1 = new FPA(inputArray1);
FPA fpInput2 = new FPA(inputArray2);

On “transforme” les tableaux afin qu’il soit utilisables pour la cible.

FPA fpStacked = PA.Add(fpInput1, fpInput2);
FPA fpOutput = PA.Divide(fpStacked, 2);

On crée les opérations entre les tableaux. On utilise le résultat de l’addition pour la division. Cela génère l’arbre d’expression en copie d’écran plus haut.

evalTarget.ToArray(fpOutput, out stackedArray);

C’est la partie la plus importante du programme. En effet, jusqu’à maintenant nous avons préparé l’arbre d’expression et les données et c’est sur cette méthode, que l’arbre est évalué et que la cible (GPU dans notre cas) réalise les opérations. Le résultat est ensuite convertir et stocké dans un tableau de float .NET.

  • Et les performances alors ?

Il  est vrai que tout ça est un peu contraignant alors on est en droit de se demander si les performances sont aux rendez vous. Sur l’exemple ci dessus par exemple sur ma machine, les performances étaient meilleures en .NET qu’en GPU. La raison est, selon moi, que le temps de copie et d’éxécution est long pour le peu d’opérations exécutées.

Pour des données plus claires sur le domaine, je vous renvoie sur le PDF de MS Research parlant des performances : Document PDF . On peut voir que les performance peuvent être jusqu’à 70x plus rapide sur certains code par rapport au C++ sur CPU.

Le kit de développement contient aussi des samples (dont un jeu de la vie) qui permettent un peu de se rendre compte de ces performances.

Il est nécessaire de rappeler que ce projet est en alpha et donc loin d’être optimisé.

  • Conclusion

Ce projet est plein de promesses même s’il reste en dessous de technologies comme CUDA qui se rapproche du langage C alors que Accelerator est basé sur des arbres d’expressions / opérations mathématiques de bases. De plus Accelerator étant basé sur le parallélisme, il est nécessaire de rappeler que, comme PFX, tous les algorithmes ne sont pas faits pour ces technologies.

  • Quelques liens utiles :

Site du projet sur Microsoft Research (Attention c’est la v1 sur ce site)

Téléchargement de la v2

Article d’un français sur le sujet : Accelerator & F# / Accelerator : F# vs C#

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).

21/04/2010

[Nouveautés .NET 4] Le Tuple

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

La classe Tuple est une nouvelle classe de base du Framework .NET 4. Cette classe permet de stocker des données ayant “une relation logique” au sein d’une application.

On peut stocker 1 à 7 valeurs au sein d’un tuple grâce aux classes suivantes :

public class Tuple<T1>

public class Tuple<T1, T2>
public class Tuple<T1, T2, T3>
public class Tuple<T1, T2, T3, T4>
public class Tuple<T1, T2, T3, T4, T5>
public class Tuple<T1, T2, T3, T4, T5, T6>
public class Tuple<T1, T2, T3, T4, T5, T6, T7>

Afin d’utiliser un tuple, il suffit de créer une instance en fournissant les types que l’on souhaite stocker en paramètres générique de la classe. On obtient ainsi une classe typés ce qui permet d’éviter les problèmes de performances liés aux boxing/unboxing.

Tuple<string, int> tsi = new Tuple<string, int>(string.Empty, 0);
Tuple<string, int,bool> tsib = new Tuple<string, int,bool>(string.Empty, 0,true);

Un tuple s’instancie comme une classe normale. On fait appel au constructeur en fournissant les données que l’on souhaite stocker conjointement. Il est intéressant de noter que, gràce à la généricité, on peut imbriquer les tuples et ainsi stocker théoriquement une infinité de données dans un tuple (voir exemple suivant).

Tuple<string, int,bool,int,string,bool,int,Tuple<int,string>> tsib3 =
new Tuple<string, int,bool,int,string,bool,int,Tuple<int,string>> (string.Empty, 0,true,2,string.Empty,true,2,new Tuple<int,string>(2,"Toto"));

L’accès au données se réalise par une propriété ItemX (avec X étant le numéro d’ordre de la donnée) en lecture seule.

public class Tuple<T1, T2> : IStructuralEquatable, IStructuralComparable, IComparable, ITuple
{
    public Tuple(T1 item1, T2 item2)
    public T1 Item1 { get; }
    public T2 Item2 { get; }
}

  • Pourquoi Tuple expose ses propriétés en lecture seule ?

La raison principale est l’immutabilité. On dit qu’une classe est “immutable” si on ne peut modifier ses données qu’au travers de son constructeur. Un des intérêts est que, dans le cas de programmation parralèlle, une classe immutable ne peut être l’objet de verrou ou de race situation car ses données sont figés dans le RAM après la création de la classe. C’est donc uniquement pour des soucis de performances que ces propriétés sont en read only.

Il est toutefois possible de se créer une classe Tuple modifiable en créant la même classe et en rajoutant l’accesseur set sur les propriétés

public class MyTuple<T1, T2>
{
    public MyTuple(T1 t1, T2 t2) { }

    public T1 Item1 { get; set; }
    public T2 Item2 { get; set; }
}
  • Cas d’utilisation des tuples : Les valeurs retours de méthodes

Examinons un exemple pour comprendre une des utlisations des tuples. Je souhaite créer une méthode prenant des données sur une rectangle en paramètres et me fournissant les données sur les coordonnées de deux points de ce rectangle.

En .NET 3.5, je pourrais faire une première méthode avec le mot clé out :

static void CreateRectangleData(string name, int x, int y, int sizex, int sizey,out string finalname,out int p1x,out int p1y,out int p2x,out int p2y)
{
    finalname = name;
    p1x = x;
    p1y = y;
    p2x = x + sizex;
    p2y = y + sizey;
}

L’utilisation de cette méthode peut être fastidieuse car il faut créer l’ensemble des variables du coté de l’appelant. De plus, il serait plus propre de créer une classe intermédiaire pour stocker/structurer ces données.

public class MyRectangle
{
   public string name;
   public int Point1X;
   public int Point1Y;
   public int Point2X;
   public int Point2Y;
}

static MyRectangle CreateRectangleData(string name,int x,int y,int sizex,int sizey)
{
     return new MyRectangle()
     {
       name = name,
       Point1X = x,
       Point1Y = y,
       Point2X = x+sizex,
       Point2Y = y+sizey
    };
}

Meilleure solution (selon moi) pour récupérer ces données. Toutefois, lorsque l’on a trop de classe intermédiaire de ce genre (ne servant souvent qu’à récupérer plusieurs données d’une seul méthode) le code devient vite illisible. C’est là où les tuples peuvent intervenir.

static Tuple<string,int,int,int,int> CreateRectangleData(string name, int x, int y, int sizex, int sizey)
{
   return new Tuple<string, int, int, int, int>(name,x,y,x + sizex,y + sizey);
}

Le code devient donc plus propre car on peut se débarrasser des classes intermédiaires en les remplaçant par des tuples. Il faut toutefois bien être conscient de l’ordre des données afin de ne pas se tromper en les traiter après.

  • Avant j’utilisais KeyValuePair<K,T> et cela marchait très bien. Pourquoi utiliser Tuple<T1,T2> à la place ?

Il faut, tout d’abord, savoir que Tuple est une généralisation de la classe KeyValuePair. On peut décrire KeyValuePair comme étant un Tuple à 2 valeurs alors que Tuple peut prendre N valeurs.

Une différence notable est que Tuple est une classe (type référence) alors que KeyValuePair est une structure (type valeur).

public class Tuple<T1, T2> : /**/
{
    public Tuple(T1 item1, T2 item2)
    public T1 Item1 { get; }
    public T2 Item2 { get; }
}

public struct KeyValuePair<TKey, TValue>
{
    public KeyValuePair(TKey key, TValue value);
    public TKey Key { get; }
    public TValue Value { get; }
}

Définition de la classe Tuple Définition de la structure KeyValue Pair

Le choix entre KeyValuePair<K,T> et Tuple<T1,T2> doit aussi être fait selon la logique de votre code. Il est plus logique de stocker des données d’un dictionnaire selon un système de clé/valeur en utilisant KeyValuePair  et d’utiliser Tuple<T1,T2> pour stocker les données d’une méthode ayant deux valeurs retours.

15/04/2010

[Silverlight] Sortie de Silverlight 4 RTW

Filed under: .NET, Débutant, Silverlight, Silverlight 3, Silverlight 4 — Étiquettes : , , , , — sebastiencourtois @ 23:40

Ca y est : Silverlight 4 est disponible à la page suivante : http://www.silverlight.net/getstarted/

Beaucoup de nouveautés (drag/drop, impression,clic droit …) dont certains ont déja été décrit sur ce blog :

D’autres posts sur le sujets arriveront bientôt.

EDIT : Silverlight 4 Tools NE FONCTIONNE PAS SUR VS 2008…. Il faut obligatoirement VS 2010 pour développer en SL4… De plus les outils de développement sont en RC pour le moment car il fallait que Silverlight 4 soit complètement terminé pour finaliser les outils de développement (ils ont du finir sur le fil SL4 :)).

[Nouveautés C# .NET 4] Paramètres nommés et optionnels

Filed under: .NET, Débutant — Étiquettes : , , , , — sebastiencourtois @ 12:43

Suite de notre tour sur les nouveautés de C# 4. Après une présentation de la Co-Contravariance, nous allons nous intéresser à une fonctionnalité qui faisait cruellement défaut à .NET : les paramètres nommés / optionnels.

  • Paramètres optionnels

Qui n’a pas écrit ce type de code dans un programme :

public void MaFonction(int a)
{
  this.MaFonction(a, "toto");
}
public void MaFonction(int a, string b)
{
   this.MaFonction(a, b, null);
}
public void MaFonction(int a, string b, StringBuilder c)
{
   //Fait le traitement
}

une suite (parfois sans fin) de méthodes s’appelant les unes les autres avec des ajouts de paramètres pour arriver à “la fonction qui fait tout”. Cela a pour désavantage d’augmenter sensiblement (et inutilement) la taille du code source et cela peut être la source d’erreurs dans l’attribution des paramètres.

Pour parer à ces inconvénients, Microsoft a introduit, dans .NET 4, une nouvelle fonctionnalité : les paramètres optionnels. Il s’agit de définir des valeurs par défaut aux paramètres que l’on souhaite définir comme optionnel.

Le code précédent peut donc s’écrire en .NET 4 :

public void MaFonctionNET4(int a, string b = "Toto", StringBuilder c = null)
{
    //Fait le traitement
}

Au niveau de l’Intellisense, on se retrouve avec les affichages suivants : 

params1params2

Intellisense pour la version classique

Intellisense pour la version .NET 4

Remarque : Les paramètres optionnels doivent toujours apparaitre après les paramètres obligatoires. Ainsi le code suivant génère une erreur de compilation.

params3 
  • Paramètres nommés

Lorsque l’on a des méthodes avec beaucoup de paramètres, il arrive que l’on ne se rappelle plus de l’ordre de chacun des paramètres. Récupérer l’information par l’IntelliSense ou en allant voir dans le code source peut prendre parfois un peu de votre temps au combien précieux.

Pour pallier ce problème, .NET 4 permet d’utiliser les paramètres nommés.

Prenons une méthode classique :

public void MaFonctionNET4WithName(string param1, int param2 = 0, double param3 = -42.424242)
{

}

.NET 4 permet de définir les paramètres en fonction de leur nom et non plus en fonction de leur ordre. On peut ainsi écrire :

t.MaFonctionNET4WithName(param1: "Test",param2: 5);

Il est à noter que les paramètres nommés et les paramètres optionnels peuvent cohabiter.

Remarque : Lorsque l’on utilise les paramètres nommés pour l’appel d’une méthode, il faut le faire sur l’ensemble des paramètres il faut indiquer les paramètres nommés après les paramètres classiques (qui, eux, doivent être dans l’ordre)  . Dans le cas contraire,on récupère l’exception suivante. Dans le cas contraire,on récupère l’exception suivantes.

params4 

En revanche, l’exemple suivant est valide :

  t.MaFonctionNET4WithName("Toto", param3: 34.45, param2: 8); // Fonctionne car les paramètres non nommés sont en premiers

  • Conclusion

J’attendais cette fonctionnalité depuis un moment (surtout parce que je l’avais déjà utilisé dans d’autres langages). Il faut avouer que c’est une fonctionnalité simple,rapide à mettre en place et qui apporte une meilleur lisibilité et stabilité au code produit.

Merci à François Guillot pour avoir détecté l’erreur concernant l’ordre des paramètres.

28/01/2010

[.NET] Le mot clé sealed sur une classe permet-il permettre de gagner en performance ?

Filed under: .NET, Débutant, Intermediaire, Optimisation — Étiquettes : , , , , , — sebastiencourtois @ 00:19

Voici la question que Wilfried Woivre (MSP ainsi que “Ask The Expert” sur Azure aux prochains Tech Days 2010).

Ma première réponse a été… “J’en sais rien” :). En effet, le mot clé sealed a toujours été, pour moi, utilisé pour des design OO spécifiques (type création de framework public …) où l’idée est d’interdire l’extension d’une classe (la fonction première du mot clé sealed). J’ai donc dit que je pensais que ce n’était pas le cas ni le rôle de ce mot clé.

Quelque temps plus tard, je me retrouve à lire un chapitre sur l’optimisation de code .NET dans lequel le mot clé “sealed” == performance. J’ai donc décidé de faire le benchmark suivant :

   public class ClassA
   {
       public void Method1() { }
   }

   public sealed class ClassASealed
   {
       public void Method1() { }
   }

   public class ClassAVirtual
   {
       public virtual void Method1() { }
   }

    
class Program
{
    static void Main(string[] args)
    {
        ClassA ca = new ClassA();
        ClassASealed cas = new ClassASealed();
        ClassAVirtual cav = new ClassAVirtual();

        Stopwatch swca = new Stopwatch();
        Stopwatch swcas = new Stopwatch();
        Stopwatch swcav = new Stopwatch();
        for (int i = 0; i < 10000000; i++)
        {
            swca.Start();
            ca.Method1();
            swca.Stop();

            swcas.Start();
            cas.Method1();
            swcas.Stop();

            swcav.Start();
            cav.Method1();
            swcav.Stop();
        }

        Console.WriteLine("ClassA  : " + swca.Elapsed);
        Console.WriteLine("ClassAS : " + swcas.Elapsed);
        Console.WriteLine("ClassAV : " + swcav.Elapsed);
        Console.WriteLine("Finish");
        Console.ReadLine();
    }
}

On teste, ici, le temps d’appel des méthodes en faisant 1 000 000 d’appels. On trouve les résultats suivants :

  • Classe Normale : 22.8844652 secondes
  • Classe Sealed : 22.8523417 secondes
  • Classe Normale avec une méthode virtuelle : 23.0062174 secondes

Premier constat : Le mot clé sealed nous permet de gagner environ 1 pour 1000 en performance.

Deuxième constat : la méthode virtuelle semble faire perdre aussi des performances ( 5 ou 6 pour 1000).

Pourquoi ces écarts ?

1°)Parlons tous d’abord de l’écart normale/sealed : Au niveau du code IL, il n’y a que le sealed de différence :

.class public auto ansi beforefieldinit ClassAVirtual
.class public auto ansi sealed beforefieldinit ClassASealed

L’optimisation à lieu au niveau du Runtime. En effet, sachant que la classe est scellé, le runtime peut faire un appel direct à la méthode (dans certains cas, exécuter le code sans faire d’appel).

2°) Pour la différence méthode classique /méthode virtuelle :

.method public hidebysig instance void Method1() cil managed
.method public hidebysig newslot virtual instance void Method1() cil managed

Le virtual ici oblige le runtime a créer une table d’appel pour cette méthode virtuelle dans laquelle seront contenu les méthodes des classes dérivées qui surchargeront cette méthode. Par conséquent, un appel à cette méthode est un appel ‘virtuel’ de méthode qui doit aller chercher dans cette table pour trouver la bonne méthode à appelé. Même si dans notre cas, la table ne contient qu’un seul élément, il y a tout de même un coût supplémentaire par rapport à un appel classique (accès à la table,recherche dans la table …)

Conclusion

Effectivement, le fait de mettre rendre classe ‘sealed’ permet de gagner des performances. Toutefois les performances sont tellement faible que dans 95 % des cas (disons : les applications non temps réel ou ne travaillant pas avec des énormes volumes de données), ces gains sont négligeables et il sera plus judicieux de voir s’il n’y a pas des optimisations à faire au niveau du code ou de l’architecture du projet.

05/05/2009

Introduction à Small Basic en français.

Filed under: .NET, Débutant, Small Basic — Étiquettes : , , , , , , — sebastiencourtois @ 09:21

Après plusieurs semaines de travail, une équipe de MSP a traduit la documentation anglaise de Small Basic en français.

Pour ceux qui ne connaissent pas Small Basic, il s’agit un projet de Microsoft fournissant aux débutants en programmation un environnement simple pour commencer doucement dans ce monde parfois obscur. Avec une syntaxe proche du Visual Basic, ce langage contient de nombreuses classes simplifiées fournissant facilement les fonctionnalités réellement nécessaires aux débutants.

SB

Petit tour de l’équipe qui a réalisé la traduction :

Au passage, un grand merci à Florie Perouze de l’équipe Education de Microsoft France qui nous a permis de réaliser ce projet.

Site officiel Small Basic

Blog officiel Small Basic

Documentation Français

24/04/2009

[Virtual Earth] Développement Microsoft Virtual Earth Web Services – Part 4 : Route Service

Filed under: .NET, Débutant, Virtual Earth — Étiquettes : , , , , , — sebastiencourtois @ 15:01

Liens des posts précédents :

  1. Introduction à Microsoft Virtual Earth (Part 1)
  2. Géocoding / Géocoding Inverse (Part 2)
  3. Récupération des images satellites (Part 3)
  4. Calcul d’itinéraires (Part 4)

Suite de notre grande exploration du Web Service Virtual Earth. Aujourd’hui, nous allons nous intéresser au calcul d’itinéraire. Cette fonctionnalité de Virtual Earth permet de calculer le trajet entre une série de point de contrôles (Waypoints). Il est possible de régler les options du calcul d’itinéraire (type de déplacement, trajet le plus rapide, le plus court …).

Comme d’habitude, il faut ajouter faire “Add Service Reference” avec l’adresse : http://staging.dev.virtualearth.net/webservices/v1/routeservice/routeservice.svc 

Remarque : Afin de fonctionner, il est nécessaire régler certains paramètres WCF de ce service. Dans le fichier app.config (ou web.config ou encore ServiceReference.clientconfig selon le type de projet) de l’application, dans la partie contenant les Binding WCF, il faut trouver le binding lié à RouteService (en général : <binding name="BasicHttpBinding_IRouteService") puis modifier l’attribut maxReceivedMessageSize (mettre la valeur 9000000 à la place de celle par défaut) et l’attribut maxBufferSize (mettre la valeur 9000000 à la place de celle par défaut). Cela est du au fait que RouteService renvoie un nombre d’informations supérieur à ce dont WCF est habitué à manipuler.

Ce service contient une classe principale RouteServiceClient  qui contient deux méthodes :

      • CalculateRoute(RouteRequest request) : Calcul d’un itinéraire entre plusieurs points
      • CalculateRoutesFromMajorRoads(MajorRoutesRequest request) : Calcul d’itinéraires partant des routes principales vers un point d’arrivée spécifié (carte d’accès).

 


  • Calcul d’un itinéraire entre plusieurs points

Le calcul d’itinéraire entre plusieurs points à besoin de deux informations : Les points de passages et là façons de calculer le trajet. Ces deux informations sont contenues dans la classe RouteRequest qui sera passer en paramètre de la méthode CalculateRoute de RouteServiceClient.

Les points de passages doivent être fournis dans la propriété WayPoints de  RouteRequest. Cette propriété prend un tableau de WayPoint, un WayPoint n’étant composé que d’une localisation GPS (latitude/longitude/altitude) et d’une propriété Description pour donner un titre au point de passage.

Les options de calcul d’itinéraire se trouvent dans la propriété Options. Cette propriété est de type RouteOptions et contient les champs suivantes :

Nom de la propriété Description
Mode Type de déplacement.
Enumération TravelMode :
Driving : Voiture (par défaut)
Walking : A pied (ne marche pas pour tous les pays)
Optimization Méthode de calcul du trajet. Marche en combinaison avec TrafficUsage
Enumération RouteOptimization :

MinimizeDistance : Distance minimale
MinimizeTime : Temps minimal

RoutePathType Type de données brutes renvoyées (en plus des données d’itinéraires)
Enumération RoutePathType :
None : Aucune donnée (par défaut)
Points : Un ensemble de points décrivant le chemin de la route.
TrafficUsage Type d’adaptation du trajet en fonction du trafic
Enumération TrafficUsage :

None : On ne tient pas compte du trafic (par défaut)

TrafficBasedRouteAndTime : On change le trajet pour minimiser le temps si nécessaire

TrafficBasedTime : On ne change pas la route mais on tient compte du trafic lors du calcul du trajet.

Combinaisons de Optimization et TrafficUsage :

Optimization TrafficUsage Description

MinimizeDistance

None

Trajet le plus direct (en distance) sans prendre en compte le trafic.

MinimizeDistance

TrafficBasedRouteAndTime

Une Exception lancée. Je pense que c’est du au fait que changer la route d’un trajet le plus direct en fonction du trafic => trajet plus long => incohérence.

MinimizeDistance

TrafficBasedTime

Trajet le plus direct (en distance) en tenant compte du trafic dans les calculs de durée (le trajet n’est pas changé… on passera dans les bouchons quand même :)).

MinimizeTime

None

Trajet le plus rapide (en temps) sans prendre en compte le trafic.

MinimizeTime

TrafficBasedRouteAndTime

Trajet le plus rapide (en temps) en tenant compte du trafic et en le modifiant le cas échéant.

MinimizeTime

TrafficBasedTime

Trajet le plus rapide (en temps) en tenant compte du trafic dans les calculs de temps de trajet (la route n’est pas modifié en fonction du trafic).

Un petit code vaut mieux qu’un long discours : (les variables positionWinwise, positionMSFUniv, positionMSF sont des coordonnées géographiques calculées avant ce code)

RouteServiceClient rsc = new RouteServiceClient();
RouteRequest rq = new RouteRequest();
rq.Credentials = new VETutorial.RouteService.Credentials();
rq.Credentials.Token = token;
rq.Culture = "fr-FR";
rq.Waypoints = new Waypoint[]
{
	new Waypoint() 
	{ 					
		Description = "Winwise (Départ)", 
		Location = new VETutorial.RouteService.Location() 
		{
			Longitude = positionWinwise.Longitude,
			Latitude = positionWinwise.Latitude,
		}
	}, 
	new Waypoint() 
	{ 
		Description = "Microsoft France (Rue de l'université)", 
		Location =  new VETutorial.RouteService.Location() 
		{
			Longitude = positionMSFUniv.Longitude,
			Latitude = positionMSFUniv.Latitude,
		}
	}, 
	new Waypoint() 
	{ 
		Description = "Microsoft France (Rue du québec)", 
		Location =  new VETutorial.RouteService.Location() 
		{
			Longitude = positionMSF.Longitude,
			Latitude = positionMSF.Latitude,
		}
	},
};
rq.Options = new RouteOptions()
{
	Mode = TravelMode.Driving,
	Optimization = RouteOptimization.MinimizeTime,
	RoutePathType = RoutePathType.Points,
	TrafficUsage = TrafficUsage.TrafficBasedRouteAndTime,
};
RouteResponse routeResult = rsc.CalculateRoute(rq);

Comme vous pouvez voir dans ce code, on peut définir des informations supplémentaires comme les données d’authentifications ou encore la culture (langue) du résultat.

En réponse à l’appel de la méthode CalculateRoute, on obtient une classe de réponse de type RouteResponse contenant un champ ResponseSummary (déjà décrit dans les posts précédents) et un champ Result de type RouteResult contenant les informations sur le trajet.

La classe RouteResult fonctionne en arborescence :

Un RouteResult contient des Legs: Un Leg  est un trajet entre deux points de passages. Si plusieurs points de passages alors plusieurs Legs dans le RouteResult

Un Leg contient un tableau d’ItineraryItem (dans la propriété Itinerary) : Un ItineraryItem contient les informations entre deux changements de directions.

Chaque RouteResult, Leg et ItineraryItem contient une propriété Summary contenant le rectangle de leur localisation (propriété BoundingRectangle) ainsi que le temps de parcours en secondes (propriété TimeInSeconds) et la longueur de ce parcours dans la propriété Distance (par défaut en KM mais réglable dans la propriété UserProfile de la requête).

En dehors de ces données, la classe RouteResult contient aussi les données brutes du chemin dans la propriété RoutePath. Il contient un tableau de points géographiques représentant le trajet à faire. Cette propriété est la raison pour laquelle les données renvoyés par le service sont énormes. N’activer cette propriété que si nécessaire (désactivé par défaut : propriété RoutePathType de la requête).

La classe Leg a aussi deux propriétés spécifiques : ActualStart, ActualEnd. Ces propriétés indiquent les positions géographiques de départ et d’arrivés du Leg.

La classe ItineraryItem contient les propriétés suivantes :

Nom de la propriété Description
CompassDirection Orientation du déplacement (chaines de caractères “Nord” …)
Hints Indication permettant de mieux repérer le changement de direction (exemple : “la rue précédente est …”)
Location Position du changement de direction
ManeuverType Type de changement (tourner à gauche, droite, demi tour …)
Text Texte en format XML indiquant le changement à faire (Exemple : “<VirtualEarth:Action>Quitter</VirtualEarth:Action> <VirtualEarth:RoadName>Rue Gaillon</VirtualEarth:RoadName>”)
Warnings Danger sur la route. Lié aux infos traffic, cette propriété indique s’il y a un évènement sur votre trajet (accident, travaux …)

Voici le code d’affichage d’un trajet :

Console.WriteLine("--- Itinéraire  ---");
Console.WriteLine("Temps de calcul : "+ sw.Elapsed);
int i = 1;				
foreach (var leg in routeResult.Result.Legs)
{							
	var duree = new TimeSpan(0,0,(int)leg.Summary.TimeInSeconds);
	Console.WriteLine("-----    Partie " + i + " ------");
	Console.WriteLine("De (" + leg.ActualStart.Latitude +","+leg.ActualStart.Longitude+") à (" + leg.ActualEnd.Latitude +","+leg.ActualEnd.Longitude+")");
	Console.WriteLine("Distance " + leg.Summary.Distance + " km");
	Console.WriteLine("Temps estimé : " + duree.Hours + " h " + duree.Minutes + " min " + duree.Seconds+ " sec");
	Console.WriteLine("************ TRAJET ************");
	int j = 1;
	double distanceDone = 0;
	foreach (var turn in leg.Itinerary)
	{			
		var duree2 = new TimeSpan(0, 0, (int)turn.Summary.TimeInSeconds);
		string HumanInfos = StripXMLFromText(turn.Text);
		Console.WriteLine("Indication " + j + " " + (distanceDone) + " km ["+duree2.Hours+":" + duree2.Minutes + ":" + duree2.Seconds+ "] : " + HumanInfos);
		distanceDone += turn.Summary.Distance;
		j++;
	}
	Console.WriteLine("----- Fin Partie " + i + " -----");
	i++;
}

Le résultat obtenu est le suivant :

— Itinéraire  —

Temps de calcul : 00:00:04.3373465

—–    Partie 1 ——

De (48,868681,2,334231) à (48,861122,2,309237)

Distance 2,788 km

Temps estimé : 0 h 7 min 46 sec

************ TRAJET ************

Indication 1 0 km [0:0:11] : Quitter Rue Gaillon

Indication 2 0,13 km [0:0:49] : Prendre à gauche Avenue de l’Opéra

Indication 3 0,156 km [0:0:53] : Tourner à droite Rue Saint-Roch

Indication 4 0,569 km [0:1:17] : Tourner à droite Rue de Rivoli

Indication 5 1,215 km [0:0:5] : Continuer tout droit Place de la Concorde

Indication 6 1,257 km [0:0:9] : Continuer à gauche Place de la Concorde

Indication 7 1,338 km [0:0:58] : Au rond-point prendre 4

Indication 8 1,61 km [0:0:57] : Prendre à droite Pont de la Concorde

Indication 9 1,835 km [0:1:17] : Tourner à droite Quai d’Orsay

Indication 10 2,531 km [0:0:6] : Continuer à droite Quai d’Orsay

Indication 11 2,584 km [0:0:2] : Tourner à gauche Quai d’Orsay

Indication 12 2,61 km [0:0:56] : Continuer tout droit Rue Surcouf

Indication 13 2,777 km [0:0:6] : Tourner à droite Rue de l’Université

Indication 14 2,788 km [0:0:0] : Arrivée Microsoft France (Rue de l’université)

—– Fin Partie 1 —–

—–    Partie 2 ——

De (48,861122,2,309237) à (48,6900826,2,2157051)

Distance 33,094 km

Temps estimé : 0 h 33 min 46 sec

************ TRAJET ************

Indication 1 0 km [0:0:36] : Quitter Rue de l’Université

Indication 2 0,28 km [0:0:28] : Tourner à droite Rue Malar

Indication 3 0,439 km [0:1:9] : Tourner à gauche Quai d’Orsay

Indication 4 0,685 km [0:2:26] : Continuer tout droit Quai Branly

Indication 5 2,079 km [0:1:27] : Le nom de la voie change Quai de Grenelle

Indication 6 2,877 km [0:2:34] : Le nom de la voie change Quai André Citroën

Indication 7 4,109 km [0:0:33] : Au rond-point prendre 1

Indication 8 4,378 km [0:0:55] : Le nom de la voie change Quai d’Issy-les-Moulin

eaux

Indication 9 4,925 km [0:5:21] : Voie Ad/15 prendre à gauche Boulevard Périphéri

que Lyon

Indication 10 10,008 km [0:2:57] : Prendre à droite E15 Nantes-Bordeaux/Aéroport

Orly-Rungis/Évry-Lyon

Indication 11 12,67 km [0:2:14] : Prendre à droite E15 Véhicules Lents-Toutes Di

rections

Indication 12 15,678 km [0:2:11] : Continuer à gauche E15

Indication 13 19,625 km [0:7:10] : Prendre à droite E5/E50

Indication 14 30,882 km [0:0:54] : 9 prendre à droite Chartres Par RN/Les Ulis/Z

.A. de Courtaboeuf/Villejust

Indication 15 31,465 km [0:0:31] : Prendre à droite D118

Indication 16 31,913 km [0:0:15] : Prendre à droite

Indication 17 32,063 km [0:0:36] : Tourner à droite Avenue de la Baltique

Indication 18 32,32 km [0:1:29] : Tourner à droite Avenue du Québec

Indication 19 33,094 km [0:0:0] : Arrivée Microsoft France (Rue du québec)

—– Fin Partie 2 —–

J’avoue que c’est moins sexy qu’une belle carte en 3D avec le chemin en surbrillance mais c’est mieux que rien. Je vais essayer de voir comment afficher ce trajet sur une carte (avec le RoutePath de RouteResult je pense). Sujet d’un prochain post.


  • Calcul d’itinéraires depuis des routes principales

Le principe de la méthode CalculateRoutesFromMajorRoads est de fournir les trajet allant (ou revenant) d’un point nommé Destination vers les grands axes routiers. Au niveau de la requête, la plupart des propriétés décrites ci dessus fonctionnent avec la classe MajorRoutesRequest. Deux différences toutefois :

La propriété Waypoints disparait au profit d’une propriété Destination ne permettant de définir qu’un WayPoint.

La propriété Options devient du type MajorRoutesOptions et cela ajoute un nouveau paramètre d’options : la propriété booléenne ReturnRoutes indique si on souhaite calculer les routes entre la destination et les routes majeurs (true) où si l’on souhaite uniquement les points d’entrées sur les routes majeures les plus proches (false. valeur par défaut).

MajorRoutesRequest mrr = new MajorRoutesRequest();
mrr.Credentials = new VETutorial.RouteService.Credentials();
mrr.Credentials.Token = token;
mrr.Destination = new Waypoint()
{
	Description = "Winwise (Départ)",
	Location = new VETutorial.RouteService.Location()
	{
		Longitude = positionWinwise.Longitude,
		Latitude = positionWinwise.Latitude,
	}
};
mrr.Culture = "fr-FR";
mrr.Options = new MajorRoutesOptions()
{			  
	Mode = TravelMode.Driving,
	Optimization = RouteOptimization.MinimizeTime,
	RoutePathType = RoutePathType.Points,
	TrafficUsage = TrafficUsage.TrafficBasedRouteAndTime,
	ReturnRoutes = true
};
MajorRoutesResponse MRresult = rsc.CalculateRoutesFromMajorRoads(mrr);

Au niveau de la réponse, on reste sur les mêmes types de données. On a ainsi la propriété Routes contenant des RouteResult (décrit plus haut) donnant les itinéraires entre le point destination et une route majeure. La propriété StartingPoints est un tableau de positions géographiques indiquant les entrées sur les routes majeures environnantes.

Ici se conclut ce post sur les calculs d’itinéraire sous Virtual Earth. Il ne reste plus qu’un service à découvrir : Le SearchService puis nous terminerons par une belle application WPF reprenant tout ce que l’on a appris aux cours de ces 5 posts.

N’hésitez pas à mettre des commentaires pour enrichir ces tutoriaux.

03/03/2009

Débogage d’un processus .NET lancé par un processus externe

Filed under: .NET, Débutant — Étiquettes : , , , , , — sebastiencourtois @ 21:46

Au cours de mes temps libre, j’ai eu l’occasion de participer à un concours de programmation (TopCoder). Dans une des catégories, je devais créer un exécutable (.NET) qui était lancé dans un processus Java (fichier exécutable .jar).

Le soucis est que je voudrais pouvoir utiliser toutes les fonctionnalités de débogage du code source pendant l’exécution de mon programme.

Première solution : Lancer le processus externe après la compilation de l’exécutable :

En modifiant le projet .NET, on peut choisir de lancer un autre processus lors de la fin de la compilation de l’exécutable.

Pour accéder à ce menu : Clic droit sur le projet > Properties > Onglet “Debug”

Vous pouvez choisir entre :

  1. Ouvrir le projet courant et attacher le debugger dessus (Comportement par défaut)
  2. Ouvrir un autre exécutable et attacher le debugger sur le projet courant
  3. Ouvrir un navigateur Web sur une adresse définie (pour le débogage ASP.NET)

J’ai beaucoup utilisé la deuxième option lors d’Imagine Cup l’an passé (sur Project Hoshimi). Toutefois, cette option n’accepte que les exécutables avec une extension .exe (donc ni .bat ni .jar :().

Deuxième solution : Attacher un processus lancé à Visual Studio

Visual Studio permet de débogage n’importe quel processus lancé sur la machine.

Pour cela, il suffit d’aller dans le menu Debug > Attach To Process.

attachtoProcess

Une fenêtre apparait permettant de choisir le processus voulu parmi ceux tournant sur la machine.

blogattachtoprocesswindow

Une fois le processus sélectionné, vous avez deux possibilités :

  1. Vous possédez le code source de l’application : Dans ce cas, vous serez à même de déboguer en plaçant des points d’arrêts au sein du code source
  2. Vous ne possédez pas le code source : Dans ce cas, vous pourrez seulement trapper les exceptions sortants de l’applications et voir le code “désassemblé” (MSIL pour les codes managés / Assembleur pour les codes natifs).

Cette méthode est simple et pratique pour déboguer des applications tournant en boucle comme des services.

Dans mon cas, l’exécution de mon code était un algorithme très court. Je n’avais pas le temps d’aller chercher mon processus dans la fenêtre “Attach To Process” pour avoir accès au débuggeur.

Troisième solution : Attacher le debugger Visual Studio à un processus par code

Il faut donc trouver un moyen d’attacher le debugger Visual Studio au début du code de mon processus.

Une petite recherche sur MSDN m’a permis de trouver la classe System.Diagnostics.Debugger qui permet d’interagir avec le debugger Visual Studio au travers de mon code.

Cette classe présente assez peu de méthodes mais permet tout de même de poser des points d’arrêt, de loguer certaines informations et d’attacher un débugger.

La méthode Launch() est la méthode permettant d’attacher le debugger au processus courant. Elle est bloquante et entraine l’apparition d’une fenêtre demandant le debugger à utiliser.

Blog3

Il y a, en général, le choix d’ouvrir une nouvelle instance (dans ce cas, le débogage se fera avec le code MSIL/ASM) ou d’attacher le processus à un Visual Studio déjà ouvert (si possible contenant le code source du processus à déboguer). Je n’ai pas trouvé de moyen de supprimer cette étape de sélection par le code.

Une fois le debugger sélectionné, la phase de débogage se fait de la même manière que pour une application classique. Il est possible de vérifier si le processus est attaché à un debugger grâce à la propriété Debugger.IsAttached.

La méthode Break() permet de poser un point d’arrêt depuis le code source.

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

%d blogueurs aiment cette page :