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

Archéologie

20. février 2011

L’avantage de préparer un déménagement, c’est que l’on tombe parfois sur des petites perles.

En rangeant, j’ai en effet retrouvé mes toutes premières démos.

Nous sommes en 1996, j’ai 20 ans, le groupe s’appelle Ze Optimization Bringers (notez l’humour déjà si fin et enlevé). Parmi les points marquants, on peut apercevoir les débuts de ce qui deviendra Nova et qui alors s’appelait Z3D.

Tout cela tourne intégralement au CPU (386/486) sous DOS. C’est entièrement fait en C, moulé à la louche (Du Watcom C pour les vétérans du fond). Mon pseudo de l’époque c’était Blacklord (j’aimais déjà beaucoup Starwars).

Creation @ The Place To be 4 (Demo-party superbe organisé par mon pote Fred Brunel aka Bloody):

J’ai même oser faire quelques (sublimes) graphes sur celle la. On ne doute de rien quand on est jeune…

 

Evolution @The Volcanic Party (Grosse demo-party française de l’époque):

Un peu plus de 3D, toujours aussi peu de design.

Bien évidemment, il faut se remettre dans le contexte technologique de l’époque. Les machines avaient alors le quart de la puissance d’un Windows Phone.

On se marrait déjà pas mal en tout cas Sourire

P1010068

 

Allez pour finir la dernière démo que l’on à faite (avec mon petit Mitch d’ailleurs) : New Millenium @ Slach 3.

La ça ne déconne plus, nous sommes en 2001. Le groupe s’appelle Zalem Corp (plus politiquement correct que ZoB). Le processus de production est déjà en place : conception sous Max, moteur DirectX en C++ (Nova 3.0), un vrai graphiste (Merci Mitch^^), super musicien (Merci Laurent aka Zaac). Nous avions d’ailleurs gagné le concours Démos PC de la Slach 3 avec cette démo.

3D, Divers ,

La fin d’un cycle

19. février 2011

J’ai eu beaucoup de mal à me lancer pour faire ce post, tout simplement car je ne savais pas comment le commencer. Alors on va faire simple et efficace.

Après 12 ans de loyaux services à Bewise/Vertice, j’ai décidé de changer de vie et de me lancer dans un nouveau cycle de loyaux services pour une autre entreprise.

Ainsi, à partir du 1 avril 2011 (la bonne blague), je serai officiellement membre de la grande maison Microsoft en tant qu’Evangéliste Développeur. Une de mes principales missions sera d’animer l’éco-systèmes des développeurs autour des technologies Microsoft. Je reviens ainsi à mes premiers amours.

Bon il est vrai que je fais les choses à contre-courant de ce que l’on voit souvent:

  • J’étais le patron, je deviens employé
  • J’avais une maison, je vais vivre en appartement
  • J’avais 2500m² de terrain, je vais avoir 6m² de balcon
  • Je vivais à la campagne, j’ai dorénavant un appartement au 24eme étage dans le 15eme
  • Je faisais de la gestion d’entreprise, je vais faire de la technique et remettre les mains dans le code

 

Ce genre de choix ne se fait pas facilement surtout que j’étais imprégné jusqu’au plus profond de mon être de tout ce qui faisait Bewise et Vertice. Mais l’appel de l’aventure dans une boutique comme Microsoft avec les débouchés et les opportunités technologiques et humaines que cela implique est une force irrésistible.

Je sais que je laisse derrière moi des entreprises qui sont entre de bonnes mains avec mes chers ex-associés (Yann, Fred, Mitch, Laurent) qui tiennent fermement la barre et gardent le cap.

J’en profite d’ailleurs pour faire un gros poutou (c’est le moment girly/flon flon du post) à chaque employé (même ceux qui râlent constamment ou qui me brisaient les nouilles avec leur augmentation) de Bewise et de Vertice.  Je vous ai préparé un apéro d’adieu à base de concentré de cholestérol pour fêter ça.

Je souhaite également beaucoup de réussites à Jean-Pierre, Angélina, Christophe et Sacha dans la mission qui est la leur aujourd’hui : Faire que Bewise continue de briller dans le firmament des entreprises technologiques (petite poussée non contrôlée de lyrisme…)

Quand à moi avec l’arrivée de Windows 8, avec les Windows Phones, avec l’UX qui prend (enfin) de plus en plus de place, j’ai de quoi faire. Pleins de blogs, de sessions techniques, de démos. D’ailleurs au final ce qui me tue dans cette affaire, c’est que Microsoft va me payer pour faire ça Sourire.

Blog ,

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