Tous droits réservés

Les textures et les images

Dernière mise à jour :

Dans ce chapitre, nous allons traiter des textures. Maintenant que nous savons comment nous occuper du rendu de notre fenêtre, il nous faut voir comment faire des dessins plus évolués et c’est le but des textures. Pour commencer, nous pouvons voir les textures comme de simples paquets de pixels qu’on va coller sur la fenêtres. Par exemple, nous pourrions charger les pixels d’une image dans une texture et ensuite coller cette texture sur l’écran c’est-à-dire afficher l’image.

Les textures

Généralités sur les textures

Une texture est une structure SDL_Texture. Nous avons dit qu’on pouvait le voir comme un paquet de pixels (un rectangle plus précisément). Ce paquet de pixels, on pourra l’afficher, le modifier, etc.

Créer une texture

Voyons maintenant comment créer une texture. Cette opération se fait à l’aide de la fonction SDL_CreateTexture. Son prototype :

1
2
3
4
5
SDL_Texture* SDL_CreateTexture(SDL_Renderer* renderer,
                               Uint32        format,
                               int           access,
                               int           w,
                               int           h)

Elle prend en paramètre :

  • renderer est le renderer auquel on veut que notre texture soit associée.
  • format correspond au format de pixel (il en existe plusieurs donnés dans l’énumération SDL_PixelFormatEnum). Nous allons généralement choisir la valeur SDL_PIXELFORMAT_RGBA8888 qui correspond à la représentation que nous connaissons avec 4 chiffres entre 0 et 255.
  • access correspond aux restrictions d’accès de notre structure. On peut lui donner trois valeurs (voir l’énumération SDL_TextureAccess).
  • w et h correspondent à la largeur (width) et à la hauteur (height) de la texture (qui est, rappelons le, une sorte de rectangle de pixels).

Les trois valeurs possibles pour access sont celles-ci.

valeur

Description

SDL_TEXTUREACCESS_STATIC

La texture est rarement modifiée

SDL_TEXTUREACCESS_STREAMING

La texture est souvent modifiée

SDL_TEXTUREACCESS_TARGET

La texture peut être utilisée comme cible de rendu (comme un renderer)

Celles que nous allons utiliser ici sont SDL_TEXTUREACCESS_TARGET et SDL_TEXTUREACCESS_STREAMING.

La fonction SDL_CreateTexture renvoie un pointeur sur SDL_Texture. Si la création de la texture échoue, ce pointeur vaut NULL, sinon il pointe sur la texture créée (il ne faudra donc pas oublier de récupérer et tester la valeur retournée).

Détruire une texture

Nous pouvions le deviner. Il faut détruire la texture une fois qu’on a fini de l’utiliser. Cette action se fait avec la fonction SDL_DestroyTexture dont le prototype est :

1
void SDL_DestroyTexture(SDL_Texture* texture)

Comme d’habitude, la fonction de destruction ne renvoie rien et prend en paramètre la texture à détruire. On peut maintenant écrire notre code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <SDL2/SDL.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
    SDL_Window *window = NULL;
    SDL_Renderer *renderer = NULL;
    SDL_Texture *texture = NULL;
    int statut = EXIT_FAILURE;

    if(0 != SDL_Init(SDL_INIT_VIDEO))
    {
        fprintf(stderr, "Erreur SDL_Init : %s", SDL_GetError());
        goto Quit;
    }
    if(0 != SDL_CreateWindowAndRenderer(640, 480, SDL_WINDOW_SHOWN, &window, &renderer))
    {
        fprintf(stderr, "Erreur SDL_CreateWindowAndRenderer : %s", SDL_GetError());
        goto Quit;
    }
    texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, 
                                SDL_TEXTUREACCESS_TARGET, 200, 200);
    if(NULL == texture)
    {
        fprintf(stderr, "Erreur SDL_CreateTexture : %s", SDL_GetError());
        goto Quit;
    }
    statut = EXIT_SUCCESS;
    SDL_Delay(3000);

Quit:
    if(NULL != texture)
        SDL_DestroyTexture(texture);
    if(NULL != renderer)
        SDL_DestroyRenderer(renderer);
    if(NULL != window)
        SDL_DestroyWindow(window);
    SDL_Quit();
    return statut;
}

Notons que nous pourrions faire une fonction qui se chargerait de toutes les initialisations et des créations nécessaires.

