NopControls 1.1.0

29. mars 2011

Je suis inarrêtable. Vivement que je reprenne le travail ^^. Donc, sinon, nouvelle version des NopControls pour Silverlight.

Cette nouvelle version intègre:

  • Un nouveau contrôle : le ColorPicker
  • Un nouveau projet de test pour Windows Phone (en supplément de celui pour Silverlight Desktop)
  • La correction de quelques vilains bugs qui faisaient rien que de m’embêter

 

imageimageimage

 

Le tout toujours disponible sur Codeplex : http://nopcontrols.codeplex.com

D’ailleurs sur le conseil d’une future collègue, je suis en train d’essayer de voir pour intégrer les NopControls dans les Coding4Fun controls (http://coding4fun.codeplex.com)…Affaire à suivre donc.

.Net, NopControls, Silverlight

NopControls

28. mars 2011

Je viens de mettre en ligne un nouveau projet Codeplex destiné à héberger mes contrôles Silverlight (pour Windows Phone et Desktop).

Le projet s’appelle NopControls et se trouve ici:

http://nopcontrols.codeplex.com

Le but est de fournir le code de quelques contrôles sympathiques que vous pouvez réutiliser dans vos projets si le cœur vous en dit.

Au programme pour le moment deux contrôles:

  • PolygonTabControl : Ce contrôle mappe ses enfants sur la face d’un volume à n faces. Par exemple si il a 5 enfants, il crée un pentagone extrudé et met chaque enfant sur une face.
  • ExpandableTab : Ce contrôle permet de créer des fiches qui se docke sur un bord et qui peuvent apparaitre lorsque l’on clique sur l’onglet qui les représente.

 

Pour en savoir plus, reportez vous au site Codeplex, il y a un exemple en Silverlight Sourire

.Net, Silverlight, Windows Phone

Bien comprendre le fonctionnement du AllowTransparency en WPF

21. mars 2011

Lors d’un débat hautement philosophique dans un troquet parisien (mon dieu, je m’embourgeoise à une vitesse…), je discutais avec des amis (D**k et Mi**u, pour respecter leur anonymat) de la possibilité de changer la valeur de AllowTransparency au runtime.

Mais avant d’aller plus loin, remettons sur la table le fonctionnement de cette propriété. Son but est simple : activer l’opacité par pixel (per-pixel opacity) qui permet en plus de la couleur de chaque pixel d’y associer une valeur d’alpha qui permet de rendre ce dernier plus ou moins transparent.

Windows supporte deux possibilités pour faire de la transparence au niveau des fenêtres:

 

Les fenêtres Layered en mode System Redirected Content (SRC)


Dans ce mode, Windows crée une image pour la fenêtre et chaque appel au GDI effectué par la fenêtre ou les contrôles est redirigé vers l’image en question. Ce mode est transparent pour la fenêtre qui ne sait pas que ces ordres de dessins sont redirigés. Toutefois il n’est pas possible dans ce mode d’avoir de per-pixel opacity car les appels GDI n’en contiennent pas. Tout au plus on peut activer une transparence globale pour la fenêtre.

Dans notre cas, nous pourrions essayer ce code sur notre fenêtre WPF:

[DllImport("user32.dll")]
static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);

[DllImport("user32.dll")]
static extern bool SetLayeredWindowAttributes(IntPtr hwnd, uint crKey, 
byte bAlpha, uint dwFlags); const int GWL_ID = -12; const int GWL_STYLE = -16; const int GWL_EXSTYLE = -20; const int LWA_ALPHA = 0x2; const int WS_EX_LAYERED = 0x80000; public MainWindow() { InitializeComponent(); } private void Button_Click(object sender, RoutedEventArgs e) { WindowInteropHelper helper = new WindowInteropHelper(this); SetWindowLong(helper.Handle, GWL_EXSTYLE,
GetWindowLong(helper.Handle, GWL_EXSTYLE) ^ WS_EX_LAYERED); SetLayeredWindowAttributes(helper.Handle, 0, 128, LWA_ALPHA); }

