08 Lumière : Concepts de base

La dernière fois nous avons vu comment afficher des objets en 3D mais ils étaient tout noir car il n'y avait pas de lumière dans la scène. Entre le moment où j'avais écrit ça et maintenant, quelqu'un m'a fait remarquer qu'on pouvait simplement rajouter cette ligne de code dans InitializeGraphics :

device.RenderState.Lighting = false;

En désactivant l'éclairage de cette façon, nous aurions dit à Direct3D de ne pas s'embêter à calculer comment faire en sorte que les choses soient illuminées mais de simplement les afficher en utilisant les informations de couleur fournies par les sommets. Bien sûr, mettre en place un bon éclairage est une grosse étape dans la construction d'une scène réaliste, donc parlons en.

Il y a 4 types de lumières dans Direct3D : les lumières point, les lumières directionnelles, lesprojecteurs et les lumières d'ambience.

Les lumières point diffusent de la lumière dans toutes les directions à partir d'un certain point dans l'espace. Vous devriez utiliser ce type de lumière si vous vouliez modéliser une ampoule.

Les lumière directionnelles ne proviennent pas d'un certain point dans l'espace. En fait, elles sont considérées comme étant situées infiniment loin. Grace à cela, tous les rayons de lumière sont parallèles les uns aux autres ; ils vont dans la même direction. Vous utiliserez souvent une lumière directionnelle pour modéliser un éclairage naturel, comme celui du soleil ou de la lune.

Les projecteurs (spot lights en anglais), comme les lumières point, diffusent de la lumière à partir d'un certain point de l'espace. Mais plutôt que d'éclairer dans toutes les directions, elles pointent dans une direction en particulier. La lumière forme un cône dans l'espace et il y a des paramètres qu'on peut définir pour spécifier l'envergure du cône et à la rapidité à laquelle la lumière s'affaiblit sur les bords.

La lumière d'ambience est différente. Elle est prévue pour modéliser une lumière qui rebondit sur les objets, atteint d'autres objets et rebondit sur ceux la, et ainsi de suite. Le résultat final est un espece d'éclairage général qui illumine tout plus ou moins équitablement. En fait c'est exactement comme ça que Direct3D décide de la modéliser : une lumière sans source qui illumine tout de la même façon.