Dessiner sur une texture

Nous avons précédemment dit que si l’on utilisait la valeur SDL_TEXTUREACCESS_TARGET, c’était pour pouvoir utiliser notre texture comme un renderer. Pour être plus précis, c’est pour pouvoir utiliser notre texture comme cible (« target ») de rendu. En fait, nous allons utiliser les fonctions de dessin vu au chapitre précédent, mais la cible du rendu ne sera plus le renderer mais la texture. Pour changer la cible de rendu, nous allons utiliser la fonction SDL_SetRenderTarget. Son prototype :

1
2
int SDL_SetRenderTarget(SDL_Renderer* renderer,
                        SDL_Texture*  texture)

Elle prend en paramètre un renderer et une texture. Elle retourne 0 en cas de succès et une valeur négative en cas d’erreur (une erreur peut par exemple être que la texture passée en paramètre n’a pas été créée avec le paramètre SDL_TEXTUREACCESS_TARGET).

Après avoir appelé cette fonction, toutes les fonctions de dessin modifiant le renderer modifieront la texture passée en paramètre. Ainsi, pour dessiner sur notre texture, nous allons :

  1. Appeler SDL_SetRenderTarget pour que notre texture soit la cible de rendu ;
  2. Faire nos dessins ;
  3. Remettre le renderer en tant que cible de rendu.

Pour faire la troisième étape, il suffit d’appeler la fonction SDL_SetRenderTarget en lui passant NULL comme second argument.

Par exemple, dessinons un carré sur une texture.

1
2
3
4
5
6
7
8
texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, 
                               SDL_TEXTUREACCES_TARGET, 200, 200);
SDL_Rect rect = {50, 50, 100, 100};
SDL_SetRenderDrawColor(renderer, 150, 0, 150, 255); /* On dessine en violet */

SDL_SetRenderTarget(renderer, texture); /* On va dessiner sur la texture */
SDL_RenderFillRect(renderer, &rect);
SDL_SetRenderTarget(renderer, NULL);

En essayant ce code, on se rend compte que même après avoir mis à jour le renderer, la fenêtre garde la même couleur, cela veut bien dire que le dessin ne s’est pas fait sur elle. Nous verrons bientôt comment afficher une texture et ainsi afficher les modifications effectuées.

Notons qu’il existe une fonction SDL_GetRenderTarget qui permet d’obtenir la cible de rendu (donc de savoir si les dessins se font sur le renderer ou sur une texture). Son prototype :

1
SDL_Texture* SDL_GetRenderTarget(SDL_Renderer* renderer)

Elle prend en paramètre un renderer et retourne un pointeur sur SDL_Texture qui correspond à la texture qui est la cible de rendu. Si la cible de rendu est le renderer lui-même, la fonction renverra NULL. Cette fonction peut être utile dans le cas où on veut, dans une fonction, obtenir l’adresse de la cible du rendu (ce n’est donc pas la peine de la passer en paramètre) ou encore si on a plusieurs textures et qu’on ne sait pas laquelle est la cible du rendu. Pour le moment, elle ne nous sera pas utile.

Afficher une texture

Afficher une texture consiste à copier la texture sur le renderer puis à mettre à jour le renderer. Ainsi, on verra bien la texture à l’écran. La copie de la texture se fait avec la fonction SDL_RenderCopy dont le prototype est :

1
2
3
4
int SDL_RenderCopy(SDL_Renderer*   renderer,
                   SDL_Texture*    texture,
                   const SDL_Rect* srcrect,
                   const SDL_Rect* dstrect)

Elle prend en paramètre :

  • le renderer sur lequel doit être fait la copie ;
  • la texture à copier ;
  • un pointeur sur un rectangle qui correspond à la partie de la texture à copier (en passant NULL, on copie toute la texture) ;
  • un pointeur sur un rectangle qui correspond à l’endroit du renderer où doit être copié la texture (en passant NULL, la texture remplira tout le renderer).

La fonction retourne 0 en cas de succès et une valeur négative en cas d’erreur.

Faisons quelques tests pour voir comment elle fonctionne (on supposera avoir une texture de longueur et de hauteur 50 et un renderer associé à une fenêtre de longueur 600 et de hauteur 480).

1
2
SDL_Rect dst = {0, 0, 50, 50};
SDL_RenderCopy(renderer, texture, NULL, &dst);