Cela marcherait très bien pour une fenêtre Windows Forms ou n’importe quelle fenêtre Win32 mais cela n’a aucun effet en WPF. En effet, WPF surveille la valeur du style de sa fenêtre (au sens Win32 du terme) et donc rejette toutes modifications telles que le SetWindowLong ci-dessus.

Il n’est donc PAS POSSIBLE d’activer le AllowTransparency une fois qu’une fenêtre a été affichée. La seule solution consisterait à réinistancier la fenêtre courante et à modifier la valeur de AllowTransparency AVANT de faire le window.Show (ou à construire une fenêtre avec AllowTransparency=true et à transférer l’arbre des éléments WPF).

 

Les fenêtres Layered en mode Application Provided Content (APC)


Vous l’aurez compris, WPF a fait le choix de ce mode.

Ainsi quand on active AllowTransparency=true, la fenêtre Win32 qui sert de support à WPF est créée en mode Layered APC. Dans ce mode c’est l’application qui va crée le bitmap et qui sera responsable de le mettre à jour.

Ce mode supporte le per-pixel opacity et le bitmap est donc généré au format RGBA par WPF et envoyé à l’OS comme dessin de la fenêtre. Pour ce faire il est obligatoire que le WindowStyle soit égal à None car WPF ne saurait pas dessiner la zone non-cliente (la zone de titre, les bordures ou même les effets de distorsion d’Aero) qui sont à la charge de Windows.

Il n’y a donc plus aucun WM_PAINT et tout le mécanisme Win32 de dessin est totalement zappé. Ceci explique par exemple que si l’on a un ActiveX ou un contrôle Win32 dans sa fenêtre, il n’apparaitra pas puisque lui continue à attendre les WM_PAINT et à écrire dans le device context standard (Je referai un post pour contourner ce problème).

C’est un mode performant et qui correspond bien à WPF puisque ce dernier contrôle tout et peut envoyer ses données en RGBA (ce qui est son mode de fonctionnement interne).

Hélas, comme nous l’avons vu plus haut, ce mode n’est pas dynamique et doit être fixer une fois pour toute à cause de l’inflexibilité de WPF sur ce point.

 

Mettre en place une region


Si toutefois, l’effet recherché est de ne pas avoir une fenêtre rectangulaire, mais de faire une fenêtre aux formes bizarres de manière dynamique, il reste la solution de définir une région:

[DllImport("gdi32.dll")]
static extern IntPtr CreateRoundRectRgn(int x1, int y1, int x2, int y2,
    int cx, int cy);

[DllImport("user32.dll")]
static extern int SetWindowRgn(IntPtr hWnd, IntPtr hRgn, bool bRedraw);

public MainWindow()
{
    InitializeComponent();
}

private void Button_Click(object sender, RoutedEventArgs e)
{
    WindowInteropHelper helper = new WindowInteropHelper(this);
    SetWindowRgn(helper.Handle, CreateRoundRectRgn(0, 0, 300, 300, 8, 8), true);
}

En effet, même si WPF fait sa sauce pour dessiner le contenu, notre fenêtre de part le fait qu’elle vit sous Windows doit se conformer aux fonctionnements standards et doit donc passer par le Windows Manager. Elle peut donc avoir une région définie (ce que l’on appelle aussi “Airspace”).

 

En conclusion, si l’on veut faire du per-pixel opacity, la seule solution reste le AllowTransparency et ce dernier ne peut être activé qu’avant l’affichage de la fenêtre.

.Net, WPF

TechDays 2011–Les vidéos

17. mars 2011

Les vidéos des sessions sont disponibles sur le site des TechDays.

Voici un lien direct vers les miennes:

.Net

Fuites de mémoire avec WPF 4.0 et de grosses listes d’images

15. mars 2011

