Afficher une heightmap avec OpenGL

Dans cet article nous allons traiter d'un domaine phare de la programmation graphique, le rendu de terrain. Pour ce faire nous allons utiliser une méthode classique, celle dite des cartes de hauteur (ou heightmaps). Cette méthode est particulièrement usitée dans les jeux, et propose une manière simple et intuitive d'approcher le problème.

Image non disponible

Article lu   fois.

L'auteur

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Principe des cartes de hauteur

Une carte de hauteur est en fait un simple fichier image, souvent en noir et blanc, qui représente le relief d'une région. Un exemple étant plus parlant qu'un long discours, contemplez la heightmap suivante :

Un exemple de carte de hauteur
Un exemple de carte de hauteur

Sur cette carte, on peut distinguer des zones hautes (en blanc) et des zones basses (en noir). On peut donc reconnaître une vallée, avec un lac, entourée de montagnes.

A partir de cet exemple, vous comprendrez qu'il serait pratique de disposer d'un tableau d'entiers à deux dimensions (largeur, longueur) qui contiendrait la hauteur (donc la couleur) de chaque point de notre carte. Il nous suffirait donc de matérialiser cette image sous forme d'un tableau.

Bien sur, notre carte ne se contentera pas d'un relief en noir et blanc. Nous allons plaquer par dessus une grande texture ( en l'étirant au besoin ):

Texture principale de notre carte
Texture principale de notre carte

Enfin, dernière touche d'esthétisme, nous allons encore replaquer par dessus la texture principale, une texture de détail.
Cette dernière est chargée de donner un peu de relief à la carte, avec un petit jeu d'ombrages.
Voici quelques exemples de textures de détails :

Exemple de plaquage de différentes textures de détails
Exemple de plaquage de différentes textures de détails

II. Le choix du format

Pour représenter notre carte de hauteur, nous aimerions pouvoir utiliser n'importe quel format de fichier graphique. Par la suite, nous pourrions en revanche être amené à utiliser un format particulier créé spécialement pour notre application (par exemple pour pouvoir réunir plusieurs cartes de hauteur dans un seul fichier).

Nous allons donc profiter d'un idiome assez en vogue dans le monde du C++, qui a le mérite de ne pas couter cher à implémenter: les polices (ou policies). Toute la logique de chargement de la carte de hauteur sera donc encapsulée dans un chargeur spécifique, dérivant d'une classe commune Loader.
Dans notre cas, nous nous limiteront à un chargeur réalisé grace à la librairie SFML, qui nous permettra de gérer les formats graphiques les plus cassiques : PNG, BMP, JPG...

Le principe derrière les politiques est assez simple. Il consiste à définir une classe par de multiples autres, chacune étant spécialisée pour une certaine opération.
Le mode d'assemblage de ces fragments peut se faire par composition ou par héritage. Nous avons ici choisi l'héritage, car il a le mérite de ne pas être intrusif pour le code de la classe générale.

III. Limites des cartes de hauteur

Les cartes de hauteur sont plus particulièrement adaptées aux rendus de terrain extérieurs. En effet, le but étant de modéliser des variations de relief du terrain, les cartes intérieures n'auraient que peu d'intérêt à utiliser ce type de représentation. Un autre desavantage des cartes de hauteur est qu'il n'est possible d'attribuer qu'une seule hauteur par point de la carte, autrement dit, il est impossible de modéliser des grottes, et toutes autres falaises avec une pente plus que verticale :

Problème du recouvrement
Problème du recouvrement

IV. Modélisation générale de l'application

(1) Pour illustrer le fonctionnement de notre application, commencons par créer un diagramme de classes simple.

Beaucoup font l'erreur de penser qu'un diagramme UML doit être le plus complet (et complexe) possible. Il ne s'agit pas ici d'épater la gallerie, mais de fournir une interprétation intuitive des différentes entités de l'application. Dans l'idéal, un diagramme devrait être lisible et compréhensible par n'importe qui, pour peu qu'il connaisse le minimum vital de conventions utilisées.

Diagramme général de l'application
Diagramme général de l'application

Comme nous le voyons, la classe principale est l'Application, qui se chargera d'afficher la fenêtre avec le mode vidéo choisi, de charger les différents objets ( textes, carte, ... ), et de gérer la boucle principale de rendu et de gestion d'événements.

