09 Textures : Concepts de base

La dernière fois, nous avons parlé des bases de l'éclairage d'une scène. Nous étions capable d'avoir un triangle rotatif qui devenait plus sombre ou plus clair lorsqu'il se tournait ou se détournait de la lumière. Ca ressemblait à cela :

Bien que mignon, ce triangle tricolore est difficilement utilisable pour faire des scènes réalistes. En général, ce que nous voulons est la possibilité d'appliquer des images sur les surfaces de nos triangles afin qu'ils puissent ressembler à de la brique, du métal, du cuir ou n'importe quoi.

Direct3D nous permet de le faire au travers de ce que nous appelons les textures. Une texture est simplement une image - par exemple un BMP - qui est "collée" ou "clouée" sur un triangle ou une série de triangles. Si vous le faites bien, vous pouvez obtenir des objets très détaillés faits seulement de quelques triangles et ainsi traiter le rendu très rapidement.

Pour comprendre les textures, nous avons besoin de présenter encore un autre système de coordonnées. Heureusement, ces coordonnées de texture sont assez simples. Pour commencer elles sont juste bi-dimentionnelles. Ces dimensions sont souvent référencées avec des variables u et v, un peu comme on exprime les coordonnées objet avec x, y et z.

Voici l'idée de base. Disons que j'ai une image comme celle ci :

C'est moi, quand je vais à Hawaii. Disons que pour une raison étrange, je veux appliquer cette image sur un triangle comme ceci :

Remarquez que l'image est appliquée de côté - elle a été tournée de 90° vers la droite, et bien sûr, comme elle est appliquée à la moitié du triangle, la moitié a disparu. C'est comme ça que je me sens quand je reviens de Hawaii.

Pour arriver à cela, je vais introduire un autre format de sommets : PositionNormalTextured. A partir de maintenant vous avez vu suffisament de formats de sommets pour avoir une petite idée de ce que ça signifie : chaque point du triangle contiendra une position 3D, un vecteur normal de surface pour l'éclairage, et de l'information en rapport avec les textures.

L'information de texture suppplémentaire qu'on ajoute aux sommets sont u et v, les coordonnées de textures que j'ai mentionné plus haut. Les coordonnées de texture correspondent à une position dans l'image originelle où se situerait le sommet. La coordonnée 0, 0 signifie "le coin superieur gauche de l'image"; 1, 0 signifie "le coin superieur droit de l'image" etc. Remarquez que u et v vont de 0 à 1 sans se soucier de la taille de l'image en pixels. (ndt : on peut donner une valeur supérieure à 1, l'image sera répétée).

Ainsi, pour obtenir la rotation de 90° pour l'application de texture que j'ai montrée plus haut, ce que nous désirons est quelquechose comme cela :

En spécifiant (1, 0) à la coordonnée de texture du coin inférieur droit du triangle, nous lui disons de l'appliquer au coin supérieur droite de l'image. Les coordonnées de texture, comme les couleurs et vecteurs normaux, sont extrapolées sur toute la surface du triangle. Ainsi, un point à mi-chemin sur le bord du dessous du triangle correspond à un point à mi-chemin sur le côté droit de l'image.

C'est la théorie. Comment mettre en pratique? C'est assez facile. Pour illustrer, je vais rester simple : nous utiliserons des coordonnées transformées plutot qu'encombrer le code avec le normal du sommet et l'éclairage dont on a parlé la dernière fois. Rappelez vous que les coordonnées transformées sont assimilables aux coordonnées écran.

Voici comment notre InitializeGraphics ressemblera :

