Introduction

Cet article a pour objectif d'expliquer comment créer une texture OpenGL à partir d'un fichier image, et en particulier ici, une image TGA (TARGA). Le code est en C.

Un fichier image est un fichier binaire contenant (le plus souvent) une en-tête (header) de fichier puis les données. Les données peuvent être les données de l'image et la palette de couleur, ou seulement les données si l'image ne nécessite pas de palette. L'en-tête fournis des informations quant à la nature des données : les dimensions de l'image, le type de l'image (avec palette ou non), le type de compression, etc. Un format d'image peut contenir plusieurs types d'images (c'est le cas du format TGA par exemple) comme une image 24 bits non compressées, une image 32 bits, une image 8 bits avec palette, etc. Les informations que l'on trouve sont spécifiques au format de fichier.

Le format TGA peut contenir des images 8, 16, 24 et 32 bits avec ou sans compression. La méthode de compression est l'algorithme RLE, qui permet une compression non destructrice de l'image. La compression JPEG est dite « destructrice », puisque les données compressées ne seront pas identiques à celles d'origine (il y a perte d'information). Les images 8 bits peuvent utiliser une palette de couleur.

Certains formats de fichier ont aussi un « pied » (footer) contenant des informations additionnelles. C'est le cas du format TGA, mais nous ne le verront pas ici ces données ne nous seront pas utiles.

Nous allons voir dans cet article comment lire un fichier d'image TGA ; c'est-à-dire lire son en-tête afin de déterminer comment extraire les données de l'image, puis lire ces données et les stocker en mémoire pour créer à partir de cette image, une texture OpenGL.

Les types utilisés ont les tailles suivantes :

Texture OpenGL

Avant de commencer à lire le fichier, il est nécessaire de connaître les données dont nous avons besoin pour créer une texture OpenGL.

Il nous faut bien évidemment un tableau de données pour stocker les pixels de l'image. OpenGL peut prendre plusieurs types de données pour créer une texture (octets, entiers, etc.). Nous nous restreindront ici au type « octet non signé » pour stocker nos données.

Nous avons également besoin de connaître les dimensions de l'image, le nombre de composantes R, V, B, A ou Luminance et Alpha de l'image et le format (RVB, RVBA, Luminance ou Luminance Alpha).

Nous allons regrouper ces données dans une type structure gl_texture_t :

/* OpenGL texture info */
struct gl_texture_t
{
  GLuint width;           /* largeur */
  GLuint height;          /* hauteur */

  GLenum format;          /* RVB, RVBA, Luminance, Luminance Alpha */
  GLint  internalFormat;  /* composantes d'un texel */
  GLuint id;

  GLubyte *texels;        /* données de l'image */
};

Pour la lecture d'une image TGA, nous créeront une fonction qui prendra en paramètre un nom de fichier et un pointeur vers un objet de type gl_texture_t. Une fois l'image chargée, nous pourront utiliser les fonctions OpenGL pour créer une texture à partir de notre objet gl_texture_t.

Lecture de l'en-tête TGA

L'en-tête d'un fichier TGA possède divers champs et est invariable d'un fichier à un autre (on retrouve toujours les mêmes champs dans le même ordre et le même nombre). On peut donc regrouper toutes ces informations à l'intérieure d'une structure de données que l'on lira d'un bloc :

#pragma pack(push, 1)
/* TGA header */
struct tga_header_t
{
  GLubyte id_lenght;          /* size of image id */
  GLubyte colormap_type;      /* 1 is has a colormap */
  GLubyte image_type;         /* compression type */

  short	cm_first_entry;       /* colormap origin */
  short	cm_length;            /* colormap length */
  GLubyte cm_size;            /* colormap size */

  short	x_origin;             /* bottom left x coord origin */
  short	y_origin;             /* bottom left y coord origin */

  short	width;                /* picture width (in pixels) */
  short	height;               /* picture height (in pixels) */

  GLubyte pixel_depth;        /* bits per pixel: 8, 16, 24 or 32 */
  GLubyte image_descriptor;   /* 24 bits = 0x00; 32 bits = 0x80 */
};
#pragma pack(pop)

