Plus....

Beginning Game Development

Part VI – Eclairage, Matériels and Terrain

Traduit par Valentin BILLOTTE

Consultez cet article en anglais  

Cet article n’a pas été traduit par Microsoft. Il n’a pas été relu ou vérifié par Microsoft.

Sur cette page

Introduction Introduction
nettoyage de code nettoyage de code
Fonts Fonts
Eclairage Eclairage
Materials (matériaux) Materials (matériaux)
Coleur Coleur
Type de luminosité Type de luminosité
Ajout de lumières Ajout de lumières
Ajouter un terrain Ajouter un terrain
Height Map (carte d'altitudes) Height Map (carte d'altitudes)
Exemple d'Height map Exemple d'Height map
classe Terrain classe Terrain
Conclusion Conclusion

Introduction

Bienvenue dans le sixième article sur l'apprentissage de la programmation de jeux. Dans le dernier article, je vous ai promis d'aborder l'éclairage, la génération de terrain, la détection de collisions. La génération de terrain et la détection de collisions sont deux sujets tellement vastes et consistants que nous pourrions les aborder sur une dizaine d'articles. Je vais donc modifier le sommaire de cet article : nous allons couvrir la luminosité, les matériaux et introduire de manière simplifiée la génération de terrain. Nous verrons ce dernier sujet couplé à la détection de collision plus en détails dans le prochain article.

Avant de commencer, revenons sur notre obligatoire nettoyage de code, qui incorpore tous les retours que vous m'avez fait.

Haut de page Haut de page

nettoyage de code

Le nettoyage réalisé pour cet article consiste principalement à la mise à jour des versions du SDK DirectX et quelques menus améliorations pour la performance de notre jeu.

  • Mise à jour de la version release de Visual Studio Express
  • Mise à jour du SDK à la version October 2005 DirectX SDK.
  • Ajout des deux instructions suivantes à la méthode : ConfigureDevice. Le Depth stencils permet l'application d'un masque sur certaines portions d'une scène affichée à l'écran afin de les cacher. De cette façon les performances se trouvent accrues. Les deux lignes suivantes autorisent un Z-Buffer de 16 bits.
    • presentParams.AutoDepthStencilFormat = DepthFormat.D16;
    • presentParams.EnableAutoDepthStencil = true;
  • Ajout du flag ClearFlags.ZBuffer en paramètre de Device.Clear dans la méthode Render. Ceci pour supporter le Depth Stencil que nous venons d'ajouter.
  • Ajouter d'un paramètre concernant la vitesse des tanks dans la classe Tank.
Haut de page Haut de page

Fonts

Le seul retour que nous donnons au joueur pendant le jeu, jusqu'à présent se trouve être le FPS que nous affichons dans la barre de titre de la fenêtre. Nous avons affiché quelques informations dans la console mais se n'est pas un emplacement très utile et lisible lorsque l'on joue un jeu. En plus du FPS je veux afficher diverses informations en rapport directe avec le jeu (notamment notre location). Nous avons besoin pour cela des fonts DirectX. L'exemple qui suit donne le moyen le plu simple et directe d'afficher du texte à l'écran. ; reportez vous au programme d'exemple concernant l'usage des fonts avec DirectX fourni avec le sdk pour en savoir plus.

La première étape consiste à déclarer une variable à la classe GameEngine. Nous allons utiliser un qualificatif de type complet afin d'éviter tout homonymie entre la classe Font du namespace Direct3D et la classe Font de System.Drawing.

Visual C#

private Microsoft.DirectX.Direct3D.Font _font;

Visual Basic

Private m_font As Microsoft.DirectX.Direct3D.Font

Initialisons maintenant notre objet dans le constructeur de la classe GameEngine.

Visual C#

font = new Microsoft.DirectX.Direct3D.Font
(_device, new System.Drawing.Font("Arial", 14.0f, FontStyle.Italic));

Visual Basic

m_font = New Microsoft.DirectX.Direct3D.Font
(m_device, New System.Drawing.Font("Arial", 14.0F, FontStyle.Italic))

Les paramètres ne nécessitent pas d'être expliqués. Notons au passage que la classe Font de DirectX utilise un objet de type Drawing Font dans son constructeur GameEngine.

Toutes les fonctionnalités pour afficher les différentes valeurs en rapport avec notre jeu à l'écran se trouvent dans la méthode RenderFonts de la classe.

Visual C#

