Outils pour utilisateurs

Outils du site


stu:progc:sdl

Utiliser la SDL 2 en C

Ce document est en cours de rédaction.

La SDL (Simple DirectMedia Layer) est une bibliothèque multi plates-formes d'affichage 2D et 3D (OpenGL) très rapide (elle g-re ausi les périphériques de type Joystick, CD, et le son).

Pour démarrer avec la SDL, une surcouche est disponible. Elle est détaillée dans [[stu:progc:sdls|Utiliser simplement la SDL 2 en C : SDLS]].

- Quelques principes et vocabulaire

Trois objets ont une importance particulière lorsqu'on utilise la SDL : les fenêtres (SDL_Window), les méthodes de rendu (SDL_Renderer), les textures (Textures). Ces dernière jouent un rôle similaire aux surface de la SDL 1.2 (qui exitent encore dans la SDL 2.0 mais qu'il convient de ne plus utiliser si possible).

La SDL 2 permet de gérer plusieurs fenêtres graphiques. Plusieurs objets SDL_Window peuvent donc coexister. À chaque fenêtre est attachée une méthode de rendu (SDL_Renderer), qui permet de dessiner directement dans la fenêtres ou d'y plaquer des textures.

Pour compiler un programme utilisant la SDL2, il faut penser sous Windows à lier les bibliothèques : mingw32 SDL2main et SDL2. L'option de linkage -mwindows est aussi nécessaire.

- Créer une fenêtre et un contexte de rendu

La fenêtre et le contexte de rendu peuvent être créés l'un après l'autre [[wsdl>SDL_CreateWindow]] puis [[wsdl>SDL_CreateRenderer]] ou bien simultanément avec [[wsl>SDL_CreateWindowAndRenderer]]. Voyons la seconde méthode. Le programme suivant initialise la SDL, crée une fenêtre, le contexte associé (qui ne sert à rien pour le moment), peind le fond de la fenêtre en noir, attend 4 secondes, puis se termine.

#include<SDL2/SDL.h>
#include<stdio.h>
 
const int WIDTH  = 640;
const int HEIGHT = 480;
 
int main(int argc, char** argv)
{
  SDL_Window *win = 0;
  SDL_Renderer *ren = 0;
 
  /* Initialisation de la SDL. Si ça se passe mal, on quitte */
  if (SDL_Init(SDL_INIT_EVERYTHING) < 0)
  {
      fprintf(stderr,"Erreur initialisation\n");
      return -1;
  }
  /* Création de la fenêtre et du renderer */
  SDL_CreateWindowAndRenderer(WIDTH, HEIGHT, 0, &win, &ren); // SDL_WINDOW_SHOWN|SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC,&win,&ren);
 
  if (!win || !ren) 
	 {
		fprintf(stderr,"Erreur à la création des fenêtres\n");
		SDL_Quit();
		return -1;
	 }
  /* Affichage du fond noir */
  SDL_SetRenderDrawColor(ren, 0, 0, 0, 255);
  SDL_RenderClear(ren);
  SDL_RenderPresent(ren);
 
  SDL_Delay(4000);
 
  SDL_DestroyRenderer(ren);
  SDL_DestroyWindow(win);
  SDL_Quit();
  return 0;
}

- Dessiner dans la fenêtre

Il existe plusieurs manières de dessiner dans la fenêtre. Nous allons détailler ici 4 manières de procéder.

  • La première permet de lire et modifier la couleurs des pixels dans un Renderer. C'est la plus basique.
  • La seconde consiste à utiliser les fonctions graphiques 2D accélérées bas niveaux fournies en standard. Nous ne pourrons faire que des points, des lignes, ou des rectangles.
  • La troisième consiste à utiliser une librairie annexe, comme SDL_gfx, contenant des primitives graphiques (cercles, ellipses…)
  • Enfin, la quatrième consiste à travailler directement au niveau du bloc de pixels. C'est la plus basique, mais aussi la plus rapide.

Dans les exemples qui suivent le code est donné sous la forme d'une fonction à insérer dans le programme précédent. Cette fonction devra être appelée juste avant SDL_Delay(400), de cette manière :

...
dessin(ren) ;
SDL_Delay(400);

- Pixels dans un Renderer

Les fonctions utiles sont :