Vient ensuite la classe Map, qui comme vous le devinez sera chargée de stocker tout le nécessaire pour pouvoir s'afficher correctement ( points, textures, caméra... ).

Voilà pour ce petit tour d'horizon de l'application. Maintenant, il convient de s'intéresser au déroulement général de l'application. C'est ce que nous allons voir avec le diagramme de séquence suivant.

Diagramme de séquence général de l'application
Diagramme de séquence général de l'application

Ce dernier diagramme étant largement commenté, il est inutile d'y revenir, nous allons donc pouvoir (enfin) rentrer dans le vif du sujet avec la programmation effective de l'application.

V. Configuration externe

Pour plus de souplesse, nous utiliserons un petit fichier de configuration XML dans le même répertoire que l'executable. Celui ci nous permettra de paramétrer le mode vidéo, et de fournir les différents fichiers de la carte de hauteur, de manière à ce que vous puissiez tester sans avoir à tout recompiler.
Voici à quoi ressemble ce fichier:

 
Sélectionnez

<?xml version="1.0" encoding="iso-8859-1"?>
<configuration>
  <videomode>
    <width>1024</width&>
    <height>768</height>
    <bpp>32</bpp>
  </videomode>
  <map>
    <name>Example map</name>
    <description>OpenGL heightmap article by NewbiZ.</description>
    <heightmap path="map_height.png" precision="1" />
    <texture   path="map_texture.png" />
    <details   path="map_details.png" />
  </map>
</configuration>

Pour le chargement de ce fichier, nous utiliserons une petite librairie XML très pratique et légère réalisée (2) par Frank Vanden Berghen .

VI. Chargement de la carte

Comme nous l'avons dit plus haut, le chargement de la carte se fait via une classe de politique appelée SFMLLoader. Comme le montre l'en-tête ci-dessous, un loader est quelquechose de très simple:

 
Sélectionnez

#ifndef SFMLLOADER_HH
#define SFMLLOADER_HH

#include <string>

#include "loader.hh"

class SFMLLoader : public Loader
{
public:
  virtual bool loadHeightmap( const std::string& filename );
};

#endif // SFMLLOADER_HH

Vous l'imaginez, l'essentiel du travail de récupération des données est fait dans la classe parente Loader. Tout ce que vous avez à faire dans un Loader, c'est de remplir le tableau de pixels, et de définir la hauteur et la largeur de la carte.

Commençons par faire un petit récapitulatif de comment va se passer le chargement avant de nous lancer dans le code.
Penchons nous donc sur la notion de précision de notre carte, que les plus attentifs auront décelé dans le fichier de configuration XML.

Un fichier image est composé d'énormément de pixels, et il n'est pas toujours approprié de vouloir assigner à chacun de ces pixels une hauteur sur notre carte. On peut volontairement réduire cette précision en "sautant" plusieurs points lors du chargement.

Les points en rouge sur l'image sont ceux qui feront partie de la géométrie de la carte de hauteur
Les points en rouge sur l'image sont ceux qui feront partie de la géométrie de la carte de hauteur

Nous sommes maintenant parés pour charger le terrain de notre heightmap.

 
Sélectionnez

bool SFMLLoader::loadHeightmap( const std::string& filename )
{
  // L'image servant à récupérer les pixels
  sf::Image image;
  
  // Si le fichier n'existe pas ou est invalide, on retourne sans rien faire,
  // sinon on peut continuer avec l'image chargée
  if ( !image.LoadFromFile(filename) )
    return false;
  
  // Assignations
  setWidth ( image.GetWidth () );
  setHeight( image.GetHeight() );
  
  // Le tableau renvoyé par GetPixelsPtr() contient les 4 composantes RGBA
  // sous forme de char
  static const int elemSize = 4;
  
  // La taille totale du tableau renvoyé par GetPixelsPtr()
  const int size = image.GetWidth() * image.GetHeight() * 4;
  
  // Création de notre propre tableau qui ne contiendra qu'une composante (noir et blanc)
  pixels_  = new unsigned char[size/elemSize];
                    
  // On stocke un pointeur vers le tableau renvoyé par GetPixelsPtr() pour y accéder plus
  // simplement
  const unsigned char* const px = image.GetPixelsPtr();
  
  // On copie les pixels
  for ( int i=0; i<size/elemSize; ++i )
    pixels_[i] = px[i*elemSize];
  
  return true;
}