Le type tga_header_t doit avoir une taille de 18 octets. Certains compilateurs utilisent par défaut un alignement différent (sur 4 octets par exemple) et qui font qu'une structure de 18 octets occupe 20 octets (multiple de 4). C'est pour cela qu'on utilise les directives préprocesseur #pragma pack pour aligner le type tga_header_t sur un octet.

Étudions les champs de cette structure : id_lenght est la taille d'un champ de données situé juste après l'en-tête du fichier. Ce champ ne nous est pas utile et n'est pas toujours présent. id_lenght nous permet de sauter cette zone mémoire pour atteindre les données de l'image.

colormap_type indique si l'image utilise une palette de couleur ou non. Si colormap_type vaut 1, alors on doit trouver la palette au début du champ de données, juste avant les données des pixels.

image_type indique le type d'image contenu dans le fichier. Les valeurs qu'il peut prendre sont les suivantes :

Valeur Type d'image
0 pas de données images
1 image 8 bits non compressée en mode index couleur (avec palette)
2 image 16, 24 ou 32 bits non compressée en mode BVR
3 image 8 ou 16 bits non compressée en mode dégradé de gris
9 image 8 bits compressée en mode index couleur (avec palette)
10 image 16, 24 ou 32 bits compressée en mode BVR
11 image 8 ou 16 bits compressée en mode dégradé de gris

Les trois variables commençant par cm sont spécifiques à la palette de couleur. cm_first_entry indique l'index de la première couleur dans la palette. Il est possible en effet que la palette soit d'une taille plus grande que nécessaire et les données de la palette peuvent ne pas se trouver au début de ce champs (au milieu par exemple). cm_length est le nombre de couleurs de la palette. Enfin, cm_size est le nombre de bits par couleur de la palette. Il peut être de 15, 16, 24 ou 32.

x_origin et y_origin sont les coordonnées absolues du coin bas gauche de l'image, et elles ne nous serviront pas.

width et height sont les dimensions de l'image (largeur et hauteur). pixel_depth indique le nombre de bits utilisés pour stocker un pixel de l'image. Typiquement 8, 16, 24 ou 32. Le champ image_descriptor n'est pas utile dans notre cas.

À partir de cette structure, nous pouvons déjà obenir un certain nombre d'informations sur l'image et initialiser tous les champs de notre objet gl_texture_t à l'exception de texels. Nous pouvons néanmoins réserver une zone mémoire pour stocker les données pixels de l'image. Voyons la lecture de l'en-tête :

struct gl_texture_t *
ReadTGAFile (const char *filename)
{
  FILE *fp;
  struct gl_texture_t *texinfo;
  struct tga_header_t header;
  GLubyte *colormap = NULL;

  fp = fopen (filename, "rb");
  if (!fp)
    {
      fprintf (stderr, "error: couldn't open \"%s\"!\n", filename);
      return NULL;
    }

  /* Lecture de l'en-tête du fichier */
  fread (&header, sizeof (struct tga_header_t), 1, fp);

  texinfo = (struct gl_texture_t *)malloc (sizeof (struct gl_texture_t));
  GetTextureInfo (&header, texinfo);
  fseek (fp, header.id_lenght, SEEK_CUR);

  /* Lecture de la palette... */

  /* Allocations mémoire nécessaires... */

  /* Lecture des données pixels de l'image... */

  /* Désallocations mémoire nécessaires... */

  fclose (fp);
  return texinfo;
}

La fonction ReadTGAFile() a pour but de lire un fichier image TGA et de stocker ses données dans texinfo. En cas d'échec elle retourne NULL, sinon un objet gl_texture_t alloué dynamiquement.

Pour le moment notre fonction ne fait pas grand chose : ouverture du fichier en mode binaire ("rb"), lecture de l'en-tête dans header, initialisation des variables de texinfo et déplacement au début des données de l'image (rappelez-vous, il peut y avoir des informations additionnelles entre l'en-tête et les données de l'image). La fonction GetTextureInfo() est détaillée dans la section suivante.

Type de texture OpenGL

À partir de l'en-tête TGA, nous pouvons en tirer toutes les informations nécessaire quant au type de texture OpenGL que l'on va utiliser. C'est le travail de la fonction GetTextureInfo() :