La fonction suivante balaie une tiers du renderer et permute les composantes de chaque pixel :

void  dessin(SDL_Renderer * ren) 
{
    SDL_Rect rect;
    int cr,cv,cb;
    int res;
    unsigned int value;
    int i,j;
 
    rect.w=1;
    rect.h=1;
    for (i=0;i<WIDTH/3; i++) {
	for(j=0;j<HEIGHT;j++){
	    rect.x=i;
	    rect.y=j;
	    res = SDL_RenderReadPixels(ren,&rect,SDL_PIXELFORMAT_ARGB8888,&value,32*WIDTH);
	    if (res<0) {
		    printf("Lecture erronée : %s\n", SDL_GetError());
		}
	    cr = (value & 0xFF0000)>>16;
	    cv = (value & 0x00FF00)>>8;
	    cb = (value & 0x0000FF);
 
	    SDL_SetRenderDrawColor(ren,cv,cr,255,0);
	    SDL_RenderDrawPoint(ren,i,j);
 
	}
	printf("%d\n",i);
    }
    SDL_RenderPresent(ren);
}

- Fonction grahiques 2D

Les fonctions principales sont : SDL_RenderDrawLine, SDL_RenderDrawLines, SDL_RenderDrawPoint, SDL_RenderDrawPoints, SDL_RenderDrawRect, SDL_RenderDrawRects, SDL_RenderFillRect, SDL_RenderFillRects.

Elles permettent de tracer respectivement des lignes, points, contours de rectangles ou rectangles pleins. SDL_RenderPresent permet de forcer l'affichage, [[wsdl>SDL_SetRenderDrawColor]] modifie la couleur du tracé et [[wsdl>SDL_SetRenderDrawBlendMode]] fixe la gestion de la transparence.

La fonction suivante fait apparaître au hasard des rectangles de différentes couleurs à l'écran.

void dessin(SDL_Renderer * ren) 
{
  int colr, colg, colb;
  SDL_Rect r;
  int i;
 
  SDL_SetRenderDrawBlendMode(ren,SDL_BLENDMODE_BLEND);
  for(i=0;i<30;i++) 
	 {
		r.x = rand()%WIDTH;
		r.y = rand()%HEIGHT;
		r.w = rand()%(WIDTH-r.x);
		r.h = rand()%(HEIGHT-r.y);
		colr = rand()%256;
		colg = rand()%256;
		colb = rand()%256;
		SDL_SetRenderDrawColor(ren,colr,colg,colb,20);
		SDL_RenderFillRect(ren,&r);
	 }
  SDL_RenderPresent(ren);
}

- Utilisation de SDL_gfx

