Écrit par David Henry, le 27 décembre 2004
Mis à jour le 18 août 2007
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 :
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
.
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.
À 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.
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 !
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 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); } }
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 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 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.
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.
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.
Cet article est mis à disposition sous un contrat Creative Commons (licence CC-BY-ND).