Dans le cadre d’UrzaGatherer (http://urzagatherer.codeplex.com), j’ai découvert un problème bien embêtant avec une liste d’images en haute définition.

En effet j’ai une ListBox qui me présente les cartes d’une collection dans un WrapPanel (qui porte une jolie animation):

image

Or, l’utilisateur en se promenant de collections en collections va faire changer de nombreuses fois la source de la liste et va donc déclencher de nombreuses fois le chargement de ces grosses images.

Et j’ai constaté que lorsque je réaffecte le DataContext de ma liste à une nouvelle source, les anciennes données semblent ne pas être nettoyées de la mémoire.

Ainsi, UrzaGatherer peut allègrement occuper 2Go de RAM après avoir visité une dizaine de collections (chaque collection peut peser jusqu’à 300 Mo).

Je me suis donc mis en quête de la fuite.

Mais avant d’en venir à cette dernière regardons déjà comment le binding est fait et comment la liste est construite:

 

1 - Notre ListBox


La liste est donc une ListBox avec comme ItemPanel un WrapPanel dans lequel j’ai glissé un behavior en provenance de Blend pour faire un effet sympa d’animations:

            <ListBox ItemsSource="{Binding}" x:Name="imagesList">
                <ListBox.ItemTemplate>
                    <DataTemplate>
                        <Image Source="{Binding Bitmap, Mode=OneWay}"/>
                    </DataTemplate>
                </ListBox.ItemTemplate>
                <ListBox.ItemsPanel>
                    <ItemsPanelTemplate>
                        <WrapPanel Orientation="Horizontal" ItemHeight="300">
                            <Interactivity:Interaction.Behaviors>
                                <Layout:FluidMoveBehavior Duration="00:00:00.5">
                                </Layout:FluidMoveBehavior>
                            </Interactivity:Interaction.Behaviors>
                        </WrapPanel>
                    </ItemsPanelTemplate>
                </ListBox.ItemsPanel>
            </ListBox>

 

On peut voir que l’ItemTemplate se branche sur la propriété Bitmap de notre source.

2 - La source de données


Cette dernière est effectivement composée d’une liste de cartes qui portent une propriété Bitmap que je construis ainsi:

public BitmapImage Bitmap
{
    get
    {
        if (CompletePath == null)
            return null;

        BitmapImage bitmapImage = new BitmapImage();
        bitmapImage.BeginInit();
        bitmapImage.CreateOptions = BitmapCreateOptions.None;
        bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
        bitmapImage.UriSource = new Uri(CompletePath);
        bitmapImage.EndInit();
        bitmapImage.Freeze();

        return bitmapImage;
    }
}

A partir du chemin CompletePath, je produis une BitmapImage. L’objectif étant de contrôler le cache et de pouvoir créer les images dans un thread séparé pour les affecter par la suite. Toutefois, dans notre exemple, je simplifie cette étape en branchant directement le contrôle Image sur mon Bitmap sans passer par la création asynchrone.

 

3 – La fuite (les fuites?)


A partir de ce code relativement simple, il me suffit de brancher 3 ou 4 collections de cartes pour voir que la mémoire n’est jamais libérée.

Mes investigations m’ont permis de voir deux soucis:

  • Le behavior de Blend garde des références sur les images et de ce fait bloque le Garbage Collector et donc laisse les images en mémoire
  • L’utilisation d’une BitmapImage semble également laisser des traces en mémoire et malgré mes efforts je n’ai pas pu libérer ces ressources. La seule solution : faire un binding vers le chemin (CompletePath) plutôt que vers ma Bitmap.

 

Je me suis donc résigner à ne plus utiliser ni le behavior ni le chargement asynchrone. Toutefois pour ce dernier point, j’ai pu faire un contournement ainsi:

  • Créer une variable BindPath sur mes cartes
  • L’initialiser à null et mettre le binding du contrôle Image dessus
  • Binder la liste (donc pour le moment aucune image ne s’affiche puisque BindPath = null)
  • Faire une boucle dans une Task qui toutes les 20 millisecondes prend une carte et met sa propriété BindPath à CompletePath. De ce fait via le fait que mes cartes sont INotityPropertyChanged, la ListBox se met à jour progressivement et donne un effet progressif agréable (plutot qu’un bon vieux figeage de l’interface à l’ancienne)

 

Si vous aussi vous trouvez des fuites dans l’utilisation de WPF 4.0, n’hésitez pas à me le signaler Sourire

WPF, .Net, UrzaGatherer

Porter une application SqlServerCE 3.5 vers la nouvelle version 4.0

13. mars 2011

Avec la sortie du SP1 de Visual Studio 2010, je me suis motivé pour porter UrzaGatherer vers SqlServerCE 4.0.

Voici donc les étapes que j’ai suivies:

using (SqlCeEngine sqlCeEngine = new SqlCeEngine(“connectionString”))
{
      sqlCeEngine.Upgrade();
}

Par la suite il est possible d’utiliser les nouveautés style la génération de clefs mais dans mon cas comme je ne veux pas modifier ma base, cela s’arrêtera la.

En ce qui concerne Entity Framework,j’ai juste eu à changer la chaine de connexion pour remplacer le numéro de version:

provider=System.Data.SqlServerCe.3.5—>provider=System.Data.SqlServerCe.4.0

Il faut également modifier le .edmx à la main pour modifier la référence au bon provider:

<Schema Namespace="UrzaGathererModel.Store" Alias="Self" Provider="System.Data.SqlServerCe.3.5" ProviderManifestToken="3.5" … >

—>

<Schema Namespace="UrzaGathererModel.Store" Alias="Self" Provider="System.Data.SqlServerCe.4.0" ProviderManifestToken="4.0" … >

 

Une fois toutes ces modifications effectuées, il est possible de se servir de SqlServerCE 4.0 sans soucis et en profitant de toutes les améliorations.

Parmi ces dernières, celle qui m’a intéressé c’est le déploiement privé qui permet d’embarquer tout ce qu’il faut pour SqlServerCE 4.0 sans avoir ainsi besoin d’installer quoi que ce soit. Il suffit pour cela de rajouter et d’embarquer dans notre projet l’intégralité du répertoire C:\Program Files (x86)\Microsoft SQL Server Compact Edition\v4.0\Private et le tour est joué!

 

Pour retrouver tout ça en live, vous pouvez télécharger le SUBLIME UrzaGatherer : http://urzagatherer.codeplex.com

NB: Je viens de voir que le SKIP de Linq marche désormais! Super bonne nouvelle^^

.Net ,

Le truc à la con du jour : Enumérer les cartes DirectX11 présentes sur le système

26. février 2011

Problème du jour : Si l’on part du code d’énumérations des cartes présentes sur un système, on peut ne pas voir les cartes DirectX11.

Le code (sous SlimDX) standard pour énumérer les cartes présentes est le suivant:

FeatureLevel[] levels = {
                            FeatureLevel.Level_11_0
                        };
// Creation du device Factory factory = new Factory(); for (int index = 0; index < factory.GetAdapterCount(); index++) { Adapter adapter = factory.GetAdapter(index); try { device11 = new Device(adapter, DeviceCreationFlags.None, levels); break; } catch { ... } }

Le fonctionnement est donc:

  • Définir notre FeatureLevel (ici c’est bien du DirectX11)
  • Créer une Factory pour accéder à la liste des adapters (les cartes graphiques)
  • Tenter de construire notre device sur l’adapter avec le FeatureLevel attendu

 

Or si on se contente de faire cela, nous n’accèderons pas à notre carte DirectX11. En effet ce code ne marche que pour DirectX9 et 10.

Pour DirectX11 (et même si je trouve ça regrettable), il faut changer la classe de notre Factory pour que cette dernière reconnaisse les cartes DX11:

Factory1 factory = new Factory1();

C’est aussi bête que cela…

DirectX, .Net

Moteur de particules pour Windows Phone 7

24. février 2011

L’objectif de ce tutoriel est essentiellement de nous amuser avec la partie 3D de XNA pour Windows Phone 7.

Pour que cela soit ludique, nous allons produire un applicatif qui permettra à l’utilisateur de bouger des émetteurs de particules. Cela dans un but uniquement contemplatif (un peu comme le simulateur d’eau sur la table Surface).

Le code complet est disponible juste là.

 

Qu’est-ce qu’un simulateur de particules?


Une particule est une entité autonome définie par plusieurs paramètres parmi lesquels comptent surtout la position et la direction de déplacement.

Ainsi un simulateur de particules est un ensemble de particules qui se déplacent simultanément. Selon ce que l’on souhaite gérer, on peut rajouter de nombreuses fioritures. Dans notre cas, nous allons gérer la position, le déplacement, la couleur, la taille, la gravité et même les collisions avec le sol.

La structure de notre particule va donc être:

    public class Particle
    {
        public Vector3 Position;
        public float Size;
        public Color Color;
        public int Age;
        public int DeadAge;
        public Vector3 Direction;
        public float Speed;
    }

On peut noter que la particule possède un Age et un DeadAge. En effet, lors de l’émission de la particules, cette dernière est âgée de 0 frame. A chaque update, son âge augmente de 1 et dès que l’âge = DeadAge, la particule est détruite et remplacée par une nouvelle.

 

Mise en place du mécanisme de rendu


Pour dessiner nos particules sur WP7, nous allons utiliser XNA 4.0. Ce dernier va nous permettre d’avoir un accès rapide au GPU car si l’on veut dessiner plus de 2000 particules en simultané, il faudra de la puissance.

Pour commencer nous allons initialiser un GraphicsDeviceManager. Ce dernier est notre intermédiaire avec le GPU et l’accélération graphique:

            graphics = new GraphicsDeviceManager(this)
                           {
                               PreferredBackBufferWidth = 480,
                               PreferredBackBufferHeight = 800,
                               PreferredDepthStencilFormat = DepthFormat.Depth24,
                               IsFullScreen = true
                           };
            Content.RootDirectory = "Content";

            // Frame rate is 30 fps by default for Windows Phone.
            TargetElapsedTime = TimeSpan.FromTicks(333333);

Nous demandons ici un rendu sur toute la surface (480x800). Il est à noter que nous pourrions demander moins pour avoir plus de puissance, sachant que le WP7 fournit un matériel dédié pour la remise à taille (en gros on dessine en 240x400 par exemple et le matériel du WP7 fait le zoom sans consommer de puissance CPU/GPU).

Nous demandons également un Depth Buffer de 24bits (le plus précis que l’on puisse avoir). Ce qui veut dire que chaque pixel aura sa profondeur stocké dans un flottant de 24 bits. Cette profondeur permet de savoir si le pixel que l’on va écrire est plus proche que celui qui a déjà été écrit. Ainsi comme il n’est pas possible de garantir que l’on écrive les pixels dans le bon ordre, le Depth Buffer garantit que c’est toujours le pixel le plus proche qui est conservé.

Pour dessiner nos particules, nous allons avoir également besoin d’une texture (celle qui sera dessinée sur chaque particule) et d’un effet. Il existe plusieurs effets dans XNA 4.0 pour WP7, ces derniers sont en fait des shaders (pixel + vertex) qui permettent de rendre des objets 3D avec diverses options supportées.
Dans notre cadre, nous allons utiliser un BasicEffect (l’effet de base qui gère une texture, un éclairage et la coloration par vertex):

            particleTexture = Content.Load<Texture2D>("part");

            // Effect
            effect = new BasicEffect(graphics.GraphicsDevice)
                         {
                             Alpha = 1.0f,
                             TextureEnabled = true,
                             FogEnabled = false,
                             VertexColorEnabled = true,
                             LightingEnabled = false,
                             World = Matrix.Identity
                         };
Ici, nous configurons notre effet pour qu’il ne gère que la texture et la couleur par vertex. Nous n’avons pas besoin de l’éclairage.

 

Stocker nos particules pour la carte graphique


Une notion importante dans le cadre de XNA est la séparation des mémoires. En effet, le CPU et le GPU ne travaillent pas sur les mêmes mémoires. Ainsi, tout ce que devra traiter le GPU doit être mis dans sa mémoire.

C’est ici qu’interviennent les vertex et index buffers. Ces buffers servent à stocker les données de rendus nécessaires au GPU.

Le vertex buffer contient la liste des vertices d’un objet 3D (les points dans l’espace). XNA est suffisamment souple pour permettre de stocker ce que l’on souhaite par vertex. Dans notre cas nous allons définir nos vertices ainsi:

    public struct ParticleVertex : IVertexType
    {
        public const int Stride = 24;

        public Vector3 Position;
        public Vector2 TextureCoordinates;
        public Color Color;

        public VertexDeclaration VertexDeclaration
        {
            get
            {
                return new VertexDeclaration(new[]
                                                 {
new VertexElement(0, VertexElementFormat.Vector3, 
VertexElementUsage.Position, 0),

new
VertexElement(12, VertexElementFormat.Vector2,
VertexElementUsage.TextureCoordinate, 0),

new
VertexElement(20, VertexElementFormat.Color,
VertexElementUsage.Color, 0), } ); } } }

Notre vertex contient donc une position (obligatoire), des coordonnées de textures (pour afficher notre texture) et une couleur (cela permettra de faire des particules multicolores).

Notons au passage que notre structure implémente l’interface IVertexType qui nous permet de décrire à XNA la structure (VertexDeclaration) d’un vertex. Ceci est en effet nécessaire pour que les shaders des effets lisent correctement nos structures.

Les index buffers contiennent la définition des faces sous la forme de 3 entiers par face qui donnent les numéros des vertices de chaque face. Ainsi l’index buffer [0, 1, 2, 0, 2, 3] définit deux faces qui pointent vers 4 vertices différents. La face 0 contient les vertices 0, 1 et 2. La face  contient les vertices 0, 2 et 3. Ainsi on peut définir une réutilisabilité pour défnir nos triangles. Sans index buffer, il faudrait 6 vertices pour faire 2 triangles. Avec un index buffer, il n’en faut plus que 4 puisque dans notre cas les 2 faces partagent 2 vertices.

Ainsi, chaque système de particules va donc posséder un vertex buffer et un index buffer.

Pour rendre les particules, le code donne donc:

  device.SetVertexBuffer(vertexBuffer);
  device.Indices = indexBuffer;
            
  device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, 
currentParticlesCount * 4, 0, currentParticlesCount * 2);

