Programmation avec la SDL/Le renderer

Programmation avec la SDL
Sommaire
L' affichage vidéo
L'essentiel
Approfondissement
La gestion des évènements
Annexes
Modifier ce modèle

Certes, il est utile de savoir manipuler des fenêtres, mais lorsqu'il n'y a rien dedans, on est bien loin de la création d'un jeu en 2D ou d'un logiciel quelconque... C'est pourquoi dans ce chapitre nous allons enfin commencer à remplir notre fenêtre. Ceci, nous allons le faire grâce à un renderer, c'est-à-dire un moteur de rendu pour notre fenêtre. Pour parler plus clairement, le renderer est en fait l'espace où nous allons pouvoir dessiner, écrire... Or, pour qu'un dessin ou un mot s'affiche à l'écran, il faut faire un rendu, c'est-à-dire créer l'image finale que l'on veut afficher, puis l'afficher.

Création du renderer

modifier

On ne peut associer qu'un seul renderer à chaque fenêtre (celle-ci étant créée avant le renderer). En effet, la fenêtre ne nécessite qu'un seul contexte d'affichage... S'il y en avait trop, ils se marcheraient dessus.

Avec la SDL, le renderer est contenu dans une structure appelée SDL_Renderer. Elle contient les informations du contexte d'affichage de la fenêtre. Ainsi, pour créer un renderer, nous devons en tout premier lieu déclarer un pointeur de SDL_Renderer. La raison de la déclaration d'un pointeur plutôt que la structure directement est purement pratique : toutes les fonctions de la SDL utilisent des pointeurs de renderer et non la structure en elle-même pour des raisons d'optimisation. En effet, il est moins long de passer en argument une adresse plutôt qu'une structure SDL_Renderer assez lourde. Il est donc plus simple d'utiliser un pointeur. La structure pointée contiendra ensuite les informations renvoyées par la fonction SDL_CreateRenderer que voici :

SDL_Renderer* SDL_CreateRenderer(SDL_Window* window, int index , Uint32 flags);

Tout d'abord, vous remarquerez que cette fonction renvoie un pointeur de SDL_Renderer, ou NULL en cas d'erreur. Observons maintenant les arguments de cette fonction :

  • window : c'est la fenêtre à laquelle associer votre renderer.
  • index : Ne vous en occupez pas et donnez -1 comme argument.
  • flags : À force de les voir, vous devriez commencer à y être habitué. Voici les valeurs qu'ils peuvent prendre :
SDL_RendererFlags
Description
SDL_RENDERER_ACCELERATED Utilise l'accélération matérielle, c'est à dire la carte graphique. Il est fortement recommandé de le mettre puisque c'est un des atouts majeurs de la SDL 2.0 par rapport à la SDL 1.2 et surtout parce que c'est beaucoup plus rapide.
SDL_RENDERER_PRESENTVSYNC Synchronise le rendu avec la fréquence de rafraîchissement. La fréquence de rafraîchissement correspond au nombre de fois où l'écran est mis à jour par seconde. Ainsi, le rendu sera synchronisé avec celle-ci.
SDL_RENDERER_TARGETTEXTURE Utilisez ce flag lorsque vous souhaitez associer un renderer à une texture.
SDL_RENDERER_SOFTWARE Utilisez ce flag lorsque vous souhaitez associer un renderer à une surface.

Vous vous doutez bien que lorsqu'on crée un renderer, il faut aussi le détruire... Et pour cela, nous allons utiliser SDL_DestroyRenderer.

void SDL_DestroyRenderer(SDL_Renderer* renderer);

Maintenant, vous devriez savoir sans l'aide de personne déclarer un renderer. Mais au cas où vous n'y arriveriez pas par vous-même, voici le code complet de la création d'un renderer avec tout ce qui va avec :

SDL_Window* fenetre;
SDL_Renderer* renderer;//Déclaration du renderer

if(SDL_VideoInit(NULL) < 0) // Initialisation de la SDL
{
    printf("Erreur d'initialisation de la SDL : %s",SDL_GetError());
    return EXIT_FAILURE;
}