La bibliothèque annexe SDL2_gfx contient : des primitives graphiques, des fonctions de transformation (rotation…). La documentation est accessible ici [[http://www.ferzkopp.net/Software/SDL2_gfx/Docs/html/files.html]]

Nous ne nous intéresserons ici qu'aux primitives graphiques, dont voici une liste non exhaustive :

  • pixelRGBA (SDL_Renderer *renderer, Sint16 x, Sint16 y, Uint8 r, Uint8 g, Uint8 b, Uint8 a)
  • boxRGBA (SDL_Renderer *renderer, Sint16 x1, Sint16 y1, Sint16 x2, Sint16 y2, Uint8 r, Uint8 g, Uint8 b, Uint8 a)
  • lineRGBA (SDL_Renderer *renderer, Sint16 x1, Sint16 y1, Sint16 x2, Sint16 y2, Uint8 r, Uint8 g, Uint8 b, Uint8 a)
  • thickLineRGBA (SDL_Renderer *renderer, Sint16 x1, Sint16 y1, Sint16 x2, Sint16 y2, Uint8 width, Uint8 r, Uint8 g, Uint8 b, Uint8 a)
  • ellipseRGBA (SDL_Renderer *renderer, Sint16 x, Sint16 y, Sint16 rx, Sint16 ry, Uint8 r, Uint8 g, Uint8 b, Uint8 a)
  • filledEllipseRGBA (SDL_Renderer *renderer, Sint16 x, Sint16 y, Sint16 rx, Sint16 ry, Uint8 r, Uint8 g, Uint8 b, Uint8 a)
  • polygonRGBA (SDL_Renderer *renderer, const Sint16 *vx, const Sint16 *vy, int n, Uint8 r, Uint8 g, Uint8 b, Uint8 a)
  • filledPolygonRGBA (SDL_Renderer *renderer, const Sint16 *vx, const Sint16 *vy, int n, Uint8 r, Uint8 g, Uint8 b, Uint8 a)
  • stringRGBA (SDL_Renderer *renderer, Sint16 x, Sint16 y, const char *s, Uint8 r, Uint8 g, Uint8 b, Uint8 a)
void dessin(SDL_Renderer * ren) 
{
  Uint8 r,g,b,a;
  Sint16 x,y,rx,ry;
  int n;
 
  for(n=0;n<50;n++)
	 {
		x = rand()%WIDTH;
		y = rand()%HEIGHT;
		rx = rand()%30+10;
		ry = rand()%30+10;
		r = rand()%256;
		g = rand()%256;
		b = rand()%256;
		a = rand()%256;
		filledEllipseRGBA(ren,x,y,rx,ry,r,g,b,a);
	 }
  SDL_RenderPresent(ren);
 
}

Pout utiliser ce code, il faut penser à ajouter #include <SDL2/SDL2_gfxPrimitives> et à lier la nouvelle bibliothèque SDL2_gfx.

- Blocs de pixels

L'affichage peut se faire aussi très rapidement au niveau du pixel. L'idée est d'avoir à disposition un tableau contenant les valeurs des pixels. Les valeurs stockées dans le tableau sont modifiées pour refléter les couleurs des pixels. Lorsque le tableau est prêt, un appel à [[wsdl>SDL_UpdateTexture]] permet de mapper les couleurs dans une texture. Puis [[wsdl>SDL_RenderCopy]] est utilisé pour passer la texture à la méthode de rendu.

La création du bloc de mémoire qui servira à stocker les couleurs est fait avec les fonctions C classiques : malloc pour l'allocation et free pour la libération de la mémoire. La difficulté réside dans la représentation des couleurs : dans notre bloc de mémoire, les couleurs doivent être représentées de la même manière que dans la texture que nous allons utiliser. Or, lors de la création d'une texture, nous pouvons justement spécifier la manière dont les couleurs sont codées. Par exemple :

tex = SDL_CreateTexture(ren,
  SDL_PIXELFORMAT_ARGB8888,    /* Chaque couleur sur 4 octets, dans l'ordre Alpha, Red, Green Blue */
  SDL_TEXTUREACCESS_STREAMING, /* La texture va être modifiée très souvent (demande d'optimisation) */
  640, 480);

Dans ce modèle de couleurs, chaque couleur est stockée sur 4 octets. Connaissant, chaque composante r,g,b de la couleur (chacune sur 1 octet), la couleur finale sera1)

1)
attention à la priorité des opérateurs, celle de ''« est plus faible que celle de +)) : <code c> ((r«8)+g << 8) +b </code> Le code suivant isole dans une fonction à part le calcul de la couleur. La fonction dessin contient l'allocation de la texture et du tableau de pixels et calcule un dégradé de couleurs.
Uint32 couleur(int r, int g, int b)
{ return (((r<<8)+ g)<<8 )+ b; }
 
void dessin(SDL_Renderer * ren) 
{
  Uint32 * pixels, *p;
  SDL_Texture * tex;
  Uint8 r,g,b;
  int x,y;
 
  pixels = (Uint32*) malloc(WIDTH*HEIGHT*sizeof(Uint32));
  if (!pixels) {fprintf(stderr,"Erreur allocation\n"); return;}
 
  tex = SDL_CreateTexture(ren, SDL_PIXELFORMAT_ARGB8888,
	SDL_TEXTUREACCESS_STREAMING, WIDTH, HEIGHT);
 
  p = pixels;
  for(y=0;y<HEIGHT;y++)
	 {
		for(x=0;x<WIDTH;x++)
		  {
			 r = 255 * y / HEIGHT;
			 g = 255 * x / WIDTH;
			 b = 255 * (x+y) / (WIDTH+HEIGHT);
			 *p=couleur(r,g,b);
			 p++;
		  }
	 }
  SDL_UpdateTexture(tex, NULL, pixels, WIDTH * sizeof (Uint32));
  SDL_RenderCopy(ren,tex, NULL, NULL);
  SDL_RenderPresent(ren);
  SDL_DestroyTexture(tex);
  free(pixels);
}
L'intérêt de cette méthode, a priori plus technique, n'apparaît que si on souahite un affichage très rapide. Dans ce cas, l'allocation de la texture et du tableau de pixels ne doit pas être réalisé à chaque affichage mais plutôt une fois pour toutes. Le programme suivant, complet, reprend ce principe et recalcule le dégradé en faisant varier un de ses paramètres.
snippet3.c
 #include<SDL2/SDL.h>