On indique au système notre vertex buffer, notre index buffer et on demande le rendu d’une liste de triangles.

 

Préparation des buffers


Le principe de fonctionnement général de notre système est de faire évoluer nos particules sur le CPU et avant chaque rendu de préparer un vertex buffer avec les informations nécessaires pour le rendu sur le GPU.

Le vertex buffer devra contenir 2 triangles par particules (chaque particule est en effet un quad toujours orienté vers la caméra). Le vertex buffer mesurera donc 4 x nb_particules. (Dans les précédentes versions de XNA, il nous aurait été possible de mettre un vertex par particule en utilisant le PointList, mais cette topologie de rendu n’est plus supportée par XNA 4.0).

L’index buffer sera statique puisqu’il définit des suites de triangles fonctionnants sur 4 vertices:

indexBuffer = new IndexBuffer(device, typeof(short), 
maxParticlesCount * 6, BufferUsage.WriteOnly);
short[] indices = new short[maxParticlesCount * 6]; for (int index = 0; index < maxParticlesCount; index++) { indices[index * 6] = (short)(index * 4); indices[index * 6 + 1] = (short)(index * 4 + 1); indices[index * 6 + 2] = (short)(index * 4 + 2); indices[index * 6 + 3] = (short)(index * 4); indices[index * 6 + 4] = (short)(index * 4 + 2); indices[index * 6 + 5] = (short)(index * 4 + 3); } indexBuffer.SetData(indices);

 