Ce cas est le plus simple. On copie toute la texture dans un rectangle de même dimension qui est placé dans le coin en haut à gauche du renderer.

1
2
3
SDL_Rect dst = {0, 0, 20, 30};
SDL_Rect src = {10, 10, 20, 30};
SDL_RenderCopy(renderer, texture, &src, &dst);

Ici, on ne copie qu’une partie de la texture. On copie cette partie dans un rectangle qui a les même dimensions et qui est placé en haut à gauche.

1
2
3
SDL_Rect dst = {0, 0, 40, 60};
SDL_Rect src = {10, 10, 20, 30};
SDL_RenderCopy(renderer, texture, &src, &dst);

Ici, nous copions une partie de la texture, mais nous la copions dans un rectangle deux fois plus grand. Résultat : ce qui est affiché est la texture redimensionnée (étirée) pour remplir le rectangle de destination. C’est toujours ce qui se passera, la partie de la texture à afficher sera redimensionnée pour remplir le rectangle de destination. Voyons un dernier exemple.

1
SDL_RenderCopy(renderer, texture, NULL, NULL);

Ici, on copie toute la texture, et on veut qu’elle remplisse tout le renderer. Notre texture sera donc redimensionnée pour remplir le renderer.

La vraie texture n’est pas redimensionnée. Il s’agit d’un redimensionnement à la volée, au moment de la copie. Notre variable texture n’est pas modifiée.

Tout ce que nous venons de dire à propos du redimensionnement implique que pour afficher une texture dans ses vraies dimensions, il faut connaître ses dimensions. Si nous ne les avons pas, il est possible de les récupérer avec la fonction SDL_QueryTexture comme nous allons le faire dans le code qui suit.

1
2
3
SDL_Rect dst = {0, 0, 0, 0};
SDL_QueryTexture(texture, NULL, NULL, &dst.w, &dst.h);
SDL_RenderCopy(renderer, texture, NULL, &dst);

Comme nous aurions pu le deviner grâce au code précédent, le prototype de SDL_QueryTexture est :

1
2
3
4
5
int SDL_QueryTexture(SDL_Texture* texture,
                     Uint32*      format,
                     int*         access,
                     int*         w,
                     int*         h)

Elle renvoie 0 en cas de succès et une valeur négative en cas d’erreur et prend en paramètre la texture dont on veut les paramètres et quatre pointeurs qui seront remplis avec, dans l’ordre, le format d’accès de la texture, son type d’accès, sa largeur et sa hauteur.

Dans le code suivant, nous avons passé format et access à NULL car leur valeur ne nous intéresse pas. Celui-ci crée une surface, dessine dessus (fond bleu et rectangle rouge dans le coin en bas à droite) et la colle à l’écran.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include <SDL2/SDL.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
    SDL_Window *window = NULL;
    SDL_Renderer *renderer = NULL;
    SDL_Texture *texture = NULL;
    int statut = EXIT_FAILURE;
    SDL_Rect rect = {100, 100, 100, 100}, dst = {0, 0, 0, 0};
    SDL_Color rouge = {255, 0, 0, 255}, bleu = {0, 0, 255, 255};

    if(0 != SDL_Init(SDL_INIT_VIDEO))
    {
        fprintf(stderr, "Erreur SDL_Init : %s", SDL_GetError());
        goto Quit;
    }
    if(0 != SDL_CreateWindowAndRenderer(640, 480, SDL_WINDOW_SHOWN, &window, &renderer))
    {
        fprintf(stderr, "Erreur SDL_CreateWindowAndRenderer : %s", SDL_GetError());
        goto Quit;
    }
    texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, 
                                SDL_TEXTUREACCESS_TARGET, 200, 200);
    if(NULL == texture)
    {
        fprintf(stderr, "Erreur SDL_CreateTexture : %s", SDL_GetError());
        goto Quit;
    }

    SDL_SetRenderTarget(renderer, texture);
    /* La texture est la cible de rendu, maintenant, on dessine sur la texture. */
    SDL_SetRenderDrawColor(renderer, bleu.r, bleu.g, bleu.b, bleu.a);
    SDL_RenderClear(renderer);
    SDL_SetRenderDrawColor(renderer, rouge.r, rouge.g, rouge.b, rouge.a);
    SDL_RenderFillRect(renderer, &rect); /* On dessine un rectangle rouge sur la texture. */

    SDL_SetRenderTarget(renderer, NULL); /* Le renderer est la cible de rendu. */

    /* On récupère les dimensions de la texture, on la copie sur le renderer
       et on met à jour l’écran. */
    SDL_QueryTexture(texture, NULL, NULL, &dst.w, &dst.h);
    SDL_RenderCopy(renderer, texture, NULL, &dst);
    SDL_RenderPresent(renderer);
    statut = EXIT_SUCCESS;
    SDL_Delay(3000);