void
GetTextureInfo (const struct tga_header_t *header, struct gl_texture_t *texinfo)
{
  texinfo->width = header->width;
  texinfo->height = header->height;

  switch (header->image_type)
    {
    case 3:  /* Grayscale 8 bits */
    case 11: /* Grayscale 8 bits (RLE) */
      {
	if (header->pixel_depth == 8)
	  {
	    texinfo->format = GL_LUMINANCE;
	    texinfo->internalFormat = 1;
	  }
	else /* 16 bits */
	  {
	    texinfo->format = GL_LUMINANCE_ALPHA;
	    texinfo->internalFormat = 2;
	  }

	break;
      }

    case 1:  /* 8 bits color index */
    case 2:  /* BGR 16-24-32 bits */
    case 9:  /* 8 bits color index (RLE) */
    case 10: /* BGR 16-24-32 bits (RLE) */
      {
	/* 8 bits and 16 bits images will be converted to 24 bits */
	if (header->pixel_depth <= 24)
	  {
	    texinfo->format = GL_RGB;
	    texinfo->internalFormat = 3;
	  }
	else /* 32 bits */
	  {
	    texinfo->format = GL_RGBA;
	    texinfo->internalFormat = 4;
	  }

	break;
      }
    }
}

Les dimensions de l'image (width et height) sont récupérées directement à partir de l'en-tête. En revanche il faut déterminer le nombre de composantes et le format de l'image à partir de son type TGA et du nombre de bits par pixel.

Les images en 8 et 16 bits seront converties à la lecture en 24 bits sauf pour les images en dégradé de gris, où l'on utilise le format GL_LUMINANCE. Les autres sont au format GL_RGB ou GL_RGBA. Une image 16 bits en dégradé de gris est composé de 8 bits pour l'intensité de gris et d'un canal alpha de 8 bits.

La palette de couleurs

Les images 8 bits (sauf dégradé de gris) nécessitent une palette de couleur. Chaque octet représentant un pixel dans les données de l'image, est en fait un index pour aller chercher sa couleur dans la palette. La palette est un tableau de couleurs.

Dans cet article on supposera que la palette a toujours une profondeur de 24 bits par couleur. C'est-à-dire, chaque couleur est composé de 8 bits pour le bleu, 8 bits pour le vert et 8 bits pour le rouge. La palette est en BVR (et non RVB !).

Dans notre fonction ReadTGAFile(), nous nous sommes arrêtés au début des données de l'image. Nous devons maintenant tester s'il y a une palette de couleur et, le cas échéant, la stocker. Nous la stockeront dans le tableau colormap déclaré en début de fonction :

/* Read color map */
if (header.colormap_type)
  {
    /* NOTE: color map is stored in BGR format */
    colormap = (GLubyte *)malloc (sizeof (GLubyte)
      * header.cm_length * (header.cm_size >> 3));
    fread (colormap, sizeof (GLubyte), header.cm_length
      * (header.cm_size >> 3), fp);
  }

L'expression (header.cm_size >> 3) nous donne la taille en octet d'une couleur à partir d'une taille en bits (division par 23 ou 8). On peut ainsi connaître la taille en octets de la palette et la lire.

Attention : ici nous allouons dynamiquement de la mémoire pour stocker la palette ; il ne faudra pas oublier de la libérer à la fin de la fonction, lorsque nous auront lus les données !

Lecture des données

Il nous faut maintenant lire les données pixels de l'image. Elle se trouvent juste après la palette couleur si celle-ci existe, sinon après le petit champ d'informations additionnelles suivant l'en-tête.

Il nous faut allouer un espace mémoire pour stocker les données pixels :

/* Memory allocation */
texinfo->texels = (GLubyte *)malloc (sizeof (GLubyte) *
      texinfo->width * texinfo->height * texinfo->internalFormat);
if (!texinfo->texels)
  {
    free (texinfo);
    return NULL;
  }

À l'aide des variables image_type et pixel_depth de l'en-tête, nous allons devoir déterminer quelle méthode utiliser pour lire ces données. Le plus pratique je pense est de faire un choix conditionnel (switch) suivant image_type puis pour chaque cas, voir avec pixel_depth si c'est nécessaire.