Rien d'incroyable ici, la SFML a fait tout le travail pour nous. Nous avons fait charger une image par la SFML, puis avons utilisé la méthode GetPixelsStr() qui renvoit un tableau de unsigned char sous la forme RGBA. Il ne restait plus qu'à stocker ces informations dans notre tableau pixels_.
Notez qu'ici nous avons prit la composante Rouge, mais nous aurions en fait pu prendre n'importe laquelle. Dans une image en noir et blanc, toutes les composantes ont la même valeur.

Comme vous l'aurez remarqué, les points sont stockés linéairement dans pixels_. Ne vous inquiétez pas, il nous sera facile d'y accéder plus tard grace à la convertion suivante :
tableau_multidimensionnel[x][y] == tableau_monodimensionnel[y*height+x] Le schéma ci-dessous clarifiera les choses:

Conversion indice monodimensionnel <-> indices multidimensionnel
Conversion indice monodimensionnel <-> indices multidimensionnel

VII. Rendu de la carte de hauteur

VII-A. Aperçu de notre classe de carte

Un morceau de code valant plus que de longs discours, voici le fichier d'entête de notre classe de carte:

 
Sélectionnez

#ifndef MAP_HH
#define MAP_HH

#include <string>

#include "SFML/Window.hpp"
#include "SFML/Graphics.hpp"

#include "camera/camera.hh"

template <class MapLoader>
class Map : public MapLoader
{
public:
  // Constructeurs / Desctructeurs
   Map( void );
  ~Map( void );

  // Méthodes publiques
  bool loadTexture( const std::string& filename );
  bool loadDetails( const std::string& filename );
  void compile    ( void                        );
  void render     ( sf::RenderWindow& w         );

  // Accesseurs
  const std::string& getName        ( void ) const;
  const std::string& getDescription ( void ) const;
  unsigned int       getPrecision   ( void ) const;

  // Mutateurs
  void setName        ( const std::string& name        );
  void setDescription ( const std::string& description );
  void setPrecision   ( unsigned int precision         );
  void setPosition    ( unsigned int x, unsigned int y );

private:
  // Méthodes privées
  void renderTerrain ( sf::RenderWindow& w );
  void renderInfos   ( sf::RenderWindow& w );

private:
  // Attributs privés
  sf::String   name_;
  sf::String   description_;
  sf::Image    texture_main;
  sf::Image    texture_details;
  Camera       camera_;
  sf::Sprite   minimap_;
  GLuint       list_;
  unsigned int precision_;
};

#include "map.inl"

#endif // MAP_HH

Les méthodes publiques ont un nom assez explicite pour se passer de description, nous allons donc nous concentrer sur la description des méthodes privées et des attributs.

renderTerrain et renderInfos sont chargées respectivement d'afficher la carte, et les informations (nom, description et minimap).