// Création de la fenêtre :
fenetre = SDL_CreateWindow("Une fenetre SDL" , SDL_WINDOWPOS_CENTERED , SDL_WINDOWPOS_CENTERED , 600 , 600 , SDL_WINDOW_RESIZABLE);
if(fenetre == NULL) // Gestion des erreurs
{
    printf("Erreur lors de la creation d'une fenetre : %s",SDL_GetError());
    return EXIT_FAILURE;
}

renderer = SDL_CreateRenderer(fenetre, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC); // Création du renderer

if(renderer == NULL)//gestion des erreurs
{
    printf("Erreur lors de la creation d'un renderer : %s",SDL_GetError());
    return EXIT_FAILURE;
}
SDL_Delay(3000);//pause de 3 secondes

// Destruction du renderer et de la fenêtre :
SDL_DestroyRenderer(renderer); 
SDL_DestroyWindow(fenetre);
SDL_Quit(); // On quitte la SDL

Créer un renderer et une fenêtre

modifier

Pour finir ce paragraphe sur la création d'un renderer, sachez que l'on peut créer un renderer et une fenêtre à partir d'une même fonction, SDL_CreateWindowAndRenderer :

SDL_CreateWindowAndRenderer(int width, int height, Uint32 window_flags,
                                SDL_Window **window, SDL_Renderer **renderer);

Les arguments sont les mêmes que pour la création d'une fenêtre à ceci près que l'on vous demande un pointeur de pointeur de fenêtre et un pointeur de pointeur de renderer. Si vous souhaitez savoir les flags qu'utilise le renderer créé avec cette fonction, utilisez SDL_RendererGetInfo qui est présentée dans le paragraphe suivant. Avec les mêmes variables que nous avons utilisées dans le code ci-dessus, la création du renderer devient donc :

if(SDL_CreateWindowAndRenderer( 600 , 600 , SDL_WINDOW_RESIZABLE,&fenetre,&renderer)<0)
{
    printf("Erreur lors de la creation d'un renderer : %s",SDL_GetError());
    return EXIT_FAILURE;
}

Le rendu

modifier

Nous savons maintenant comment créer un moteur de rendu. Seulement, il nous faudrait savoir faire un rendu pour qu'il soit utile. Il faut bien comprendre que lorsque vous modifierez le renderer, bien que vous ne sachiez pas encore le faire, cette modification n'apparaîtra pas à l'écran tant que vous n'aurez pas fait de rendu. Nous allons employer pour cela la fonction SDL_RenderPresent dont voici le prototype :

void SDL_RenderPresent(SDL_Renderer* renderer);

Vous emploierez donc cette fonction ainsi :

SDL_RenderPresent(renderer);

Étant donné que tout ce que vous avez à savoir sur le rendu consiste en la fonction SDL_RenderPresent, nous allons nous occuper de deux fonctions en relation directe avec celui-ci : SDL_GetRenderer et SDL_GetRendererInfo.

La première fonction sert simplement à récupérer le renderer associée à la fenêtre que l'on passe en argument.

SDL_Renderer* SDL_GetRenderer(SDL_Window* window);

Cette fonction peut être utile lorsque vous gérez plusieurs fenêtre en même temps. En effet, vous pourrez alors n'utiliser qu'une seule variable de type SDL_Renderer de telle sorte que celle-ci pointe vers le renderer de la fenêtre active. À chaque fois que le focus passe d'une fenêtre à une autre, il vous suffit donc de récupérer le renderer de la fenêtre active.

SDL_GetRendererInfo va nous permettre d'obtenir des informations sur le renderer (le nom de la fonction est très explicite, mais on ne sait jamais).

int SDL_GetRendererInfo(SDL_Renderer* renderer ,SDL_RendererInfo* info);

La fonction renvoie 0 en cas de succès et une valeur négative en cas d'échec. Ensuite, la fonction demande l'adresse du renderer dont vous voulez obtenir des informations puis un pointer de SDL_RenderInfo. Comme vous vous en doutez, c'est une structure adaptée pour contenir des informations sur un renderer. Voici les champs qu'elle contient :

  • char* name : le nom du renderer
  • Uint32 flags : les flags associés au renderer. Attention, ceux-ci fonctionnent d'une manière un peu particulière (voir exemple).
