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
IntroductionBienvenue 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.
nettoyage de codeLe 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.
FontsLe 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 Visual Basic m_font = New Microsoft.DirectX.Direct3D.Font 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() { Visual Basic Private Sub RenderFonts() ' display the heading and pitch 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.
EclairageJusqu'à 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 :
Il existe deux familles de lumières dans DirectX :
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.)
ColeurDirectX 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.
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.
Assez de théorie ! Place à la pratique.
Ajout de lumièresLa 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 ) Visual Basic If m_device.DeviceCaps.MaxActiveLights = 0 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 ) { Visual Basic Else If m_device.DeviceCaps.MaxActiveLights > 1 Then 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.
Ajouter un terrainAvez-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.
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.
Exemple d'Height mapLa 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 :
classe TerrainPour 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 ) Visual Basic Public Sub LoadHeightMapFromRAW(ByVal fileName As String) 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 ) Visual Basic Private Sub ComputeValues(ByVal width As Integer, ByVal height As Integer) A partir de là, nous sommes en mesure de charger le contenu du vertex buffer. Visual C# private void LoadVertexBuffer ( ) Visual Basic Private Sub LoadIndexBuffer() 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 ( ) Visual Basic Private Sub ComputeNormals() 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 ( ) { Visual Basic Private Sub LoadIndexBuffer() 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; Visual Basic Public Sub Render() _device.Material = _material 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 ); Visual Basic m_terrain = New Terrain("Down.jpg", m_device) 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.
ConclusionWahouh ! 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!
|