#include<stdio.h>
 
const int WIDTH  = 640;
const int HEIGHT = 480;
 
Uint32 couleur(int r, int g, int b)
{ return (((r<<8)+ g)<<8 )+ b; }
 
void dessin(SDL_Renderer * ren, SDL_Texture * tex, Uint32 * pixels, float alpha) 
{
  Uint32  *p;
  Uint8 r,g,b;
  int x,y;
  float beta = 1-alpha;
  pixels = (Uint32*) malloc(WIDTH*HEIGHT*sizeof(Uint32));
  if (!pixels) {fprintf(stderr,"Erreur allocation\n"); return;}
 
  tex = SDL_CreateTexture(ren, SDL_PIXELFORMAT_ARGB8888,
								  SDL_TEXTUREACCESS_STREAMING, WIDTH, HEIGHT);
 
  p = pixels;
  for(y=0;y<HEIGHT;y++)
	 {
		for(x=0;x<WIDTH;x++)
		  {
			 r = 255 * y / HEIGHT;
			 g = 255 * x / WIDTH;
			 b = 255 * (x+y) / (WIDTH+HEIGHT);
 
			 *p=couleur(alpha*r+beta*g,alpha*g+beta*b,alpha*b+beta*r);
			 p++;
		  }
	 }
  SDL_UpdateTexture(tex, NULL, pixels, WIDTH * sizeof (Uint32));
  SDL_RenderCopy(ren,tex, NULL, NULL);
  SDL_RenderPresent(ren);
  SDL_DestroyTexture(tex);
  free(pixels);
}
int main(int argc, char** argv)
{
  SDL_Window *win = 0;
  SDL_Renderer *ren = 0;
  Uint32 * pixels = 0;
  SDL_Texture * tex = 0;
  float alpha = 0.0;
  float pas = 0.03;
  int n;
  /* Initialisation de la SDL. Si ça se passe mal, on quitte */
  if (SDL_Init(SDL_INIT_EVERYTHING) < 0)
  {
      fprintf(stderr,"Erreur initialisation\n");
      return -1;
  }
  /* Création de la fenêtre et du renderer */
  SDL_CreateWindowAndRenderer(WIDTH, HEIGHT, 0, &win, &ren);
 
  if (!win || !ren) 
	 {
		fprintf(stderr,"Erreur à la création des fenêtres\n");
		SDL_Quit();
		return -1;
	 }
  /* Affichage du fond noir */
  SDL_SetRenderDrawColor(ren, 0, 0, 0, 255);
  SDL_RenderClear(ren);
  SDL_RenderPresent(ren);
 
  pixels = (Uint32*) malloc(WIDTH*HEIGHT*sizeof(Uint32));
  if (!pixels) {fprintf(stderr,"Erreur allocation\n"); return;}
 
  tex = SDL_CreateTexture(ren, SDL_PIXELFORMAT_ARGB8888,
				 SDL_TEXTUREACCESS_STREAMING, WIDTH, HEIGHT);
 
  for (n=0;n<1000L;n++) {
	 dessin(ren,tex,pixels,alpha);
	 alpha += pas;
	 if (alpha > 1.0) { alpha = 1.0; pas = -pas;}
	 if (alpha < 0.0) { alpha = 0.0; pas = -pas;}
  }
 
  SDL_Delay(1000);
 
  SDL_DestroyRenderer(ren);
  SDL_DestroyWindow(win);
  SDL_Quit();
  return 0;
}
Le code précédent recalcule 1000 fois le dégradé. Sur une machine un peu ancienne, il s'exécute en 14 secondes, ce qui signifie que le dégradé complet de la fenêtre 640x480 est recalculé et affiché environ 70 fois par seconde. ===== - Charger et afficher des images ===== Par défaut, la SDL manipule uniquement le format d'images BMP. L'extension standard SDL_image, pour peu qu'on dispose des bibliothèques appropriées permet de gérer d'autres formats (comme PNG). Pour charger et afficher une image contenue dans un fichier au format BMP, l'idée est la suivante :
  1. utiliser SDL_LoadBMP pour obtenir une surface à partir du fichier
  2. convertir la surface en texture avec SDL_CreateTextureFromSurface (cette conversion ne peut être réalisée que si on connaît le renderer sur lequel sera utilisée la surface)
  3. libérer la mémoire utilisée par la surface
  4. afficher la texture sur le renderer avec SDL_RenderCopy