Maintenant, chacune de ces lumières a différentes composantes. Chacune des lumières a une couleur de diffusion et une couleur spéculaire (ndt : je n'ai pas de traduction possible pour "specular"). Il s'avère que les lumières ont aussi une couleur d'ambience, mais c'est un sujet un peu avancé pour ce que nous avons besoin de faire à présent. Nous laisserons la spécularité pour notre discution sur les matériaux. Il ne reste que la couleur de diffusion.

J'ai été embrouillé par le terme "diffusion" la première fois que je l'ai vu. Après avoir parcouru des livres sur le graphisme informatique, j'ai trouvé d'où ce terme venait, et ai immédiatement oublié son origine. Aujourd'hui, je considère la couleur de diffusion juste comme étant une composante "ordinaire" d'une lumière, c'est exactement cela.

Le but ultime dans Direct3D est de dessiner des pixels à l'écran, un procédé connu sous le nom de rendu. L'éclairage dans une scène a une large influence sur la couleur qu'auront ces pixels : si la lumière est très proche ou tres lumineuse, un objet sera dessiné avec des pixels plus clairs (en considérant que l'objet n'est pas noir). Si la lumière est plus faible ou plus loin de l'objet, alors évidemment l'objet deviendrait plus sombre.

Cependant ce n'est pas aussi simple. Regardez cette image de sphère reposant sur une surface noire :

J'y ai mis une simple lumière directionnelle provenant du côté droit. Remarquez comment l'objet s'assombrit en s'éloignant de la lumière. C'est évident maintenant que vous y pensez, mais vous n'aviez peut être jamais remarqué.

Ce qui se passe ici est le fait que Direct3D utilise ce qu'on appelle le vecteur normal de surface pour calculer comment la lumière interagit avec un objet. "Vecteur normal" est un terme géométrique qui signifie "un vecteur qui est perpendiculaire à quelquechose". Ainsi, un normal de surface est un vecteur qui sort de la surface, il pointe vers l'exterieur. Dans le cas de notre sphère, le normal de surface pointe toujours à l'opposé du centre de la sphère, mais pour des objets plus complexes, ce ne serait pas forcément le cas.

La façon dont les normaux interviennent avec la lumière est assez simple. Pour chaque point à la surface d'un objet, Direct3D calcule l'angle entre la lumière et le normal à ce point. Plus l'angle est proche de 0, plus l'objet est éclairé à ce point. Plus l'angle est proche de 90°, plus l'objet est faiblement éclairé à ce point. Pour les angles dépassant 90°, le normal ne pointe plus vers la source de lumière et l'objet n'est pas du tout illuminé à ce point.

Vous pouvez voir cela sur notre sphère. Sur le côté droit, où le normal de surface pointe vers la lumière, la sphère est presque d'un jaune pur (sa vraie couleur). Vers la gauche, l'objet n'est pas très illuminé car le normal fait un angle avec la source de lumière superieur ou égal à 90°.

Et en fait, ce n'est pas aussi simple. Ce rendu de sphère fait qu'elle est presque lisse. Mais nous savons que les objets sont faits de sommets qui constituent des triangles. A moins que je n'utilise un nombre ridicule de sommets, l'objet devrait plus ressembler à ceci :

Le vertex shading est une chose merveilleuse. Cela nous permet d'utiliser un objet à facettes, mais en le rendant lisse. Et ça a un grand rapport avec la façon dont l'éclairage fonctionne.

Il s'avère que les modèles de Direct3D ne stoquent pas en fait le normal de surface pour chaque position de l'objet. Ce serait très couteux en perfs. A la place, nous pouvons stoquer un vecteur normal pour chaque sommet. Ainsi, Direct3D extrapole les normaux sur la surface du triangle de la même façon qu'il extrapolait les couleurs pour un triangle multicolore. Etant donné que le normal change progressivement sur la surface, l'intensité de lumière fait de même, rendant l'objet lisse.

Bien sûr ce n'est qu'une illusion, la surface n'est pas courbée. Si vous faites attention au contour de la première sphère, vous pouvez voir les facettes : ça se rapproche plus d'un signe stop que d'un cercle qu'on aurait avec une vraie sphère. Ce genre de phénomènes sont particulièrement visibles quand vous utilisez un petit nombre de triangles. Par exemple :

Evidemment, l'illusion n'est plus très efficace. C'est parce que j'ai utilisé moins de triangles cette fois, comme vous pouvez le voir si je désactive le shading :

Le nombre de triangles à utiliser est une décision importante si vous faites attention aux perfomances. Plus vous en utilisez, plus la scène sera jolie et plus ce sera lent à afficher. Trouver l'équilibre dependra de vos spécifications particulières.

OK, alors comment on fait tout ceci avec du code? Et bien, rajoutons une méthode SetupLights à notre code. La voilà :

protected void SetupLights() { // Utiliser une lumière directionnelle blanche provenant de derrière notre épaule droite device.Lights[0].Diffuse = Color.White; device.Lights[0].Type = LightType.Directional; device.Lights[0].Direction = new Vector3(-3, -1, 3); device.Lights[0].Update(); device.Lights[0].Enabled = true; // Rajoutons une petite lumière ambiente à la scène device.RenderState.Ambient = Color.FromArgb(0x40, 0x40, 0x40); }

Détaillons une ligne après l'autre. Nous voulons mettre en place une lumière blanche, directionnelle provenant de quelque part derriière notre "épaule droite". Voici comment démarrer :

device.Lights[0].Diffuse = Color.White;

OK. On accède ici au tableau de Lights du Device. Ce tableau est une resource limitée - vous n'avez que quelques lumières disponibles. Sur mon système, le nombre est fixé à 8, mais ca peut être différent sur votre système. Peu importe combien il y en a, vous accédez chacune individuellement au travers du tableau Lights.

Ici nous définissons la couleur de diffusion pour la lumière. Souvenez vous, "couleur de diffusion" signifie "couleur ordinaire" (je dois me passer du terme "normal" vu qu'on l'utilise pour les vecteurs, maintenant). C'est juste la couleur de la lumière. Vous utiliserez la couleur blanche d'habitude bien que d'autres couleurs pourraient être utiles dans certaines situations.

Ensuite nous définissons le type de lumière. Nous utilisons une lumière directionnelle mais nous pourrions aussi avoir une lumière point ou un projo.

device.Lights[0].Type = LightType.Directional;

Les lumières directionnelles ont besoin d'une direction :

device.Lights[0].Direction = new Vector3(-3, -1, 3);

Si nous utilisions une lumière point, nous n'aurions pas besoin de ça vu qu'elle brille dans toutes les directions. Cependant nous aurions eu besoin de définir une position, ce que nous avons pas besoin de faire avec les lumières directionnelles puisqu'elles sont considérées comme étant tres, tres loin. Les projecteurs auraient besoin à la fois d'une direction et d'une position.

Après avoir défini les lumières, nous disons à Direct3D d'accepter les modifications

device.Lights[0].Update();

(notez que pour un SDK d'avant Octobre 2004, cette fonction s'appellait Commit())

et d'allumer la lumière :

device.Lights[0].Enabled = true;

C'est certainement une bonne idée d'ajouter un petit peu de lumière ambiente à la scène :

device.RenderState.Ambient = Color.FromArgb(0x40, 0x40, 0x40);

Comme déclaré plus haut, la lumière d'ambience illumine la scène entière de la même façon, sans tenir compte des normaux de surface. Nous l'utilisons car sans cela, les parties de l'objet pointant à l'opposé de la lumière sont totallement noires, ce qui ne semble pas très réaliste. Même le dessous de votre bureau reçoit un petit peu de lumière par reflexions sur le sol et autres parties du bureau.

A ce point, notre éclairage est bien mis en place. Tout ce qui nous reste à faire est de s'assurer que SetupLights est appelé quelquepart, soit dans InitializeGraphics si nous n'allons pas le changer ou dans Render si nous voulons faire évoluer l'éclairage ou la position de la lumière au fil du temps. Cependant, si c'était la seule modif que nous aurions entrepris, notre object serait toujours rendu complètement noir. Que se passe t il?

Souvenez vous que l'éclairage interagit avec la surface par rapport à son vecteur normal. Et bien jusque là nous n'avions pas encore vu comment définir les normaux de surface sur un objet, à part mentionner brièvement que cette information est stoquée dans les sommets. Heureusement, c'est assez simple. Nous avons juste à utiliser un des formats de sommets qui nous autorise à stoquer le vecteur normal, commeCustomVertex.PositionNormalColored. Le constructeur d'un sommet PositionNormalColored prend 3 arguments de plus que le PositionColored : les composants x, y et z du vecteur normal à la surface.

Etant donné qu'on utilise ce format, nous devrons le dire au Device comme ceci

device.VertexFormat = CustomVertex.PositionNormalColored.Format;

Sinon, le Device attendra des sommets dans un autre format et de Mauvaises Choses arriveront. Vous pouvez mettre cette ligne un peu n'importe où dans le code, étant donné qu'une fois positionné, ça restera positionné jusqu'à ce que vous le rechangiez. On pourrait le mettre dans InitializeGraphics.

Pour des raisons qui seront précisés ultérieurement, j'ai séparé le code qui crée le VertexBuffer du code qui remplit le buffer avec les sommets. Voici le code :

protected VertexBuffer CreateVertexBuffer(Device device) { VertexBuffer buf = new VertexBuffer( typeof(CustomVertex.PositionNormalColored), 3, device, 0, CustomVertex.PositionNormalColored.Format, Pool.Default); PopulateVertexBuffer(buf); return buf; } protected void PopulateVertexBuffer(VertexBuffer vertices) { CustomVertex.PositionNormalColored[] verts = (CustomVertex.PositionNormalColored[]) vertices.Lock(0, 0); int i = 0; verts[i++] = new CustomVertex.PositionNormalColored( 0, 1, 0, // position du sommet 0, 0, 1, // normal du sommet Color.Red.ToArgb()); // couleur du sommet verts[i++] = new CustomVertex.PositionNormalColored( -0.5F, 0, 0, 0, 0, 1, Color.Green.ToArgb()); verts[i++] = new CustomVertex.PositionNormalColored( 0.5F, 0, 0, 0, 0, 1, Color.Blue.ToArgb()); vertices.Unlock(); } // Ce qui est presque identique au code qu'on a utilisé jusqu'à maintenant, avec l'ajout // des sommets PositionNormalColored et des 3 arguments supplémentaires dans le constructeur

Ici j'ai décidé de mettre les 3 normaux à la même valeur car je veux que notre triangle rotatif soit plat. Souvenez vous que si je les fais pointer dans des directions différentes, le normal sera calculé sur toute la surface et donnera l'illusion d'une courbe. Jettez un oeil au tutoriel d'éclairage dans le SDK Direct3D de C# : ils affichent un cylindre qui utilise un code très similaire à ce qu'on fait ici, et calculent les normaux pour rendre la surface courbée.

Si vous exécutez ce code, vous remarquerez que le triangle devient plus clair lorsqu'il est tourné vers la lumière, et plus sombre lorsqu'il s'en détourne, rendant une scène bien plus réaliste. Mais en fonction de ce que vous avez mis dans Device.RenderState.CullMode, vous allez obtenir un des deux effets complètement indésirables.

Si vous avez laissé le CullMode a sa valeur par défaut, ce que vous verrez est le même triangle disparaissant qu'on a vu la dernière fois. Et pour la même raison, Direct3D n'affiche pas les faces considérées comme tournant le dos à la caméra. La solution à cela fut de positionner CullMode à Cull.None. Mais cela engendre d'autres problèmes.

Ce que vous verrez si vous éteignez le culling des faces de dos vous semblera un peu space. Comme le triangle pivotera, vous remarquerez qu'une face réagira à la lumière, devenant plus sombre et plus claire lorsqu'elle se tournera ou se détournera de la source de lumière. Mais l'autre face restera noire! Voici deux captures d'écran montrant la "face" et le "dos" du triangle :

Nous avons tous les éléments pour comprendre ce comportement. Souvenez vous que le taux d'éclairage que reçoit une face dépend de l'angle entre la source de lumière et le normal de surface. Et bien, vu que le triangle tourne, nos vecteurs normaux pointent parfois vers la lumière et parfois dans l'autre direction! Quand cela arrive, l'angle entre les deux vecteurs est bien plus grand que 90° et Direct3D n'illumine pas du tout la surface.

La solution à cela est bizarre mais simple. Nous avons besoin en fait d'afficher 2 triangles en même temps, un dont les normaux pointent dans une direction et un autre dont les normaux pointent à l'apposé. Nous avons besoin aussi d'activer le culling de telle sorte que Direct3D n'affiche que celui qui fait face à la caméra, et nous devons nous assurer que l'ordre des sommets est correct afin qu'il sélectionne le bon triangle à chaque fois.

La seule chose à modifier dans CreateVertexBuffer est de s'assurer qu'il est suffisament grand pour contenir 6 sommets au lieu de 3 :

protected VertexBuffer CreateVertexBuffer(Device device) { VertexBuffer buf = new VertexBuffer( typeof(CustomVertex.PositionNormalColored), 6, // ATTENTION! device, 0, CustomVertex.PositionNormalColored.Format, Pool.Default); PopulateVertexBuffer(buf); return buf; }

Dans PopulateVertexBuffer, nous devons juste ajouter la définition du 2ème triangle :

protected void PopulateVertexBuffer(VertexBuffer vertices) { CustomVertex.PositionNormalColored[] verts = (CustomVertex.PositionNormalColored[]) vertices.Lock(0, 0); int i = 0; verts[i++] = new CustomVertex.PositionNormalColored( 0, 1, 0, 0, 0, 1, Color.Red.ToArgb()); verts[i++] = new CustomVertex.PositionNormalColored( -0.5F, 0, 0, 0, 0, 1, Color.Green.ToArgb()); verts[i++] = new CustomVertex.PositionNormalColored( 0.5F, 0, 0, 0, 0, 1, Color.Blue.ToArgb()); verts[i++] = new CustomVertex.PositionNormalColored( 0, 1, 0, 0, 0, -1, Color.Red.ToArgb()); verts[i++] = new CustomVertex.PositionNormalColored( 0.5F, 0, 0, 0, 0, -1, Color.Blue.ToArgb()); verts[i++] = new CustomVertex.PositionNormalColored( -0.5F, 0, 0, 0, 0, -1, Color.Green.ToArgb()); vertices.Unlock(); }

Et nous devons aussi changer l'appel à DrawPrimitives dans notre méthode Render pour afficher 2 triangles au lieu d'un :

device.DrawPrimitives(PrimitiveType.TriangleList, 0, 2);

Avec ces changements en place, notre triangle s'affichera correctement, interagissant de manière réaliste avec la lumière lorsqu'il pivote.

J'ai joint ci dessous le code complet pour cet article. Pour vous faciliter le bidouillage de ce dernier, j'ai mis 2 constantes symboliques au début du fichier. Les commenter et les décommenter vous permettra de tester avec et sans culling et d'afficher un triangle ou 2 triangles.

La prochaine fois, nous évoquerons comment dépasser l'utilisation de simples triangles colorés. Nous parlerons des textures, ce qui vous permet d'appliquer des images sur vos objets.

Le Code

// Commentez cette ligne pour avoir 2 triangles //#define SINGLETRIANGLE // Commentez cette ligne pour utiliser le culling par défaut //#define CULLNONE 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 = 400; app.InitializeGraphics(); app.Show(); while (app.Created) { app.Render(); Application.DoEvents(); } } protected bool InitializeGraphics() { PresentParameters pres = new PresentParameters(); pres.Windowed = true; pres.SwapEffect = SwapEffect.Discard; device = new Device(0, DeviceType.Hardware, this, CreateFlags.SoftwareVertexProcessing, pres); vertices = CreateVertexBuffer(device); #if CULLNONE device.RenderState.CullMode = Cull.None; #endif device.VertexFormat = CustomVertex.PositionNormalColored.Format; return true; } #if SINGLETRIANGLE protected void PopulateVertexBuffer(VertexBuffer vertices) { CustomVertex.PositionNormalColored[] verts = (CustomVertex.PositionNormalColored[]) vertices.Lock(0, 0); int i = 0; verts[i++] = new CustomVertex.PositionNormalColored( 0, 1, 0, // position du sommet 0, 0, 1, // vecteur normal Color.Red.ToArgb()); // couleur du sommet verts[i++] = new CustomVertex.PositionNormalColored( -0.5F, 0, 0, 0, 0, 1, Color.Green.ToArgb()); verts[i++] = new CustomVertex.PositionNormalColored( 0.5F, 0, 0, 0, 0, 1, Color.Blue.ToArgb()); vertices.Unlock(); } protected VertexBuffer CreateVertexBuffer(Device device) { VertexBuffer buf = new VertexBuffer( typeof(CustomVertex.PositionNormalColored), 3, device, 0, CustomVertex.PositionNormalColored.Format, Pool.Default); PopulateVertexBuffer(buf); return buf; } #else protected void PopulateVertexBuffer(VertexBuffer vertices) { CustomVertex.PositionNormalColored[] verts = (CustomVertex.PositionNormalColored[]) vertices.Lock(0, 0); int i = 0; verts[i++] = new CustomVertex.PositionNormalColored( 0, 1, 0, 0, 0, 1, Color.Red.ToArgb()); verts[i++] = new CustomVertex.PositionNormalColored( -0.5F, 0, 0, 0, 0, 1, Color.Green.ToArgb()); verts[i++] = new CustomVertex.PositionNormalColored( 0.5F, 0, 0, 0, 0, 1, Color.Blue.ToArgb()); verts[i++] = new CustomVertex.PositionNormalColored( 0, 1, 0, 0, 0, -1, Color.Red.ToArgb()); verts[i++] = new CustomVertex.PositionNormalColored( 0.5F, 0, 0, 0, 0, -1, Color.Blue.ToArgb()); verts[i++] = new CustomVertex.PositionNormalColored( -0.5F, 0, 0, 0, 0, -1, Color.Green.ToArgb()); vertices.Unlock(); } protected VertexBuffer CreateVertexBuffer(Device device) { VertexBuffer buf = new VertexBuffer( typeof(CustomVertex.PositionNormalColored), 6, // ATTENTION!!! device, 0, CustomVertex.PositionNormalColored.Format, Pool.Default); PopulateVertexBuffer(buf); return buf; } #endif 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, 10.0F); } protected void SetupLights() { device.Lights[0].Diffuse = Color.White; device.Lights[0].Type = LightType.Directional; device.Lights[0].Direction = new Vector3(-1, -1, 3); device.Lights[0].Update(); device.Lights[0].Enabled = true; device.RenderState.Ambient = Color.FromArgb(0x40, 0x40, 0x40); } protected void Render() { device.Clear(ClearFlags.Target, Color.Bisque, 1.0F, 0); device.BeginScene(); SetupMatrices(); SetupLights(); device.SetStreamSource(0, vertices, 0); #if SINGLETRIANGLE device.DrawPrimitives(PrimitiveType.TriangleList, 0, 1); #else device.DrawPrimitives(PrimitiveType.TriangleList, 0, 2); #endif device.EndScene(); device.Present(); } } }