Quit:
    if(NULL != texture)
        SDL_DestroyTexture(texture);
    if(NULL != renderer)
        SDL_DestroyRenderer(renderer);
    if(NULL != window)
        SDL_DestroyWindow(window);
    SDL_Quit();
    return statut;
}

Les surfaces

Un vestige de la SDL 1

Nous allons maintenant parler des surfaces. Les surfaces sont l’équivalent des textures dans les anciennes versions de la SDL. On représente une surface avec le type SDL_Surface. La plus grande différence entre les surfaces et les textures est que les textures sont gérées par le GPU et les surfaces par le CPU. Cela donne quelques avantages aux textures qui sont ainsi affichées plus rapidement que ne l’étaient les surfaces et qui peuvent être redimensionnés à l’affichage (comme nous l’avons vu précédemment).

Les surfaces restent cependant utiles. Par exemple, elles peuvent être modifiées pixel par pixel plus facilement que les textures. Mais là où elles nous seront utiles, c’est pour gérer les images.

Créer une surface

La création de surface se fait avec la fonction SDL_CreateRGBSurface. Son prototype :

1
2
3
4
5
6
7
8
SDL_Surface* SDL_CreateRGBSurface(Uint32 flags,
                                  int    width,
                                  int    height,
                                  int    depth,
                                  Uint32 Rmask,
                                  Uint32 Gmask,
                                  Uint32 Bmask,
                                  Uint32 Amask)

Elle prend en paramètre :

  • une liste de drapeaux (ce paramètre doit être à 0) ;
  • la largeur de la surface ;
  • la hauteur de la surface ;
  • le nombre de bits par pixels (en général, on travaille avec 32 bits par pixels) ;
  • les quatre derniers paramètres permettent à la SDL de savoir comment extraire la couleur de chaque pixel. Nous allons passer la valeur 0 pour ces quatre paramètres.

La fonction retourne un pointeur sur SDL_Surface qui vaudra NULL en cas d’échec de la fonction et qui dans le cas contraire pointe sur la surface créée.

Détruire une surface

Comme les autres ressources, il nous faut détruire chaque surface créée. La libération des données se fait avec la fonction SDL_FreeSurface dont le prototype est :

1
void SDL_FreeSurface(SDL_Surface* surface)

Elle ne renvoie rien et prend en paramètre la surface à libérer.

Finalement, avec le code qui suit, on crée une surface de longueur 300 et de hauteur 200.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/* Le début de notre code */
SDL_Surface *surface = NULL;
surface = SDL_CreateRGBSurface(0, 300, 200, 32, 0, 0, 0, 0);
if(NULL == surface)
{
    fprintf(stderr, "Erreur SDL_CreateRGBSurface : %s", SDL_GetError());
    goto Quit;
}
SDL_FreeSurface(surface);
/* La fin de notre code */

Opérations sur les surfaces

Colorer une surface

La SDL nous offre des fonctions pour dessiner sur des surfaces, mais il y en a beaucoup moins que celles pour dessiner sur les textures. En fait, il n’y en a qu’une seule. Il s’agit de la fonction SDL_FillRect qui nous permet, comme son nom l’indique, de « remplir » un rectangle avec une couleur. Son prototype :

1
2
3
int SDL_FillRect(SDL_Surface*    dst,
                 const SDL_Rect* rect,
                 Uint32          color)

Elle prend en paramètre la surface qui doit être remplie, un pointeur sur SDL_Rect qui représente la partie de la surface à remplir (en passant NULL, on demande à remplir toute la surface) et la couleur voulue. Elle retourne 0 en cas de succès et une valeur négative en cas d’erreur.

Notons que la couleur passée en paramètre doit avoir le même format que la surface. Nous devons donc transformer le triplet RGB représentant notre nombre. Pour ce faire, nous allons utiliser la fonction SDL_MapRGB. Son prototype :