La mise à jour va donc parcourir chaque particule pour effectuer le calcul de son évolution:

for (int i = 0; i < particles.Count; i++)
{
    Particle particle = particles[i];
    particle.Age++;

    if (particle.Age >= particle.DeadAge)
    {
        SpawnNewParticle(i);
        continue;
    }
    
particle.Position += particle.Direction * particle.Speed + Gravity;
// Ground collisions if (particle.Position.Y < GroundHeight + particle.Size) { particle.Position.Y = GroundHeight + particle.Size; particle.Direction = particle.Direction + Gravity; particle.Direction.Y = Math.Abs(particle.Direction.Y); }

On peut noter que dès que l'âge est supérieur ou égal au DeadAge, nous appelons SpawnNewParticle(i) qui permet remplacer la particule i par une nouvelle particule.

Une fois que chaque particule a été traitée, il faut la transférer dans le vertex buffer via le code suivant:

for (int i = 0; i < particles.Count; i++)
{
    …
// Update Color color = particle.Color; color.A = (byte) (255 * (particle.DeadAge - particle.Age)
/ (float) particle.DeadAge); Vector3 position = Vector3.Transform(particle.Position, viewMatrix); vertices[index * 4].Position = position + new Vector3(-1, -1, 0) *
particle.Size; vertices[index * 4].Color = color; vertices[index * 4 + 1].Position = position + new Vector3(1, -1, 0) *
particle.Size; vertices[index * 4 + 1].Color = color; vertices[index * 4 + 2].Position = position + new Vector3(1, 1, 0) *
particle.Size; vertices[index * 4 + 2].Color = color; vertices[index * 4 + 3].Position = position + new Vector3(-1, 1, 0) *
particle.Size; vertices[index * 4 + 3].Color = color; currentParticlesCount++; index++; }