private void RenderFonts() {
// display the heading and pitch
_font.DrawText(null, string.Format( "Heading={0:N000},
Pitch ={1:N000}", _camera.Heading, _camera.Pitch),
new Rectangle(0, 0, this.Width, this.Height),
DrawTextFormat.NoClip |
DrawTextFormat.ExpandTabs | DrawTextFormat.WordBreak, Color.Yellow);
// Display the Postion Direction
_font.DrawText(null, string.Format("X={0}, Y={1}, Z={2}",
_camera.Position.X, _camera.Position.Y, _camera.Position.Z),
new Rectangle(0, 20, this.Width, this.Height), DrawTextFormat.NoClip |
DrawTextFormat.ExpandTabs | DrawTextFormat.WordBreak, Color.Yellow);
// Display the frame rate
_font.DrawText(null, string.Format("FPS={0}", FrameRate.CalculateFrameRate()),
new Rectangle(0, 60,this.Width, this.Height),
DrawTextFormat.NoClip | DrawTextFormat.ExpandTabs
| DrawTextFormat.WordBreak, Color.Yellow);
// display Lighting state
_font.DrawText(null, string.Format("Lights={0}", _lightOn ? "On" : "Off"),
new Rectangle(this.Width - 110, 0,this.Width, this.Height),
DrawTextFormat.NoClip | DrawTextFormat.ExpandTabs
| DrawTextFormat.WordBreak, Color.Yellow); }

Visual Basic

Private Sub RenderFonts() ' display the heading and pitch
m_font.DrawText(Nothing, String.Format( "Heading={0:N000},
Pitch ={1:N000}", m_camera.Heading, m_camera.Pitch),
New Rectangle(0, 0, Me.Width, Me.Height),
DrawTextFormat.NoClip Or DrawTextFormat.ExpandTabs
Or DrawTextFormat.WordBreak, Color.Yellow)
' Display the Postion Direction
m_font.DrawText(Nothing, String.Format( "X={0}, Y={1}, Z={2}",
m_camera.Position.X, m_camera.Position.Y, m_camera.Position.Z),
New Rectangle(0, 20, Me.Width, Me.Height), DrawTextFormat.NoClip
Or DrawTextFormat.ExpandTabs Or DrawTextFormat.WordBreak, Color.Yellow)
' Display the frame rate
m_font.DrawText(Nothing, String.Format( "FPS={0}",
FrameRate.CalculateFrameRate()),
New Rectangle(0, 60, Me.Width, Me.Height),
DrawTextFormat.NoClip Or DrawTextFormat.ExpandTabs
Or DrawTextFormat.WordBreak, Color.Yellow) ' display Lighting state
If m_lightOn = True Then m_font.DrawText(Nothing, "Lights=On",
New Rectangle(Me.Width - 110, 0, Me.Width, Me.Height),
DrawTextFormat.NoClip Or DrawTextFormat.ExpandTabs Or
DrawTextFormat.WordBreak, Color.Yellow) Else
m_font.DrawText(Nothing, "Lights=Off",
New Rectangle(Me.Width - 110, 0, Me.Width, Me.Height),
DrawTextFormat.NoClip Or DrawTextFormat.ExpandTabs Or
DrawTextFormat.WordBreak, Color.Yellow) End If End Sub

Les deux premiers appels à la méthode DrawText de la classe GameEngine affichent les propriétés de la caméra à l'écran (position orientation). Cela permet au joueur de s'orienter dans le monde. Les deux items qui suivent ajoutent le FPS (j'ai supprimé celui présent dans la barre des titres) et un indicateur sur la prise en charge ou non de l'éclairage.

La position du texte à l'écran est spécifiée par l'intermédiaire d'un objet de type Rectangle. Les deux premières valeurs indiquent la position du point haut gauche du rectangle (dans les coordonnées de l'écran), les deux dernières indiquent la taille du rectangle.

Haut de page Haut de page

Eclairage

Jusqu'à présent, nous avions annulé tout effet d'éclairage en donnant à la propriété Lighting de RenderState la valeur false. Chaque vertex était affiché avec sa propre couleur. Autoriser l'éclairage ajustera la couleur de chaque vertex en combinant :

  • Sa couleur matériel (Material) propre.
  • Les texels (combinaisons de plusieurs textures).
  • Les couleurs diffuses et spéculaires.
  • La couleur et l'intensité de toutes les lumières présentes sans la scène (incluant la couleur ambiante).

Il existe deux familles de lumières dans DirectX :

  1. Ambiante : Une lumière ambiante est une lumière tellement dispersée qu'elle n'a plus ni direction ni source. Une lumière ambiante illumine avec la même intensité en tout point de la scène. Elle se définit par sa couleur et son intensité. Elle ne contribue à aucune réflexion spéculaire et est complètement indépendante des autres lumières présentes dans la scène. Elle est la moins onéreuse (en termes de calculs CPU) de tous les types de lumières. Une lumière ambiante se définit dans la propriété RenderState de la classe Device.
  2. Directionnelle : Comme son nom l'indique, une lumière directionnelle possède une direction en plus d'une couleur et d'une intensité. Dans DirectX, la direction est la distance comprise entre le point d'origine et la position courante de la lumière. Quand une lumière directionnelle est réfléchie sur une surface, elle ne modifie pas la lumière ambiante mais elle contribue à modifier la luminosité spéculaire. On trouve trois sortes de lumières directionnelles (stockées dans le tableau Lights de la classe Device).
    • Directionelle. Il s'agit d'une source de lumière. Elle ne possède pas de position et produit une lumière qui parcourt la scène en droite ligne. Dans les jeux, elle est souvent utilisée pour reproduire la lumière du soleil ou de la lune. Elle n'est pas très onéreuse mais à consommer toutefois avec modération pour éviter de faire pâlir votre fps.
    • Point. Un point de lumière ne possède pas de direction et illumine de manière égale dans toutes les directions. Un feu de camps est un bon exemple de lumière point, une ampoule aussi. Ces lumières sont plus onéreuses que les lumières directionnelles. A la différence de ces dernières, une lumière point possède une atténuation (l'intensité de la lumière décroît avec la distance) et une distance d'éclairage (distante au-delà de laquelle la lumière n'illumine plus).
    • Spot. Une lumière spot est comparable à celle d'une torche électrique ou des phares d'une automobile. Il s'agit de la lumière la plus compliquée à implémenter et la plus onéreuse en CPU. Elle possède une direction et une position. L'intensité de la lumière est séparée en deux cônes. Dans le cône intérieur la lumière est plus intense que dans le cône extérieur. Seuls les objets se trouvant dans un de ces cônes se trouvent illuminé. En plus de la position, direction, atténuation et distance d'éclairage, vous devez définir la taille des cônes et la façon dont ils s'imbriquent.
Haut de page Haut de page

Materials (matériaux)

Dans le dernier article, nous avons brièvement abordé les matériaux. Ils définissent la manière dont la lumière est réfléchie sur la surface d'un objet. Dans DirectX vous pouvez définir comment chaque matériau réfléchit les lumières ambiantes, diffuses et spéculaires.

Normale: Pour toutes les lumières en cours d'émission, DirectX doit connaître la normale de chaque face des objets à éclairer (pour un cube il y'a donc 6 faces). Une normale n'est rien d'autre qu'un vecteur faisant un angle à 90° avec n'importe quel vecteur coplanaire de la face. (Reportez vous à l'excellente définition d'une normale contenue dans la documention du SDK DirectX. Rendez vous à Introducing DirectX 9.0 > Direct3D Graphics > Getting Started with Direct3D > 3-D coordinate Systems and Geometry > Face and Vertex Normal Vectors.)

Haut de page Haut de page

Coleur

DirectX définit une couleur comme l'assemblage de quatre composantes : Rouge, vert, bleu et alpha (RGBA). Les valeurs de ces composantes oscillent entre 0.0f et 1.0f. Les matériaux et les lumières utilisent la lumière d'une manière différente. Pour la lumière, la couleur représente la puissance de la lumière émise dans chacune des composantes. Une valeur de 0.0f indique que la composante n'est pas visible. 1.0f indique que la composante de la lumière possède une luminosité maximale. Une valeur négative au contraire supprime une couleur de la scène.

Pour les matériaux, les composantes représentent la quantité de lumière qui est réfléchie par une surface. Une valeur de 0.0F indique que la composante n'est pas réfléchie, 1.0f indique que toute la lumière reçue est réfléchie.

Haut de page Haut de page

Type de luminosité

Chaque type de lumières peut émettre 4 couleurs. La couleur d'un type de lumière interagit avec la couleur d'un type de matériaux. Par exemple la couleur diffuse d'une lumière n'interagit qu'avec la matière diffuse d'un matériau.

  1. Ambiance : La couleur ambiante est la couleur d'ambiance générale de la scène. Elle est la même en tout point de la scène.
  2. Diffuse : La couleur diffuse est aussi éparpillée dans la scène mais garde un semblant de direction. Une surface diffuse réfléchit la lumière venant de n'importe quelle direction dans n'importe quel angle. La valeur de la lumière en un point d'un objet dépend de l'angle de la lumière avec la normale de la surface. Elle donne à l'objet un aspect matte.
  3. Speculaire : L'opposé de diffuse. La lumière spéculaire n'est pas éparpillée mais plus focalisée. Elle se calcule par rapport à la normale et par la position de la caméra par rapport à l'objet. Elle donne un aspect brillant à la matière. Il faut autoriser l'utilisation de la lumière spéculaire dans le Render State pour pouvoir l'utiliser.
  4. Emissive : Il s'agit d'une lumière qui semble être émise par un objet.

Assez de théorie ! Place à la pratique.

Haut de page Haut de page

Ajout de lumières

La première étape consiste à modifier la propriété RenderState.Lighting à true. Vous pouvez aussi supprimer la ligne entièrement puisque true est la valeur par défaut. Nous allons faire ceci dans la méthode ConfigureDevice.

A la fin du de la méthode ConfigureDevice ajoutez le code suivant :

Visual C#

_device.RenderState.Lighting = true;

Visual Basic

m_device.RenderState.Lighting = True

Une scène ne peut posséder qu'une seule lumière ambiante, mais plusieurs lumières directionnelles. Pour cette raison, la lumière ambiante se définit dans la propriété RenderState, alors que les autres types de lumières sont stockés dans le tableau Lights de la classe Device. Pour BattleTank2005nous allons ajouter une lumière ambiante blanche.

A la fin de la méthode ConfigureDevice, ajoutez le code suivant immédiatement après l'instruction que nous venons d'ajouter :

Visual C#

_device.RenderState.Ambient = Color.White;

Visual Basic

m_device.RenderState.Ambient = Color.White

La plupart des cartes graphiques modernes supportent les techniques d'éclairage avancées comme les lumières directionnelles et peuvent gérer jusqu'à 8 lumières différentes actives dans une scène (il est possible d'en définir une infinité toutefois). La propriété DeviceCaps.MaxActiveLights du Device permet de savoir combien de lumières une carte graphique peut afficher. A vous d'adapter l'affichage de votre jeu en fonction de cette valeur.

Ajoutez une méthode nommée CreateLights à la classe GameEngine contenant le code suivant :

Visual C#

if ( _device.DeviceCaps.MaxActiveLights == 0 )
{ _device.RenderState.Ambient= Color.White; }

Visual Basic

If m_device.DeviceCaps.MaxActiveLights = 0
Then m_device.RenderState.Ambient = Color.White End If

Pour BattleTank2005, nous allons ajouter une seule lumière directionnelle pour simuler le soleil. Dans la méthode CreateLights ajoutez le code suivant :

Visual C#

else { if ( _device.DeviceCaps.MaxActiveLights > 1 ) {
// This directional Light is our "sun" _device.Lights[0].Type =
LightType.Directional;
// Point the light straight down _device.Lights[0].Direction =
new Vector3( 0f, -1.0f, 0f);
_device.Lights[0].Diffuse = System.Drawing.Color.LightYellow;
_device.Lights[0].Enabled = true; } }

Visual Basic

Else If m_device.DeviceCaps.MaxActiveLights > 1 Then
' This directional Light is our
"sun" m_device.Lights(0).Type = LightType.Directional
' Point the light down m_device.Lights(0).Direction =
New Vector3(0.0F, -1.0F, 0.0F)
m_device.Lights(0).Diffuse = System.Drawing.Color.
White m_device.Lights(0).Enabled = True End If End If

La dernière étape vise à appeler cette méthode. Ajoutez le code suivant au constructeur de la classe GameEngine juste après l'appel à la méthode CreateTanks.

CreateLights ( );

Une dernière modification doit être opérée : il nous faut annuler toute prise en charge de la luminosité lors de l'affichage de la skybox afin de ne pas la rendre dépendante de nos lumières (un ciel illuminé n'est pas très logique, c'est plutôt à lui d'illuminer). Dans la méthode Render de la classe Skybox, ajoutez le code suivant juste après le code qui invalide le Z-Buffer.

Visual C#

_device.RenderState.Lighting = false;

Visual Basic

m_device.RenderState.Lighting = False

Après avoir affiché la skybox nous devons rétablir la gestion des lumières. Ajoutez le code suivant juste après le rétablissement du Z-Buffer.

Visual C#

_device.RenderState.Lighting = true;

Visual Basic

m_device.RenderState.Lighting = True

Notre gestion des lumières est terminée. Le meilleur moyen une fois encore pour tout comprendre sur l'éclairage et les matériaux est bien entendu d'entrer dans le code source et de le modifier afin d'étudier le résultat à l'écran.

Haut de page Haut de page

Ajouter un terrain

Avez-vous déjà remarqué que la plupart des jeux se jouent dans l'espace ou en intérieur ? La raison de cela est simple : créer un terrain extérieur réaliste est très complexe.

La création de terrain est un sujet sans fin. De nombreux développeurs se spécialisent uniquement dans les techniques de génération de terrain et travaillent sur des algorithmes toujours plus complexes pour afficher le terrain le plus réaliste possible en consommant le moins de ressources possible. Bien entendu nous n'avons pas la place ici de couvrir toutes ces techniques. Nous nous limiterons donc à une approche basique. Je vous donnerai toutefois quelques pistes afin de vous permettre d'améliorer votre rendu avec la technique de votre choix.

Haut de page Haut de page

Height Map (carte d'altitudes)

Un terrain est avant tout une grille 3D régulière. Dans une telle grille, tous les points se trouvent à égale distance les uns des autres. Chaque point possède une location X, Z et une valeur Y qui traduit l'altitude du terrain en ce point.

Si vous aviez à créer un terrain de 3x3, la grille ressemblerai à ceci :

Ce simple terrain 3x3 possède 18 triangles (ou 9 carrés) et se compose de 36 vertices permettant de dessiner 18 triangles. Cette énumération de chiffres est importante à comprendre car nous allons utiliser ces notions de manière intensive.

Le meilleur moyen de sauvegarder la hauteur d'un terrain en chaque point est d'utiliser une height map représentée par une image en noir et blanc. Chaque pixel dans l'image représente une hauteur. Les couleurs sombres représentent les altitudes faibles, les clairs les hauteurs élevées. A partir du moment où nous avons 256 couleurs de gris différentes, nous pouvons spécifier 256 altitudes différentes.

Haut de page Haut de page

Exemple d'Height map

La plupart des applications utilisent le format RAW qui est simplement une suite de bytes. Le format RAW ne possède aucune entête, ni aucune information sur l'image. Le chargement d'une image au format RAW est donc très rapide puisqu'il n'y a aucune analyse à faire. J'ai inclus deux méthodes dans le code, une pour le format RAW, une autre pour un format d'image classique, à vous d'expérimenter avec chacun d'elles. L'avantage du format d'image classique est de permettre de voir la heightmap dans un éditeur d'images. Il est bien sûr possible d'exporter d'un format standard vers le format RAW à l'aide programme comme HME ou Terragen.

Il est possible d'utiliser des algorithme comme le "Fault Formation" ou "Midpoint Displacement" afin de générer un terrain de manière "programmatique". Une approche à privilégier si vous voulez créer un générateur de terrain.

Quelque soit la manière dont vous créez/chargez les données concernant l'altitude de votre terrain, le processus qui débute par une heightmap et se termine par un terrain réaliste en 3D suit toujours les mêmes étapes :

  1. Charger les données du heightmap à l'intérieur d'un tableau.
  2. Stocker les vertices de la grille dans un vertex buffer.
  3. Stocker les indexes de la grille dans un index buffer.
  4. Calculer les normales pour chaque triangle.
  5. Afficher les vertices (a l'aide du type de primitive "triangle strip").
Haut de page Haut de page

classe Terrain

Pour afficher le terrain dans BattleTank2005, j'ai ajouté une classe Terrain. Cette classe encapsule tout ce qui touche à la gestion du terrain dans le jeu de près ou de loin. La classe sera initialisée dans le constructeur et affichera son contenu dans la boucle de jeu.

La première étape va charger les hauteurs dans la mémoire. Le code va charger ces données depuis une image au format RAW.

Visual C#

public void LoadHeightMapFromRAW ( string fileName )
{ _isHeightMapRAW = true; _elevations = null;
using ( Stream stream = File.OpenRead ( fileName ) )
{ _elevationsRAW = new byte[(int)stream.Length];
stream.Read ( _elevationsRAW, 0, (int)stream.Length );
ComputeValues ( (int)Math.Sqrt( (double)stream.Length ),
(int)Math.Sqrt( (double)stream.Length )); }
// Now load the buffers LoadVertexBuffer ( );
LoadIndexBuffer ( ); }

Visual Basic

Public Sub LoadHeightMapFromRAW(ByVal fileName As String)
m_isHeightMapRAW = True _elevations = Nothing
Dim stream As New FileStream(fileName, FileMode.Open)
_elevationsRAW = New Byte(stream.Length) {}
stream.Read(_elevationsRAW, 0, CType(stream.Length, Integer))
ComputeValues(CType(Math.Sqrt(CType(stream.Length, Double)), Integer),
CType(Math.Sqrt(CType(stream.Length, Double)), Integer))
' Now load the buffers LoadVertexBuffer()
LoadIndexBuffer() End Sub

Les deux premières lignes ne sont là que pour offrir un support de chargement de données à partir d'un format RAW ou un format d'image classique. Le point central de la méthode réside dans l'appel à stream.Read. Cette méthode copie le contenu du buffer correspondant au contenu du fichier dans un tableau de bytes. L'utilisation d'un bloc Using nous assure que le flux est correctement fermé et nettoyé à la fin de son utilisation

Une fois que les données sont chargées, nous utilisons la longueur du flux lu pour calculer les différentes propriétés liées au terrain à générer.

Visual C#

private void ComputeValues ( int width, int height )
{ // Vertices _numberOfVerticesX = width; _numberOfVerticesZ = height;
_totalNumberOfVertices = _numberOfVerticesX * _numberOfVerticesZ;
// Quads _numberOfQuadsX = _numberOfVerticesX - 1;
_numberOfQuadsZ = _numberOfVerticesZ - 1;
_totalNumberOfQuads = _numberOfQuadsX * _numberOfQuadsZ;
_totalNumberOfTriangles = _totalNumberOfQuads * 2;
_totalNumberOfIndicies = _totalNumberOfQuads * 6; }

Visual Basic

Private Sub ComputeValues(ByVal width As Integer, ByVal height As Integer)
' Vertices _numberOfVerticesX = width
_numberOfVerticesZ = height _totalNumberOfVertices =
_numberOfVerticesX * _numberOfVerticesZ
' Quads _numberOfQuadsX = _numberOfVerticesX - 1
_numberOfQuadsZ = _numberOfVerticesZ - 1
_totalNumberOfQuads = _numberOfQuadsX * _numberOfQuadsZ
_totalNumberOfTriangles = _totalNumberOfQuads * 2
_totalNumberOfIndicies = _totalNumberOfQuads * 6 End Sub

A partir de là, nous sommes en mesure de charger le contenu du vertex buffer.

Visual C#

private void LoadVertexBuffer ( )
{ // This is the buffer we are going to store the vertices in
_vb = new VertexBuffer ( typeof(CustomVertex.PositionNormalTextured),
_totalNumberOfVertices, _device, Usage.WriteOnly,
CustomVertex.PositionNormalTextured.Format,
Pool.Managed ); // All the vertices are stored in a 1D array
_vertices = new CustomVertex.PositionNormalTextured[ _totalNumberOfVertices];
// Load vertices into the buffer one by one for (
int z = 0; z &#lt; _numberOfVerticesZ; z++ )
{ for ( int x = 0; x &#lt; _numberOfVerticesX; x++ ) {
CustomVertex.PositionNormalTextured vertex; vertex.X = x;
vertex.Z = z; // Set the Y to the elevation value in the elevation array
if ( _isHeightMapRAW ) vertex.Y = (float)_elevationsRAW[
( z * _numberOfVerticesZ ) + x]; else vertex.Y = _elevations[x,z];
// Set the u,v values so one texture covers the entire terrain
vertex.Tu = (float)x /
_numberOfQuadsX; vertex.Tv = (float)z / _numberOfQuadsZ;
// Set up a bogus normal vertex.Nx = 0; vertex.Ny = 1; vertex.Nz = 0;
// Add it to the array // Note: this is the same formula used in the elevations
//computation // to map the 2D array coordaintes into a 1D array _vertices[
( z * _numberOfVerticesZ ) + x ] = vertex; } }
// No overide the bogus normal computations with
a real one ComputeNormals ( );
// finally set assign the vertices array to the buffer
_vb.SetData ( _vertices, 0, LockFlags.None ); }

Visual Basic

Private Sub LoadIndexBuffer()
Dim numIndices As Integer = (_numberOfVerticesX * 2) *
(_numberOfQuadsZ) + _numberOfVerticesZ - 2 _indices = New Integer(numIndices) {}
_ib = New IndexBuffer(GetType(Integer), _indices.Length, _device, Usage.WriteOnly,
Pool.Managed) Dim index As Integer = 0 Dim z As Integer = 0
While z &#lt; _numberOfQuadsZ If z Mod 2 = 0 Then Dim x As Integer x = 0 x = 0
While x &#lt; _numberOfVerticesX _indices(System.Math.Min(
System.Threading.Interlocked.Increment(index), index - 1)) = x + (z * _numberOfVerticesX)
_indices(System.Math.Min( System.Threading.Interlocked.Increment(index), index - 1)) = x +
(z * _numberOfVerticesX) + _numberOfVerticesX
System.Math.Min(System.Threading.Interlocked.Increment(x), x - 1) End While
If Not (z = _numberOfVerticesZ - 2) Then
_indices(System.Math.Min( System.Threading.Interlocked.Increment(index), index - 1))
= System.Threading.Interlocked.Decrement(x) + (z * _numberOfVerticesX) End If
Else Dim x As Integer x = _numberOfVerticesX - 1 x = _numberOfVerticesX - 1
While x >= 0 _indices(System.Math.Min( System.Threading.Interlocked.Increment(index),
index - 1)) = x + (z * _numberOfVerticesX) _indices(System.Math.Min
( System.Threading.Interlocked.Increment(index), index - 1)) = x +
(z * _numberOfVerticesX) + _numberOfVerticesX System.Math.Max
(System.Threading.Interlocked.Decrement(x), x + 1) End While
If Not (z = _numberOfVerticesZ - 2) Then
_indices(System.Math.Min( System.Threading.Interlocked.Increment(index), index - 1))
= System.Threading.Interlocked.Increment(x) + (z * _numberOfVerticesX) End If
End If System.Math.Min(System.Threading.Interlocked.Increment(z), z - 1) End While
_ib.SetData(_indices, 0, 0) End Sub

Cette méthode parcoure les dimensions du terrain et créé un vertex pour chaque point, soit deux coordonnées X, Z et une hauteur Y. A ce point nous ne calculons pas encore la normale car nous avons besoin d'avoir tous les points de créés.

Les valeurs Tu et Tv values explicitent la façon dont une texture est appliqué sur un vertex. Il est possible de voir les valeurs Tu et Tv comme l'abscisse et l'ordonnée de la texture. Tu et Tv sont des valeurs flottantes qui oscillent entre 0.0f et 1.0f. Une paire de coordonnées u.v coordinates est appelée Texel. Nous reviendrons les détails sur les textures et les terrains plus en détails dans le prochain article. Pour l'heure nous ne verrons que l'affichage de terrain en utilisant qu'une seule texture.

Maintenant que le vertex buffer est chargé, nous pouvons maintenant revenir sur les normales et les calculer.

Visual C#

private void ComputeNormals ( )
{ // compute normals for ( int z = 1; z &#lt; _numberOfQuadsZ; z ++)
{
for ( int x = 1; x &#lt; _numberOfQuadsX; x ++)
{ // Use the adjoing vertices along both axis to compute the new
//normal Vector3 X = Vector3.Subtract
( _vertices[ z * _numberOfVerticesZ + x + 1 ].Position,
_vertices[ z *_numberOfVerticesZ + x - 1].Position );
Vector3 Z = Vector3.Subtract
( _vertices[ (z+1) * _numberOfVerticesZ + x ].Position,
_vertices[(z-1)*_numberOfVerticesZ+x].Position );
Vector3 Normal = Vector3.Cross ( Z, X ); Normal.Normalize(); _vertices[
( z *_numberOfVerticesZ ) + x].Normal = Normal; } } }

Visual Basic

Private Sub ComputeNormals()
' compute normals Dim z As Integer = 1
While z &#lt; _numberOfQuadsZ Dim x As Integer = 1
While x &#lt; _numberOfQuadsX
' Use the adjoing vertices along both axis to
' compute the new normal
Dim VX As Vector3 = Vector3.Subtract
(_vertices(z * _numberOfVerticesZ + x + 1).Position, _vertices
(z * _numberOfVerticesZ + x - 1).Position)
Dim VZ As Vector3 = Vector3.Subtract(_vertices((z + 1) *
_numberOfVerticesZ + x).Position, _vertices((z - 1) *
_numberOfVerticesZ + x).Position) Dim Normal As
Vector3 = Vector3.Cross(VZ, VX) Normal.Normalize()
_vertices((z * _numberOfVerticesZ) + x).Normal =
Normal x = x + 1 End While z = z + 1 End While

Nous utilisons simplement les deux valeurs voisines le long des axes X et Z pour calculer une normale.

Note : Si vous désirez aller plus loin dans l'utilisation des normales rendez vous à http://www.gamedev.net/r eference/articles/article2264.asp.

Après avoir créé le vertex buffer, nous devons créer l'index buffer. Les Index buffers sont un mécanisme assez similaire aux jeux pour enfant de type "relier les points". Nous définissons tous les points de la forme à créer, puis nous indiquons l'ordre des points à relier pour former l'objet 3D. La documentation DirectX possède un article très intéressant sur les index buffer (DirectX 9.0 > Direct3D Graphics > Getting Started with Direct3D > Direct3D Rendering > Rendering Primitives > Rendering from Vertex and Index Buffers in the DirecxtX managed)

Dans la création de terrain, l'utilisation d'un nombre de vertices très important rend l'utilisation d'index buffers essentielle.

Visual C#

private void LoadIndexBuffer ( ) {
int numIndices = (_numberOfVerticesX * 2) *
(_numberOfQuadsZ) + (_numberOfVerticesZ - 2); _indices = new int[numIndices];
_ib = new IndexBuffer ( typeof( int ), _indices.Length, _device, Usage.WriteOnly,
Pool.Managed ); int index = 0; for ( int z = 0; z &#lt; _numberOfQuadsZ; z++ ) {
if ( z % 2 == 0 ) { int x; for ( x = 0; x &#lt; _numberOfVerticesX; x++ ) {
_indices[index++] = x + (z * _numberOfVerticesX); _indices[index++] =
x + (z * _numberOfVerticesX) + _numberOfVerticesX; } if ( z != _numberOfVerticesZ - 2) {
_indices[index++] = --x + (z * _numberOfVerticesX); } } else { int x; for ( x =
_numberOfVerticesX - 1; x >= 0; x-- ) { _indices[index++] = x + (z * _numberOfVerticesX);
_indices[index++] = x + (z * _numberOfVerticesX) + _numberOfVerticesX; } if ( z !=
_numberOfVerticesZ - 2) { _indices[index++] = ++x + (z * _numberOfVerticesX); } } }
_ib.SetData( _indices, 0, 0 ); }

Visual Basic

Private Sub LoadIndexBuffer()
Dim numIndices As Integer = (_numberOfVerticesX * 2) *
(_numberOfQuadsZ) + _numberOfVerticesZ - 2 _indices = New Integer(numIndices) {}
_ib = New IndexBuffer(GetType(Integer), _indices.Length, _device, Usage.WriteOnly,
Pool.Managed) Dim index As Integer = 0 Dim z As Integer = 0 While z &#lt; _numberOfQuadsZ
If z Mod 2 = 0 Then Dim x As Integer x = 0 x = 0 While x &#lt; _numberOfVerticesX
_indices(System.Math.Min( System.Threading.Interlocked.Increment(index), index - 1)) =
x + (z * _numberOfVerticesX) _indices(System.Math.Min( System.Threading.Interlocked.Increment(index),
index - 1)) = x + (z * _numberOfVerticesX) +
_numberOfVerticesX System.Math.Min(System.Threading.Interlocked.Increment(x), x - 1) End While
If Not (z = _numberOfVerticesZ - 2) Then
_indices(System.Math.Min( System.Threading.Interlocked.Increment(index), index - 1)) =
System.Threading.Interlocked.Decrement(x) + (z * _numberOfVerticesX) End If Else Dim x As Integer
x = _numberOfVerticesX - 1 x = _numberOfVerticesX - 1 While x >= 0
_indices(System.Math.Min( System.Threading.Interlocked.Increment(index), index - 1)) =
x + (z * _numberOfVerticesX) _indices(System.Math.Min( System.Threading.Interlocked.Increment(index),
index - 1)) = x + (z * _numberOfVerticesX) + _numberOfVerticesX
System.Math.Max(System.Threading.Interlocked.Decrement(x), x + 1) End While If Not (z =
_numberOfVerticesZ - 2) Then _indices(System.Math.Min( System.Threading.Interlocked.Increment(index),
index - 1)) =System.Threading.Interlocked.Increment(x) + (z *
_numberOfVerticesX) End If End If System.Math.Min(System.Threading.Interlocked.Increment(z), z - 1)
End While _ib.SetData(_indices, 0, 0) End Sub

La creation d'un index buffer est sans doute la notion la plus compliquée à comprendre. Pour notre terrain nous créons un seul TriangleStrip à la manière d'un serpent. Nous commençons du bas du terrain vers le haut. La première ligne se créé de gauche vers droite, la seconde ligne de droite vers gauche et ainsi de suite.

La partie sensible de cet algorithme porte sur le passage du dernier triangle affiché pour une ligne à celui situé sur la ligne supérieure.  C'est sans aucun doute là, la partie le plus complexe de notre apprentissage. Ne vous inquiétez pas, même moi j'ai eu réellement beaucoup de mal au départ. Reportez vous à la documentation DirectX, amusez vous avec le VertexBuffer et l'IndexBuffer en créant une petite grille (par exemple 3x3) pour voir plus facilement les modifications à l'écran de ce que vous faites dans le code. Travaillez aussi sur papier pour reproduire ce qui se passe à l'écran.

L'étape qui sut va afficher le terrain à l'écran.

Visual C#

public void Render ( ) { _device.Material = _material;
// Adjust the unit to the selected scale _device.Transform.World =
Matrix.Scaling ( 1.0f, 0.3f, 1.0f ); _device.SetTexture ( 0, _terrainTexture );
_device.Indices = _ib; _device.SetStreamSource ( 0, _vb, 0 ); _device.VertexFormat =
CustomVertex.PositionNormalTextured.Format; _device.DrawIndexedPrimitives (
PrimitiveType.TriangleStrip, 0, 0, _totalNumberOfVertices, 0, _indices.Length - 2 ); }

Visual Basic

Public Sub Render() _device.Material = _material
' Adjust the unit to the selected scale _device.Transform.World =
Matrix.Scaling(1.0F, 0.3F, 1.0F) _device.SetTexture(0, _terrainTexture)
_device.Indices = _ib _device.SetStreamSource(0, _vb, 0) _device.VertexFormat =
CustomVertex.PositionNormalTextured.Format _device.DrawIndexedPrimitives(PrimitiveType.TriangleStrip, 0,
0, _totalNumberOfVertices, 0, _indices.Length - 2) End Sub

Ce code doit vous sembler familier. La principale différence qu'on y trouve par rapport aux codes vu dans les précédents articles réside dans l'utilisation d'index buffer et l'appel à la méthode DrawIndexedPrimitives. Nous utilisons en outre un matrice d'homothétie (agrandir/réduire) pour aplanir quelque peu le terrain.

L'intégration de la classe Terrain au jeu, nous oblige à ajouter une variable à la classe GameEngine.

Visual C#

private Terrain _terrain;

Visual Basic

Private m_terrain As Terrai

Nous initialisons la classe Terrain appelons la méthode LoadHeightMap dans le constructeur de la classe GameEngine.

Visual C#

_terrain = new Terrain ( "Down.jpg", this._device );
_terrain.LoadHeightMapFromRAW ( " Heightmap256.raw" );

Visual Basic

m_terrain = New Terrain("Down.jpg", m_device)
m_terrain.LoadHeightMapFromRAW("Heightmap256.raw")

Le résultat final nous donne quelque chose d'assez similair à ceci :

Visual C#

_terrain.Render ( )

Visual Basic

_terrain.Render ( )

Dans cette image, j'ai affiché le monde en mode fil de fer.

J'ai ajouté la possibilité de pouvoir passer d'un mode d'affichage à un autre. Pressez la touche F1 pour voir la scène en mode fil de fer, F2 pour voir la scène en mode solide. F3 pour la voir en mode point. F4 et F5 activent ou annulent l'éclairage directionnel.

Haut de page Haut de page

Conclusion

Wahouh ! Nous avons déjà vu beaucoup sans avoir abordé un dixième de ce que nous pourrions voir (mapping de texture, luminisité adapté à l'altitude, light maps, Level Of Detail, ROAM, Geomipmapping, quadtrees et culling). Nous avons vu le principal toutefois pour notre jeu. Nous savons encore voir la détection de collisions, l'adaptation de la caméra aux spécificités géographiques de notre terrain afin de donner l'impression de conduire un tank. J'espère que vous continuez à vous familiariser avec le code sans trop de difficultés. Nous verrons dans le prochain article la génération de terrain plus en détails, et aborderons les collisions.

D’ici là, joyeux développements!

Haut de page Haut de page Précédent 6 sur 7 Suivant