SDL_RendererInfo info;
rend = SDL_CreateRenderer(fenetre, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
SDL_GetRendererInfo(rend,&info);
if(info.flags == SDL_RENDERER_ACCELERATED)
     return  -1;
else if(info.flags == SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC)
     return 0;

//Ce code retournera 0. En effet, le | ne s'utilise pas comme opérateur binaire mais simplement pour accumuler les flags.

Comme vous l'avez vu, il n'y a pas grand chose à dire sur le rendu. En revanche, vous allez vous rendre compte que le renderer offre de relativement nombreuses possibilités que nous allons maintenant explorer.

Dessiner sur le renderer

modifier

Le renderer, en plus d'être un simple moteur de rendu, permet de créer des formes basiques de manière plus optimisée que si l'on collait une texture dessus (vous saurez bientôt ce que cela veut dire).

Les couleurs

modifier

Pour commencer, voyons comment changer la couleur de fond du renderer, qui est noire par défaut. Nous devons tout d'abord définir la couleur avec laquelle nous allons travailler grâce à SDL_SetRenderDrawColor. La couleur que nous allons déterminer ici est en fait une couleur qui servira pour tous les dessins que vous ferez avec le renderer jusqu'à ce que nous la changions.

int SDL_SetRenderDrawColor(SDL_Renderer* renderer, Uint8 r,  Uint8 g , Uint8 b , Uint8  a);

Cette fonction renvoie 0 en cas de succès et une valeur négative en cas d'échec. Vous devez être en train de vous dire qu'une fois de plus, il va falloir faire une gestion des erreurs. Mais puisque vous commencez à présent à faire partie des initiés de la SDL, un grand secret va vous être révélé : il n'y a pas grand monde qui vérifie les erreurs à la sortie de chaque fonction. Par conséquent, dorénavant, vous serez épargné de la vérification d'erreurs. Par contre, si un jour votre programme plante, n'oubliez pas ce long début de cours à devoir écrire une vérification des erreurs pour chaque fonction, car à ce moment, celle-ci vous sera probablement d'une grande utilité.

Revenons à notre fonction. Elle nous demande en argument le renderer pour lequel nous voulons déterminer la couleur de travail, mais aussi quatre Uint8 répondant aux noms de r, g, b et a. Ceux-ci vont nous permettre de donner à la fonction la couleur avec laquelle nous allons remplir le renderer. Arrêtons nous sur le fonctionnement des couleurs.

R, g, b et a correspondent en anglais à red, green, blue et alpha, ce qui en français correspond à rouge, vert, bleu et alpha (la transparence). Un Uint8 faisant un octet, r,v,b et a prennent des valeurs situées entre 0 et 255. Afin de créer la quasi totalité des couleurs différentiables par l’œil, nous allons combiner le rouge, le vert et le bleu comme lorsqu'en peinture vous obtenez du orange en mélangeant le rouge et le jaune. Le problème ici, c'est que ce ne sont pas les trois couleurs primaires habituelles. En fait, tout cela n'est qu'une histoire de physique. Lorsque vous faites de la peinture, vous faites ce que l'on appelle une synthèse soustractive des couleurs si bien que lorsque vous mélangez du rouge, du jaune et du bleu, vous obtenez du noir. Au contraire, ici, vous allez utiliser une synthèse additive : rouge + vert + bleu = blanc. Ceci est très résumé, mais étant donné que ce livre traite de la SDL et non de la restitution des couleurs, nous vous recommandons pour approfondir de lire la restitution des couleurs : cela ne vous prendra pas longtemps et vous comprendrez mieux comment fonctionne ce rgba.

Le a, quant à lui correspond à la transparence de la couleur du pixel. Plus il est près de 0, plus la couleur est transparente, plus il est près de 255, plus la couleur est opaque. Pour en savoir plus sur la transparence, reportez-vous au chapitre sur la transparence.

Voici un tableau qui vous donnera une idée de la manière de combiner les couleurs pour obtenir la teinte que vous désirez.

Les couleurs
Rouge Vert Bleu Code hexadécimal Résultat
0 0 0 0x000000
80 80 80 0x505050
160 160 160 0xA0A0A0
255 255 255 0xFFFFFF
160 0 160 0xA000A0
0 0 255 0x0000FF
0 255 255 0x00FFFF
0 255 0 0x00FF00
255 255 0 0xFFFF00
255 140 0 0xFF8C00
255 0 0 0xFF0000
255 0 255 0xFF00FF
255 144 208 0xFFA0A0

Certes, vous pouvez créer beaucoup plus de couleur (il y en a 16 777 216). Pour bien comprendre comment fonctionne ce codage des couleurs rgb, vous pouvez aller vous amuser à en créer sur le Gimp ou sur Paint...

Si vous souhaitez savoir quelle couleur vous utilisez,vous pouvez employer la fonction SDL_GetRenderDrawColor.

int SDL_GetRenderDrawColor(SDL_Renderer* renderer, Uint8* r, Uint8* g, Uint8* b, Uint8* a);

Cette fonction renvoie les valeurs habituelles pour la gestion des erreurs (0 en cas de succès, valeur négative en cas d'erreur). Elle s'utilise ainsi :

Uint8 r,g,b,a;
SDL_GetRenderDrawColor(renderer, &r, &g, &b, &a);
printf("Voici la couleur que vous utilisez : %d,%d,%d,%d",r,g,b,a);

Revenons à notre objectif de changer la couleur de fond du renderer. Pour remplir celui-ci avec la couleur déterminée par SDL_SetRenderDrawColor, utilisez la fonction SDL_RenderClear

int SDL_RenderClear(SDL_Renderer* renderer)

Cette fonction renvoie 0 en cas de succès et une valeur négative en cas d'erreur. Vous pourrez trouver un exemple d'utilisation de celle-ci à la partie "Points, lignes et rectangles".

Points et lignes

modifier

Vous avez peut-être été un petit peu abusé lorsqu'on vous a annoncé que l'on pouvait dessiner sur le renderer. En fait, tout ce qu'on peut faire, c'est remplir des points (en l’occurrence des pixels), des lignes et des rectangles avec la couleur que vous avez choisie avec SDL_SetRenderDrawColor.

Avant de voir comment "dessiner", rappelons comment fonctionne les positions sur la SDL, et en informatique en général. L'origine du repère se trouve en haut à gauche et l'axe des abscisses est dirigé vers la droite et l'axe des ordonnés est dirigé vers le bas. Forts de ces données, la fonction de coloriage d'un point va nous paraître extrêmement simple.

int SDL_RenderDrawPoint(SDL_Renderer* renderer, int x , int y);

Rappelons, si c'est encore nécessaire, que le int renvoyé prend la valeur 0 en cas de succès et une valeur négative en cas d'erreur. Ensuite, on donne à la fonction le renderer sur lequel on veut dessiner et bien sûr la position en x et en y du point.

Jusqu'ici, tout est très simple. Mais lorsqu'on veut remplir beaucoup de points, cette fonction n'est pas très adaptée. Or, cela tombe bien, il existe une fonction faite pour remplir plusieurs points à la fois, SDL_RenderDrawPoints.

int SDL_RenderDrawPoints(SDL_Renderer* renderer,  const SDL_Point* points,  int nombre_de_points);

Pour le retour et le premier argument, cette fonction est identique à SDL_RenderDrawPoint. En revanche, elle nécessite un tableau de structure SDL_Point et la taille de ce tableau. La structure SDL_Point ne comporte que deux champs : x et y.

Par exemple, on peut coder une ligne commençant à la position (4;100) et se terminant à la position (30;100) ainsi (vous pouvez essayer de le faire vous-même avant de regarder ce code) :

//ce  code s'inscrit dans la continuité du paragraphe Création du renderer, d'où quelques variables non déclarées
SDL_Point points[27];
for(int i = 0; i!=27;i++)
{
    points[i].x = i+4;
    points[i].y = 100;
}
SDL_RenderDrawPoints(renderer,points,27);
SDL_RenderPresent(renderer);

Même s'il n'est pas d'un grand effort de dessiner une ligne, la SDL nous facilite le travail en nous offrant une fonction toute prête :

int SDL_RenderDrawLine(SDL_Renderer* renderer, int x1 , int y1 , int x2 , int y2);

Vous savez à quoi sert l'entier renvoyé, ne nous y attardons plus. Pour les arguments, vous savez déjà presque tout. La fonction vous demande le renderer sur lequel dessiner et les positions des deux extrémité de la ligne (les pixels aux extrémités sont inclus dans la ligne). Le code suivant est donc l'équivalent du code utilisant les SDL_Point ci-dessus :

SDL_RenderDrawLine(renderer,4,100,30,100);
SDL_RenderPresent(renderer);

C'est tout de même plus simple qu'avec SDL_RenderDrawPoints...

La SDL propose une fonction pour dessiner un chemin. Elle permet en fait de relier plusieurs points.

int SDL_RenderDrawLines(SDL_Renderer* renderer,  const SDL_Point* points,  int nombre_de_points);

Comme vous avez pu le remarquer, cette fonction appelle les mêmes arguments que la fonction SDL_RenderDrawPoints. En fait, pour tracer un chemin ou un polygone, il suffit de créer un tableau de SDL_Point de telle sorte que le premier point soit le départ de la forme que vous voulez créer et que le point suivant soit celui que vous voulez relier au précédent.

Par exemple, vous pouvez, pour vous entraîner, essayer de tracer un triangle quelconque.

Voici la solution :

//ce  code s'inscrit dans la continuité du paragraphe Création du renderer, d'où quelques variables non déclarées
SDL_Point triangle[4];//Création du chemin
triangle[0].x = triangle[0].y = 100;
triangle[1].x = triangle[1].y = 300;
triangle[2].x = 200;
triangle[2].y = 300;
triangle[3] = triangle[0];//On n'oublie pas de revenir au point de départ

SDL_RenderDrawLines(renderer,triangle,4);//On dessine le triangle.
SDL_RenderPresent(renderer);//On fait un rendu pour afficher notre triangle.

Rectangles

modifier

Ce paragraphe est très important pour la suite de ce livre et, lorsque vous aurez fini de le lire, il faut que vous sachiez bien vous servir des rectangles avec la SDL. En effet, la SDL ne permet globalement que de gérer des rectangles et il faut que vous sachiez comment ceux-ci sont stockés avant de lire le chapitre suivant. Cependant, n'ayez pas peur, vous allez voir qu'il n'y a rien de difficile.

Dans la SDL, il existe une structure spécialement faite pour stocker un rectangle qui s'appelle SDL_Rect. Celle-ci comporte quatre champs, tous des int, que vous connaissez déjà :

  • x et y : la position du rectangle par rapport au repère de votre fenêtre
  • w : la largeur du rectangle
  • h : la hauteur du rectangle

Voici comment on le déclare :

SDL_Rect rectangle = {0,0,250,250};//Ceci est un rectangle  de dimensions 250*250 et de position (0;0)

Maintenant que vous savez comment fonctionne la structure SDL_Rect, retournons à notre renderer. Sur celui-ci, vous allez pouvoir soit dessiner un rectangle, soit remplir un rectangle. La différence réside dans le fait que si vous dessinez le rectangle, celui-ci n'est pas plein (cela paraît évident mais ceux qui utilise depuis longtemps la SDL n'avait jusqu'à présent à leur disposition que la fonction de remplissage).

Voici donc la fonction qui permet de dessiner un seul rectangle et, au passage, celle qui permet d'en dessiner plusieurs dans la même foulée.

int SDL_RenderDrawRect(SDL_Renderer* renderer, const SDL_Rect* rect);//Pour dessiner un seul rectangle
int SDL_RenderDrawRects(SDL_Renderer* renderer, const SDL_Rect* rect, int nombre_de_rectangle);//Pour dessiner un ou plusieurs rectangles

Attention, n'oubliez pas que pour dessiner un seul rectangle, il vous faut envoyer un pointeur de celui-ci. La fonction permettant de dessiner plusieurs rectangles fonctionne de manière identique à celle permettant de dessiner plusieurs points.

Enfin, voici les fonctions permettant de remplir un ou plusieurs rectangles. Celles-ci sont presque identique aux fonctions de dessin d'un rectangle, c'est pourquoi vous avez seulement besoin du prototype. Après avoir étudié celles-ci, nous en aurons fini avec les fonctions de dessin.

int SDL_RenderFillRect(SDL_Renderer*   renderer, const SDL_Rect* rect);//Pour dessiner un seul rectangle
int SDL_RenderFillRects(SDL_Renderer*   renderer, const SDL_Rect* rect, int nombre_de_rectangle);//Pour dessiner un ou plusieurs rectangles

Il y a encore quelque chose que vous devez savoir sur les rectangles avec le renderer. Vous pouvez créer une zone de travail sur votre renderer, semblable aux sélections pour ceux qui ont déjà travaillé sur un logiciel de graphisme. Lorsque vous allez associer une zone de travail à votre renderer, vous allez interdire l'accès à tous les pixels à l'extérieur du rectangle et définir un nouveau repère : désormais, la position (0;0) correspond au coin en haut à gauche du rectangle délimitant votre zone de travail. Cette fonctionnalité est très utile par exemple lorsque vous avez créé un menu placé en haut de votre fenêtre et que vous ne voulez plus vous en occuper, ou bien si vous développez une interface graphique avec des boîtes d'outil et que vous ne voulez vous occuper que de ces boîtes à un moment donné.

int SDL_RenderSetViewport(SDL_Renderer* renderer, const SDL_Rect* rect);

Étant donné que cette fonction ressemble à celles que nous avons vu, nous n'allons nous attarder que sur le second argument. En effet, il y a un piège : la fonction demande seulement un pointeur du rectangle délimitant la zone sur laquelle vous désirez travailler mais ce rectangle est un peu spécial. En effet, il semble y avoir une contradiction dans la SDL puisque le repère permettant de déterminer la position du rectangle prend son origine dans le coin en bas à gauche du renderer et que par conséquent, l'axe des abscisses est dirigé vers la droite et l'axe des ordonnées vers le haut. Ce défaut sera peut-être corrigé d'ici peu, le seul moyen de le savoir est de tester la fonction pour se rendre compte.

Il manque encore quelque chose : lorsqu'on a une fonction "set", on a souvent "get" qui l'accompagne. Vous pouvez donc récupérer la zone sur laquelle vous travaillez grâce à la fonction SDL_RenderGetViewport :

void SDL_RenderGetViewport(SDL_Renderer* renderer, const SDL_Rect* rect);//on récupère la zone de travaille dans rect

Deux petits TPs

modifier

Le chapitre que nous venons de voir était dense et représente une partie importante dans l'affichage avec la SDL. Or, comme vous le savez, en programmation, la théorie seule ne suffit pas. Voici donc deux travaux pratiques que vous pouvez réaliser afin de vous entraîner. Le second est un petit peu plus délicat et nécessite quelques connaissances mathématiques bien que celles-ci soient rudimentaires : un niveau de seconde générale devrait suffire. Sachez cependant que réaliser le second TP est plus formateur que de réaliser le premier.

Bien évidemment, il y a une infinité de manière de coder. Une solution est proposée, mais votre code même s'il n'est pas identique peut être tout à fait correct.

Un échiquier

modifier

Dans ce TP, nous allons simplement afficher un échiquier dans une fenêtre non redimensionable de dimensions 800x800 pixels. Pour mieux délimiter les cases, des bordures rouges d'épaisseur un pixel séparent celles-ci. À vous de jouer !

Un peu de géométrie

modifier

Le but de ce TP est de dessiner un octogone régulier inscrit dans un cercle lui-même inscrit dans le carré que constituera la fenêtre. Tout d'abord, vous devez définir une fenêtre carrée, non redimensionnable (on ne va pas compliquer les choses). Une définition de 800x800 conviendra sans doute.

Étant donné que ce n'est peut-être pas très claire pour certains, nous allons ici donner quelques astuces pour y réussir:

Pour ceux qui voudraient en faire encore un petit peu plus, pourquoi ne pas essayer de tracer les diagonales de l'octogone?