Il y a quatre grand type d'images : les images en 16-24-32 bits, appelées également images en couleurs vraies, les images 8 bits avec palette, les images en dégradé de gris 8 ou 16 bits, et les images compressées (où l'on retrouve les trois premiers types à chaque fois). On va commencer par le plus simple et finir par les images compressées.

Les images en couleurs vraies

Les images en couleurs vraies sont des images en 16, 24 ou 32 bits où les données « complètes » de chaque pixel se suivent. Pour les images TGA, les pixels sont en Bleu-Vert-Rouge (BVR). Pour OpenGL, nous avons besoin de données stockées en Rouge-Vert-Bleu (RVB) (à moins d'utiliser une extension permettant d'utiliser directement des données au format BVR). Il suffit donc de lire le nombre d'octets approprié pour autant de pixels qu'il y'a dans l'image (pour connaître le nombre de pixels, il faut multiplier la hauteur par la largeur) :

void
ReadTGA24bits (FILE *fp, struct gl_texture_t *texinfo)
{
  int i;

  for (i = 0; i < texinfo->width * texinfo->height; ++i)
    {
      /* Read and convert BGR to RGB */
      texinfo->texels[(i * 3) + 2] = (GLubyte)fgetc (fp);
      texinfo->texels[(i * 3) + 1] = (GLubyte)fgetc (fp);
      texinfo->texels[(i * 3) + 0] = (GLubyte)fgetc (fp);
    }
}

Pour chaque pixel, on lit bien 3 octets (24 bits). Pour connaître la position des données d'un pixel, il faut multiplier par 3 sa position i dans l'image, car on utilise 3 octet par pixel. Ensuite, on peut accéder aux composantes rouge, vert et bleu en ajoutant 0, 1 ou 2 à la position du début des données du pixel.

Pour une image 32 bits, c'est pareil sauf que l'on utilise 4 octets par pixel. Le dernier octet est la composante alpha (transparance) :

void
ReadTGA32bits (FILE *fp, struct gl_texture_t *texinfo)
{
  int i;

  for (i = 0; i < texinfo->width * texinfo->height; ++i)
    {
      /* Read and convert BGRA to RGBA */
      texinfo->texels[(i * 4) + 2] = (GLubyte)fgetc (fp);
      texinfo->texels[(i * 4) + 1] = (GLubyte)fgetc (fp);
      texinfo->texels[(i * 4) + 0] = (GLubyte)fgetc (fp);
      texinfo->texels[(i * 4) + 3] = (GLubyte)fgetc (fp);
    }
}