Afin de faire en sorte que la particule regarde toujours la caméra, nous partons de la position de la particule et nous la transformons par la matrice de vue pour avoir directement sa position dans le monde de la caméra.

A partir de cette nouvelle position, il suffit de générer 4 vertices qui prennent pour centre cette position et qui se décalent pour former un carré.

De plus, pour être efficace, nous ne travaillons pas avec un VertexBuffer mais avec un DynamicVertexBuffer qui possède l’énorme avantage de pouvoir être modifié par le CPU autant que l’on le souhaite:

vertexBuffer.SetData(vertices, 0, currentParticlesCount * 4);

 

Génération d’une particule


Chaque particule est définie par un ensemble de propriétés que nous avons évoquées plus haut.

La génération d’une particule doit donc renseigner toutes ses propriétés et pour ajouter un peu de hasard dans notre système, nous tirons aléatoirement chaque valeur dans une plage définie par le système:

        void SpawnNewParticle(int index)
        {
            Particle particle = new Particle
{
  Color = Tools.GetRandomColor(MinColor, MaxColor),
Position = Center + new Vector3(Tools.GetRandomFloat(-EmitRadius, EmitRadius),
Tools.GetRandomFloat(-EmitRadius, EmitRadius),
Tools.GetRandomFloat(-EmitRadius, EmitRadius)),
Size = Tools.GetRandomFloat(MinSize, MaxSize), DeadAge = Tools.GetRandomInt(MinDeadAge, MaxDeadAge), Direction = Tools.GetRandomVector3(MinDirection, MaxDirection), Speed = Tools.GetRandomFloat(MinSpeed, MaxSpeed) }; if (index >= 0) { particles[index] = particle; } else particles.Insert(0, particle); }