1
2
3
4
Uint32 SDL_MapRGB(const SDL_PixelFormat* format,
                  Uint8                  r, 
                  Uint8                  g, 
                  Uint8                  b)

Elle prend en paramètre un format de pixel et les trois composantes d’une couleur et retourne un pixel de cette couleur dans le format voulu.

Le format d’une surface est obtenu avec le champ format de cette surface.

Par exemple, pour colorer une surface en rouge, nous pouvons utiliser le code suivant.

1
2
3
SDL_Surface *surface;
surface = SDL_CreateRGBSurface(0, 100, 100, 32, 0, 0, 0, 0);
SDL_FillRect(surface, NULL, SDL_MapRGB(s->format, 255, 0, 0));

Coller une surface sur une autre

Une autre opération possible sur les surfaces est le « blit ». Cette opération consiste à copier une surface (ou une partie d’une surface) sur une autre surface. On peut donc la voir comme l’équivalent de SDL_RenderCopy. Le blit s’effectue grâce à la fonction SDL_BlitSurface. Son prototype :

1
2
3
4
int SDL_BlitSurface(SDL_Surface*    src,
                    const SDL_Rect* srcrect,
                    SDL_Surface*    dst,
                    SDL_Rect*       dstrect)

Ses différents paramètres sont :

  • src, la surface source est la surface qui sera copiée ;
  • srcrect est le rectangle source, c’est-à-dire la partie de la surface source qui sera copiée ;
  • dst est la surface de destination, celle sur laquelle sera copiée la source ;
  • dstrect est le rectangle de destination, celui où sera copiée la source.

Notons que cette fonction ne fait pas de redimensionnement à la volée, les champs h et w de dstrect n’ont aucune incidence sur la copie.

Une fenêtre avec une icône

La SDL nous permet de donner une icône à notre programme grâce à la fonction SDL_SetWindowIcon. Son prototype est :

1
2
void SDL_SetWindowIcon(SDL_Window*  window,
                       SDL_Surface* icon)

Elle prend en paramètre la fenêtre dont on veut changer l’icône et une surface qui correspond à l’icône que l’on veut donner à la fenêtre. Cette fonction ne retourne rien. Par exemple, ici, on va faire une icône composée de quatre carrés.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/* Cette structure permet de représenter un carré par sa couleur et un SDL_Rect. */
struct carre {
   SDL_Rect rect;
   Uint32 couleur;
};

int main(int argc, char *argv[])
{
    SDL_Window *window;
    SDL_Surface *surface;
    size_t i;

    SDL_Init(SDL_INIT_VIDEO);
    window = SDL_CreateWindow("icone", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
                              800, 600, SDL_WINDOW_RESIZABLE);
    surface = SDL_CreateRGBSurface(0, 32, 32, 32, 0, 0, 0, 0);

    /* On crée quatre carré pour notre icône. */
    struct carre carre[4] = {
        { {  4,  4, 10, 10 }, SDL_MapRGB(surface->format, 0, 0, 0) }, /* Noir */
        { {  4, 18, 10, 10 }, SDL_MapRGB(surface->format, 0, 0, 255) }, /* Bleu */
        { { 18,  4, 10, 10 }, SDL_MapRGB(surface->format, 0, 255, 0) }, /* Vert */
        { { 18, 18, 10, 10 }, SDL_MapRGB(surface->format, 255, 255, 255) } /* Blanc */
    };

    /* On remplit notre surface grâce à nos carrés. */
    for(i = 0; i < 4; i++)
        SDL_FillRect(surface, &carre[i].rect, carre[i].couleur);

    SDL_SetWindowIcon(window, surface);
    SDL_Delay(2000);

    /* Libérations */
    return 0;
}

Passer de la texture à la surface

Tout ça, c’est très bien, mais on n’a toujours pas vu comment afficher une surface. En fait, on ne peut pas afficher directement une surface. Il nous faut passer par les textures. Il nous faut donc transformer notre surface en texture, puis afficher la texture obtenue. Le passage de la surface à la texture se fait avec la fonction SDL_CreateTextureFromSurface dont le prototype est le suivant.

1
2
SDL_Texture* SDL_CreateTextureFromSurface(SDL_Renderer* renderer,
                                          SDL_Surface*  surface)