Attribut Type Description
name_ sf::String C'est le nom de la carte. Il est modifiable dans le fichier XML de configuration, et sera affiché en haut à gauche de la carte. A noter que nous le stockons ici directement sous forme de sf::String (3) , donc en tant qu'objet affichable, et non en tant que std::string. La même remarque vaut pour la description.
description_ sf::String C'est un court texte de description de la carte. Il est aussi modifiable dans lefichier XML de configuration, et sera affiché juste en dessous du nom de la carte.
texture_main sf::Image Image (4) représentant la texture principale à plaquer sur notre carte.
texture_details sf::Image Image représentant la texture de détails à replaquer par dessus notre texture principale.
camera_ Camera Contient la caméra active de notre carte. La classe caméra est ici minimaliste et ne permet pas la rotation de la vue, le but étant de ne pas obscurcir l'article par des considérations mathématiques scabreuses.
minimap_ sf::Sprite Ce sprite (objet graphique affichable représentant tout ou partie d'une image) est en fait l'équivalent redimensionné de la texture principale. Il sera affiché en bas à droite de l'écran et permettra d'avoir rapidement une idée de la oùse situe la vue, et permettra en clickant dessus de se déplacer.
list_ GLuint OpenGL définit toute une batterie de types préfixés par "GL" qui ont l'avantage d'assurer une taille standarde sur toutes les plateformes. Nous utilisons ici un unsigned int (GLuint) pour stocker l'identifiant de la display list contenant notre carte de hauteur.
precision_ unsigned int Comme expliqué plus haut, la précision représente le nombre de pixels qui seront considérés lors de l'affichage de la carte.

Comme expliqué en début d'article, nous avons fait le choix de l'héritage pour notre politique de chargement de la carte. Ca a le mérite de la simplicité (plutot que de remapper les appels vers l'objet composé).

VII-B. Les différentes modes de rendu

Il existe 2 modes majeurs de rendu avec OpenGL: Le mode immédiat, et les display lists.
Le mode immédiat est le plus simple, il consiste à faire des appels directs à OpenGL pour lui envoyer (par exemple) les primitives à dessiner, les changements d'état, des modifications de matrice... Le problème de ce mode est qu'il nécessite des communication constantes entre le client et le serveur (i.e. le programme client et le serveur graphique).
A contrario, les display lists sont un ensemble d'instructions OpenGL qu'il est possible de compiler. Le résultat pouvant (selon la distribution d'OpenGL) être stocké directement en mémoire graphique, ou tout du moins, être sauvegardé sous une forme optimisée. De manière générale, tant que vous avez plus de quelques primitives à rendre, considérez l'utilisation de display lists.

La création de display lists est très simple, et relativement transparente par rapport au mode immédiat. Tout ce qu'il convient de faire, est de demander un nouvel identifiant de liste à OpenGL (fonction glGenLists), puis de lui indiquer que les instructions qui vont suivre devront être compilées dans une certaine liste (entourer les instructions de glNewList et glEndList).

VII-C. Choix des primitives appropriées

Nous devons définir à l'avance quel type de primitives nous allons utiliser pour rendre notre carte (quadrangles ou triangles?) ainsi que le mode de spécification de ces primitives (énumération ou strips?).

Intuitivement on pourrait être tenté de vouloir utiliser des quadrangles pour dessiner notre carte. Ce serait malheureusement une assez mauvaise idée. Imaginez la situation suivante:

Le problème de l'interprétation des quadrangles lors de vertices non coplanaires
Le problème de l'interprétation des quadrangles lors de vertices non coplanaires. Dans les cas 1) et 2), les vertices sont coplanaires, il n'y a donc pas d'ambiguité. En revanche dans les cas 3) et 4), il y a deux façons possibles de découper le quadrangle

Lorsque vous essayez de founir à OpenGL des points pour créer un quadrangle, et que ces points ne se trouvent pas sur le même plan, OpenGL n'a aucun moyen d'inférer comment redécouper ce quadrangle en triangles (puisqu'au final, pour la carte graphique, tout sera triangle). Il va donc nous falloir créer nous même nos triangles.

Pour ce qui est du choix de la diagonale délimitant nos triangles, nous choisirons arbitrairement [(x,z)(x+1,z+1)].

Image non disponible
Choix de la diagonale délimitant nos triangles
Choix de la diagonale délimitant nos triangles

Reste une question: énumération des triangles, ou strips ?

Différence entre strips et énumération
Différence entre strips et énumération

Les flèches grises représentent la "completion" qu'opère OpenGL dans le cas de points à considérer comme des énumérations (en haut), ou des strips (en bas).
Dans le cas d'une énumération, chaque triangle est composé par trois points, OpenGL se chargeant de lier le 3ème avec le premier.
Pour les strips, OpenGL considère à chaque fois que le point qui lui est passé (au delà du deuxième) est le dernier d'un nouveau triangle. Ainsi il se chargera de lier chaque point passé avec l'avant dernier passé.

Les strips présentent le gros avantage de diminuer considérablement le nombre de points nécessaires pour exprimer une géométrie.
En revanche, ils deviennent très laborieux à utiliser lorsqu'il faut définier des coordonnées de texture pour ces points (ce sera notre cas) ou lorsqu'il faut définir des surfaces "par bandes" (ce sera aussi notre cas). Nous opterons donc pour de simple énumérations de triangles.

Ne vous impatientez pas, avant d'arriver au code, il va nous falloir discuter d'un dernier point de détail: L'ordre d'affichage des vertices.
Il est évident qu'un triangle possède deux faces distinctes. La question se pose alors de leur différenciation.

OpenGL n'a aucun moyen de savoir ce qui constitue la face avant (front) et la face arrière (back) d'un triangle, si ce n'est l'ordre dans lequel vous lui fournissez les vertices. Typiquement, on apelles ces deux ordres clockwise (sens des aiguilles d'une montre) ou counter-clockwise (sens contraire des aiguilles d'une montre, ou trigonométrique).

Ordre de déclaration des vertices pour la détermination des faces
Ordre de déclaration des vertices pour la détermination des faces

Le sens par défaut est le sens trigonométrique. Vous pouvez changer ce comportement grace aux instructions suivantes:

 
Sélectionnez
glFrontFace( GL_CW  ); //         ClockWise: Aiguilles d'une montre
glFrontFace( GL_CCW ); // Counter ClockWise: Trigonométrique

L'intéret de spécifier à OpenGL quelles sont les faces avant et les faces arrières est qu'il nous sera ensuite possible de lui demander de n'afficher que certaines de ces faces, pour économiser un peu de temps de rendu. Cette méthode s'apelle le backface culling (élimination des faces arrière).

 
Sélectionnez
glEnable(GL_CULL_FACE);

Et vous pouvez ensuite spécifier à OpenGL quelles faces éliminer (front ou back) grace à l'instruction:

 
Sélectionnez
glCullFace(GL_BACK);

Dernièrement, il ne nous faudra pas oublier d'activer l'interpolation des textures pour l'affichage. Si vous venez du "monde 2D", ceci peut paraitre surprenant, mais étant donné la taille de la carte par rapport à sa texture principale, le smooth est indispensable pour un rendu correct.

A gauche, avec interpolation. A droite, sans interpolation.
A gauche, avec interpolation. A droite, sans interpolation.

VII-D. Compilation de la display list

Nous pouvons (enfin) nous pencher sur le code qui va être compilé dans notre display list. Trève de bavardages, le voici :

 
Sélectionnez
template <class MapLoader>
void Map<MapLoader>::compile( void )
{
  // Génération d'un identifiant pour notre display lsit
  list_ = glGenLists( 1 );
  
  // On demande à OpenGL de compiler ce qui va suivre dans
  // une display list identifiée par list_, que l'on vient de
  // générer.
  glNewList( list_, GL_COMPILE );
  {
    // Nous informons OpenGL que ce qui va suivre est une
    // énumération de triangles.
    glBegin( GL_TRIANGLES );
    {
      //  Pour chaque ligne, avec un pas dépendant de la précision souhaitée
      for ( unsigned int x=0; x<(getHeight()-getPrecision()); x+=getPrecision())
      {
        // Pour chaque colonne, avec un pas dépendant de la précision souhaitée
        for ( unsigned int z=0; z<(getWidth()-getPrecision()); z+=getPrecision())
        {
          // Définition des coordonnées des points
          Point3D<GLfloat> vertex1( x,
                                    getPixel( x, z ),
                                    z,
                                    x/getWidth (),
                                    1.f-(z/getHeight()) );
                                    
          Point3D<GLfloat> vertex2( getPrecision()+x,
                                    getPixel( getPrecision()+x, z ),
                                    z,
                                    (x+getPrecision())/getWidth(),
                                    1.f-(z/getHeight()) );
                                    
          Point3D<GLfloat> vertex3( getPrecision()+x,
                                    getPixel( getPrecision()+x, getPrecision()+z ),
                                    getPrecision()+z,
                                    (x+getPrecision())/getWidth(),
                                    1.f-((z+getPrecision())/getHeight()) );
                                    
          Point3D<GLfloat> vertex4( x,
                                    getPixel( x, getPrecision()+z ),
                                    getPrecision()+z,
                                    (x)/(float)getWidth (),
                                    1.f-((z+getPrecision())/getHeight()) );
          
          // Premier triangle
          vertex3.sendWithText();
          vertex2.sendWithText();
          vertex1.sendWithText();
          
          // Deuxième triangle
          vertex4.sendWithText();
          vertex3.sendWithText();
          vertex1.sendWithText();
        }
      }
    }
    glEnd();
  }
  glEndList();
}

Une petite explication s'impose.
La classe Point3D est très simple. Trois attributs publicsx, y et z, ainsi qu'un constructeur prenant en paramètre les coordonnées du point, ainsi que d'optionnelles coordonnées de texture.
La méthode sendWithText (i.e. "Envoyer avec coordonnées de texture" ) se content de générer l'instruction glTexCoord2f et glVertex3f avec les paramètres appropriés.
Voici le code de cette classe:

 
Sélectionnez
#ifndef POINT3D_HH
#define POINT3D_HH

template <class T>
class Point3D
{
public:
   Point3D ( T mx=0, T my=0, T mz=0, float mtx=0, float mty=0 ):
   x ( mx  ),
   y ( my  ),
   z ( mz  ),
   tx( mtx ),
   ty( mty )
   {
   }

  ~Point3D ( void )
  {
  }

  void send( void ) const
  {
    glVertex3f( static_cast<float>(x),
                static_cast<float>(y),
                static_cast<float>(z) );
  }

  void sendWithText( void ) const
  {
    glTexCoord2f( tx, ty );
    glVertex3f( static_cast<float>(x),
                static_cast<float>(y),
                static_cast<float>(z) );
  }

public:
  T x;
  T y;
  T z;

  float tx;
  float ty;
};

#endif // POINT3D_HH

Notez bien pour les coordonnées de texture que l'on est obligé d'exprimer leur hauteur comme l'inverse de ce qu'attend OpenGL. Ceci est du à la différence d'interprétation des coordonnées de texture entre OpenGL (origine en bas à gauche) et SFML (en haut à gauche).

VII-E. Affichage de la display list

Cette partie est peut-être la plus simple de toutes. Maintenant que la display list a été compilée, tout ce qu'il reste à faire, c'est de l'appeler avec glCallList.

 
Sélectionnez
template <class MapLoader>
void Map<MapLoader>::render( sf::RenderWindow& w )
{
  renderTerrain( w );           // Affichage du terrain
  renderInfos  ( w );           // Affichage des informations
  camera_.renderPosition( w );  // Affichage de la position sur la minimap
}

template <class MapLoader>
void Map<MapLoader>::renderInfos( sf::RenderWindow& w )
{
  w.Draw        ( name_            );  // Affichage de l'ombre du nom
  name_.Move    ( -2.f, -2.f       );  // Décalage pour le texte en blanc
  name_.SetColor( sf::Color::White );  // Le prochain affichage sera blanc
  w.Draw        ( name_            );  // Rendu en blanc
  name_.SetColor( sf::Color::Black );  // Retour à la couleure de l'ombre
  name_.Move    (  2.f,  2.f       );  // Retour à la place initiale
  
  w.Draw               ( description_     ); // Idem qu'au dessus
  description_.Move    ( -1.f, -1.f       );
  description_.SetColor( sf::Color::White );
  w.Draw               ( description_     );
  description_.SetColor( sf::Color::Black );
  description_.Move    (  1.f,  1.f       );

  w.Draw( minimap_ ); // Affichage de la minimap
}

template <class MapLoader>
void Map<MapLoader>::renderTerrain( sf::RenderWindow& )
{
  glClear          ( GL_DEPTH_BUFFER_BIT ); // Réinitialisation z-buffer
  camera_.focus    (                     ); // gluLookAt
  glScalef         ( 1.f, 0.15f, 1.f     ); // Diminution du rapport de hauteur
  glEnable         ( GL_TEXTURE_2D       ); // Activation du texturing
  texture_main.Bind(                     ); // Sélection de la texture principale
  glCallList       ( list_               ); // Appel de la display list
}

Avant d'attaquer le multitexturing pour gérer la texture de détails, une petite pause avec le résultat actuel :

Rendu de la carte de hauteur sans texture de détails
Rendu de la carte de hauteur sans texture de détails

Les différents diagrammes que vous trouverez dans cet article ont été réalisés avec le logiciel Poseidon For UML, Community Edition. Pour votre usage personnel, je vous conseille l'excellent ArgoUML qui, tout en gardant toutes les fonctionnalités de Poseidon, a l'avantage de la gratuité.
Vous pouvez retrouver Frank Vanden Berghen sur son site http://www.applied-mathematics.net/. N'hésitez pas à le lui signaler si vous utilisez sa librairie, il en sera très content.
SFML définit une classe appelée sf::String permettant d'afficher des chaines de caractère à partir d'un texte et d'une police au format True Type Font. Nous parlons bien ici d'objets graphiques, ce ne sont en aucun cas des conteneurs pour un texte "normal".
SFML distingue les Image des Sprite. Les premières sont la représentation informatique d'un fichier graphique. Grossièrement, on pourrait l'associer à un tableau de pixels. Les sprites sont des objets graphiques permettant l'affichage de tout ou partie d'une Image. Plusieurs Sprite peuvent donc partager la même image, mais en afficher des parties différentes, à des tailles différentes, à des positions différentes...

  

Copyright © 2008 Aurélien Vallée. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.