Ainsi pour définir un système émettant 1000 particules bleus vers le bas, cela donne:

blueSystem = new ParticlesSystem(graphics.GraphicsDevice, 1000)
                {
                    Gravity = new Vector3(0, -0.09f, 0),
                    MinDirection = new Vector3(-1, -1, -1),
                    MaxDirection = new Vector3(1, 0, 1),
                    MinColor = Color.DarkBlue,
                    MaxColor = Color.Blue
                };

 

Gestion de la caméra


Finalement, pour interagir avec notre utilisateur nous allons fournir une caméra de type ArcRotate, c’est à dire qui se déplace sur la surface d’une sphère centrée sur un point cible.

Cette caméra a besoin de deux angles pour définir sa position sur la sphère (Alpha : angle de rotation autour de l’axe Y et Béta : angle de rotation autour de X):

float cosa = (float)Math.Cos(Alpha);
float sina = (float)Math.Sin(Alpha);
float cosb = (float)Math.Cos(Beta);
float sinb = (float)Math.Sin(Beta);

Vector3 cameraPosition = new Vector3(Radius * cosa * sinb, Radius * cosb, 
Radius * sina * sinb);
Vector3 target = new Vector3(0, 0, 0); viewMatrix = Matrix.CreateLookAt(cameraPosition, target, Vector3.Up);