Elle prend en paramètre la surface qui nous permettra de créer la texture et le renderer auquel la texture créée doit être associée et retourne cette texture ou NULL en cas d’erreur.

Après avoir créé une texture à partir d’une surface, il ne faut pas oublier de libérer cette surface. Pour obtenir une texture à partir d’une surface on peut donc faire ceci.

1
2
3
4
5
6
7
texture = SDL_CreateTextureFromSurface(renderer, surface);
SDL_FreeSurface(surface);
if(NULL == texture)
{
    fprintf(stderr, "Erreur SDL_CreateTextureFromSurface : %s", SDL_GetError());
    goto Quit;
}

Bien sûr, nous pouvons libérer la surface à la fin de notre programme, mais autant la libérer dès que l’on n’en a plus besoin.

Les images

Charger une image

Afficher une image est une opération simple en théorie : une image est un paquet de pixels, on lit ce paquet de pixels, on le met dans une texture et on affiche cette texture. Nous pourrions faire une fonction qui prend en paramètre le chemin d’une image et retourne la texture associée à cette image. Heureusement, ce n’est pas la peine, les images sont gérées par la SDL.

Seul le format BMP est géré nativement par la SDL.

Pour charger une image, nous allons utiliser la fonction SDL_LoadBMP. Son prototype :

1
SDL_Surface* SDL_LoadBMP(const char* file)

Elle prend en paramètre le chemin (relatif ou absolu) de l’image à charger et retourne une surface contenant cet objet ou NULL en cas d’erreur. Si nous avons parlé des surfaces, c’est parce que nous les utilisons ensuite pour les images.

On peut donc obtenir une surface contenant l’image grâce à SDL_LoadBMP. Il nous suffit de créer une texture à partir de cette image, et on peut ensuite l’afficher. Sans oublier de libérer la surface.

Il est conseillé de toujours vérifier le retour de SDL_LoadBMP. En effet, si nous ne le faisons pas, nous risquons ensuite de faire des opérations sur un pointeur nul, ce qui causera des problèmes.

On peut alors écrire un code de ce genre pour charger une image dans une texture.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
SDL_Surface *tmp = NULL; 
SDL_Texture *texture = NULL;
tmp = SDL_LoadBMP("test.bmp");
if(NULL == tmp)
{
    fprintf(stderr, "Erreur SDL_LoadBMP : %s", SDL_GetError());
    goto Quit;
}
texture = SDL_CreateTextureFromSurface(renderer, tmp);
SDL_FreeSurface(tmp); /* On libère la surface, on n’en a plus besoin */
if(NULL == texture)
{
    fprintf(stderr, "Erreur SDL_CreateTextureFromSurface : %s", SDL_GetError());
    goto Quit;
}

Ici, on aurait aussi pu libérer la surface à la fin en même temps que nos autres libérations, mais autant la libérer directement et ne pas occuper de la mémoire inutilement. Imaginons un code dans lequel nous chargerions plusieurs dizaines d’images. Il vaudrait mieux que chaque surface soit libérée aussitôt qu’elle n’a plus d’utilité.

Dessiner sur l’image

Selon la documentation, le type d’accès de la texture créée est SDL_TEXTUREACCESS_STATIC. On ne pourra donc pas modifier cette texture en dessinant dedans à moins de faire en sorte d’obtenir une texture avec le bon type d’accès. Pour cela, il nous suffit de copier notre texture dans une nouvelle texture qui, elle, a le bon type d’accès. Cela va donc se faire de cette manière.

  • Charger notre image.
  • Créer une texture à partir de notre image.
  • Créer une autre texture de même dimension que la surface.
  • Placer cette dernière texture en tant que cible de rendu.
  • Copier la première texture sur la deuxième (avec SDL_RenderCopy).

Bien sûr, il faudra faire les libérations adéquates au bon moment et ne pas oublier de vérifier les valeurs retournées par les fonctions à risque. On se retrouve donc avec ce code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
SDL_Surface *surface = NULL; 
SDL_Texture *texture, *tmp = NULL;
surface = SDL_LoadBMP("test.bmp");
if(NULL == surface)
{
    fprintf(stderr, "Erreur SDL_LoadBMP : %s", SDL_GetError());
    goto Quit;
}
tmp = SDL_CreateTextureFromSurface(renderer, surface);
if(NULL == tmp)
{
    fprintf(stderr, "Erreur SDL_CreateTextureFromSurface : %s", SDL_GetError());
    goto Quit;
}
texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, 
                            SDL_TEXTUREACCESS_TARGET, surface->w, surface->h);