Il est de bon ton, dans la mesure où la surface n'est qu'une étape intermédiaire, d'écrire une fonction qui prend en paramètre un nom de fichier, et un pointeur vers un renderer et renvoie la texture :
SDL_Texture* loadTexture(char * filename, SDL_Renderer *ren){
        SDL_Texture *texture = 0;
        SDL_Surface *loadedImage = 0;
        loadedImage = SDL_LoadBMP(filename);
        if (!loadedImage)
          {
          fprintf(stderr,"Erreur chargement image : %s\n",SDL_GetError());
          return 0;
          }
 
        texture = SDL_CreateTextureFromSurface(ren, loadedImage);
        SDL_FreeSurface(loadedImage);
        if (texture == 0)
          {
          fprintf(stderr,"Erreur creation texture : %s\n",SDL_GetError());
          return 0;
          }
        return texture;
}
Utiliser cette fonction permet en outre de ne plus avoir à se soucier des vérification d'absence d'erreur, qu'on oublie trop souvent. Munie de la fonction précédente, nous pouvons maintenant charger et afficher une image, comme celle-ci :
void dessin(SDL_Renderer * ren) 
{
  SDL_Texture * tex = 0;
  SDL_Rect dst;
 
  tex = loadTexture("arch.bmp",ren);
  if (tex == 0) return;
 
  // Récupération de la taille de la texture 
  SDL_QueryTexture(tex, NULL, NULL, &dst.w, &dst.h);
 
  // Centrage
  dst.x = (WIDTH - dst.w)/2;
  dst.y = (HEIGHT- dst.h)/2;
 
  // Affichage
  SDL_RenderCopy(ren, tex, NULL, &dst);
  SDL_RenderPresent(ren);
}
Il est possible de jouer sur les valeurs stockées dans dst pour ... ==== - D'autres formats avec SDL_image ==== La bibliothèque annexe SDL_image permet d'utiliser d'autres formats que BMP (par exemple PNG). En outre, la fonction IMG_LoadTexture renvoie directement une Texture plutôt qu'une Surface. Nous pouvons réécrire la fonction loadTexture ainsi :
SDL_Texture* loadTexture(char * filename, SDL_Renderer *ren){
        SDL_Texture *texture = 0;
        texture = IMG_LoadTexture(ren, filename);
        if (!texture)
          {
          fprintf(stderr,"Erreur chargement image : %s\n",SDL_GetError());
          return 0;
          }
        return texture;
}
Pour utiliser IMG_LoadTexture, il faut penser à ajouter #include <SDL2/SDL_image.h> en tête de fichier, et à lier la bibliothèque ''SDL2_image''.
Vous pouvez tester la fonctionnalité avec l'image suivante : ===== - Gestion des événements ===== Pour écrire un programme interactif, la SDL utilise le proncope de la programmation événementielle. Le corps du programme se résume à une boucle, qui traite les événements (appui sur une touche, clic souris etc…) lorsqu'ils se présentent. La structure de la boucle des événements est presque tout le temps la même :
int done = 0;
SDL_Event event;
 
while(done == 0)
  {
    while ( SDL_PollEvent(&event) )
     {
         switch(event.type)
             {
             case SDL_QUIT : 
                 done = 1;
                 break;
             default :
                 print("Evénement non géré\n");
             }
             /* Code rapide à effectuer en tâche de fond */
             // ...
      }
    }
Une bonne pratique est d'appeler une fonction dédiée à chaque événement (une pour le clavier, une pour la souris etc…) Le programme suivant illustre ce principe.
snippet_7.c
 
stu/progc/sdl.txt · Dernière modification: 2014/03/31 17:24 (modification externe)