private Texture texture; protected bool InitializeGraphics() { PresentParameters pres = new PresentParameters(); pres.Windowed = true; pres.SwapEffect = SwapEffect.Discard; device = new Device(0, DeviceType.Hardware, this, CreateFlags.SoftwareVertexProcessing, pres); // Nouvelle ligne texture = CreateOverlayTexture(device); vertices = CreateVertexBuffer(device); return true; }

Il n'y a vraiment que 2 nouveaux bouts de code ici. CreateOverlayTexture est l'endroit où nous chargeons le fichier qu'on veut appliquer à notre triangle, et le nouveau membre à la classe "texture" est là où nous stoquons la texture chargée. En regardant CreateOverlayTexture, nous pouvons constater que c'est très simple :

protected Texture CreateOverlayTexture(Device device) { Texture t = TextureLoader.FromFile(device, "texture.bmp"); return t; }

La fonction TextureLoader.FromFile fait tout le dur travail. Nous passons un Device et un nom de fichier, et ça nous rend un objet Microsoft.DirectX.Direct3D.Texture. Il ya juste un hic : TextureLoader se situe en fait dans l'assemblage Microsoft.DirectX.Direct3DX, pas dans Microsoft.DirectX.Direct3D, comme tout ce que nous avons manipulé jusque là. Cela signifie que vous devrez ajouter une référence à Direct3DX sinon ça ne compilera pas. Direct3DX est un assemblage utilitaire qui est plein à craquer de choses utiles. Nous verrons certaines de ses fonctionnalités dans de futurs articles.

Avec notre objet texture en main, nous pouvons créer notre nouvel objet texturé. Nous faisons cela en définissant un VertexBuffer.

protected VertexBuffer CreateVertexBuffer(Device device) { VertexBuffer buf = new VertexBuffer( typeof(CustomVertex.TransformedTextured), 3, device, 0, CustomVertex.TransformedTextured.Format, Pool.Default); PopulateVertexBuffer(buf); return buf; } protected void PopulateVertexBuffer(VertexBuffer vertices) { // Notez qu'on utilise TransformedTextured CustomVertex.TransformedTextured[] verts = (CustomVertex.TransformedTextured[]) vertices.Lock(0, 0); int i = 0; verts[i++] = new CustomVertex.TransformedTextured( Width / 2, Height / 4, 0.5F, // position du sommet 1, // rhw (avancé) 0, 1); // coordonnées texture verts[i++] = new CustomVertex.TransformedTextured( Width * 3 / 4, Height * 3 / 4, 0.5F, 1, 1, 0); verts[i++] = new CustomVertex.TransformedTextured( Width / 4 , Height * 3 / 4, 0.5F, 1, 1, 1); vertices.Unlock(); }

Tout ceci devrait vous sembler familier. J'ai mis en évidence la seule nouvelle chose : la création des sommets TransformedTextured, et même, c'est assez proche de ce qu'on a déjà fait jusque là.

J'utilise des coordonnées transformées pour garder un code propre. Rappelez vous que les coordonnées transformées ne sont que les coordonnées écran, ce qui veut dire qu'on n'a pas à mettre en place des transformateurs monde, de vue ou de perspective. Ainsi les arguements du constructeur pour les sommets TransformedTextured sont : les position écran x y, une valeur z qui n'a pas d'importance, une valeur rhw qui devrait toujours être à 1, et enfin nos coordonnées de texture.

Ce que j'ai fait ici est de mettre en place un triangle plus ou moins équilatarel où le sommet du haut est le coin inferieur gauche de l'image, le sommet en bas à droite est le coin supérieur droit de l'image et le sommet en bas à gauche est le coin inférieur droit de l'image. A l'affichage, ça ressmeble à ceci :

Mais comment afficher cela? Après tout, nulle part dans les sommets j'ai dit "utilise cette image de Craig à Hawaii". Les textures ne fonctionnent pas comme ça en Direct3D. A la place, nous faisons ceci :

protected void Render() { device.Clear(ClearFlags.Target, Color.Black, 1.0F, 0); device.BeginScene(); // Nouvelle ligne device.SetTexture(0, texture); device.VertexFormat = CustomVertex.TransformedTextured.Format; device.SetStreamSource(0, vertices, 0); device.DrawPrimitives(PrimitiveType.TriangleList, 0, 1); device.EndScene(); device.Present(); }

Là aussi, ce code devrait vous être familier sauf pour la nouvelle ligne. Ce que fait SetTexture est de dire "Hey, si quelqu'un veut afficher un sommet avec de l'information de texture, il devrait utiliser celle ci". En l'occurence, vous pouvez avoir en fait plusieurs textures actives en même temps, ce qui explique le 1er argument. Les textures multiples est un sujet avancé que nous aborderons une autre fois, pour l'instant passons simplement 0 pour indiquer qu'on veut utiliser uniquementn la première texture.

Comme vous le voyez, le reste est constitué des choses qu'on a déjà vues - appeler BeginScene, utiliser DrawPrimitives, etc..

J'ai donné le listing complet ci-dessous. Allez donc faire des expériences en changeant les coordonnées de texture pour contrôler la façon dont l'image s'applique sur la surface. Si vous êtes vraiment ambitieux, tentez d'appliquer la texture sur un carré en ajoutant un second triangle.

La prochaine fois, nous verrons comment nous passer des triangles en utilisant des formes bien plus compliquées : les Meshes.

Le Code

using System; using System.Drawing; using System.Collections; using System.ComponentModel; using System.Windows.Forms; using System.Data; using Microsoft.DirectX; using Microsoft.DirectX.Direct3D; namespace Craig.DirectX.Direct3D { public class Game : System.Windows.Forms.Form { private Device device; private VertexBuffer vertices; private Texture texture; static void Main() { Game app = new Game(); app.Width = 800; app.Height = 600; app.InitializeGraphics(); app.Show(); while (app.Created) { app.Render(); Application.DoEvents(); } app.DisposeGraphics(); } protected bool InitializeGraphics() { PresentParameters pres = new PresentParameters(); pres.Windowed = true; pres.SwapEffect = SwapEffect.Discard; device = new Device(0, DeviceType.Hardware, this, CreateFlags.SoftwareVertexProcessing, pres); texture = CreateOverlayTexture(device); vertices = CreateVertexBuffer(device); return true; } protected VertexBuffer CreateVertexBuffer(Device device) { VertexBuffer buf = new VertexBuffer( typeof(CustomVertex.TransformedTextured), 3, device, 0, CustomVertex.TransformedTextured.Format, Pool.Default); PopulateVertexBuffer(buf); return buf; } protected void PopulateVertexBuffer(VertexBuffer vertices) { CustomVertex.TransformedTextured[] verts = (CustomVertex.TransformedTextured[]) vertices.Lock(0, 0); int i = 0; verts[i++] = new CustomVertex.TransformedTextured( Width / 2, Height / 4, 0.5F, // position du sommet 1, // rhw (avancé) 0, 1); // coorodnnées texture verts[i++] = new CustomVertex.TransformedTextured( Width * 3 / 4, Height * 3 / 4, 0.5F, 1, 1, 0); verts[i++] = new CustomVertex.TransformedTextured( Width / 4 , Height * 3 / 4, 0.5F, 1, 1, 1); vertices.Unlock(); } protected Texture CreateOverlayTexture(Device device) { Texture t = TextureLoader.FromFile(device, "texture.bmp"); return t; } protected void Render() { device.Clear(ClearFlags.Target, Color.Black, 1.0F, 0); device.BeginScene(); device.SetTexture(0, texture); device.VertexFormat = CustomVertex.TransformedTextured.Format; device.SetStreamSource(0, vertices, 0); device.DrawPrimitives(PrimitiveType.TriangleList, 0, 1); device.EndScene(); device.Present(); } protected void DisposeGraphics() { vertices.Dispose(); device.Dispose(); } } }

Le code si vous voulez qu'il tourne! mais avec la texture

Merci à adudley pour sa contribution!

using System; using System.Drawing; using System.Collections; using System.ComponentModel; using System.Windows.Forms; using System.Data; using Microsoft.DirectX; using Microsoft.DirectX.Direct3D; namespace Craig.Direct3D { public class Game : System.Windows.Forms.Form { private Device device; private VertexBuffer vertices; private Texture texture; static void Main() { Game app = new Game(); app.InitializeGraphics(); app.Show(); while (app.Created) { app.Render(); Application.DoEvents(); } app.DisposeGraphics(); } protected bool InitializeGraphics() { PresentParameters pres = new PresentParameters(); pres.Windowed = true; pres.SwapEffect = SwapEffect.Discard; device = new Device(0, DeviceType.Hardware, this, CreateFlags.SoftwareVertexProcessing , pres); //changer cela plus tard device.RenderState.CullMode = Cull.None; device.RenderState.Lighting = false; texture = CreateOverlayTexture(device); vertices = CreateVertexBuffer(device); return true; } protected Texture CreateOverlayTexture(Device device) { Texture t = TextureLoader.FromFile(device, "texture.bmp"); return t; } protected void Render() { // Paint la surface en noir partout device.Clear(ClearFlags.Target, Color.Bisque, 1.0F, 0); device.BeginScene(); device.SetTexture(0, texture); // Appels aux fonction 3D viennent ici SetupMatrices(); device.SetStreamSource(0, vertices, 0); device.DrawPrimitives(PrimitiveType.TriangleList, 0, 1); device.EndScene(); device.Present(); } protected VertexBuffer CreateVertexBuffer(Device device) { device.VertexFormat = CustomVertex.PositionTextured.Format; VertexBuffer buf = new VertexBuffer( typeof(CustomVertex.PositionTextured ), //Quel type 3, // Combien device, // Le device 0, // Utilisation par défaut CustomVertex.PositionTextured.Format, // Format de sommet Pool.Default); // Pooling par défaut PopulateVertexBuffer(buf); return buf; } protected void PopulateVertexBuffer(VertexBuffer vertices) { CustomVertex.PositionTextured [] verts = (CustomVertex.PositionTextured[]) vertices.Lock(0, 0); int i = 0; verts[i++] = new CustomVertex.PositionTextured ( 0, 1, 0,0,1); verts[i++] = new CustomVertex.PositionTextured( -0.5F, 0, 0, 1,0); verts[i++] = new CustomVertex.PositionTextured( 0.5F, 0, 0, 1,1); vertices.Unlock(); } protected void SetupMatrices() { float angle = Environment.TickCount / 500.0F; device.Transform.World = Matrix.RotationY(angle); device.Transform.View = Matrix.LookAtLH(new Vector3(0, 0.5F, -3), new Vector3(0, 0.5F, 0), new Vector3(0, 1, 0)); device.Transform.Projection = Matrix.PerspectiveFovLH((float)Math.PI/4.0F,1.0F, 1.0F, 5.5F); } protected void DisposeGraphics() { device.Dispose(); } } }