if(NULL == texture) 
{
    fprintf(stderr, "Erreur SDL_CreateTextureFromSurface : %s", SDL_GetError());
    goto Quit;
}
SDL_SetRenderTarget(renderer, texture); /* La cible de rendu est maintenant texture. */
SDL_RenderCopy(renderer, tmp, NULL, NULL); /* On copie tmp sur texture */
SDL_DestroyTexture(tmp);
SDL_FreeSurface(surface);
SDL_SetRenderTarget(renderer, NULL); /* La cible de rendu est de nouveau le renderer. */
/* On peut maintenant dessiner sur notre texture */

Se faire des fonctions

Ici, notre but sera de pouvoir écrire au minimum un code de ce genre.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <SDL2/SDL.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv)
{
    SDL_Window *window = NULL;
    SDL_Renderer *renderer = NULL;
    SDL_Texture *image = NULL;
    int statut = EXIT_FAILURE;
    SDL_Color blanc = {255, 255, 255, 255};
    if(0 != init(&window, &renderer, 640, 480)) /* ecrire cette fonction */
        goto Quit;

    image = loadImage("image.bmp", renderer); /* ecrire cette fonction*/  
    if(NULL == image)
        goto Quit;

    statut = EXIT_SUCCESS;
    setWindowColor(renderer, blanc); /* ecrire cette fonction */
    SDL_RenderPresent(renderer);
    SDL_Delay(3000);

    Quit:
    if(NULL != image)
        SDL_DestroyTexture(image);
    if(NULL != renderer)
        SDL_DestroyRenderer(renderer);
    if(NULL != window)
        SDL_DestroyWindow(window);
    SDL_Quit();
    return statut;
}

Écrivons les fonctions. Commençons par la fonction init. Pour que les pointeurs passés en paramètre soient modifiés, il faut lui passer en paramètre des doubles pointeurs. On écrit donc cette fonction.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
int init(SDL_Window **window, SDL_Renderer **renderer, int w, int h)
{
    if(0 != SDL_Init(SDL_INIT_VIDEO))
    {
        fprintf(stderr, "Erreur SDL_Init : %s", SDL_GetError());
        return -1;
    }
    if(0 != SDL_CreateWindowAndRenderer(w, h, SDL_WINDOW_SHOWN, window, renderer))
    {
        fprintf(stderr, "Erreur SDL_CreateWindowAndRenderer : %s", SDL_GetError());
        return -1;
    }
    return 0;
}

La fonction loadImage doit charger une image et renvoyer la texture correspondante. Elle doit donc charger l’image dans une surface, convertir cette surface en texture et renvoyer cette texture sans oublier de vérifier les retours des fonctions employées.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
SDL_Texture *loadImage(const char path[], SDL_Renderer *renderer)
{
    SDL_Surface *tmp = NULL; 
    SDL_Texture *texture = NULL;
    tmp = SDL_LoadBMP(path);
    if(NULL == tmp)
    {
        fprintf(stderr, "Erreur SDL_LoadBMP : %s", SDL_GetError());
        return NULL;
    }
    texture = SDL_CreateTextureFromSurface(renderer, tmp);
    SDL_FreeSurface(tmp);
    if(NULL == texture)
    {
        fprintf(stderr, "Erreur SDL_CreateTextureFromSurface : %s", SDL_GetError());
        return NULL;
    }
    return texture;
}

On aurait pu faire la fonction loadImage pour que la texture renvoyée ait un accès SDL_TEXTUREACCESS_TARGET. La fonction setWindowColor a déjà été écrite dans le chapitre précédent.

On pourrait même rendre ces fonctions plus personnalisables et en faire plus.

La manière de charger des images nous montre bien que les surfaces restent utiles et ne sont pas totalement dépréciées. Pour en savoir plus à leur propos, consultons la page de la documentation de la SDL à propos des surfaces.


Avec ce chapitre, nous sommes maintenant capable de dessiner sur la fenêtre, de charger des images et de les afficher. Au prochain chapitre, nous verrons une autre manière de modifier les textures et les surfaces en changeant la couleur des pixels que l’on veut.