Ce code permet de calculer la View Matrix (la matrice de la caméra) qui indique donc la position de la caméra et ce qu’elle regarde.

Rendu des systèmes


Pour conclure, dans le cadre du rendu de plusieurs systèmes, il nous suffit de régler notre effet pour:

  • Activer la transparence de type NonPremultiplied, c’est à dire que chaque pixel s’écrit avec la formule suivante : AlphaSource * ColorSource + (1 – AlphaSource) * DestinationColor
  • Mettre le DepthBuffer en mode ReadOnly pour que les particules ne se masquent pas entre elles tout en tenant compte des autres objets de la scène
  • Désactivation du culling pour voir les particules de chaque coté (pas d’élimination des faces cachées)

 

Donc cela donne:

// System
graphics.GraphicsDevice.DepthStencilState = DepthStencilState.DepthRead;
graphics.GraphicsDevice.BlendState = BlendState.NonPremultiplied;
graphics.GraphicsDevice.RasterizerState = RasterizerState.CullNone;

effect.View = Matrix.Identity;
effect.Projection = camera.ProjectionMatrix;
effect.Texture = particleTexture;

foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
    pass.Apply();

    blueSystem.Render();
    yellowSystem.Render();
}

Ajout du sol


Pour donner un point de référence visuel, nous allons ajouter un sol issu d’un modèle FBX (exporté depuis 3DS Max).

L’intégration est simplissime avec XNA:

ground = Content.Load<Model>("ground");

Dans la phase de rendu, avant de dessiner les particules, nous demandons le rendu de notre sol:

// Ground
graphics.GraphicsDevice.DepthStencilState = DepthStencilState.Default;
graphics.GraphicsDevice.BlendState = BlendState.Opaque;
graphics.GraphicsDevice.RasterizerState = RasterizerState.CullCounterClockwise;
graphics.GraphicsDevice.SamplerStates[0] = textureState;
ground.Draw(Matrix.CreateTranslation(0, groundHeight, 0), camera.ViewMatrix, 
camera.ProjectionMatrix);

Conclusion


La touche finale est mise en place avec la gestion du touchscreen pour déplacer le centre de chaque système. Quelques sprites pour faire l’interface utilisateur et le tour est joué.

Grâce à la simplicité de XNA, à l’accèlération matérielle et aux DynamicVertexBuffer, il est possible sur nos WP7 de faire tourner une scène avec plus de 2000 particules en simultané et cela de manière fluide;

Nous vivons une belle époque Sourire

.Net, Windows Phone, Tutorial

Les démos de mes sessions des TechDays 2011

10. février 2011

Voila, c’est fini. Je me suis encore une fois bien amusé lors de mes deux sessions aux TechDays avec une mention spéciale pour la Coding4Fun ou j’ai pris un grand plaisir dans ce non-espace plein de n’importe quoi accompagné de mes deux valeureux acolytes (Mitsu Furuta et Pierre Cauchois).

En attendant les webcasts qui seront disponibles sur le site officiel, vous pourrez télécharger ci-dessous les démos des deux sessions:

 

Voila et comme d’habitude n’hésitez pas à poser des questions ou laisser des commentaires j’y réponds avec plaisir.

.Net

TechDays 2011–Les immanquables

7. février 2011

Demain, (Mardi donc pour les deux du fond qui parlent) c’est le début des TechDays 2011.

Si vous ne devez voir que 2 sessions, je vous invitent à venir découvrir votre serviteur dans ses œuvres:

Mardi 08/02/2011 13h00/14h00 : DÉVELOPPER DES INTERFACES RÉACTIVES AVEC WPF 4
Mardi 08/02/2011 17h30/18h30
CODING4FUN

 

Par la suite, je serai sur le stand de Bewise ou je me ferai un plaisir de parler des sujets (techniques ou non) qui vous intéresserez.

Donc en gros, n’hésitez pas, venez me voir, ca me fait plaisir Sourire

.Net