Les images en 16 bits sont un peu plus délicates, car il faut extraire les 3 composantes d'une zone mémoire de 2 octets. Chaque composante est codée sur 5 bits ; le bit de poids fort (le plus à gauche) de ces deux octets n'est pas utilisé (c'est une image 15 bits en fait).

Pour extraire chaque composante, on applique un masque binaire :

Composante Masque Masque en Binaire
rouge couleur & 7C00 01111100 00000000
vert couleur & 03E0 00000011 11100000
bleu couleur & 001F 00000000 00011111

Une fois la composante isolée, il faut faire un décalage de 10 bits vers la droite pour le rouge et 5 bits pour le vert afin d'avoir une valeur comprise entre 0 et 31. Il faut ramener cette valeur sur un intervalle de 0 à 255. Pour cela, on multiplie chaque composante obtenue par 8 (ou on fait un décalage à gauche de 3 rangs).

Pour chaque pixel, on lit 2 octets. On peut les lire d'un bloc ou les lire octet par octet. Dans de dernier cas, il faut décaller le premier octet de 8 rangs pour reconstituer la valeur sur 16 bits.

void
ReadTGA16bits (FILE *fp, struct gl_texture_t *texinfo)
{
  int i;
  unsigned short color;

  for (i = 0; i < texinfo->width * texinfo->height; ++i)
    {
      /* Read color word */
      color = fgetc (fp) + (fgetc (fp) << 8);

      /* Convert BGR to RGB */
      texinfo->texels[(i * 3) + 0] = (GLubyte)(((color & 0x7C00) >> 10) << 3);
      texinfo->texels[(i * 3) + 1] = (GLubyte)(((color & 0x03E0) >>  5) << 3);
      texinfo->texels[(i * 3) + 2] = (GLubyte)(((color & 0x001F) >>  0) << 3);
    }
}

Les images 8 bits avec palette couleurs

La lecture d'images 8 bits avec palette n'est pas difficile non plus. Chaque « donnée pixel » est codée sur un octet. Cet octet est l'index de la couleur 24 bits (dans le cadre de cet article) dans la palette.

Pour créer une texture OpenGL à partir d'une image 8 bits, on va d'abord la convertir en 24 bits.

void
ReadTGA8bits (FILE *fp, const GLubyte *colormap, struct gl_texture_t *texinfo)
{
  int i;
  GLubyte color;

  for (i = 0; i < texinfo->width * texinfo->height; ++i)
    {
      /* Read index color byte */
      color = (GLubyte)fgetc (fp);

      /* Convert to RGB 24 bits */
      texinfo->texels[(i * 3) + 2] = colormap[(color * 3) + 0];
      texinfo->texels[(i * 3) + 1] = colormap[(color * 3) + 1];
      texinfo->texels[(i * 3) + 0] = colormap[(color * 3) + 2];
    }
}

L'accès aux composantes des couleurs de la palette se fait de la même façon que pour une image 24 bits. On en profite ici pour échanger les composantes bleu et rouges dans notre tableau de pixels.

Les images en dégradé de gris

Les images en dégradé de gris sont simples à traiter. Pour les images 8 bits, chaque octet représente la valeur de l'intensité lumineuse. Pour les images 16 bits, le second octet est la valeur du cannal alpha.

void
ReadTGAgray8bits (FILE *fp, struct gl_texture_t *texinfo)
{
  int i;

  for (i = 0; i < texinfo->width * texinfo->height; ++i)
    {
      /* Read grayscale color byte */
      texinfo->texels[i] = (GLubyte)fgetc (fp);
    }
}

Pour ce type d'image, on peut même lire les données en une instruction :

fread (texinfo->texels, sizeof (GLubyte), texinfo->width * texinfo->height, fp);

Pour une image 16 bits, seule une ligne change :

void
ReadTGAgray16bits (FILE *fp, struct gl_texture_t *texinfo)
{
  int i;

  for (i = 0; i < texinfo->width * texinfo->height; ++i)
    {
      /* Read grayscale color + alpha channel bytes */
      texinfo->texels[(i * 2) + 0] = (GLubyte)fgetc (fp);
      texinfo->texels[(i * 2) + 1] = (GLubyte)fgetc (fp);
    }
}

Les images avec compression RLE

Les images compressées utilisent un algorithme de compression appelé Run Lenght Encoding (RLE). Il est est simple et non destructeur (les données de l'image ne sont pas altérées : on retrouve au décodage la même chose qu'à l'encodage).

Le principe est le suivant : les champs de pixels consécutifs égaux sont remplacés par deux valeurs, un pour leur nombre, l'autre pour leur valeur. Ainsi on on gagne à partir de trois pixels consécutifs égaux pour une image 8 bits (la valeur et le pixel sont codés sur un octet chacun). Dans le cas où il y a une large zone de pixels complètement différents les un des autres, on l'indique sur un octet ainsi que leur nombre (les deux sur le même octet). La première valeur est toujours stockée sur un octet. La seconde dépend du type d'image (1, 2, 3 ou 4 octets).

Concrètement, ce que l'on fait : on lit un octet. Le bit de poids fort (le plus à gauche) indique s'il s'agit un champs d'octets consécutifs égaux (1) ou pas (0). Les 7 bits restant sont le nombre d'octets égaux ou non égaux du champs que l'on va traiter. Dans le cas d'un champs d'octets égaux, on lit un pixel, qui est la valeur de tous les pixels de ce champs, et on initialise ces dernier avec. Si le champs n'est pas constitué de pixels égaux, on lit chaque pixel à partir du fichier.

Remarque : comme le nombre de pixels par champs est codé sur 7 bits, on ne peut avoir au plus que 128 pixels traités par packet.

Voici comment faire pour une image 24 bits. Les autres types d'image se lisent pareil, avec les méthodes de décodage des octets vues plus haut sans compression. Voyez les sources complètes pour les autres types d'image (voir fin de l'article) :

void
ReadTGA24bitsRLE (FILE *fp, struct gl_texture_t *texinfo)
{
  int i, size;
  GLubyte rgb[3];
  GLubyte packet_header;
  GLubyte *ptr = texinfo->texels;

  while (ptr < texinfo->texels + (texinfo->width * texinfo->height) * 3)
    {
      /* Read first byte */
      packet_header = (GLubyte)fgetc (fp);
      size = 1 + (packet_header & 0x7f);

      if (packet_header & 0x80)
	{
	  /* Run-length packet */
	  fread (rgb, sizeof (GLubyte), 3, fp);

	  for (i = 0; i < size; ++i, ptr += 3)
	    {
	      ptr[0] = rgb[2];
	      ptr[1] = rgb[1];
	      ptr[2] = rgb[0];
	    }
	}
      else
	{
	  /* Non run-length packet */
	  for (i = 0; i < size; ++i, ptr += 3)
	    {
	      ptr[2] = (GLubyte)fgetc (fp);
	      ptr[1] = (GLubyte)fgetc (fp);
	      ptr[0] = (GLubyte)fgetc (fp);
	    }
	}
    }
}

Comme il peut y avoir d'autres données après les données de l'image, on doit veiller à ne pas dépasser la taille de l'image. On utilise un masque binaire pour extraire le premier bit et les sept autres.

Création de la texture

Maintenant que l'on a lu et stocké les données de l'image sous un format exploitable par OpenGL, on peut aisément créer notre texture à partir de l'objet gl_texture_t :

/* Load TGA image */
struct gl_texture_t *gltex = ReadTGAFile (filename);
if (!gltex)
  {
    fprintf (stderr, "error: couldn't load %s!\n", filename);
    exit (EXIT_FAILURE);
  }

/* Generate texture */
glGenTextures (1, &texid);
glBindTexture (GL_TEXTURE_2D, texid);

/* Setup texture filters */
glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

glTexImage2D (GL_TEXTURE_2D, 0, gltex->internalFormat, gltex->width, gltex->height,
              0, gltex->format, GL_UNSIGNED_BYTE, gltex->texels);

/* OpenGL has its own copy of texture data */
free (gltex->texels);
free (gltex);

D'abord on utilise notre fonction ReadTGAFile() pour initialiser gltex, un objet de type gl_texture_t. En cas de succès de la lecture de l'image, on crée la texture OpenGL avec les fonctions faites pour. Vérifiez bien que les dimensions de votre image sont multiple de 2 ou alors il vous faudra utiliser gluScaleImage() ou bien utiliser gluBuild2DMipmaps() à la place de glTexImage2D() (dans ce cas il faut aussi modifier le paramètre de filtrage associé à GL_TEXTURE_MAG_FILTER).

Une fois la texture créée, on libère la mémoire allouée pour stocker les données des pixels de l'image car OpenGL possède sa propre copie de ces données, il est donc inutile d'encombrer la mémoire avec ça.

Conclusion

Cet article vous a permis de voir comment on crée une texture à partir d'une image stockée dans un fichier, ici un fichier TGA. Pour d'autres formats, ça se passe un peu de la même manière : lecture de l'en-tête, choix de la méthode appropriée pour lire les données suivant le type de l'image, etc.

Il y a deux façons de lire un fichier : lire les éléments un par un, comme ici, ou lire le fichier tout entier dans une mémoire tampon puis extraire les données à partir de cette zone mémoire. Cette dernière est peut-être plus rapide car demande moins d'accès fichiers. Elle est d'ailleurs facilement adaptable ici.

Pour certains formats, des bibliothèques toutes prêtes existent pour manipuler les fichiers images comme libpng pour le format PNG, libjpeg pour le format JPEG ou libtiff pour les images TIFF. Il existe églament des bibliothèques permettant de gérer plusieurs formats de fichiers image avec une seule fonction, comme SDL_image.

Deux programmes d'exemples sont disponibles :

Je me suis inspiré d'une documentation plus complète trouvée sur le net il y'a longtemps, je ne sais plus où exactement... Vous pouvez tester le programme avec des images de test TGA de différents types. Contact : tfc.duke (CHEZ) gmail (POINT) com.

Creative Commons Logo Contrat Creative Commons

Cet article est mis à disposition sous un contrat Creative Commons (licence CC-BY-ND).