La dernière fois nous avons vu comment utiliser les matériaux pour définir des propriétés comme la couleur et la spécularité d'un objet. Ceci deviendra important lorsque nous parlerons des meshes, la prochaine fois. Une dernière chose à étudier avant d'aborder les meshes : les z-buffers.
Le problème que resoud les z-buffers est qu'il est très difficile dans une scène complexe de définir qu'est-ce qui vient devant quoi. Pour illuster cela, considérons un cas simple : 2 triangles. Faisons en un rouge et proche, et l'autre jaune et loin. Le code pour mettre en place ceci ressemblerait à celà :
protected void PopulateVertexBuffer(VertexBuffer vertices) { // Crée 2 triangles CustomVertex.PositionNormalColored[] verts = (CustomVertex.PositionNormalColored[]) vertices.Lock(0, 0); int i = 0; verts[i++] = new CustomVertex.PositionNormalColored(0, 0.5f, 0, 0, 0, -1, Color.Red.ToArgb()); verts[i++] = new CustomVertex.PositionNormalColored(0.3f, 0, 0, 0, 0, -1, Color.Red.ToArgb()); verts[i++] = new CustomVertex.PositionNormalColored(-0.3f, 0, 0, 0, 0, -1, Color.Red.ToArgb()); verts[i++] = new CustomVertex.PositionNormalColored(0, 0.5f, 2, 0, 0, -1, Color.Yellow.ToArgb()); verts[i++] = new CustomVertex.PositionNormalColored(0.3f, 0, 2, 0, 0, -1, Color.Yellow.ToArgb()); verts[i++] = new CustomVertex.PositionNormalColored(-0.3f, 0, 2, 0, 0, -1, Color.Yellow.ToArgb()); vertices.Unlock(); }
Lorsque nous lançons le programme, on obtient ceci :
Ce qui ne semble pas trop bizarre jusqu'à ce qu'on réalise que les triangles ont la même taille - le jaune est juste un peu plus loin. Mais il apparaît devant le triangle rouge, ce qui est du au fait qu'il est dessiné par dessus le triangle rouge.
Un moyen de résoudre ceci est d'inverser l'ordre dans lequel on dessine les triangles. On pourrait échanger les trois premiers sommets et les trois derniers comme ceci :
protected void PopulateVertexBuffer(VertexBuffer vertices) { // Crée 2 triangles CustomVertex.PositionNormalColored[] verts = (CustomVertex.PositionNormalColored[]) vertices.Lock(0, 0); int i = 0; verts[i++] = new CustomVertex.PositionNormalColored(0, 0.5f, 2, 0, 0, -1, Color.Yellow.ToArgb()); verts[i++] = new CustomVertex.PositionNormalColored(0.3f, 0, 2, 0, 0, -1, Color.Yellow.ToArgb()); verts[i++] = new CustomVertex.PositionNormalColored(-0.3f, 0, 2, 0, 0, -1, Color.Yellow.ToArgb()); verts[i++] = new CustomVertex.PositionNormalColored(0, 0.5f, 0, 0, 0, -1, Color.Red.ToArgb()); verts[i++] = new CustomVertex.PositionNormalColored(0.3f, 0, 0, 0, 0, -1, Color.Red.ToArgb()); verts[i++] = new CustomVertex.PositionNormalColored(-0.3f, 0, 0, 0, 0, -1, Color.Red.ToArgb()); vertices.Unlock(); }
Nous obtiendrions quelquechose de plus acceptable, comme ceci :
La raison pour laquelle ceci marche est que le triangle jaune est dessiné en premier, suivi du rendu du triangle rouge qui écrase simplement les pixels jaunes précédemment dessinés. Direct3D ne cherche pas à savoir lequel des 2 est le plus près, il les dessine simplement dans l'ordre qu'on lui donne.
Ordonner manuellement les triangles est une pratique relativement acceptable si nous avons que 2 triangles, mais lorsque la scène devient plus complexe, ou si la caméra bouge, cela devient rapidement insoluble. Quand nous parlerons des meshes, nous verrons que nous connaissons rarement le nombre de triangles qu'il y a dans la scène, leurs positions relatives à eux ou à la caméra. Et pour courronner le tout, lorsque les triangles se rencontrent comme ceci :
..ça devient impossible d'ordonner les triangles correctement. Peu importe ce que fassiez, ça sera faux, car des parties du triangle rouge cachent le jaune, et des parties du jaune cachent le rouge. Pour obtenir cette scène correctement, je dois m'appuyer sur quelquechose appelé z-buffer.
Le z-buffer est en fait une idée assez simple. Le principe de base est de maintenir un buffer spécial de la même taille que la surface où on dessine. Mais au lieu de dessiner des couleurs sur cette surface comme nous le ferions sur une surface de dessin habituelle, nous y dessinons des valeurs de profondeur (parfois un z-buffer est appelé "depth buffer", buffer de profondeur). Voilà comment ça marche.
Alors que nous dessinons des triangles sur une surface de dessin normale, nous dessinons aussi les triangles sur le z-buffer ; quels que soient les pixels qu'on touche sur la surface principale, nous les touchons dans le z-buffer. La différence est qu'au lieu d'écrire des valeurs de couleur, nous écrivons une valeur qui représente la distance entre le pixel qu'on dessine et la caméra. Ainsi le z-buffer maintient une espèce de carte de profondeur définissant ce qui a déja été dessiné. La vraie utilité intervient lorsqu'on dessine quelquechose par dessus quelquechose qui a déja été dessiné. Un test sur la valeur de la profondeur auprès du z-buffer est alors exécuté dans ce cas là. Si la valeur dans le z-buffer est plus grande que la valeur du pixel en cours, ça veut dire que ce qu'on dessine est plus près de ce que nous avions déjà dessiné, du coup le pixel peut être dessiné sur la surface. Si la valeur est plus petite, alors ce que nous dessinons est plus loin que ce qui a déja été dessiné, et le pixel ne sera pas dessiné.
La dernière chose que nous avons à faire est d'intialiser le z-buffer à chaque fois, à une valeur qui voudrait dire "vraiment très loin" de telle sorte que la première fois qu'on essaie de dessiner par dessus un certain pixel, cela soit forcément plus près de ce qu'il y avait déjà. La façon dont fonctionne Direct3D à ce sujet est d'utiliser une valeur normalisée comprise entre 0 et 1, plutot que d'utiliser une distance entre la caméra et chaque pixel. 0 signifie "aussi près de la lentille que tu peux" et 1 signifie "aussi loin de la camera que tu peux". Si vous vous rappelez de l'article sur les systèmes de coordonnées, vous devriez vous souvenir desplans limite spécifiés à la définission du transformateur de perspective :
Matrix projTxfm = Matrix.PerspectiveFovLH( (float)Math.PI/4.0F, // Champ de vue standard 1.0F, // Rapport d'aspect 1.0F, 10.0F // Coupe les objets à une distance > 10 // ou < 1 unités de la caméra );
Et bien il s'avère que ces nombres ne sont pas utilisés que pour couper les choses plus ou moins loin de la camera, ils sont aussi utilisés pour définir la fourchette de valeurs dans notre z-buffer. En d'autre termes, pour l'appel à Matrix.PerspectiveFovLH ci-dessus, chaque pixel se trouvant à 1.0 unité de la caméra recevra la valeur 0 dans le z-buffer, et chaque pixel se trouvant à 10.0 unités de la caméra aura une valeur de 1 dans le z-buffer.
Ceci démontre également pourquoi il est important d'avoir les plans limite aussi proches des objets les plus près/les plus loins de la scène. Etant donné qu'on utilise des plans limite pour stoquer une distance dans une valeur comprise entre 0 et 1, si nous définissions le premier plan limite à une distance trop proche ou le deuxième plan limite à une distance trop éloignée alors nous perdrions de la précision dans la définition des profondeurs. Cela peut amener des erreurs d'arrondi subtiles faisant foirer le calcul de profondeur pour des objets très proches l'un de l'autre. Des bugs d'affichage pourraient arriver, comme des choses traversant un objet solide. Vous avez peut être rencontré ce genre de phénomènes dans les jeux, lorsque par exemple le bras ou la tentacule d'un monstre ou n'importe quoi apparaît momentanément au travers d'un mur, révélant sa position.
Ce qui est chouette avec le système de z-buffer est que ca fonctionne pixel par pixel ; ça marche aussi lorsque des triangles se croisent et ca nous oblige pas à savoir dans quel ordre dessiner les triangles. Le seul inconvenient semble être le fait qu'on a à faire ce travail supplémentaire d'écrire à la fois sur le buffer d'affichage et sur le z-buffer. Heureusement, Direct3D s'occupe de cela automatiquement. Tout ce qu'on a besoin est 2 légères modifications à notre code.
La première chose qu'on a à faire est de demander à Direct3D de créer un z-buffer pour nous. Nous faisons cela en positionnant une paire de flags sur l'objet PresentParametrs que nous passons au constructeur du Device dans notre code d'initialisation :
protected bool InitializeGraphics() { PresentParameters pres = new PresentParameters(); pres.Windowed = true; pres.SwapEffect = SwapEffect.Discard; // Demande à Direct3D de créer un ZBuffer pour nous pres.EnableAutoDepthStencil = true; pres.AutoDepthStencilFormat = DepthFormat.D16; device = new Device(0, DeviceType.Reference, this, CreateFlags.SoftwareVertexProcessing, pres); }
J'ai mis en évidence les 2 nouvelles lignes de code qui nous intéressent. Elles sont assez simple : la première indique à Direct3D que nous voudrions qu'il gère automatiquement le z-buffer pour nous, et la deuxième lui donne le format à utiliser pour le z-buffer. Il y a plusieurs formats disponibles, mais DepthFormat.D16 spécifie un format de buffer 16-bit qui marcherait bien dans la plupart des situations.
L'autre chose qu'on a à faire est de s'assurer qu'on efface le z-buffer à chaque image de telle sorte que chaque pixel soit positionné à "très très loin". Souvenez vous que la valeur pour cela est 1.0. Vous vous rappelerez aussi qu'on a déjà vu une fonction pour effacer un buffer avec une certaine valeur : Device.Clear. Tout ce que nous avons à faire pour effacer le z-buffer plutôt que la cible du rendu est de spécifier en plus le flag ClearFlags.ZBuffer et de spécifier qu'on voudrait mettre le buffer à 1.0 partout. L'appel ressemble à cela :
device.Clear(ClearFlags.Target | ClearFlags.ZBuffer, Color.Black, 1.0F, 0);
Avec ces deux modifications en place, nous avons tout ce qu'il faut pour explorer les meshes, que nous traiterons la prochaine fois. Un code d'exemple complet utilisant le z-buffer apparaît ci-dessous.
Update
Si vous utilisez le SDK d'Octobre 2004 (ou supérieur), vous aurez besoin de modifier l'appel à Commitdans SetupLights en un appel à Update.
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; static void Main() { Game app = new Game(); app.Width = 400; app.Height = 300; 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; // Demande à Direct3D de créer un z-buffer pour nous pres.EnableAutoDepthStencil = true; pres.AutoDepthStencilFormat = DepthFormat.D16; device = new Device(0, DeviceType.Hardware, this, CreateFlags.SoftwareVertexProcessing, pres); device.RenderState.CullMode = Cull.None; vertices = CreateVertexBuffer(device); return true; } protected void OnCreateVertexBuffer(object o, EventArgs e) { PopulateVertexBuffer(vertices); } protected void PopulateVertexBuffer(VertexBuffer vertices) { // Crée 2 triangles CustomVertex.PositionNormalColored[] verts = (CustomVertex.PositionNormalColored[]) vertices.Lock(0, 0); int i = 0; verts[i++] = new CustomVertex.PositionNormalColored(0, 0.5f, 0.1F, 1, 0, 0, Color.Yellow.ToArgb()); verts[i++] = new CustomVertex.PositionNormalColored(0, 0, 0.4f, 1, 0, 0, Color.Yellow.ToArgb()); verts[i++] = new CustomVertex.PositionNormalColored(0, 0, -0.2F, 1, 0, 0, Color.Yellow.ToArgb()); verts[i++] = new CustomVertex.PositionNormalColored(0, 0.5f, 0, 0, 0, -1, Color.Red.ToArgb()); verts[i++] = new CustomVertex.PositionNormalColored(0.3f, 0, 0, 0, 0, -1, Color.Red.ToArgb()); verts[i++] = new CustomVertex.PositionNormalColored(-0.3f, 0, 0, 0, 0, -1, Color.Red.ToArgb()); vertices.Unlock(); } protected VertexBuffer CreateVertexBuffer(Device device) { VertexBuffer buf = new VertexBuffer(typeof(CustomVertex.PositionNormalColored), 6, device, 0, CustomVertex.PositionNormalColored.Format, Pool.Default); PopulateVertexBuffer(buf); return buf; } protected void SetupMatrices() { float angle = Environment.TickCount / 2000.0F; device.Transform.World = Matrix.RotationY(angle); device.Transform.View = Matrix.LookAtLH(new Vector3(0.7f, 0.3F, -1.0F), new Vector3(0, 0.3F, 0), new Vector3(0, 1, 0)); device.Transform.Projection = Matrix.PerspectiveFovLH((float)Math.PI/4.0F, 1.0F, 1.0F, 10.0F); } protected void SetupLights() { device.RenderState.Lighting = true; device.Lights[0].Diffuse = Color.White; device.Lights[0].Specular = Color.White; device.Lights[0].Type = LightType.Directional; device.Lights[0].Direction = new Vector3(0, -1, 3); device.Lights[0].Commit(); device.Lights[0].Enabled = true; device.RenderState.Ambient = Color.FromArgb(0x20, 0x20, 0x20); device.RenderState.AmbientMaterialSource = ColorSource.Color1; } protected void Render() { // Efface le z-buffer également. Important ! device.Clear(ClearFlags.Target | ClearFlags.ZBuffer, Color.Black, 1.0F, 0); device.BeginScene(); SetupMatrices(); SetupLights(); device.VertexFormat = CustomVertex.PositionNormalColored.Format; device.SetStreamSource(0, vertices, 0); device.DrawPrimitives(PrimitiveType.TriangleList, 0, 2); device.EndScene(); device.Present(); } protected void DisposeGraphics() { vertices.Dispose(); device.Dispose(); } } }