Utiliser La SDL en Langage C
Utiliser La SDL en Langage C
Utiliser La SDL en Langage C
Pouet_forever
29 décembre 2018
Table des matières
1. Introduction 4
2. La SDL 5
2.1. Présentation de la SDL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
2.1.1. Qu’est-ce que la SDL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
2.1.2. Que peut faire la SDL . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
2.1.3. Pourquoi choisir la SDL . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.2. Configurer un projet avec la SDL . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.2.1. Windows . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.2.2. Linux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
2.2.3. OSX . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
2.3. Prérequis et état d’esprit du tutoriel . . . . . . . . . . . . . . . . . . . . . . . . 9
2.3.1. Prérequis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.3.2. Le but du tutoriel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.3.3. La documentation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
1
Table des matières
7. La transparence 76
7.1. La transparence alpha . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
7.1.1. Un problème embêtant . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
7.1.2. Les modes de fusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
7.1.3. Les modes de fusion de la SDL . . . . . . . . . . . . . . . . . . . . . . . 77
7.2. Gérer la transparence alpha . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
7.2.1. Avoir un renderer transparent . . . . . . . . . . . . . . . . . . . . . . . . 79
7.2.2. Avoir une texture transparente . . . . . . . . . . . . . . . . . . . . . . . 80
7.2.3. Des collisions entre les textures et le renderer . . . . . . . . . . . . . . . 81
7.3. Avec les surfaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
7.3.1. La transparence alpha . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
7.3.2. Rendre une couleur transparente . . . . . . . . . . . . . . . . . . . . . . 85
2
Table des matières
8.1.3. Négatifs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
8.2. De la luminosité . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
8.2.1. Éclaircir une image - version naïve . . . . . . . . . . . . . . . . . . . . . 91
8.2.2. Éclaircir une image - version améliorée . . . . . . . . . . . . . . . . . . . 93
8.2.3. Contraster une image . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
8.3. Floutage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
8.3.1. Un peu de théorie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
8.3.2. Le flou gaussien . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
8.3.3. Du détourage ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
3
1. Introduction
Vous connaissez le langage C et vous souhaitez l’utiliser pour faire des petits programmes
graphiques? Vous êtes curieux et vous voulez découvrir la SDL? Ou encore, vous venez de finir
d’apprendre les bases du langage C et voulez les mettre en pratique en utilisant une bibliothèque
graphique? Vous êtes au bon endroit.
Dans ce tutoriel, nous verrons comment utiliser la SDL afin de faire des programmes graphiques.
i
Prérequis
Savoir programmer en langage C (un tutoriel est disponible ici ).
Prérequis optionnel
Avoir des bases en représentation des nombres en machines.
Connaître les opérateurs bits à bits (un tutoriel est disponible ici ).
Savoir utiliser les drapeaux.
Objectifs
Apprendre à utiliser une nouvelle bibliothèque.
Introduire à la documentation de la SDL.
Apprendre à faire des petits jeux.
4
2. La SDL
Dans ce premier chapitre, nous allons voir ce qu’est la SDL et apprendre à faire un projet
l’utilisant. C’est donc un chapitre d’introduction qui nous informera sur la SDL et sur le tutoriel.
Nous y apprendrons les prérequis nécessaires pour suivre ce tutoriel et comment celui-ci se
déroulera.
La SDL c’est-à-dire la Simple DirectMedia Layer est une bibliothèque multimédia. Elle permet
un accès de bas-niveau à l’audio, au clavier, à la souris, au joystick, aux graphiques… Cela veut
dire qu’elle permet d’afficher des fenêtres, d’afficher des images, de jouer des sons, de gérer le
clavier…
Il s’agit d’une bibliothèque libre, multiplateforme et assez connue. Elle est écrite en C, mais
peut également être utilisée en C++ et de nombreux portages existent dans d’autres langages,
notamment en C#, en OCaml ou encore en Python. Depuis sa version 2, elle est placée sous la
licence zlib .
5
2. La SDL
?
Mais concrètement, quel type de projet peut-on faire avec?
La SDL permet de faire différents types de projet. Elle est typiquement utilisée pour faire des
jeux 2D, mais ce n’est pas sa seule utilité. Elle se veut être une bibliothèque qui permette de
faire assez de choses. Notamment, la version 2 de la bibliothèque, sortie en 2013, rajoute des
fonctionnalités utiles.
Pour faire un petit jeu 2D et s’amuser en C, c’est un bon choix. La SDL est assez généraliste
pour nous permettre de faire n’importe quel type de jeu. Réaliser un Tétris, un Casse-brique,
un Pong ou un Bomberman ne pose pas de problèmes. On peut même faire:
— des «Shoot ’em up» (Space Invaders);
— des jeux de combats;
— des jeux de plateforme (Super Mario Bros par exemple);
— des RPG 2D (Zelda par exemple).
Bien sûr, réaliser ces jeux demande plusieurs compétences. Par exemple, dans le cas du RPG, il
faudra gérer:
— le «tile mapping»;
— les collisions;
— les évènements;
— les combats.
Tout ça sans oublier le son, les animations et tout le contenu. Ce travail est facilité par l’utilisation
d’un moteur de jeu (nous pouvons créer un moteur de jeu minimaliste avec la SDL pour nous
faciliter ce travail).
Figure 2.2. – Portage sous Android du jeu Blip & Blop (Shoot ’em up à scrolling horizontal).
La SDL peut également être utilisée pour créer de petits utilitaires, mais ce n’est clairement pas
son rôle premier. Dans ce cas, il faut plutôt se renseigner sur des bibliothèques comme GTK+
.
6
2. La SDL
?
Quels sont les avantages de la SDL sur les autres bibliothèques?
Le plus grand avantage de la SDL est sa grande souplesse. En effet, comme nous l’avons dit, elle
permet de faire des petits jeux plus ou moins facilement.
De plus, elle ne dispose pas de beaucoup de fonctions (moins de 600), et est donc assez rapide à
appréhender. Son utilisation n’est cependant pas simple, mais elle reste un bon choix, notamment
pour l’apprentissage. Notons que de petites bibliothèques viennent compléter la SDL ce qui
nous permet par exemple de manipuler les réseaux ou le son plus facilement qu’avec seulement
la SDL.
La licence peut aussi motiver le choix de cette bibliothèque. En voici les termes.
Permission is granted to anyone to use this software for any purpose, including commercial
applications, and to alter it and redistribute it freely, subject to the following restrictions :
1. The origin of this software must not be misrepresented ; you must not claim that you
wrote the original software. If you use this software in a product, an acknowledgment
in the product documentation would be appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be misrepre-
sented as being the original software.
3. This notice may not be removed or altered from any source distribution.
Nous pouvons utiliser la SDL dans n’importe quel projet, libre ou non, gratuit ou payant, et nous
pouvons même modifier la SDL; tout cela avec très peu de restrictions. Ce qui nous intéresse
dans ce tutoriel, ce n’est pas de modifier la SDL, mais seulement de l’utiliser. La seule restriction
à cela est de ne pas mentir en revendiquant la création de la SDL. La licence ne nous oblige
même pas à mentionner l’usage de la SDL (même si c’est apprécié)!
Un autre argument pour l’utilisation de la SDL est le fait qu’elle soit multiplateforme. Nous
pouvons en effet réaliser des programmes pour Windows, Linux, OSX et pour une multitude
d’autres plateformes. Nous pouvons même écrire des programmes pour Android!
Bien sûr, le meilleur argument reste les projets qu’elle permet de réaliser.
2.2.1. Windows
Les utilisateurs de Code : :Blocks ou de MinGW peuvent utiliser ce tutoriel qui les aidera
à configurer un projet qui utilise la SDL. Sinon, Internet et le site de la SDL donnent des
informations à ce propos.
7
2. La SDL
2.2.2. Linux
Sous Linux, la méthode la plus simple est d’utiliser son gestionnaire de paquet pour installer le
paquet de développement de la SDL. Par exemple, sous Debian et ses dérivés, cette commande
peut être utilisée.
Une grande majorité des distributions disposent du paquet de développement (peut-être sous un
autre nom que libsdl2-dev) dans leurs paquets, mais s’il n’est pas disponible, il est également
possible de compiler les sources soi-même (voir la page d’installation de la SDL ).
Une fois ceci fait, il nous faut juste modifier notre ligne de compilation pour indiquer que nous
utilisons la SDL. Avec gcc par exemple, notre ligne pour compiler un fichier ressemblera à ça
(nous conseillons de rajouter d’autres options de compilation comme -Wall et -Wextra).
2.2.3. OSX
Le site de la SDL (et d’autres ressources sur Internet) indiquent comment configurer un projet
pour utiliser la SDL.
1 ###include <SDL2/SDL.h>
2
3 int main(int argc, char *argv[])
4 {
5 SDL_Init(SDL_INIT_VIDEO);
6 return 0;
7 }
Normalement, la compilation devrait se faire sans erreur. Le programme créé ne fait rien, mais
dès le prochain chapitre, nous verrons comment ouvrir une fenêtre.
8
2. La SDL
2.3.1. Prérequis
Pour suivre ce tutoriel, certaines connaissances sont requises. Des bases en langage C naturelle-
ment. Il n’est pas nécessaire d’être particulièrement à l’aise avec le langage (ce tutoriel peut
d’ailleurs servir à pratiquer) quelques connaissances sont quand même indispensables:
— les pointeurs et les tableaux;
— la gestion de la mémoire;
— les structures;
— la gestion des fichiers.
De plus, l’instruction goto sera utilisée à des fins de gestion d’erreur. Il faut juste voir son
fonctionnement et savoir à quoi elle sert, son utilisation dans ce tutoriel étant assez basique.
!
Il ne sert à rien d’essayer d’utiliser une bibliothèque en C sans avoir acquis les bases non
seulement du langage, mais aussi de la programmation.
Ce tutoriel par exemple, apprend à manipuler les notions citées. Le but n’est pas de le
survoler, mais de prendre son temps, de bien l’exploiter, de regarder d’autres ressources, afin de
connaître les bases nécessaires pour suivre ce tutoriel.
Le but de ce tutoriel est de présenter la SDL bien entendu. Mais un but plus profond est de
présenter une première bibliothèque, d’apprendre à l’utiliser sans se sentir perdu, et de devenir
indépendant.
Un autre des buts de ce tutoriel est de donner de bonnes habitudes. Le C est un langage
difficile à bien utiliser. Il demande de la rigueur et écrire un code propre n’est pas facile. Ainsi,
en proposant un code propre et gérant les erreurs et autres cas critiques pouvant toucher le
programme, nous espérons aider le lecteur à acquérir de bonnes pratiques. Cependant, tous les
codes ne feront pas de vérification (mais il faudra quand même les faire).
Finalement, le but (et c’est quand même le principal) est que cette présentation de la SDL soit
bénéfique et apprenne à utiliser une nouvelle bibliothèque. Si quelqu’un a pour but de faire
un petit programme et réussit, grâce à ce tutoriel, à le faire avec la SDL, alors ce but a été
atteint.
i
Ce tutoriel ne présente que la SDL et pas des méthodes de conception de projet qui sont
nécessaires pour mener correctement un projet.
Ce tutoriel présente parfois des notions qui ne sont pas forcément nécessaires pour créer un jeu. Le
chapitre sur la modification pixels par pixels n’est par exemple pas essentiel à la compréhension
9
2. La SDL
de ce tutoriel et est en plus assez technique (mais il reste très intéressant). Nous pourrons donc
passer dessus rapidement voire le sauter quitte à revenir dessus plus tard.
Néanmoins, il nous faut veiller à passer suffisamment de temps sur chaque chapitre et à pratiquer
ce que nous apprenons. La programmation est un domaine où lire passivement n’aide pas à la
progression, et bien que le tutoriel possède des TPs, il faut également pratiquer de son côté et
faire ses propres tests pour s’approprier les notions vues.
2.3.3. La documentation
!
La documentation de la SDL (comme la documentation de la plupart des bibliothèques)
est en anglais. Ce n’est pas de l’anglais très compliqué et avec un minimum d’efforts,
même un débutant pourra comprendre ce qui est écrit assez rapidement.
Voici le lien de la documentation . Nous pouvons déjà la parcourir et voir comment elle est
construite avant de passer à la suite.
Dans ce chapitre introductif, nous n’avons pas du tout programmé, mais nous avons vu ce
qu’était la SDL, les possibilités qu’elle donnait et comment ce tutoriel allait se présenter. Dans
le chapitre suivant, nous commencerons à programmer, en voyant comment manipuler les
fenêtres.
10
3. Les premières fenêtres
Dans ce chapitre, nous commencerons enfin à utiliser la SDL. Nous verrons comment initialiser
la SDL, comment créer une fenêtre et comment la gérer.
Voyons d’abord le code de base que nous devrons utiliser avec la SDL:
1 ###include <SDL2/SDL.h>
2
3 int main(int argc, char *argv[])
4 {
5 return 0;
6 }
Un code tout à fait basique composé d’une fonction main et d’une directive de préprocesseur
pour inclure la SDL. Maintenant, commençons à utiliser la SDL.
Pour être utilisée, la SDL doit d’abord être initialisée. Pour cela, nous devons utiliser la fonction
SDL_Init . Cette fonction doit être utilisée avant toutes les autres fonctions de la SDL car
elle charge tout ce dont les autres fonctions ont besoin pour fonctionner. Voici son prototype.
Cette fonction prend en paramètre une liste de drapeaux sous la forme d’un entier, ceux-ci
correspondants aux sous-systèmes que l’on veut initialiser. La SDL est en effet composée de
plusieurs parties. Voici ses différentes parties et le drapeau à fournir pour l’initialiser.
Drapeaux Description
SDL_INIT_TIMER Initialise le système de gestion du temps
SDL_INIT_AUDIO Initialise le système de gestion de l’audio
11
3. Les premières fenêtres
Tous les drapeaux ne sont pas présents, mais nous avons le lien vers la documentation pour voir
les autres.
i
Pour charger plusieurs systèmes à la fois, nous devons utiliser l’opérateur |.
Par exemple, pour charger les systèmes audio et vidéo, il nous faudra utiliser cette ligne.
1 SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO);
i
La fonction SDL_Init permet de charger la SDL. C’est donc le point de départ d’un
programme en SDL. Aucune fonction de la SDL ne doit être utilisée avant elle.
1 ###include <SDL2/SDL.h>
2 #include <stdlib.h>
3
4 int main(int argc, char *argv[])
5 {
6 if(0 != SDL_Init(SDL_INIT_VIDEO))
7 return EXIT_FAILURE;
8 return EXIT_SUCCESS;
9 }
12
3. Les premières fenêtres
Cependant, la fonction SDL_Init, même si nous ne savons pas comment elle fonctionne, fait
certainement quelques allocations, et il faut donc désallouer tout ça. Pour cela, la SDL nous
fournit une fonction, la fonction SDL_Quit . Voici son prototype.
1 void SDL_Quit(void)
Elle ne prend pas d’arguments et ne renvoie rien. Elle permet juste de quitter proprement la
SDL.
i
Toutes nos manipulations doivent être effectuées entre la fonction SDL_Init qui est le
point de départ de notre programme et la fonction SDL_Quit qui est son point d’arrivée.
Au vu de cela, notre code n’est pas correct. Nous n’avons pas quitté la SDL. Le bon code…
1 ###include <SDL2/SDL.h>
2 #include <stdlib.h>
3
4 int main(int argc, char *argv[])
5 {
6 if(0 != SDL_Init(SDL_INIT_VIDEO))
7 return EXIT_FAILURE;
8 SDL_Quit();
9 return EXIT_SUCCESS;
10 }
La fonction SDL_Init comme beaucoup d’autres fonctions de la SDL que nous verrons peut
échouer. Dans ce cas, elles renvoient une valeur d’erreur (une valeur négative pour SDL_Init).
Cela nous permet de ne pas poursuivre le programme en cas d’erreur. Cependant, nous aimerions
bien connaître la raison de cette erreur. Les fonctions strerror et perror (déclarées dans
l’en-tête stdio.h) permettent d’obtenir des informations sur les erreurs des fonctions standards.
La SDL dispose d’une fonction identique, la fonction SDL_GetError dont le prototype est le
suivant.
13
3. Les premières fenêtres
Cette fonction ne prend aucun paramètre et renvoie une chaine de caractères qui est un message
à propos de la dernière erreur que la SDL a rencontrée. Nous pouvons donc l’écrire dans le flux
de sortie standard d’erreur (c’est-à-dire stderr).
!
Pour utiliser stderr, il est nécessaire d’inclure l’en-tête stdio.h.
1 ###include <SDL2/SDL.h>
2 #include <stdio.h>
3 ###include <stdlib.h>
4
5 int main(int argc, char *argv[])
6 {
7 if(0 != SDL_Init(SDL_INIT_VIDEO))
8 {
9 fprintf(stderr, "Erreur SDL_Init : %s",
SDL_GetError());
10 return EXIT_FAILURE;
11 }
12 SDL_Quit();
13 return EXIT_SUCCESS;
14 }
Et après avoir appris tout ceci, nous pouvons visiter la page de la documentation de la SDL
à propos de l’initialisation . Nous pourrons y découvrir les fonctions de la SDL ayant un
rapport avec son initialisation (après tout, comme nous l’avons dit, un tutoriel ne peut pas être
exhaustif, et à un moment, la documentation devient essentielle pour apprendre de nouvelles
choses).
Notre code précédent initialise peut-être la SDL, mais il ne fait rien d’autre. Notre but est de
créer des fenêtres, voyons comment faire. La SDL nous propose une fonction qui permet de créer
une fenêtre très facilement, la fonction SDL_CreateWindow . Voici son prototype.
14
3. Les premières fenêtres
5 int h,
6 Uint32 flags)
Drapeaux Description
SDL_WINDOW_FULLSCREEN Crée une fenêtre en plein écran
SDL_WINDOW_FULLSCREEN_DESKTOP Crée une fenêtre en plein écran à la résolution
du bureau
SDL_WINDOW_SHOWN Crée une fenêtre visible
SDL_WINDOW_HIDDEN Crée une fenêtre non visible
SDL_WINDOW_BORDERLESS Crée une fenêtre sans bordures
SDL_WINDOW_RESIZABLE Crée une fenêtre redimensionnable
SDL_WINDOW_MINIMIZED Crée une fenêtre minimisée
SDL_WINDOW_MAXIMIZED Crée une fenêtre maximisée
i
Là encore, nous pouvons utiliser l’opérateur | pour choisir plusieurs drapeaux. De plus, le
drapeau SDL_WINDOW_SHOWN est un drapeau par défaut. La fenêtre est toujours visible
sauf si le drapeau SDL_WINDOW_HIDDEN a été passé en paramètre. Cependant, nous devons
tout de même donner un paramètre à la fonction, donc, dans le cas ou nous voulons juste
que la fenêtre soit visible, nous pouvons passer 0 en paramètre (0 signifie qu’on n’envoie
pas de drapeaux, et donc la fenêtre sera visible puisqu’elle l’est par défaut). Cependant,
dans un souci de clarté, nous préférerons utiliser le drapeau SDL_WINDOW_SHOWN.
15
3. Les premières fenêtres
1 ###include <SDL2/SDL.h>
2 #include <stdio.h>
3 ###include <stdlib.h>
4
5 int main(int argc, char *argv[])
6 {
7 SDL_Window *window = NULL;
8 if(0 != SDL_Init(SDL_INIT_VIDEO))
9 {
10 fprintf(stderr, "Erreur SDL_Init : %s",
SDL_GetError());
11 return EXIT_FAILURE;
12 }
13 window = SDL_CreateWindow("SDL2", SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED,
14 640, 480, SDL_WINDOW_SHOWN);
15 if(NULL == window)
16 {
17 fprintf(stderr, "Erreur SDL_CreateWindow : %s",
SDL_GetError());
18 return EXIT_FAILURE;
19 }
20 /* On agit sur la fenêtre ici */
21 SDL_Quit();
22 return EXIT_SUCCESS;
23 }
Avec ce code, notre fenêtre apparaît et… Disparaît immédiatement. Ceci est tout à fait normal:
juste après l’avoir ouverte on quitte la SDL. Pour laisser notre fenêtre à l’écran, nous allons
utiliser une autre fonction de la SDL, la fonction SDL_Delay dont voici le prototype.
1 SDL_Delay(3000);
16
3. Les premières fenêtres
Et là, tout comme il faut quitter la SDL après l’avoir initialisée, il faut obligatoirement détruire
la fenêtre après l’avoir créée. Pour cela, nous allons utiliser la fonction SDL_DestroyWindow
dont le prototype est le suivant.
Elle prend en argument un pointeur sur SDL_Window c’est-à-dire le pointeur qui représente la
fenêtre qui doit être détruite (quand on disait qu’il fallait récupérer la valeur retournée par
SDL_CreateWindow c’était pas pour rien) et ne retourne rien. Notre code devient le suivant.
1 ###include <SDL2/SDL.h>
2 #include <stdio.h>
3 ###include <stdlib.h>
4
5 int main(int argc, char *argv[])
6 {
7 SDL_Window *window = NULL;
8 if(0 != SDL_Init(SDL_INIT_VIDEO))
9 {
10 fprintf(stderr, "Erreur SDL_Init : %s",
SDL_GetError());
11 return EXIT_FAILURE;
12 }
13 window = SDL_CreateWindow("SDL2", SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED,
14 640, 480, SDL_WINDOW_SHOWN);
15 if(NULL == window)
16 {
17 fprintf(stderr, "Erreur SDL_CreateWindow : %s",
SDL_GetError());
18 return EXIT_FAILURE;
19 }
20 SDL_Delay(3000);
21 SDL_DestroyWindow(window);
22 SDL_Quit();
23 return EXIT_SUCCESS;
24 }
?
Pourquoi doit-on passer à la fonction SDL_DestroyWindow la variable window? Il n’y a
qu’une seule fenêtre à détruire, il peut très bien s’en passer, non?
Non, il ne peut pas s’en passer, pour la simple raison qu’avec la version 2 de la SDL, on peut
créer plusieurs fenêtres. Essayons ce code.
17
3. Les premières fenêtres
1 ###include <SDL2/SDL.h>
2 #include <stdio.h>
3 ###include <stdlib.h>
4
5 int main(int argc, char *argv[])
6 {
7 SDL_Window *window = NULL, *window2 = NULL;
8 if(0 != SDL_Init(SDL_INIT_VIDEO))
9 {
10 fprintf(stderr, "Erreur SDL_Init : %s",
SDL_GetError());
11 return EXIT_FAILURE;
12 }
13 window = SDL_CreateWindow("SDL2", 100, 100,
14 640, 480, SDL_WINDOW_SHOWN);
15 if(NULL == window)
16 {
17 fprintf(stderr, "Erreur SDL_CreateWindow : %s",
SDL_GetError());
18 return EXIT_FAILURE;
19 }
20 window2 = SDL_CreateWindow("SDL2", 700, 500,
21 640, 480, SDL_WINDOW_SHOWN);
22 if(NULL == window2)
23 {
24 fprintf(stderr, "Erreur SDL_CreateWindow 2 : %s",
SDL_GetError());
25 return EXIT_FAILURE;
26 }
27 SDL_Delay(3000);
28 SDL_DestroyWindow(window);
29 SDL_DestroyWindow(window2);
30 SDL_Quit();
31 return EXIT_SUCCESS;
32 }
Deux fenêtres sont créées. On aurait très bien pu vouloir fermer la première avant d’ouvrir la
deuxième ou faire des opérations entre les deux fermetures. Cela n’est possible que parce qu’on
ferme séparément chaque fenêtre grâce au paramètre de la fonction SDL_DestroyWindow.
Bon, maintenant que nous savons ouvrir une fenêtre et la refermer, analysons notre code.
Avons-nous un bon code? Regardons :
— nous utilisons SDL_Quit pour quitter la SDL;
— nous détruisons la fenêtre créée;
— nous testons le retour des fonctions qui peuvent échouer.
18
3. Les premières fenêtres
?
Faisons-nous ces actions dans tous les cas?
Et là, catastrophe, la réponse est non. Par exemple, si la création de la fenêtre a échoué, nous
quittons le programme sans quitter la SDL.
Il faut faire en sorte de quitter la SDL dans tous les cas (et il faudra faire en sorte de fermer
toutes nos fenêtres et de désallouer toute la mémoire allouée). Nous pourrions utiliser des
if imbriqués, mais une fois que l’on fera des programmes un peu plus long ce ne sera plus
maintenable. Nous allons plutôt utiliser la gestion des erreurs évoquée ici pour nous en tirer.
Nous allons donc placer un label dans notre code, juste avant SDL_Quit. Plus tard, nous en
placerons d’autre si besoin. On obtient ce code.
1 ###include <SDL2/SDL.h>
2 #include <stdio.h>
3 ###include <stdlib.h>
4
5 int main(int argc, char *argv[])
6 {
7 SDL_Window *window = NULL;
8 int statut = EXIT_FAILURE;
9
10 if(0 != SDL_Init(SDL_INIT_VIDEO))
11 {
12 fprintf(stderr, "Erreur SDL_Init : %s",
SDL_GetError());
13 goto Quit;
14 }
15 window = SDL_CreateWindow("SDL2", SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED,
16 640, 480, SDL_WINDOW_SHOWN);
17 if(NULL == window)
18 {
19 fprintf(stderr, "Erreur SDL_CreateWindow : %s",
SDL_GetError());
20 goto Quit;
21 }
22
23 statut = EXIT_SUCCESS;
24 SDL_Delay(3000);
25 SDL_DestroyWindow(window);
26 Quit:
27 SDL_Quit();
28 return statut;
29 }
19
3. Les premières fenêtres
Pour commencer, nous allons voir comment changer (et obtenir) les paramètres de la fenêtre.
?
De quels paramètres parlons-nous?
Nous parlons des paramètres que nous avons choisis lors de la création de la fenêtre, c’est-à-dire:
— la taille de la fenêtre;
— la position de la fenêtre;
— le titre de la fenêtre;
— les drapeaux.
La SDL offre des fonctions pour changer tous ces paramètres et ce sont ces fonctions que nous
allons maintenant voir. Pour nous faciliter la tâche, elle a le bon goût d’avoir toutes ses fonctions
construites de la même manière:
— les fonctions pour changer les paramètres commencent toutes par SDL_SetWindow;
— les fonctions pour obtenir les paramètres commencent toutes par SDL_GetWindow;
— le premier paramètre de toutes ces fonctions est un pointeur sur SDL_Window, c’est-à-dire
la fenêtre concernée.
Grâce à ceci nous pouvons quasiment deviner toutes les fonctions que nous allons voir.
La fonction pour obtenir le titre d’une fenêtre est la fonction SDL_GetWindowTitle . Son
prototype:
Nous aurions pu le deviner, elle prend en paramètre (comme prévu) la fenêtre dont on veut
déterminer le titre et retourne une chaine de caractères correspondant au titre en question.
La fonction pour donner un titre à une fonction n’est guère plus compliquée à comprendre. Il
s’agit de la fonction SDL_SetWindowTitle . Son prototype:
20
3. Les premières fenêtres
Elle ne retourne rien et prend deux paramètres, la fenêtre dont on veut changer le titre et le
nouveau titre à donner sous la forme d’une chaine de caractères.
On retrouve en paramètre la fenêtre, mais surprise, la fonction ne renvoie rien et prend aussi en
paramètre deux pointeurs sur des entiers. Ces deux pointeurs correspondent à la position en X
et en Y de la fenêtre. En effet, on ne peut pas renvoyer deux valeurs et donc la fonction modifie
les deux valeurs pointées pour qu’elles valent finalement la position de la fenêtre.
La fonction pour changer la position de la fenêtre est la fonction SDL_SetWindowPosition .
Son prototype:
Son prototype est vraiment très proche de celui de la fonction SDL_GetWindowPosition. Elle
prend en paramètre la fenêtre dont la position doit être changée, la nouvelle position en X et la
nouvelle position en Y.
i
Notons que les nouvelles positions en X et Y peuvent aussi être SDL_WINDOWPOS_CENTERED
ou SDL_WINDOWPOS_UNDEFINED que nous avons vu précédemment.
Pour obtenir la taille de la fenêtre, il nous faut utiliser la fonction SDL_GetWindowSize . Son
prototype:
21
3. Les premières fenêtres
Elle prend en paramètre la fenêtre dont on veut changer la taille, sa nouvelle largeur et sa
nouvelle hauteur.
Elle prend en paramètre la fenêtre dont on veut obtenir les drapeaux et retourne un entier.
i
Quoi? Un entier? Mais comment tester si un drapeau est présent alors?
La fonction renvoie le même type de données qu’on a passé à la fonction SDL_Init. Pour savoir
si un drapeau est présent, il faut tout simplement utiliser l’opérateur & avec le drapeau et le
retour de la fonction. Par exemple, pour regarder si la fenêtre est redimensionnable, on peut
utiliser ce code:
22
3. Les premières fenêtres
Pour tester plusieurs drapeaux simultanément, il faut aussi utiliser l’opérateur & mais cette fois
avec les différents drapeaux sur lesquels on aura utilisé l’opérateur |. Par exemple, pour tester
si la fenêtre est redimensionnable et si elle n’a pas de bordures, on peut utiliser ce code:
On ne peut pas changer directement les drapeaux d’une fenêtre (il n’existe pas de fonction
SDL_SetWindowFlags), mais on dispose de plusieurs fonctions pour faire des actions telles que
réduire la fenêtre. Nous allons voir quelques-unes de ces fonctions.
Pour agrandir la fenêtre, nous devons utiliser la fonction SDL_MaximizeWindow . Son proto-
type:
Nous aurions pu deviner son prototype, elle prend juste en paramètre la fenêtre à restaurer.
23
3. Les premières fenêtres
Et finalement pour la mettre en avant-plan (devant toutes les autres fenêtres), nous utiliserons
la fonction SDL_RaiseWindow . Son prototype:
Nous pouvons le voir, ces fonctions sont très simples à utiliser. Elles prennent en paramètre la
fenêtre sur laquelle on veut agir et ne renvoient pas de valeur.
Nous pouvons placer notre fenêtre en plein écran durant sa création, mais nous pouvons
également le faire plus tard. Par exemple, dans un jeu, nous pourrions proposer à l’utilisateur
de mettre la fenêtre en plein écran ou non. La fonction SDL_SetWindowFullScreen permet
de le faire. Son prototype:
Elle prend en paramètre la fenêtre qu’on veut gérer et un drapeau. Ce drapeau peut-être:
— SDL_WINDOW_FULLSCREEN pour placer la fenêtre en plein écran;
24
3. Les premières fenêtres
i
Généralement, on ne quitte pas un programme parce que cette fonction a échoué. On se
contente de noter l’erreur et de continuer le programme après avoir averti l’utilisateur
qu’il était impossible de changer de mode.
Nous savons maintenant comment créer et manipuler des fenêtres. Mais s’il y a une chose à
retenir de ce chapitre c’est qu’il ne faut pas:
— oublier de vérifier le retour des fonctions à risque;
— oublier de libérer les ressources.
25
4. Dessiner dans la fenêtre
Après avoir vu comment manipuler des fenêtres, nous allons maintenant dessiner dedans.
drapeaux Description
SDL_RENDERER_SOFTWARE Le renderer est logiciel, le rendu sera
effectué par le CPU et les
données seront stockées en mémoire
vive.
SDL_RENDERER_ACCELERATED Le renderer utilise l’accélération matérielle. Les
données sont
en mémoire vidéo, plus rapide que la mémoire
vive.
26
4. Dessiner dans la fenêtre
La plupart du temps, nous voulons un rendu avec l’accélération matérielle (utilisant la carte
graphique), mais dans le cas où celle-ci n’est pas disponible on peut utiliser un rendu logiciel
(c’est donc une solution de repli). Synchroniser le rendu avec la fréquence de rafraîchissement de
l’écran peut être une bonne idée, mais la plupart du temps, on préfèrera le gérer nous-mêmes.
Nous pouvons également passer 0 comme argument, dans ce cas, la SDL essaye de fournir un
renderer qui utilise l’accélération matérielle.
La fonction SDL_CreateRenderer retourne un pointeur sur SDL_Renderer, pointeur que nous
devons récupérer car nous en aurons besoin dès que nous voudrons dessiner un peu. Si la création
du renderer a échoué, elle retourne NULL. Il ne faut donc pas oublier de tester la valeur retournée.
C’est là qu’intervient le renderer logiciel, on l’utilise généralement si on ne peut pas utiliser
l’accélération matérielle. On devrait donc fonctionner de la manière suivante.
i
En fait, la SDL fait ceci automatiquement quand l’index passé à SDL_CreateRenderer
vaut -1.
Désolé, mais nous allons encore une fois gérer nos ressources puisqu’il faut détruire le renderer
créé. Pour cela, nous allons utiliser la fonction SDL_DestroyRenderer . Son prototype:
Elle prend en paramètre le renderer qu’il faut détruire et ne retourne rien. Grâce à ça, nous
pouvons faire notre premier code avec un renderer:
1 ###include <SDL2/SDL.h>
2 #include <stdio.h>
3 ###include <stdlib.h>
27
4. Dessiner dans la fenêtre
4
5 int main(int argc, char *argv[])
6 {
7 SDL_Window *window = NULL;
8 SDL_Renderer *renderer = NULL;
9 int statut = EXIT_FAILURE;
10
11 if(0 != SDL_Init(SDL_INIT_VIDEO))
12 {
13 fprintf(stderr, "Erreur SDL_Init : %s",
SDL_GetError());
14 goto Quit;
15 }
16 window = SDL_CreateWindow("SDL2", SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED,
17 640, 480, SDL_WINDOW_SHOWN);
18 if(NULL == window)
19 {
20 fprintf(stderr, "Erreur SDL_CreateWindow : %s",
SDL_GetError());
21 goto Quit;
22 }
23 renderer = SDL_CreateRenderer(window, -1,
SDL_RENDERER_ACCELERATED);
24 if(NULL == renderer)
25 {
26 fprintf(stderr, "Erreur SDL_CreateRenderer : %s",
SDL_GetError());
27 goto Quit;
28 }
29
30 statut = EXIT_SUCCESS;
31 SDL_Delay(3000);
32
33 Quit:
34 if(NULL != renderer)
35 SDL_DestroyRenderer(renderer);
36 if(NULL != window)
37 SDL_DestroyWindow(window);
38 SDL_Quit();
39 return statut;
40 }
Dans l’ordre, nous initialisons la SDL, puis nous créons la fenêtre et enfin nous créons le renderer.
Nous faisons les destructions dans l’autre sens, le renderer, la fenêtre et enfin nous terminons en
quittant la SDL. Sans oublier bien sûr de vérifier le retour de nos fonctions.
28
4. Dessiner dans la fenêtre
Nous avons lié le renderer à une fenêtre et avons dit que chaque renderer était associé à une
fenêtre. Ce serait donc bien de pouvoir créer le renderer et la fenêtre en même temps. Tant
mieux, la SDL a une fonction qui permet de faire ça. Il s’agit de la fonction SDL_CreateWindo
wAndRender . Son prototype:
Juste en voyant son prototype, nous devrions comprendre comment elle marche. Elle prend en
paramètre:
— la largeur de la fenêtre;
— la hauteur de la fenêtre;
— les drapeaux de la fenêtre (vus dans le chapitre précédent ;
— un double pointeur sur SDL_Window;
— un double pointeur sur SDL_Renderer.
Les deux doubles pointeurs sont compréhensibles: la fonction doit modifier la valeur des deux
pointeurs pour qu’ils pointent sur la fenêtre et le renderer créé, or, pour modifier la valeur d’une
variable dans une fonction, il faut lui passer l’adresse de cette variable, c’est-à-dire des doubles
pointeurs dans notre cas. Aucun drapeau n’est demandé pour le renderer, la fonction essaye de
créer un renderer avec le drapeau SDL_RENDERER_ACCELERATED.
La fonction retourne un entier. Il s’agit de:
— 0 si tout s’est bien passé;
— -1 en cas d’erreur.
On peut comme d’habitude récupérer l’erreur en question avec la fonction SDL_GetError.
La base de l’affichage d’un ordinateur est le pixel. Et un pixel c’est un point, un petit point
d’un écran. Il paraît donc logique que la SDL puisse représenter un point.
29
4. Dessiner dans la fenêtre
?
Mais comment représenter un point?
Comme en mathématiques, on représente un point avec son abscisse et son ordonnée. Ainsi, un
point est représenté en SDL par une structure qui a pour champ ces deux composantes. Il s’agit
de la structure SDL_Point . Ses champs:
— x représente l’abscisse du point ;
— y représente l’ordonnée du point.
!
Le point de coordonnées (0, 0) est en haut à gauche, et si les abscisses augmentent en
allant vers la droite, les ordonnés, elles, augmentent en allant vers le bas.
30
4. Dessiner dans la fenêtre
On a dit que la base de tout était le pixel, mais en fait, avec la SDL, ce que l’on peut vraiment
considérer comme base est plutôt le rectangle. On pourrait se dire qu’un rectangle n’est qu’une
amélioration du point et que le point reste donc la base, mais on utilise le rectangle vraiment
beaucoup plus que le point.
?
Comment pouvons-nous définir un rectangle?
31
4. Dessiner dans la fenêtre
Notons que ce que l’on vient de coder s’appelle une fonction de collision.
On vient d’écrire une fonction qui permet de tester si un point est dans un rectangle. On voudrait
peut-être aussi écrire une fonction pour savoir si deux rectangles se touchent ou encore pour
avoir l’intersection de deux rectangles.
?
Quoi? Mais, on ne va pas écrire tout ça?
Heureusement, on n’a pas à les écrire. La SDL a pensé à nous et offre plusieurs fonctions de
collisions. Voyons en quelques-unes.
Commençons par la fonction qui permet de savoir si deux rectangles se touchent (ce dont on
parlait précédemment). Il s’agit de la fonction SDL_HasIntersection . Son prototype:
32
4. Dessiner dans la fenêtre
Elle prend en paramètre deux pointeurs sur SDL_Rect, c’est-à-dire deux pointeurs sur les deux
rectangles sur lesquelles doit porter la collision et renvoie un SDL_bool qui vaut SDL_TRUE s’ils
se touchent et SDL_FALSE sinon.
En fait, nous pouvons même obtenir le rectangle qui correspond à l’intersection grâce à la
fonction SDL_IntersectRect . Son prototype:
Elle agit exactement comme la fonction SDL_HasIntersection: elle prend en paramètre les
pointeurs sur deux rectangles à tester et retourne SDL_TRUE s’il y a intersection et SDL_FALSE
sinon. Cependant, elle prend un troisième argument qui correspond au rectangle résultant de
l’intersection.
Pour finir, voyons une dernière fonction. Elle permet de tester si un point est dans un rectangle (en
fait, c’est la fonction que l’on a codé tout à l’heure). Il s’agit de la fonction SDL_PointInRect
. Son prototype:
Nous ne sommes pas surpris en apprenant qu’elle prend en paramètre un pointeur sur SDL_Point
et un autre sur SDL_Rect et renvoie SDL_TRUE si le point est dans le rectangle et SDL_FALSE
sinon.
Et nous renvoie à la page de la documentation de la SDL à propos des rectangles pour y voir
les autres fonctions à ce propos (oui oui, nous renvoyons beaucoup à la documentation).
Comme entraînement pour nous familiariser avec les rectangles et les points, nous pouvons
essayer de réécrire toutes ces fonctions.
Le système de couleurs de la SDL est le même que celui utilisé sur les ordinateurs, c’est-à-dire
qu’il se base sur 3 nombres entiers dont la valeur est comprise entre 0 et 255. Ces trois valeurs
correspondent aux composantes rouge, verte et bleue de la couleur qu’on veut représenter (on
parle de système RGB pour Red-Green-Blue) et permettent de représenter toutes les autres
couleurs. Ainsi, chaque couleur est représentée par:
— sa quantité de rouge;
— sa quantité de vert;
— sa quantité de bleu.
Notons que ce système s’appelle la synthèse additive .
33
4. Dessiner dans la fenêtre
On obtient par exemple un jaune particulier avec le triplet (255, 255, 0) (le jaune est un
mélange de rouge et de vert).
À ces trois valeurs, on ajoute parfois une quatrième qui correspond à la composante alpha de
la couleur. Cette composante ne change pas la couleur mais permet de gérer la transparence, 0
correspondant à de la transparence totale et 255 à de l’opacité totale.
La plupart des fonctions de la SDL nous demanderons les 3 valeurs (ou les 4 pour certaines).
De plus, la SDL dispose d’une structure pour représenter une couleur. Il s’agit de la structure
SDL_Color . Ses champs:
— r est un entier entre 0 et 255 et représente la composante rouge de la couleur ;
— g est un entier entre 0 et 255 et représente la composante verte de la couleur ;
— b est un entier entre 0 et 255 et représente la composante bleue de la couleur ;
— a est un entier entre 0 et 255 et représente la composante alpha de la couleur.
Cette structure est intéressante dans le cas où on manipule plusieurs couleurs en même temps.
Par exemple, supposons que l’on ait une fonction colorier qui prend en paramètre les quatre
composantes d’une couleur et colorie la fenêtre. Si on veut la colorier en bleu, on utilisera ce
code.
34
4. Dessiner dans la fenêtre
Là ça va, pas de difficulté. Par contre, si on veut colorier en bleu, puis en orange, puis en rose
fuchsia, il vaudrait mieux faire ainsi.
Ou encore mieux : on pourrait faire une fonction qui prend en paramètre un SDL_Color et se
charge d’appeler la fonction colorier ou bien modifier directement la fonction colorier afin
qu’elle utilise un SDL_Color comme paramètre.
4.3.1. Le principe
Le principe des renderer de la SDL est vraiment très simple. Voyons un exemple.
1 ###include <SDL2/SDL.h>
2 #include <stdio.h>
3 ###include <stdlib.h>
4
5
6 int main(int argc, char *argv[])
7 {
8 SDL_Window *window = NULL;
9 SDL_Renderer *renderer = NULL;
10 int statut = EXIT_FAILURE;
11 SDL_Color orange = {255, 127, 40, 255};
12
13 /* Initialisation, création de la fenêtre et du renderer.
*/
14 if(0 != SDL_Init(SDL_INIT_VIDEO))
15 {
16 fprintf(stderr, "Erreur SDL_Init : %s",
SDL_GetError());
17 goto Quit;
18 }
35
4. Dessiner dans la fenêtre
36
4. Dessiner dans la fenêtre
Pour commencer, nous choisissons une «couleur de travail» pour le renderer. En fait, il faut
voir le renderer comme un outil de dessin; nous choisissons quelle couleur utiliser avec cet outil.
Pour cela, nous utilisons la fonction SDL_SetRendererDrawColor . Son prototype:
Elle prend en paramètre le renderer et les composantes de la couleur en question. Elle renvoie
comme d’habitude 0 si tout s’est bien passé et une valeur négative en cas d’erreur. Nous devrions
donc savoir l’utiliser. La couleur que l’on utilise pour dessiner sera celle-là jusqu’au prochain
changement de couleur.
Dans notre code d’exemple, nous choisissons l’orange comme «couleur de travail» grâce à ces
lignes.
Elle prend en paramètre le renderer qui doit être nettoyé et renvoie 0 en cas de succès et une
valeur négative sinon.
C’est donc cette partie de notre code d’exemple.
37
4. Dessiner dans la fenêtre
1 if(0 != SDL_RenderClear(renderer))
2 {
3 fprintf(stderr, "Erreur SDL_SetRenderDrawColor : %s",
SDL_GetError());
4 goto Quit;
5 }
Pourtant, même après avoir fait tout ça, notre fenêtre n’est toujours pas en orange. En effet, on
a modifié le renderer, mais on n’a pas mis à jour l’écran. La mise à jour de l’écran à partir du
renderer se fait avec la fonction SDL_RenderPresent . Son prototype:
1 SDL_RenderPresent(renderer);
Nous pouvons remarquer que nous avons placé un SDL_Delay avant cette ligne. En fait, sans
cela, notre fenêtre passe instantanément à l’orange. Le SDL_Delay nous permet de ne pas
changer la couleur de la fenêtre tout de suite. C’est grâce à cela que l’on voit le passage à
l’orange.
Pour avoir un meilleur code, on pourrait créer une fonction pour changer la couleur de la fenêtre,
pour avoir un code de ce genre avec une fonction réutilisable.
38
4. Dessiner dans la fenêtre
i
Dans ce dernier code, la réussite de la fonction setWindowColor ne nous intéresse pas.
Ce sera souvent le cas lorsque nous allons dessiner. L’échec d’une fonction de dessin ne
provoquera pas de crash, vérifier qu’elle a réussi n’est donc pas fondamental (à moins bien
sûr que notre programme ait absolument besoin de sa réussite).
C’est ce que l’on fait tout le long d’un programme. On dessine et on met à jour, on redessine,
on remet à jour...
L’étape 2 se fait très simplement à l’aide de la fonction SDL_RenderPresent.
La première étape peut se décomposer en plusieurs étapes (choix d’une couleur de travail, et
utilisation de cette couleur pour dessiner). Dans notre exemple, nous avons changé la couleur de
l’écran, mais il y a plusieurs manières de dessiner:
— on peut changer la couleur de fond de la fenêtre ;
— on peut afficher un point ;
— on peut afficher une image.
— etc.
Toutes ces opérations se font à l’aide du renderer qui est, comme nous l’avons dit, notre outil
de dessin.
Maintenant que nous connaissons la méthode, voyons comment dessiner un point. Pour cela,
nous allons choisir notre couleur, puis utiliser la fonction SDL_RenderDrawPoint . Son
prototype:
39
4. Dessiner dans la fenêtre
Elle prend en paramètre le renderer sur lequel dessiner et les coordonnées du point à dessiner et
retourne 0 en cas de succès et une valeur négative en cas d’erreur.
Pour dessiner un point rouge, puis un point bleu et finalement un point vert, nous pourrions
écrire ceci.
Elle prend en paramètre le renderer sur lequel dessiner, un tableau de SDL_Point et le nombre
de points à dessiner (donc ce nombre doit être plus petit ou égal à la taille de ce tableau). Elle
retourne comme d’habitude une valeur négative en cas d’erreur et 0 en cas de succès.
Utilisons la pour dessiner une ligne sur l’écran.
1 SDL_Point point[640];
2 SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255);
3 size_t i = 0;
4 for(i = 0; i < 640; i++)
5 {
6 point[i].x = i;
7 point[i].y = 200;
8 }
9 SDL_RenderDrawPoints(renderer, point, 640);
10 SDL_RenderPresent(renderer);
40
4. Dessiner dans la fenêtre
On a utilisé un tableau de SDL_Point. Le champ y de tous ces points est égal à 200 et le champ
x va en croissant, finalement on obtient donc une ligne horizontale d’ordonnée 200.
?
Et pour dessiner une ligne sur la diagonale de l’écran?
Il existe plusieurs algorithmes de tracés de segment . Nous pourrions les implémenter et créer
une fonction qui trace un segment reliant deux points. Mais ce n’est même pas la peine: la SDL
possède déjà une fonction pour tracer un segment, la fonction SDL_RenderDrawLine . Son
prototype:
Elle prend en paramètre le renderer sur lequel dessiner le segment et les coordonnées des deux
points à relier. Elle retourne comme d’habitude 0 en cas de succès et une valeur négative en cas
d’erreur.
Pour relier les coins haut gauche et bas droit d’une fenêtre en 640x480, nous allons donc faire
ceci.
Elle prend en paramètre le renderer, un tableau de points et la taille de ce tableau. Elle retourne
(encore une fois) 0 en cas de succès et une valeur négative en cas d’erreur.
Cette fonction dessine count - 1 segments: elle relie le premier point au second, le second au
troisième, le troisième au quatrième, ..., l’avant-dernier au dernier. Ainsi, pour dessiner un carré
de longueur 20 pixels, nous pourrions faire ceci.
1 SDL_Point point[5];
2 point[0].x = 100;
3 point[0].y = 100;
41
4. Dessiner dans la fenêtre
4 point[1].x = 200;
5 point[1].y = 100;
6 point[2].x = 200;
7 point[2].y = 200;
8 point[3].x = 100;
9 point[3].y = 200;
10 point[4].x = 100;
11 point[4].y = 100;
12 SDL_RenderDrawLines(renderer, point, 5);
13 SDL_RenderPresent(renderer);
On crée 5 points parce qu’il ne faut pas oublier de relier le dernier point au premier pour «
fermer » le carré. Le dernier point est donc le même que le premier.
La SDL a aussi des fonctions pour dessiner des rectangles. Voyons pour commencer la fonction
SDL_RenderDrawRect . Son prototype:
Elle prend en paramètre le renderer sur lequel dessiner et un pointeur sur SDL_Rect qui
représente le rectangle à dessiner et retourne 0 en cas de succès et une valeur négative en cas
d’erreur. Ainsi, pour dessiner le carré de l’exemple précédent, nous pouvons faire ceci.
42
4. Dessiner dans la fenêtre
Et
Comme d’habitude, elles retournent 0 en cas de succès et une valeur négative en cas d’erreur.
Elles prennent en paramètre le renderer sur lequel dessiner, un tableau de SDL_Rect et le
nombre de rectangles à dessiner.
Essayons par exemple d’utiliser SDL_RenderFillRects pour dessiner un damier. Voici un code
possible.
!
Dans ce chapitre nous n’avons quasiment pas fait de vérifications du retour des fonctions
de la SDL. Il ne faut pas oublier de les faire dans notre code. Le plus simple est de créer
des fonctions qui se chargent de cela.
43
4. Dessiner dans la fenêtre
Contenu masqué
1 SDL_Rect rect[50];
2 size_t i = 0;
3 for(i = 0; i < 50; i++)
4 {
5 rect[i].w = 50;
6 rect[i].h = 50;
7 rect[i].x = 100 * (i % 5) + 50 * ((i / 5) % 2);
8 rect[i].y = 50 * (i / 5);
9 }
10 SDL_RenderFillRects(renderer, rect, 50);
11 SDL_RenderPresent(renderer);
Bien sûr, il existe d’autres méthodes pour faire ce travail. Retourner au texte.
44
5. Les textures et les images
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.
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.
Voyons maintenant comment créer une texture. Cette opération se fait à l’aide de la fonction
SDL_CreateTexture . Son prototype:
45
5. Les textures et les images
valeur Description
SDL_TEXTUREACCESS_STATIC La texture est rarement modifiée
SDL_TEXTUREACCESS_STREA La texture est souvent modifiée
MING
SDL_TEXTUREACCESS_TARGET La texture peut être utilisée comme cible de rendu (comme un rendere
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 ###include <SDL2/SDL.h>
2 #include <stdio.h>
3 ###include <stdlib.h>
4
5 int main(int argc, char *argv[])
6 {
7 SDL_Window *window = NULL;
8 SDL_Renderer *renderer = NULL;
9 SDL_Texture *texture = NULL;
10 int statut = EXIT_FAILURE;
11
12 if(0 != SDL_Init(SDL_INIT_VIDEO))
13 {
14 fprintf(stderr, "Erreur SDL_Init : %s",
SDL_GetError());
15 goto Quit;
16 }
46
5. Les textures et les images
Notons que nous pourrions faire une fonction qui se chargerait de toutes les initialisations et des
créations nécessaires.
47
5. Les textures et les images
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:
i
Pour faire la troisième étape, il suffit d’appeler la fonction SDL_SetRenderTarget en lui
passant NULL comme second argument.
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:
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.
48
5. Les textures et les images
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_Ren
derCopy dont le prototype est:
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.
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.
49
5. Les textures et les images
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.
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.
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 ###include <SDL2/SDL.h>
2 #include <stdio.h>
50
5. Les textures et les images
3 ###include <stdlib.h>
4
5 int main(int argc, char *argv[])
6 {
7 SDL_Window *window = NULL;
8 SDL_Renderer *renderer = NULL;
9 SDL_Texture *texture = NULL;
10 int statut = EXIT_FAILURE;
11 SDL_Rect rect = {100, 100, 100, 100}, dst = {0, 0, 0, 0};
12 SDL_Color rouge = {255, 0, 0, 255}, bleu = {0, 0, 255,
255};
13
14 if(0 != SDL_Init(SDL_INIT_VIDEO))
15 {
16 fprintf(stderr, "Erreur SDL_Init : %s",
SDL_GetError());
17 goto Quit;
18 }
19 if(0 != SDL_CreateWindowAndRenderer(640, 480,
SDL_WINDOW_SHOWN, &window, &renderer))
20 {
21 fprintf(stderr,
"Erreur SDL_CreateWindowAndRenderer : %s",
SDL_GetError());
22 goto Quit;
23 }
24 texture = SDL_CreateTexture(renderer,
SDL_PIXELFORMAT_RGBA8888,
25 SDL_TEXTUREACCESS_TARGET, 200,
200);
26 if(NULL == texture)
27 {
28 fprintf(stderr, "Erreur SDL_CreateTexture : %s",
SDL_GetError());
29 goto Quit;
30 }
31
32 SDL_SetRenderTarget(renderer, texture);
33 /* La texture est la cible de rendu, maintenant, on dessine
sur la texture. */
34 SDL_SetRenderDrawColor(renderer, bleu.r, bleu.g, bleu.b,
bleu.a);
35 SDL_RenderClear(renderer);
36 SDL_SetRenderDrawColor(renderer, rouge.r, rouge.g, rouge.b,
rouge.a);
37 SDL_RenderFillRect(renderer, &rect); /* On dessine un
rectangle rouge sur la texture. */
38
39 SDL_SetRenderTarget(renderer, NULL); /* Le renderer est la
cible de rendu. */
51
5. Les textures et les images
40
41 /* On récupère les dimensions de la texture, on la copie
sur le renderer
42 et on met à jour l’écran. */
43 SDL_QueryTexture(texture, NULL, NULL, &dst.w, &dst.h);
44 SDL_RenderCopy(renderer, texture, NULL, &dst);
45 SDL_RenderPresent(renderer);
46 statut = EXIT_SUCCESS;
47 SDL_Delay(3000);
48
49 Quit:
50 if(NULL != texture)
51 SDL_DestroyTexture(texture);
52 if(NULL != renderer)
53 SDL_DestroyRenderer(renderer);
54 if(NULL != window)
55 SDL_DestroyWindow(window);
56 SDL_Quit();
57 return statut;
58 }
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.
52
5. Les textures et les images
6 Uint32 Gmask,
7 Uint32 Bmask,
8 Uint32 Amask)
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:
53
5. Les textures et les images
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 :
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 :
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 SDL_Surface *surface;
2 surface = SDL_CreateRGBSurface(0, 100, 100, 32, 0, 0, 0, 0);
3 SDL_FillRect(surface, NULL, SDL_MapRGB(s->format, 255, 0, 0));
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 :
54
5. Les textures et les images
La SDL nous permet de donner une icône à notre programme grâce à la fonction SDL_SetWin
dowIcon . Son prototype est :
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.
55
5. Les textures et les images
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.
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.
56
5. Les textures et les images
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.
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:
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.
57
5. Les textures et les images
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é.
58
5. Les textures et les images
8 }
9 tmp = SDL_CreateTextureFromSurface(renderer, surface);
10 if(NULL == tmp)
11 {
12 fprintf(stderr, "Erreur SDL_CreateTextureFromSurface : %s",
SDL_GetError());
13 goto Quit;
14 }
15 texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888,
16 SDL_TEXTUREACCESS_TARGET, surface->w,
surface->h);
17 if(NULL == texture)
18 {
19 fprintf(stderr, "Erreur SDL_CreateTextureFromSurface : %s",
SDL_GetError());
20 goto Quit;
21 }
22 SDL_SetRenderTarget(renderer, texture); /* La cible de rendu est
maintenant texture. */
23 SDL_RenderCopy(renderer, tmp, NULL, NULL); /* On copie tmp sur
texture */
24 SDL_DestroyTexture(tmp);
25 SDL_FreeSurface(surface);
26 SDL_SetRenderTarget(renderer, NULL); /* La cible de rendu est de
nouveau le renderer. */
27 /* On peut maintenant dessiner sur notre texture */
1 ###include <SDL2/SDL.h>
2 #include <stdio.h>
3 ###include <stdlib.h>
4
5 int main(int argc, char *argv)
6 {
7 SDL_Window *window = NULL;
8 SDL_Renderer *renderer = NULL;
9 SDL_Texture *image = NULL;
10 int statut = EXIT_FAILURE;
11 SDL_Color blanc = {255, 255, 255, 255};
12 if(0 != init(&window, &renderer, 640, 480)) /* ecrire cette
fonction */
13 goto Quit;
14
59
5. Les textures et les images
É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.
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
60
5. Les textures et les images
On aurait pu faire la fonction loadImage pour que la texture renvoyée ait un accès SDL_TEXTU
REACCESS_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.
61
6. Modification pixels par pixels
Après avoir vu le fonctionnement des textures et des surfaces, nous allons voir comment les
manipuler pixels par pixels.
i
Cette partie sur le format des pixels est assez technique. Elle traite notamment de repré-
sentation des nombres en machine. Il n’est pas nécessaire de la comprendre parfaitement
pour poursuivre le tutoriel.
Le problème est que pour modifier un pixel et lui donner une autre couleur, il faut savoir
comment il est stocké et comment ses couleurs sont représentées. Heureusement, c’est nous qui
choisissons cette représentation lorsque nous créons une texture en spécifiant le format de pixel.
La plupart du temps, nous choisissons le format SDL_PIXELFORMAT_RGBA8888. Cela veut dire
qu’un pixel est représenté avec 8 bits pour chacune de ses composantes. Dans l’ordre :
— 8 bits pour le rouge;
— 8 bits pour le vert;
— 8 bits pour le bleu;
— 8 bits pour la composante alpha.
Ainsi, 0xFF0000FF représente du rouge, 0x00FF00FF représente du vert et 0x0000FFFF repré-
sente du bleu.
Les pixels sont donc codés sur 32 bits dans ce format. Nous allons coder ces pixels avec le type
Uint32 (il s’agit d’un type de 32 bits permettant de représenter des entiers) et chacune de ses
composantes avec le type Uint8. Dans ce format, l’octet de poids fort représente le rouge et
celui de poids faible représente le canal alpha.
Nous pouvons dès lors créer une fonction qui transformera une couleur donnée par ses quatre
composantes en pixel de cette couleur dans le format SDL_PIXELFORMAT_RGBA8888.
62
6. Modification pixels par pixels
Bien sûr, si le format de pixel de la texture n’est pas le même, cette fonction devra être modifiée.
Nous n’allons pas utiliser cette solution, mais il est intéressant de la voir pour comprendre
comment ça se passe.
Dans le chapitre précédent, nous avions vu la fonction SDL_MapRGB. Il y a également une
fonction SDL_MapRGBA à qui l’on fournit les composantes RGBA d’une couleur et qui nous
renvoie la valeur du pixel associé. Voici son prototype (qui ne devrait pas vous surprendre).
C’est le même que celui de la fonction SDL_MapRGB avec le paramètre a en plus. C’est cette
fonction que nous allons utiliser pour obtenir un pixel d’une couleur donnée.
?
Oui, cette fonction fait ce que l’on veut, mais comment savoir quel premier paramètre
lui donner? Les surfaces ont un champ format qui correspond à ce paramètre, mais que
pouvons-nous faire pour les textures?
Cette question est en effet importante. Nous connaissons le format de notre texture sous la
forme d’un Uint32 (par exemple, la constante SDL_PIXELFORMAT_RGBA8888). Pour passer de
cet entier à une variable du type SDL_PixelFormat, nous allons utiliser la fonction SDL_Al
locFormat .
Elle prend en paramètre le format d’un pixel sous la forme d’un entier et retourne un pointeur
sur SDL_PixelFormat (donc un pointeur sur le format du pixel). En cas d’erreur, elle retourne
NULL.
Après avoir fini d’utiliser notre format, il nous faut le libérer avec la fonction SDL_FreeFormat
de prototype suivant (elle prend en paramètre le pointeur sur SDL_PixelFormat et ne retourne
rien).
63
6. Modification pixels par pixels
1 SDL_Texture *texture;
2 SDL_PixelFormat *format;
3
4 /* Initialisations. */
5
6texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888,
SDL_TEXTUREACCESS_STREAMING, 3, 3);
7 format = SDL_AllocFormat(SDL_PIXELFORMAT_RGBA8888);
8
9 /* On dessine pixels par pixels */
10
11 SDL_FreeFormat(pixels);
12
13 /* Autres libérations */
Il pourra parfois être utile d’obtenir les composantes d’un pixel donné (donc passer d’un Uint32
à un SDL_Color. Pour faire cette opération, nous utilisons la fonction SDL_GetRGBA .
1 SDL_Color c;
2 SDL_GetRGBA(pixel, format, &c.r, &c.g, &c.b, &c.a);
3 printf("(%d, %d, %d, %d)", c.r, c.g, c.b, c.a);
64
6. Modification pixels par pixels
i
Notons l’existence de la fonction SDL_GetRGB qui permet de ne récupérer que les
composantes rouge, bleue et verte. Son prototype ne diffère de celui de SDL_GetRGBA que
par la disparition du pointeur pour la composante alpha.
Pour obtenir le tableau de pixels d’une texture, nous allons utiliser la fonction SDL_LockTex
ture . Elle permet de «bloquer» une partie de la texture en lecture seule. Son prototype :
!
La texture que l’on veut bloquer doit avoir un accès SDL_TEXTUREACCESS_STREAMING.
Voyons comment les pixels sont arrangés dans le tableau en prenant l’exemple d’une image 3x3
(donc w = 3 et h = 3).
Notons que le pitch est alors de 12 (3 pixels par ligne avec chaque pixel faisant 4 octets).
Créons une texture dégradée grâce à cela.
65
6. Modification pixels par pixels
Ici, on bloque notre texture et ensuite on modifie chaque case du tableau de pixels. La case i *
WIDTH + j correspond au pixel d’abscisse j et d’ordonnée i.
Après avoir réalisé nos modifications, il nous faut débloquer la texture. Cette opération se fait
avec la fonction SDL_UnlockTexture de prototype suivant.
Elle prend comme paramètre la texture à débloquer et ne retourne rien. Il faut débloquer la
texture avant de l’afficher ou de faire d’autres opérations. Pour afficher notre dégradé, nous
allons donc rajouter ceci à notre code précédent.
1 SDL_UnlockTexture(texture);
2 SDL_RenderCopy(renderer, texture, NULL, NULL);
3 SDL_RenderPresent(renderer);
Notons que nous aurions pu créer une texture de largeur 1. Le redimensionnement de la texture
à l’affichage nous aurait quand même permis d’obtenir le dégradé pour toute la fenêtre. Nous
aurions alors fait moins de calcul et économisé de la mémoire.
66
6. Modification pixels par pixels
Les fonctions SDL_LockTexture et SDL_UnlockTexture sont parfaites dans le cas où l’on veut
modifier quelques pixels de la texture ou si on a besoin de connaître les pixels pour les modifier
(par exemple pour un flou gaussien), mais si on veut complètement changer tout ou partie d’une
texture et modifier chacun des pixels, on peut faire appel à la fonction SDL_UpdateTexture
de prototype
Elle prend en paramètre la texture, un pointeur sur SDL_Rect qui correspond à la partie de la
texture que l’on veut mettre à jour (comme d’habitude on passe NULL pour tout mettre à jour),
un tableau de pixels (les pixels doivent bien sûr être dans le bon format) et le «pitch» de notre
tableau de pixels. Ce paramètre est assez simple à calculer: on veut mettre à jour une texture
de longueur w et chaque pixel est codé sur s pixels, on a alors pitch = w * s.
i
Si la fonction SDL_UnlockTexture ne peut fonctionner qu’avec une structure d’accès
SDL_TEXTUREACCESS_STREAMING, SDL_UpdateTexture, elle, fonctionne avec toutes les
textures.
Faisons une fonction qui renvoie une texture avec le même dégradé que dans notre exemple
précédent.
67
6. Modification pixels par pixels
16 }
Ici, on s’est débarrassé de la seconde boucle car on a créé une surface de largeur 1. On a d’ailleurs
pitch = sizeof(Uint32) * 1.
Les pixels d’une surfaces sont stockés de la même manière que ceux d’une texture et pour les
modifier, il nous faut donc connaître le format dans lequel ils sont stockés. Nous choisissons
ce format lorsque nous créons une surface en spécifiant une valeur pour les quatre derniers
paramètres et en spécifiant le nombre de bits par pixel. Généralement, le nombre de bits par
pixel est de 32.
Jusqu’à maintenant, nous ne nous sommes pas occupés des valeurs des masques (nous passions
0 en paramètre), et en fait, nous allons continuer à ne pas nous en occuper. Pour cela, nous
allons utiliser la fonction SDL_CreateRGBSurfaceWithFormat .
En fait, une fois qu’on a le format sous la forme d’un SDL_PixelFormat (obtenu par exemple
à l’aide de la fonction SDL_AllocFormat), on peut obtenir les quatre derniers paramètres à
passer à SDL_CreateRGBSurface pour avoir une surface du même format (ils sont dans les
champs Rmask, Gmask et Bmask et Amask du format). La ligne de code précédent est alors
équivalente à celle-ci.
68
6. Modification pixels par pixels
2 format->Gmask, format->Bmask,
format->Amask);
Nous allons préférer la première version, notamment parce qu’elle ne demande pas d’avoir déjà
un pointeur sur le format voulu.
Contrairement aux textures, on peut accéder directement au tableau de pixels d’une surface. Il
s’agit du champ pixels de SDL_Surface. Néanmoins, avant de lire et de modifier ce tableau,
il nous faut bloquer la surface avec la fonction SDL_LockSurface de prototype :
Elle prend en paramètre la surface à bloquer et retourne 0 en cas de succès et une valeur négative
en cas d’erreur.
Après avoir fini nos modifications il faut, tout comme pour les textures, débloquer la surface. Nous
le devinons facilement, cela se fait avec la fonction SDL_UnlockSurface de prototype :
On peut alors faire un dégradé sur une surface à l’aide du code suivant.
1 Uint32 *pixels;
2 size_t i, j;
3 surface = SDL_CreateRGBSurfaceWithFormat(0, 255, 255, 32,
SDL_PIXELFORMAT_RGBA8888);
4 SDL_LockSurface(surface);
5 pixels = surface->pixels;
6 for(i = 0; i < 255; i++)
7 {
8 for(j = 0; j < 255; j++)
9 pixels[i * 255 + j] = SDL_MapRGBA(surface->format,
0, 0, (Uint8)i, 255);
10 }
Faisons maintenant une fonction setPixel qui prend en paramètre une surface débloquée, les
composantes d’une couleur et les coordonnées d’un pixel et donne à ce pixel la couleur associée.
On va considérer que les coordonnées passées en paramètre existent.
69
6. Modification pixels par pixels
En fait, une fois que l’on a un tableau de pixels, on peut s’en servir pour créer une nouvelle
surface. On peut par exemple créer une surface de la bonne taille, puis copier le tableau de
pixels dans le champ pixel de cette surface. Mais, il y a mieux. La SDL offre en effet une
fonction qui se charge de créer la surface et de faire cette copie à notre place. Il s’agit de la
fonction SDL_CreateRGBSurfaceWithFormatFrom . Son prototype est le suivant.
70
6. Modification pixels par pixels
7 {
8 for(j = 0; j < 255; j++)
9 pixels[i * 255 + j] = SDL_MapRGBA(format, (i * j) %
255, 0, i % 255, 255);
10 }
11 surface = SDL_CreateRGBSurfaceWithFormatFrom(pixels, 255, 255, 32,
255 * sizeof(Uint32),
12
SDL_PIXELFORMAT_RGBA8888);
13 SDL_FreeFormat(format);
!
Puisque le tableau à allouer peut être de taille vraiment conséquente (par exemple pour
une très grande image), la plupart du temps, nous allons allouer le tableau de pixels
dynamiquement. Et bien sûr, nous allons éviter d’avoir des nombres en durs dans notre
code, et plutôt utiliser des constantes.
Ici, on crée d’abord un tableau de Uint32. Ensuite, ce tableau est initialisé dans une boucle
for avant d’être utilisé dans la fonction SDL_CreateRGBSurfaceWithFormatFrom. On a un
pitch de 255 * sizeof(Uint32) car la surface que l’on crée à une largeur de 255 pixels et que
chaque pixel est codé sur un Uint32 (puisque son format est SDL_PIXELFORMAT_RGBA8888).
i
Il y a également une fonction SDL_CreateRGBSurfaceFrom qui est à SDL_CreateRGB
Surface ce que SDL_CreateRGBSurfaceWithFormatFrom est à SDL_CreateRGBSur
faceWithFormat. Nous ne l’utilisons pas ici, car nous préférons pouvoir donner le format
à la fonction.
Nous avons vu dans la partie précédent comment passer de la surface à la texture avec la
fonction SDL_CreateTextureFromSurface. Mais comment faire l’opération inverse? Il n’existe
malheureusement pas de fonction pour créer une surface à partir d’une texture. Si nous voulons
le faire, il nous faudra donc gérer tout ça à la main. Pour cela, nous allons:
— bloquer la texture;
— créer un tableau de pixels de même dimension;
— copier les pixels de la texture dans ce tableau;
— créer une surface avec ces pixels.
Il faudra notamment faire attention au format des pixels. En effet, si le format de la texture
n’est pas celui que nous voulons pour notre surface, nous ne devrons pas juste copier les pixels
de la texture dans le tableau, mais nous devrons aussi changer leur format. Pour cela, nous
71
6. Modification pixels par pixels
pourrions utiliser SDL_GetRGBA pour récupérer les composantes d’un pixel de la texture, puis
utiliser SDL_MapRGBA pour obtenir un pixel de cette couleur dans le format voulu.
Mais si nous créons une surface de même format que la texture, cela signifie que le tableau de
pixels de la texture peut être utilisé directement pour créer la surface, le tableau de pixels d’une
surface et celui d’une texture sont équivalents. On a alors un code plus général en récupérant
le format de la texture (grâce à SDL_QueryTexture), afin de pouvoir utiliser la fonction avec
n’importe quelle texture. On écrit alors ce code.
72
6. Modification pixels par pixels
Nous venons de voir comment passer d’une texture à une surface. Mais comment faire pour
passer de notre cible de rendu à une surface? En gros, comment accéder à ses pixels? Cette
opération est possible grâce à la fonction SDL_RenderReadPixels .
Elle prend en paramètre le renderer dont on veut les pixels, un SDL_Rect qui correspond à la
partie du renderer dont on veut récupérer les pixels (NULL si on veut la totalité), le format dans
lequel on veut que ces pixels nous soient remis (pour ne pas changer, nous utiliseront SDL_PIXEL
FORMAT_RGBA8888), un pointeur qui pointera sur le tableau de pixels après l’exécution de la
fonction et le pitch de notre tableau (on le détermine comme d’habitude avec la taille d’un
pixel et la largeur du tableau).
La fonction renvoie 0 en cas de succès et une valeur négative en cas d’erreur.
Ainsi, si notre fenêtre a une largeur WIDTH et une hauteur HEIGHT, nous pouvons récupérer ses
pixels avec le code suivant (on suppose que le renderer est rattaché à la fenêtre).
!
La fonction SDL_RenderPixels écrit dans la zone mémoire qu’on lui donne avec l’argu-
ment pixels. Il faut donc que le pointeur qu’on lui passe en paramètre pointe sur une
zone mémoire valide et l’espace réservé doit être de taille suffisante (ici, on l’alloue avec
malloc).
Nous pouvons alors faire une capture d’écran grâce à ça en créant une surface avec ces pixels et
en la sauvegardant avec la fonction SDL_SaveBMP.
Notons de plus que SDL_RenderReadPixels permet de lire les pixels de la cible de rendu.
Cela signifie que si la cible de rendu est la cible par défaut (la fenêtre), nous obtiendrons
les pixels de la fenêtre. Mais si la cible de rendu est une texture, ce sont ses pixels que nous
obtiendrons. Nous avons alors un moyen d’obtenir les pixels d’une texture dont l’accès est
SDL_TEXTUREACCES_TARGET.
73
6. Modification pixels par pixels
Bon, après avoir vu tout cela, il nous faut nous rendre compte de quelque chose: la plupart de
ces opérations sont coûteuses et lentes; enfin, relativement lente. Il ne faut donc pas en abuser.
La documentation nous prévient d’ailleurs de cela. Par exemple, sur la page de SDL_Render
ReadPixel, nous pouvons lire cet avertissement.
WARNING: This is a very slow operation, and should not be used frequently.
Documentation de la SDL
Elle ne doivent pas être utilisées fréquemment.
Encore une fois, c’est en lisant la documentation que nous pouvons accéder à ces informations.
Par exemple, regardons la partie remarque de la documentation de SDL_UpdateTexture.
This is a fairly slow function, intended for use with static textures that do not change often.
If the texture is intended to be updated often, it is preferred to create the texture as
streaming and use the locking functions referenced below. While this function will work with
streaming textures, for optimization reasons you may not get the pixels back if you lock the
texture afterward.
Documentation de la SDL
On apprend ici que SDL_UpdateTexture est une fonction lente, prévue pour être utilisée sur des
textures statiques (donc d’accès SDL_TEXTUREACCESS_STATIC). La documentation conseille
de préférer l’utilisation de SDL_LockTexture si le but est de modifier une texture d’accès
SDL_TEXTUREACCESS_STREAMING.
En plus de tout cela, il faut savoir que les textures ne sont pas faites pour une modification pixels
par pixels. Elles ont l’avantage d’êtres affichables rapidement, de pouvoir être redimensionnée
à la volée et copiée rapidement, mais elles ne sont pas prévues pour de la modification pixels
par pixels. Les surfaces sont plus adaptées à cela. Ainsi, si nous voulons modifier une image
sans l’afficher (nous le feront dans le chapitre suivant), nous préférerons utiliser une surface. Et
surtout, nous éviterons au maximum d’avoir à modifier les pixels d’une texture.
!
Pour changer facilement les pixels et travailler avec nos surfaces et nos textures, il nous
faudra veiller à ce qu’elles aient toutes le même format.
Pensons notamment aux surfaces obtenues avec SDL_LoadBMP. Nous ne savons pas quelle est
leur format de pixel. Mais pas d’inquiétude, la SDL nous fournit la fonction SDL_ConvertSur
faceFormat qui convertit une surface dans un format choisi.
74
6. Modification pixels par pixels
Elle prend en paramètre la surface à convertir, le format dans lequel doivent être convertis les
pixels et des drapeaux (nous devons passer 0 pour cet argument).
Grâce à cette fonction, on peut faire en sorte que nos surfaces chargées avec SDL_LoadBMP aient
bien le format voulu. On peut même faire une fonction qui nous renvoie la surface dans le bon
format.
Bien sûr, cela n’est utile que dans le cas où nous allons effectivement lire ou modifier les pixels
de nos surfaces. Si nous n’accédons jamais aux pixels, la manière dont ils sont stockés nous
importe peu.
Ce chapitre est maintenant fini. Dans le chapitre suivant, nous allons nous attaquer à la
transparence, que nous n’avons jamais utilisée jusqu’à maintenant.
75
7. La transparence
À ce niveau du tutoriel, nous savons jouer avec la fenêtre, afficher des images et des textures
diverses, etc. Il nous manque cependant quelque chose avant de pouvoir dire qu’on a à peu près
fait le tour de tout ce qui concerne l’affichage. Il nous faut savoir jouer avec la transparence.
Nous avons vu que nous pouvions décomposer une couleur en quatre composantes, sa composante
rouge, sa composante verte, sa composante bleue et sa composante alpha. La composante alpha
correspond au niveau de transparence de l’image. Pour le moment, nous ne l’avons jamais vrai-
ment essayé. Remplissons le renderer avec une couleur transparente.resultatsourcedestinationalpha
×
Notre fenêtre est rouge et ce rouge n’est pas du tout transparent. Même en mettant la
composante alpha à zéro, notre fenêtre est toujours rouge.
Pourtant, la composante alpha correspond bien au niveau de transparence. Pourquoi notre code
ne fonctionne-t-il pas? En fait, cela est dû au fait que la transparence n’est tout simplement pas
activée. Nous pourrions penser qu’il n’y a pas besoin d’activation et qu’il suffit de donner la
76
7. La transparence
composante alpha voulue, mais ce n’est pas aussi simple. Le fait est qu’il y a plusieurs types de
transparence et qu’il faut indiquer à la SDL lequel nous voulons utiliser.
Voyons donc quels sont les différents types de transparence. Mais avant ça, il est nécessaire de
nous poser une question.
?
Comment fait-on pour rendre un pixel transparent?
Car, même si nous disons qu’il y a plusieurs types de transparence, il faut encore savoir ce que
signifie rendre un pixel transparent.
En fait, la transparence d’un pixel n’est pas réelle et n’existe qu’à l’affichage. Un pixel n’est
jamais transparent. L’ordinateur fait juste en sorte de nous le faire apparaître transparent. Pour
cela, il fusionne la couleur du pixel à afficher avec la couleur du pixel qui est en dessous. La
composante alpha d’un pixel indique juste à quel point il faut prendre en compte la couleur
du pixel pour avoir la couleur à afficher. Ainsi, une composante alpha nulle signifie qu’il ne
faut pas prendre en compte la couleur du pixel et donc la couleur affichée sera celle du pixel du
dessous.
i
Dans la suite, nous nommerons «pixel source» ou «source» le pixel que l’on veut afficher,
«pixel destination» ou «destination» le pixel qui est déjà à l’écran et «pixel résultant» ou
«résultat», le pixel résultant de la fusion de la source et de la destination.
Cependant, on peut fusionner les pixels source et destination de plusieurs manières. Ces dif-
férentes méthodes s’appellent des modes de fusion ou blend mode en anglais. On pourrait
imaginer plusieurs modes de fusion plus ou moins fantaisistes. Le mode le plus simple étant
celui ou
=.
Ce mode correspond en fait à une absence totale de transparence (c’est ce que nous avons
obtenu précédemment avec notre exemple de fenêtre rouge). Mais ce n’est pas un mode de
fusion, puisqu’il n’y a justement pas de fusion.
En gros, les modes de fusion consistent à appliquer une fonction f telle que = f (, ).
Maintenant que nous voyons un peu mieux en quoi consiste la transparence, nous pouvons
nous renseigner sur les différents modes de fusion de la SDL. Ces différents modes peuvent être
trouvés dans l’énumération SDL_BlendMode . En regardant les différentes valeurs de cette
énumération, on voit que la SDL dispose de quatre modes de fusion (trois en fait puisque l’une
des valeurs possibles pour l’énumération correspond à l’absence de transparence).
77
7. La transparence
Le premier mode possible est celui qui correspond le plus à l’idée que l’on se fait d’une fusion
puisqu’il s’agit en fait d’une moyenne pondérée de la source et de la destination, le poids étant
déterminé grâce à la valeur de la composante alpha. Ce mode est appelé alpha blending. Il est
associé à la valeur SDL_BENDMODE_BLEND et au calcul
× (255−)×
= + .
255 255
Si la composante alpha est nulle, il n’y a pas de transparence, si elle vaut 255, seule la couleur
de la destination est prise en compte, si elle vaut 128, les deux couleurs sont parfaitement
mélangées.
Le second mode est appelé (à raison) additive blending puisqu’il consiste à ajouter la source
à la destination. Il permet ainsi de rendre les couleurs plus vives. Il est associé à la valeur
SDL_BLENDMODE_ADD et au calcul
×
= +.
255
Nous remarquons que plus la valeur de la composante alpha est élevée, plus la valeur ajoutée
sera grande c’est-à-dire qu’encore une fois, plus la valeur de la composante alpha est grande,
plus la couleur du pixel source sera prise en compte. Notons également que si la valeur du pixel
résultant est supérieure à 255, la SDL la ramène à 255. En fait, on a plutôt
×
= min 255, + .
255
Le dernier mode, appelé color modulate, n’utilise même pas la valeur de la composante alpha du
pixel source. Il est associé à la valeur SDL_BLENDMODE_MOD et au calcul
×
= .
255
Il multiplie la source par le résultat, ce qui explique qu’on l’appelle parfois «mode de multiplica-
tion».
Chaque transformation est appliquée à chacune des composantes rouge, bleue et verte du pixel.
La composante alpha, elle, subit ces transformations.
— Dans le cas du mode alpha, on a () = () + () × 255−()
255
.
— Dans les cas du mode d’addition et du mode de modulation, on a () = ().
On utilise généralement le mode alpha qui correspond à l’idée qu’on se fait de la transparence.
78
7. La transparence
Pour appliquer un mode à un renderer, il nous faut connaître une seule fonction. Il s’agit de la
fonction SDL_SetRenderDrawBlendMode dont le prototype est le suivant.
Elle permet d’indiquer à la SDL le mode de fusion que l’on veut donner à notre renderer. Pour
cela, on lui envoie le renderer et le mode de fusion voulu. Elle retourne 0 en cas de succès et
une valeur négative en cas d’erreur.
Reprenons le code du début de ce chapitre et modifions-le pour rendre notre fond transparent.
×
Ça ne fonctionne toujours pas!
C’est normal. Nous avons changé le mode de fusion de notre renderer après avoir dessiné sur
l’écran. Il nous faut donc utiliser SDL_SetRenderDrawBlendMode avant d’utiliser SDL_Ren
derFillRect.
79
7. La transparence
Cette fois, c’est bon. Le fond de notre fenêtre est devenu plus sombre (parce que le fond de
notre fenêtre est noir). On a donc bien fusionné la couleur de la source et la couleur de la
destination.
Notons que cette fonction active la transparence alpha pour les fonctions de dessin (lignes,
rectangles, ect.). La fonction SDL_RenderClear n’est pas une fonction de dessin, mais de
nettoyage. Le fond ne sera donc pas transparent s’il est fait avec SDL_RenderClear.
Maintenant, voyons comment avoir une texture transparente. En effet, mettre la texture en tant
que surface de rendu (avec SDL_SetRenderTarget) puis utiliser SDL_SetRenderDrawBlend
Mode ne fonctionne pas. Il nous faut utiliser la fonction SDL_SetTextureBlendMode pour
appliquer un mode à notre texture. Voici son prototype.
Elle prend en paramètre la texture pour laquelle on veut activer la transparence et le mode
voulu. Elle retourne 0 en cas de succès et une valeur négative en cas d’erreur. Notons de plus
que si le mode demandé n’est pas supporté par la texture, la fonction renverra -1 et activera le
mode de fusion le plus proche de celui demandé (ce cas arrive rarement).
On peut alors afficher une texture transparente.
80
7. La transparence
Nous savons maintenant appliquer de la transparence au renderer et aux textures. Mais que se
passe-t-il lorsque l’on affiche une texture transparente sur un renderer transparent? Comment
les textures interagissent-elles avec le renderer?
!
Le mode de transparence de la zone de rendu doit être SDL_BLENDMODE_NONE lorsqu’on
travaille sur des textures, sans quoi nous aurons des problèmes d’affichage. Après avoir
utilisé SDL_SetRenderTarget pour choisir la texture en tant que zone de rendu, nous
changeons donc, si besoin est, le mode de transparence de la zone de rendu en SDL_BEND
MODE_NONE.
C’est le cas le plus simple et le plus classique. La texture est opaque et le renderer est opaque,
donc on ne verra pas le renderer à travers la texture. On dessine un rectangle bleu sur un fond
rouge, le rectangle bleu cache une partie du fond rouge.
81
7. La transparence
Le résultat de cette expérience n’est pas trop surprenant. Le renderer est transparent, la texture
est opaque, donc on voit la texture (le fait que le fond soit transparent ne change rien à cela).
On dessine un rectangle bleu sur un fond transparent, donc on voit un rectangle bleu.
82
7. La transparence
C’est encore une fois un cas simple et plutôt classique. La texture est transparente et le renderer
est opaque, donc on verra le renderer à travers la texture (plus ou moins suivant la valeur de la
composante alpha de la texture). On dessine un rectangle bleu, mais transparent, sur un fond
rouge, donc on voit le fond rouge à travers le rectangle.
Ce cas est le seul un peu difficile à appréhender. Le calcul se fait normalement suivant le
mode de transparence choisi. Si la transparence du renderer et celle de la texture sont totales
(la composante alpha est nulle), aucune couleur n’apparaît puisque toutes les couleurs sont
transparentes, ce qui peut être un peu déconcertant. On dessine un rectangle transparent sur
un fond transparent, donc on obtient quelque chose de transparent.
83
7. La transparence
13 SDL_SetRenderTarget(renderer, NULL);
14
15 SDL_RenderCopy(renderer, texture, NULL, &dst);
16 SDL_RenderPresent(renderer);
Cependant, nous n’aurons que rarement à nous soucier de cas comme ceux-ci. En général, on
veut un fond opaque sur lequel on dessine des choses éventuellement transparentes.
Il nous est également possible d’appliquer de la transparence alpha sur une surface. Pour cela,
nous devons utiliser l’équivalent de la fonction SDL_SetTextureBlendMode pour les surfaces.
Elle s’appelle, de façon non originale, SDL_SetSurfaceBlendMode .
Elle prend en paramètre une surface et un pointeur sur l’énumération SDL_BlendMode. À la fin
de la fonction, la valeur pointée par blendmode vaudra le mode de fusion de la surface passée
en paramètre. La fonction renvoie 0 si elle réussit et une valeur négative sinon.
Nous aurons rarement (voire jamais) à l’utiliser, mais elle a le mérite d’exister.
Nous ne l’avons pas vu, mais elle existe également pour les textures sous le nom de SDL_Get
TextureBlendMode .
84
7. La transparence
Il est également possible de ne rendre qu’une couleur transparente. Imaginons par exemple que
l’on veuille afficher une image sur notre fenêtre. Notre image a malheureusement une couleur de
fond, et ce fond ne permet pas une bonne intégration de notre image sur la fenêtre. Par exemple,
on veut afficher l’image qui suit sur une fenêtre blanche et on veut faire disparaître son fond
noir.
85
7. La transparence
Elle prend en paramètre une surface, un drapeau (SDL_TRUE pour activer la transparence,
SDL_FALSE pour la désactiver) et un Uint32 qui correspond à la couleur à rendre transparente.
Tous les pixels de cette couleur seront alors transparents.
!
Cela signifie que si, dans notre exemple, d’autres pixels que le fond avaient la couleur
noire, ils seraient également transparent. Il faut faire attention à cela.
Pour rendre une couleur transparente, voici finalement le code que nous pouvons utiliser.
Et c’est un nouveau chapitre qui se termine ici. Nous pouvons (enfin) manipuler la transparence.
Il nous faudra néanmoins faire attention à ceci.
!
La transparence est gérée par notre programme, mais pas par les images au format BMP.
Ainsi, si nous enregistrons une surface (avec SDL_SaveBMP), la transparence ne sera pas
conservée.
86
8. TP - Effets sur des images
Nous allons maintenant faire un petit TP qui consiste à modifier des images pour augmenter
leur luminosité ou encore les flouter.
8.1.1. Principe
Bon, avant de commencer à modifier des images, il faut que nous sachions bien de quoi nous
allons parler dans ce chapitre et que nous voyions quelles méthodes nous allons utiliser. Notre
but ici est d’effectuer une transformation sur une image telle qu’augmenter sa luminosité. Pour
cela, nous allons appliquer une fonction à chacun des pixels de notre image. Par exemple, pour
obtenir la même image exactement, on applique à chaque pixel la fonction identité, c’est-à-dire
la fonction qui à un élément associe cet élément.
Il nous faudra alors trouver des fonctions pour faire ce que l’on veut. Grâce à elle, nous pourrons
créer une fonction qui renvoie une surface à laquelle on aura appliqué une transformation avec
cette fonction.
Nous aurons alors un code comme ceci.
87
8. TP - Effets sur des images
Dans l’ordre on bloque la surface pour accéder à ses pixels, on crée un tableau de pixels de la bonne
taille, on le modifie, puis on renvoie la surface créée à partir de ce tableau. Le champ format
d’une variable de type SDL_Format correspond à une valeur de l’énumération SDL_Pixel
FormatEnum. Ainsi, si notre surface de départ a le format SDL_PIXELFORMAT_RGBA8888, on
retrouvera cette valeur dans s->format->format. On l’utilise alors pour créer notre nouvelle
surface. Bien sûr, on pourrait également décider de créer la nouvelle surface dans un autre
format.
Ici, nous renvoyons une nouvelle surface, mais nous aurions également pu modifier directement
la surface passée en paramètre.
Dans les codes qui vont suivre, nous n’allons pas réécrire toute la fonction, mais seulement le
contenu de la boucle for.
!
Pour utiliser ces fonctions sur une surface créée à partir d’une image, il nous faudra au
préalable utiliser la fonction SDL_ConvertSurface pour que le tableau de pixels obtenu
soit bien au format SDL_PIXELFORMAT_RGBA8888 (une surface chargée depuis une image
au format Bitmap a généralement le format SDL_PIXELFORMAT_RGB888). Sans cela, nous
risquons des erreurs de segmentation.
En fait, il nous faudrait juste nous assurer que la surface est dans un format où les pixels sont
codés sur quatre octets (quand ils sont codés sur moins d’octet, nous faisons des lectures en
dehors du tableau de pixels). Convertir la surface dans un format qui convient est une manière
simple de s’assurer de cela.
88
8. TP - Effets sur des images
Nous allons commencer par faire une fonction pour transformer une image couleur en niveau de
gris. La question qui vient naturellement est: comment obtenir le niveau de gris d’une couleur?
Déjà, il nous faut comprendre qu’une couleur est grise si ses trois composantes ont la même
valeur. Ainsi, un pixel est gris s’il s’agit du pixel (i, i, i). Plus i est grand, plus on va vers le
blanc. (255, 255, 255) correspond à du blanc et (0, 0, 0) correspond à du noir.
Pour avoir le niveau de gris d’une couleur, nous allons faire la moyenne de ses trois composantes.
Nous pouvons alors faire une fonction qui prend en paramètre une surface et renvoie une nouvelle
surface qui correspond à la surface passée en paramètre en niveau de gris.
Notons qu’ici, nos pixels gris sont codés sur 32 bits. Pourtant, on aurait pu ne les stocker que
sur 8 pixels, puisque les composantes R, G et B sont les mêmes. Si nous enregistrons l’image
obtenue avec cette fonction, nous obtiendrons une image dont le poids en mémoire sera le même
que l’image originale, alors que si nous utilisons Paint par exemple pour convertir notre image
en niveau de gris, son poids aura diminué.
i
Nous utilisons SDL_MapRGB et pas SDL_MapRGBA car la composante alpha des pixels ne
nous intéresse pas. Une des remarques de la documentation est que quand elle est
utilisée avec un format de pixel qui a une composante alpha, elle renvoie un pixel dont la
composante alpha est 255 (le pixel est totalement opaque). Si nous ne voulons pas perdre la
composante alpha de notre image, il nous suffit d’utiliser SDL_MapRGBA et SDL_GetRGBA.
8.1.3. Négatifs
Nous allons maintenant faire en sorte d’obtenir le négatif d’une image. Mais avant d’aller plus
loin, qu’est-ce que le négatif d’une image ? Regardons Wikipédia rapidement.
Une image négative est une image dont les couleurs ont été inversées par rapport à l’originale ;
par exemple le rouge devient cyan, le vert devient magenta, le bleu devient jaune et inverse-
ment. Les régions sombres deviennent claires, le noir devient blanc. Source : Wikipédia
89
8. TP - Effets sur des images
Le négatif d’une image s’obtient donc en «inversant» chacun des pixels. La fonction mathématique
qui lui est associée est doncla fonction de [0 ; 255] dans [0 ; 255] définie par f (x) = 255 − x et
dont la représentation graphique est la suivante.
La composante de chaque pixel est 255 - composante_initiale. Il suffit alors de le faire pour
chaque pixel.
8.2. De la luminosité
Nous allons maintenant nous attarder sur la luminosité et faire des fonctions pour éclaircir,
assombrir et changer le contraste d’une image.
90
8. TP - Effets sur des images
Comment pouvons-nous éclaircir une image? Tout simplement, éclaircir une image, c’est lui
ajouter de la luminosité. Le blanc est la couleur la plus lumineuse que nous avons, et le noir est
la plus sombre. Pour éclaircir une image, nous pouvons alors faire « tendre » tous ses pixels
vers le blanc.
Pour une image en niveau de gris, un pixel de niveau de gris 100 pourrait être modifié en 120 et
un pixel de niveau de gris 0 changé en 20. En gros, on rajoute 20 à chaque fois. Plus la valeur
ajoutée est grande, plus l’image devient lumineuse.
!
Il faut cependant faire attention à ne pas dépasser la valeur 255.
Il nous faut un entier n qui correspond à la valeur à ajouter et la fonction qu’il nous faut utiliser
est alors la fonction f définie pour x appartenant à [0 ; 255] par
si x + n > 255
(
255
f (x) = .
x+n sinon
Pour assombrir une image, le principe est le même. C’est juste qu’il faut enlever une valeur
plutôt qu’en rajouter une. En fait, cela revient à éclaircir avec une valeur négative. Nous n’allons
donc pas faire de nouvelle fonction pour cela mais modifier l’ancienne (il ne faut pas passer en
dessous de 0). On a alors
si x + n > 255
255
f (x) = 0 si x + n < 0 .
sinon
x+n
On a alors ces deux courbes pour n = 50 (en rouge) et n = −50 (en vert).
91
8. TP - Effets sur des images
La courbe en bleue est celle de l’identité, c’est-à-dire qu’il s’agit du pixel normal sans aucun
éclairement. On va d’abord faire une fonction f qui applique notre transformation à une
composante d’un pixel (donc à un Uint8).
92
8. TP - Effets sur des images
Notre fonction pour éclaircir fonctionne bien, mais a un grand défaut: toutes les composantes de
pixel qui sont supérieures à 255 - valeur, avec valeur la valeur ajoutée à cette composante,
sont mises à 255. Les nuances de couleurs entre ces pixels seront annulées. Ainsi, si on veut
éclaircir notre image d’une valeur de 50 et que toutes les composantes de tous les pixels de
notre image sont supérieures à 205, on se retrouvera avec une image totalement blanche ce qui
ne correspond pas à ce que l’on veut obtenir.
En fait, ce qu’il nous faut, c’est augmenter les valeurs des composantes différemment suivant si
ces dernières sont grandes ou faibles. Ce qu’il nous faudrait, c’est que les noirs restent noirs, que
les blancs restent blancs, et que les gris soient éclaircis. En gros, il nous faut quelque chose qui
suit ce genre de courbe.
93
8. TP - Effets sur des images
Ce genre de courbe est obtenu à l’aide des fonctions puissances qui ont en effet ce type de
comportement sur [0 ; 1]. Pour «étendre» ce comportement à [0 ; 255], nous allons utiliser la
fonction f définie pour x appartenant à [0 ; 255] par
n
x
f (x) = 255 × .
255
La courbe bleue est obtenue pour n = 0.5, la verte du dessus pour n = 1/3 et la verte du
dessous pour n = 2. Pour n = 1, on obtiendra bien sûr une droite (ce qui correspond à un
changement nul de luminosité). On remarque que pour n > 1, la courbe est «en dessous» de la
droite d’équation y = x (ce qui correspond à un assombrissement) et que pour n < 1, elle est
«au-dessus» (ce qui correspond à une hausse de la luminosité). Par ailleurs, plus n est grand
devant 1 (respectivement petit devant 1), plus la courbe descendra (respectivement montera) et
donc plus l’image sera assombrie (respectivement éclairée).
Notre nouvelle fonction d’éclairage prendra alors en paramètre ce coefficient n et appliquera à
chaque pixel p la fonction f . Commençons comme tout-à-l’heure par écrire notre fonction f.
Notre boucle reste la même, on appelle la fonction pour chaque composante de chaque pixel.
Non seulement, cela nous permet de garder les nuances de notre image, mais en plus, notre code
est raccourci. Il faut juste faire attention à caster c en double avant de faire la division pour
bien faire une division flottante et non entière.
Maintenant, faisons une opération plus compliquée: créons une fonction pour augmenter (ou
diminuer) le contraste d’une image.
?
Mais avant tout qu’est-ce que le contraste d’une image?
Nous pouvons voir le contraste d’une image comme une propriété qui indique si la différence de
luminosité entre les différents pixels de l’image est grande. Ainsi, le contraste de l’image est
maximum si les pixels sombres sont entièrement noir et ceux blancs entièrement blanc, et le
contraste est nul si l’image est uniformément grise.
Cela permet de comprendre que pour augmenter le contraste, il faut en fait rendre les pixels
lumineux encore plus lumineux et ceux sombres encore plus sombre. On peut par exemple faire
cela en augmentant le niveau de gris d’un pixel s’il est entre 128 et 255 et en le diminuant
sinon. Pour diminuer le contraste de l’image, il faudrait bien sûr faire le contraire, c’est-à-dire
94
8. TP - Effets sur des images
augmenter le niveau de gris des pixels s’ils sont entre 0 et 128 et les diminuer s’ils sont entre
128 et 255.
Nous voulons donc une courbe de ce genre.
Pour l’obtenir, il faut juste ruser un peu et utiliser les fonctions utilisées pour la luminosité. En
effet, on obtient des courbes de ce genre avec la fonction f définie sur [0 ; 255] par
n h i
255 × 2x
2 255
si x ∈ 0 ; 2
255
f (x) = n i i.
255 − 255 × 2(255−x)
2 255
si x ∈ 255
2
; 255
En fait,
h la icourbe obtenue est en deux
h bouts.i Tout d’abord la courbe d’éclaircissement ramenée
sur 0 ; 2 et son symétrique sur 255
255
2
; 255 .
La courbe bleue est obtenue pour n = 2, la verte pour n = 1/3 et la rouge pour n = 1/2. On
remarque alors que plus n sera grand devant 1 (respectivement petit devant 1), plus l’image sera
contrastée (respectivement moins l’image sera contrastée). Avec n = 0, le contraste de l’image
obtenue sera nul.
Nous allons simplifier notre fonction f en remarquant que x > 255 2
, f (x) = 255 − f (255 − x).
De plus, puisque nos valeurs sont toujours entières, nous allons prendre 127 pour la moitié de
255 et donc
95
8. TP - Effets sur des images
n
si x ∈ [0 ; 127]
(
2x
127 × 255
f (x) = .
255 − f (255 − x) si x ∈ [128 ; 255]
Il est maintenant temps de coder. Nous allons tout d’abord coder une fonction qui prend en
paramètre une composante de pixel et un entier et renvoie l’image de ce pixel par la fonction
f.
8.3. Floutage
Tout comme les autres effets que nous avons codés jusqu’ici, le floutage peut-être vu comme
l’application d’une fonction à une image. La question qui se pose est: quelle fonction utiliser?
Quelles opérations faut-il faire pour que notre image soit floue?
Il faut se dire que flouter l’image revient à changer certains de ses pixels pour que l’image soit
moins reconnaissable. Par exemple, si on remplace un pixel sur trois par un pixel blanc, l’image
obtenue sera différente mais restera proche de l’originale.
Le problème est que si par exemple on a une image toute rouge, placer des pixels blancs ne
la floutera pas. En fait, on veut un effet de floutage, mais on veut que les couleurs de l’image
obtenue restent proches de celles de départ. Il faut donc remplacer les pixels par des pixels
de couleurs différentes, mais proches. Dans cette partie, nous allons voir l’algorithme de flou
gaussien qui est assez intuitif.
96
8. TP - Effets sur des images
Puisque cet algorithme modifie les pixels en utilisant les valeurs d’autres pixels, cela veut dire
que nous ne pouvons pas modifier directement le tableau de pixels. Nous sommes obligés dans
ce cas de créer un nouveau tableau de pixels.
Dans l’exemple ci-dessus, le pixel rouge sera remplacé par la moyenne de ce pixel et des huit
pixels qui l’entourent.
Pour obtenir une image encore plus floue, nous pouvons également remplacer un pixel par la
moyenne de ce pixel, des deux pixels en hauts, des deux pixels en bas, des deux à gauches, des
deux sur chaque diagonale. Nous le remplaçons par la moyenne des pixels du carré de cinq pixels
dont il est le centre.
En fait, nous le remplaçons par la moyenne d’un carré dont il est le centre et plus ce carré est
gros, plus l’image semblera floue. Pour un entier n donné (dans les deux images données en
exemple, n prend respectivement les valeurs 1 et 2) on obtient alors cette formule générale (en
considérant notre image comme une matrice) :
j+n
i+n X
1 X
ai,j = ak,l .
(2n + 1)2 k=i−n l=j−n
En gros, on fait la moyenne des pixels situés à au plus n lignes et n colonnes du pixel de départ.
Il faut juste faire attention aux pixels qui se trouvent trop près du bord de l’image. On peut
alors faire le code qui nous donne le flou voulu. Nous allons d’abord faire une fonction qui
calcule la moyenne pour un pixel c’est-à-dire pour chacune de ses composantes.
97
8. TP - Effets sur des images
On calcule d’abord nos bornes pour ne pas dépasser les limites de notre tableau. Puis on fait
notre somme à l’aide de deux boucles et enfin on la divise par le nombre d’éléments. Notre flou
est alors très simple à faire, notre double boucle est la suivante.
8.3.3. Du détourage?
Nous n’allons pas nous attarder sur le pourquoi du comment, mais cette fonction de floutage
nous permet également de faire du détourage, c’est-à-dire d’obtenir par exemple le contour d’un
personnage dans une image. Pour cela, il nous faut faire cette opération, avec p le pixel que l’on
traite et m la moyenne des pixels qui l’entourent.
La valeur absolue permet d’être sûr d’avoir un résultat plus petit que 255. En gros, il s’agit de
ce code.
Ce TP est maintenant fini. Mais nous n’avons touché que la surface du sujet. Nous pouvons par
exemple faire une image «tirée» vers le rouge en augmentant seulement la composante rouge
de ses pixels (un éclairement rouge en fait) ou faire le NON logique d’une image. On pourrait
98
8. TP - Effets sur des images
même jouer avec deux images de même taille en effectuant leur OU logique, leur ET logique, ou
encore leur XOR logique. Il y a vraiment de quoi s’amuser.
99
9. Les évènements 1
Maintenant que nous avons joué un peu (beaucoup) avec l’affichage, il est temps de voir comment
faire en sorte que notre programme réagisse aux actions du joueur comme l’appui sur une touche
du clavier, le déplacement de la souris, etc.
Nous appelons évènement toute action extérieure à notre programme et qui peut avoir un effet
sur lui. L’appui sur une touche du clavier, le déplacement de la souris, le redimensionnement
d’une fenêtre, et même une demande de fermeture du programme sont des évènements.
Durant toute la durée de vie de notre application, elle reçoit de nombreux évènements. Ceux-
ci sont placés dans une file et attendent d’être traités par notre programme. Nous pouvons
ensuite interroger la SDL pour savoir s’il y a eu un évènement. À ce moment, elle nous donne
des informations sur l’évènement qui n’a pas été traité. Cela signifie notamment qu’il faudra
réinterroger la SDL à chaque fois pour savoir s’il y a eu un nouvel évènement.
Bien sûr, il peut y avoir plusieurs évènements en attente. Dans ce cas, c’est l’évènement le plus
vieux qui sera lu, conformément au système de file: le premier arrivé est également le premier à
être traité. On peut donc voir cela comme n’importe quelle file d’attente dans la vie courante
(avec les tricheurs en moins). On peut alors imaginer quelque chose de ce genre (ici, on suppose
qu’on a prévu de fermer la fenêtre quand il y a une demande de fermeture du programme):
— début de notre programme;
— appui sur la touche A , file = [appui A];
— déplacement de la souris, file = [appui A, déplacement souris];
— lecture de la file, on apprend qu’il y a eu appui sur A , file = [déplacement souris];
— clic droit, file = [déplacement souris, clic droit];
— fermeture fenêtre, file = [déplacement souris, clic droit, fermeture fenêtre];
— lecture de la file, file = [clic droit, fermeture fenêtre];
— déplacement souris, file = [clic droit, fermeture fenêtre, déplacement sou
ris];
— lecture de la file, file = [fermeture fenêtre, déplacement souris];
— clic gauche, file = [fermeture fenêtre, déplacement souris, clic gauche];
— lecture de la file, file = [déplacement souris, clic gauche];
— on a lu un évènement «fermeture fenêtre», on ferme le programme.
100
9. Les évènements 1
Ce petit exemple nous apprend beaucoup de choses à propos de la gestion des évènements que
nous aurons à adopter. Lire un évènement à la fois est une très mauvaise idée, car pendant
qu’on en lit un plusieurs peuvent être en train de se produire. Cela signifie qu’il faudra soit
s’arranger pour lire un évènement dès qu’il survient, soit les lire tous quand on décide de lire.
La structure SDL_Event est la structure primordiale pour la gestion des évènements. Lorsque
nous demandons à la SDL si un évènement a eu lieu, elle remplit cette structure avec les données
de l’évènement correspondant (s’il y en a un). Elle contient alors toutes les informations sur le
type d’évènements qui a eu lieu (par exemple, quelle touche a été pressée, à quelle position la
souris a été déplacée, etc.) et nous est donc essentielle.
La structure SDL_Event possède un champ type qui prend pour valeur le type d’évènement
lu, et ses autres champs (un par type d’évènement) sont des structures qui contiennent des
informations précises sur l’évènement qui a eu lieu. Les valeurs pouvant être prises par ce champ
sont celles de l’énumération SDL_EventType . Voici un tableau récapitulant les données les
plus importantes.
Il y a quelques autres types d’évènements, comme les évènements relatifs aux mobiles (Android
et iOS), ou encore ceux relatifs aux écrans tactiles ou aux manettes et autres contrôleurs.
101
9. Les évènements 1
Pour lire un évènement de la file, la SDL nous propose plusieurs fonctions. Chacune récupère
l’évènement le plus ancien, mais elles ont un comportement différent.
9.1.3.1. SDL_WaitEvent
La première fonction que nous verrons est la fonction SDL_WaitEvent . Son prototype est le
suivant.
Elle prend en paramètre un pointeur sur un SDL_Event qu’elle remplira avec les informations
sur l’évènement. Elle retourne 0 en cas d’erreur et 1 en cas de succès (ce qui est contraire à
beaucoup de fonctions de la SDL).
La fonction SDL_WaitEvent est une fonction bloquante. Quand on l’utilise, notre programme
reste bloqué au niveau de cette fonction jusqu’à ce qu’un évènement ait lieu. Cela signifie que
s’il n’y a aucun évènement dans la file, la fonction va attendre jusqu’à ce qu’un évènement arrive.
Et dès qu’un évènement est arrivé, la fonction remplit le SDL_Event et termine. Faisons un
code qui attend une demande de fermeture de la fenêtre pour quitter le programme.
1 SDL_Event event;
2 SDL_bool quit = SDL_FALSE;
3 while(!quit)
4 {
5 SDL_WaitEvent(&event);
6 if(event.type == SDL_QUIT)
7 quit = SDL_TRUE;
8 }
9 /* On ferme notre fenêtre et on quitte le programme */
9.1.3.2. SDL_PollEvent
Tout comme SDL_WaitEvent, elle prend en paramètre un pointeur sur un SDL_Event qu’elle
remplira avec les informations sur l’évènement. Cependant, sa valeur de retour n’est pas liée à
la présence d’erreur, mais à la présence d’évènements. Si elle a lu un évènement (donc si la file
n’était pas vide), elle retourne 1, sinon elle retourne 0.
102
9. Les évènements 1
Nous avons précédemment dit qu’«il faudra soit s’arranger pour lire un évènement dès qu’il
survient, soit les lire tous quand on décide de lire». La fonction SDL_WaitEvent correspond
vraisemblablement au premier cas, la fonction SDL_PollEvent fait plus partie du second cas.
Pour lire tous les évènements dans la file, nous allons appeler SDL_PollEvent tant que la file
n’est pas vide, c’est-à-dire tant que le retour de SDL_PollEvent est différent de 0. Faisons avec
SDL_PollEvent le même exemple que nous avons fait avec SDL_WaitEvent.
1 SDL_Event event;
2 SDL_bool quit = SDL_FALSE;
3 while(!quit)
4 {
5 while(SDL_PollEvent(&event))
6 if(event.type == SDL_QUIT)
7 quit = SDL_TRUE;
8 }
Nous avons juste placé le SDL_PollEvent dans une boucle. Le problème de cette méthode est
que la boucle tourne indéfiniment tant qu’il n’y a pas d’évènements rencontrés, ce qui n’est pas
souhaitable niveau performance. Pour régler ce problème, nous allons effectuer une petite pause
à chaque tour de boucle, à l’aide de SDL_Delay, ce qui nous mène au code suivant.
1 SDL_Event event;
2 SDL_bool quit = SDL_FALSE;
3 while(!quit)
4 {
5 while(SDL_PollEvent(&event))
6 if(event.type == SDL_QUIT)
7 quit = SDL_TRUE;
8 SDL_Delay(20);
9 }
9.1.3.3. SDL_WaitEventTimeout
103
9. Les évènements 1
Elle prend en paramètre un pointeur sur SDL_Event, mais aussi un entier. Cet entier correspond
à un nombre de millisecondes. La fonction SDL_WaitEventTimeout attend un évènement
pendant ce nombre de millisecondes. S’il y a un évènement pendant le délai imparti, elle retourne
1, sinon, s’il n’y en a pas eu ou s’il y a eu une erreur, elle retourne 0.
1 void SDL_PumpEvents(void)
Elle ne prend aucun argument et ne renvoie rien. Elle se contente d’analyser les périphériques
afin de récupérer les nouveaux évènements et de les mettre dans la file.
Nous n’avons pas à l’appeler explicitement puisque les fonctions SDL_WaitEvent, SDL_Pol
lEVent et SDL_WaitEventTimeout y font appel. Cependant, nous verrons plus tard comment
elle peut être utilisée.
Bien gérer ses évènements n’est pas vraiment compliqué. Il suffit de suivre quelques règles
simples. Pour commencer, regardons cette citation de la documentation de SDL_PumpEvents.
WARNING : This should only be run in the thread that initialized the video subsystem, and
for extra safety, you should consider only doing those things on the main thread in any case.
SDL_PumpEvents doit être appelée uniquement dans le thread initiateur du mode vidéo et il
est même conseillé pour plus de sécurité de l’appeler uniquement dans le thread principal.
De plus, il est conseillé de gérer ses évènements (donc de faire appel à SDL_WaitEvent par
exemple) à un seul endroit du code.
1 /* Bon code */
2 while(continuer)
3 {
4 SDL_WaitEvent(&event);
5 if(event.type == SDL_QUIT)
6 continuer = SDL_FALSE;
7 if(event.type == SDL_KEYDOWN)
104
9. Les évènements 1
Appeler une fonction de gestion des évènements une seule fois dans la boucle suffit. En fait,
notre programme sera généralement de cette forme.
Finalement, pour bien gérer ses évènements, il faut bien choisir la fonction à utiliser. D’un
côté, nous avons la fonction bloquante SDL_WaitEvent, d’un autre côté nous avons la fonction
non bloquante SDL_PollEvent et au milieu des deux nous avons l’hybride SDL_WaitEventTi
meOut.
Nous allons privilégier SDL_PollEvent qui n’est pas bloquante et utiliser SDL_WaitEvent dans
le seul cas où, sans intervention de l’utilisateur, il n’y rien à faire. SDL_WaitEventTimeout est
un peu plus rarement utilisée.
9.2.1. La fenêtre
Lorsque la SDL détecte un évènement de type SDL_WINDOWEVENT, nous pouvons récupérer les
informations sur l’évènement dans le champ window du SDL_Event correspondant. Il s’agit
d’une structure, SDL_WindowEvent . Ses différents champs sont les suivants.
105
9. Les évènements 1
1 Uint32 type;
2 Uint32 timestamp;
3 Uint32 windowID;
4 Uint8 event;
5 Sint32 data1;
6 Sint32 data2;
Ce qui nous intéresse le plus est son champ event qui correspond au type d’évènement détecté.
Il peut avoir plusieurs valeurs qui correspondent aux valeurs de l’énumération SDL_WindowE
ventID . On a alors par exemple ces différentes valeurs possibles:
— SDL_WINDOWEVENT_SHOWN si la fenêtre a été rendue visible;
— SDL_WINDOWEVENT_MOVED si la fenêtre a été déplacée;
— SDL_WINDOWEVENT_RESIZED si la fenêtre a été redimensionnée;
— SDL_WINDOWEVENT_MINIMIZED si la fenêtre a été minimisée;
— SDL_WINDOWEVENT_RESTORED si la fenêtre a été restaurée;
— SDL_WINDOWEVENT_ENTER si le focus de la souris est sur la fenêtre;
— SDL_WINDOWEVENT_LEAVE si le focus de la souris n’est plus sur la fenêtre;
— SDL_WINDOWEVENT_CLOSE si le gestionnaire de fenêtre demande la fermeture de la
fenêtre.
Et d’autres que nous pouvons voir sur la page de documentation de SDL_WindowEventID. Par
exemple, on peut savoir si la fenêtre a été redimensionnée avec ce code.
1 while(!quit)
2 {
3 SDL_WaitEvent(&event)
4 if(event.type == SDL_QUIT)
5 quit = SDL_TRUE;
6 else if(event.type == SDL_WINDOWEVENT)
7 if(event.window.event == SDL_WINDOWEVENT_RESIZED)
8 printf("Fenêtre redimensionnée\n"); /*
Fenêtre redimensionnée */
9 SDL_Delay(20);
10 }
Le champ windowID est utile dans le cas où l’on a plusieurs fenêtres et nous permet de savoir
par quelle fenêtre l’évènement a été reçu.
Nous pouvons comparer sa valeur à celle retournée par la fonction SDL_GetWindowID qui
prend en paramètre une fenêtre et retourne son identifiant. Ainsi, si event.window.windowID
== SDL_GetWIndowID(window1), alors c’est la fenêtre window1 qui a été redimensionnée par
exemple.
Nous pouvons également agir autrement en utilisant la fonction SDL_GetWindowFromID qui
prend en paramètre un identifiant et renvoie la fenêtre qui a cet identifiant. Ainsi, SDL_GetWin
106
9. Les évènements 1
?
Pourquoi un évènement SDL_WINDOWEVENT_CLOSE alors qu’il y a déjà l’évènement
SDL_QUIT?
Cette question est légitime, mais il ne faut pas oublier que nous pouvons ouvrir plusieurs
fenêtres, la fermeture d’une fenêtre ne signifie donc pas forcément que l’on quitte le programme.
Quand plusieurs fenêtres sont utilisées, cliquer sur la croix de l’une d’entre elles n’entraînera
pas l’évènement SDL_QUIT.
9.2.2. Le clavier
Lorsque c’est un évènement de type SDL_KEYDOWN ou SDL_KEYUP, les informations sur l’évène-
ment sont dans le champ key de SDL_EVENT. Il s’agit cette fois de la structure SDL_Keyboar
dEvent dont les champs sont les suivants.
1 Uint32 type
2 Uint32 timestamp;
3 Uint32 windowID;
4 Uint8 state;
5 Uint8 repeat;
6 SDL_Keysym keysym;
On retrouve, comme pour les événements de type SDL_WINDOWEVENT, le champ windowID qui
correspond à l’identifiant de la fenêtre sur laquelle l’évènement a été détecté et le champ type
qui vaut alors SDL_KEYDOWN ou SDL_KEYUP. Le champ state contient l’état de la touche et
vaut SDL_PRESSED si la touche est pressée et SDL_RELEASED si elle est relâchée. Le champ
repeat est différent de 0 s’il y a eu répétition de l’appui.
Le champ qui nous intéresse le plus est keysym. Il s’agit d’une structure SDL_Keysym . C’est
dans ses champs que l’on retrouvera des informations sur la touche pressée ou libérée. Ses
champs sont les suivants.
1 SDL_Scancode scancode;
2 SDL_Keycode sym;
3 Uint16 mod;
107
9. Les évènements 1
Les champs scancode et sym correspondent tous les deux à la touche pressée (ou relâchée),
mais le premier correspond au code de la touche physique alors que le second correspond au
code de la touche virtuelle. Ils prennent comme valeur celles des énumérations SDL_Scancode
et SDL_Keycode . Par exemple, pour savoir si on a appuyé sur la touche physique A , on
vérifiera la condition event.key.keysym.scancode == SDL_SCANCODE_A et pour la touche
virtuelle on fera le test event.key.keysym.sym == SDLK_A.
?
Mais quelle est la différence entre touche physique et touche virtuelle?
En fait, la touche physique ne dépend pas du système et est la même sur tous les ordinateurs.
Ainsi, SDL_SCANCODE_A correspond à l’appui sur la touche A d’un clavier Qwerty, mais à la
touche Q d’un clavier Azerty et à la touche A d’un clavier Bépo car ce qui compte ici, c’est
la place physique de la touche sur le clavier. Au contraire, la touche virtuelle dépend du système
et non du type de clavier, SDLK_a correspond donc toujours à l’appui sur la touche A . Pour
bien voir cette différence, essayons ce code:
1 while(!quit)
2 {
3 SDL_WaitEvent(&event);
4 if(event.type == SDL_QUIT)
5 quit = SDL_TRUE;
6 else if(event.type == SDL_KEYDOWN)
7 {
8 if(event.key.keysym.scancode == SDL_SCANCODE_A)
9 printf("scancode A\n");
10 if(event.key.keysym.sym == SDLK_a)
11 printf("keysym A\n");
12 }
13 SDL_Delay(20);
14 }
En testant ce code sur un clavier Azerty, il nous faudra appuyer sur A pour que le message
«keysym A» s’affiche, mais il nous faudra appuyer sur Q pour que «scancode A» s’affiche.
Le champ mod de SDL_Keysym nous permet de savoir si l’utilisateur a appuyé sur une (ou
plusieurs) touche(s) spéciale(s) ( Alt , Ctrl , Shift , etc.). Sa valeur correspond au OU logique de
valeurs de l’énumération SDL_Keymod . Par exemple, pour savoir si on a appuyé sur Ctrl , on
vérifiera si event.key.keysym.mod & KMOD_CTRL != 0.
9.2.3. La souris
Pour la souris, il y a, comme nous l’avons vu, trois types d’évènements. Les évènements du type
déplacement de la souris, les appuis sur les touches et le déplacement de la molette.
108
9. Les évènements 1
1 Uint32 type;
2 Uint32 timestamp;
3 Uint32 windowID;
4 Uint32 which;
5 Uint8 button;
6 Uint8 state;
7 Uint8 clicks
8 Sint32 x;
9 Sint32 y;
1 while(!quit)
2 {
3 SDL_WaitEvent(&event);
4 if(event.type == SDL_QUIT)
5 quit = SDL_TRUE;
6 else if(event.type == SDL_MOUSEBUTTONUP)
7 {
8 if(event.button.button == SDL_BUTTON_LEFT &&
event.button.clicks >= 2)
9 printf("Au moins un double clic gauche\n");
10 }
11 SDL_Delay(20);
109
9. Les évènements 1
12 }
Dans le cas d’un évènement du type SDL_MOUSEMOTION, les données sont stockées dans le
champ motion de SDL_Event qui est une structure SDL_MousMotionEvent qui a les
champs suivants.
1 Uint32 type;
2 Uint32 timestamp;
3 Uint32 windowID;
4 Uint32 which;
5 Uint32 state;
6 Sint32 x;
7 Sint32 y;
8 Sint32 xrel;
9 Sint32 yrel;
Les champs windowID, which, x et y fonctionnent de la même manière que dans la structure
SDL_MousButtonEvent. Et comme d’habitude, type contient le type d’évènement (ici SDL_MOU
SEMOTION).
Le champ state nous permet de savoir sur quelles touches de la souris l’utilisateur appuyait
pendant le déplacement de la souris. C’est une combinaison de différents masques qu’on trouve
dans la documentation. Par exemple, on peut tester si event.motion.state & SDL_BUT
TON_LMASK pour savoir s’il y a appui sur la touche gauche pendant le déplacement.
Les champs xrel et yrel caractérisent le mieux le déplacement puisqu’ils nous donnent
respectivement les déplacements relatifs sur les axes des x et des y. Par exemple, si on s’est
déplacé de 1 pixel vers le haut et de 2 pixels vers la droite, alors xrel vaudra 2 et yrel vaudra
-1.
Avec le code qui suit, on affiche la position de la souris et les déplacements relatifs à chaque fois
qu’il y a déplacement et appui sur la touche gauche de la souris.
1 while(!quit)
2 {
3 SDL_WaitEvent(&event);
4 if(event.type == SDL_QUIT)
5 quit = SDL_TRUE;
6 else if(event.type == SDL_MOUSEMOTION &&
(event.motion.state & SDL_BUTTON_LEFT))
7 printf("%d %d - %d %d\n", event.motion.x,
event.motion.y, event.motion.xrel,
event.motion.yrel);
110
9. Les évènements 1
8 SDL_Delay(20);
9 }
Et finalement, lorsqu’un évènement du type SDL_MOUSEWHEEL est détecté, les informations sont
placées dans le champ wheel de SDL_Event. Il s’agit d’une structure SDL_MouseWheelEvent
. Voici ses champs.
1 Uint32 type;
2 Uint32 timestamp;
3 Uint32 windowID;
4 Uint32 which;
5 Sint32 x;
6 Sint32 y;
7 Uint32 direction;
On retrouve les champs which, type (qui vaut cette fois SDL_MOUSEWHEEL), et windowID. Le
champ y caractérise le défilement vertical. Cette valeur est positive s’il y a eu défilement vers le
haut et négative si le défilement était vers le bas. x caractérise le déplacement horizontal de la
molette (encore faut-il que la molette en soit capable) et est positif si le déplacement s’est fait
vers la droite et négatif sinon.
Ainsi, avec ce code, on affiche la valeur de déplacement à chaque déplacement de la molette.
1 while(!quit)
2 {
3 SDL_WaitEvent(&event);
4 if(event.type == SDL_QUIT)
5 quit = SDL_TRUE;
6 else if(event.type == SDL_MOUSEWHEEL)
7 printf("%d - %d - %d\n", event.wheel.x,
event.wheel.y, event.wheel.timestamp);
8 SDL_Delay(20);
9 }
En testant ce code, on s’aperçoit qu’il est en fait très compliqué, mais vraiment très compliqué
d’avoir une valeur de déplacement supérieure à 1 (ou inférieure à -1).
Nous n’avons jamais parlé du champ timestamp qui est présent dans toutes les structures
que nous avons vues ici. Ce champ a pour valeur le nombre de millisecondes écoulées entre
l’initialisation de la SDL et l’évènement que l’on est en train d’analyser. Puisque nous parlons
de timestamp, profitons-en pour présenter la fonction SDL_GetTicks qui permet d’obtenir le
temps en millisecondes depuis l’initialisation de la SDL. Son prototype est le suivant.
111
9. Les évènements 1
1 Uint32 SDL_GetTicks(void)
9.3.1. La souris
Pour récupérer l’état de la souris, il nous faut utiliser la fonction SDL_GetMouseState . Voici
son prototype.
1 Uint32 SDL_GetMouseState(int* x,
2 int* y)
Elle prend en paramètre deux pointeurs sur int qui seront remplis avec les positions de la souris.
Elle renvoie un Uint32 qui nous permettra de savoir quelles touches sont pressées en utilisant
des flags. On l’utilise comme ceci.
1 int x, y;
2 Uint32 boutons;
3
4 SDL_PumpEvents();
5 boutons = SDL_GetMouseState(&x,&y);
6 if(boutons & SDL_BUTTON(SDL_BUTTON_LEFT))
7 printf("Clic droit à la positions %d - %d", x, y);
SDL_BUTTON est une macro qu’il faudra utiliser pour tester si une touche est pressée.
Si la position de la souris ne nous intéresse pas, nous pouvons lui passer NULL en paramètre.
112
9. Les évènements 1
Si nous voulons récupérer le déplacement de la souris, nous pouvons utiliser la fonction SDL_Ge
tRelativeMouseState , dont le prototype est le suivant.
1 Uint32 SDL_GetRelativeMouseState(int* x,
2 int* y)
9.3.2. Le clavier
Elle prend en paramètre un pointeur sur int et retourne un pointeur sur Uint8. Le pointeur
qu’elle renvoie pointe sur la première case d’un tableau dont les éléments sont les états des dif-
férentes touches du clavier (1 si la touche est pressée et 0 sinon). Il s’agit d’un tableau géré par la
SDL. Il est valide pendant toute l’exécution du programme et nous n’avons pas besoin de le libérer
manuellement. Pour savoir quelle case du tableau correspond à quelle touche, il nous faut utiliser
les valeurs de l’énumération SDL_Scancode. Par exemple, tab[SDL_SCANCODE_RETURN] == 1
signifie que la touche Entrée est pressée.
La variable sur laquelle pointe numkeys sera modifiée et vaudra la longueur du tableau. Comme
nous n’avons généralement pas besoin de connaître la longueur de ce tableau, nous passons
généralement NULL en argument. On peut alors écrire ce code qui boucle tant qu’on n’appuie
pas sur Échap ou sur Entrée .
1 Uint8 *clavier;
2 while(!quit)
3 {
4 SDL_PumpEvents();
5 clavier = SDL_GetKeyboardState(NULL);
6 if(clavier[SDL_SCANCODE_ESCAPE] ||
clavier[SDL_SCANCODE_RETURN])
7 quit = SDL_TRUE;
8 }
113
9. Les évènements 1
Pour les touches spéciales du clavier, il nous faut encore utiliser une autre fonction. Il s’agit de
la fonction SDL_GetModState dont le prototype est le suivant.
1 SDL_Keymod SDL_GetModState(void)
Elle ne prend aucun paramètre et renvoie une combinaison de OU logique des touches spéciales
pressées. Pour tester si une touche est pressée, on va alors utiliser le ET logique.
1 SDL_Keymod keymod;
2 Uint8 *clavier;
3 while(!quit)
4 {
5 SDL_PumpEvents();
6 clavier = SDL_GetKeyboardState(NULL);
7 keymod = SDL_GetModeState();
8 if(keymod & KMOD_CTRL)
9 printf("Appui sur Ctrl\n");
10 if(clavier[SDL_SCANCODE_ESCAPE])
11 quit = SDL_TRUE;
12 }
Cela peut être encore mieux si nous utilisions notre propre structure pour gérer tout ça. Nous
pourrions alors placer une touche à zéro à la main (imaginons un jeu où l’on voudrait que
l’utilisateur ne puisse pas maintenir la touche de tir enfoncée, mais doive la relâcher avant de
ré-appuyer pour tirer à nouveau). Pour la mettre à jour, nous n’utiliserons pas les fonctions
d’acquisition d’état mais une boucle avec un SDL_PollEvent qui nous permettra d’y mettre
également l’évènement SDL_QUIT. D’ailleurs, on va commencer par gérer cet évènement et le
clavier.
1 struct Input
2 {
3 SDL_bool key[SDL_NUM_SCANCODES];
4 SDL_bool quit;
5 int x, y, xrel, yrel;
6 int xwheel, ywheel;
7 SDL_bool mouse[6];
8 };
114
9. Les évènements 1
La constante SDL_NUM_SCANCODES est une constante qui est plus grande que la plus grande
des valeurs de l’énumération SDL_Scancode. Nous sommes alors sûrs que notre tableau est
suffisamment grand pour contenir toutes les touches du clavier. x et y représentent la position
de la souris, xrel et yrel le déplacement de la souris, et xwheel et ywheel le déplacement de
la molette. Pour savoir quelle valeur prendre pour le tableau mouse, nous pouvons regarder les
déclarations des différentes valeurs des boutons de la souris.
1 ###define SDL_BUTTON_LEFT 1
2 #define SDL_BUTTON_MIDDLE 2
3 ###define SDL_BUTTON_RIGHT 3
4 #define SDL_BUTTON_X1 4
5 ###define SDL_BUTTON_X2 5
Dès lors, il nous suffirait d’un tableau de 5 éléments, mais il nous faudrait décaler les indices de
1. Nous allons plutôt déclarer un tableau de six éléments (de plus, le jour où nous voudrons gérer
les appareils tactiles, nous pourrons utiliser la première case pour SDL_TOUCH_MOUSEID).
Cela donne lieu à cette fonction de mise à jour de notre structure.
115
9. Les évènements 1
1 while(!in.quit)
2 {
3 updateEvent(&in);
4 if(in.key[SDL_SCANCODE_A])
5 {
6 printf("Appui sur la touche A");
7 in.key[SDL_SCANCODE_A] = SDL_FALSE;
8 }
9 if(in.key[SDL_SCANCODE_C] && in.key[SDL_SCANCODE_D])
10 printf("Appui sur les touches C et D");
11 SDL_Delay(20);
12 }
Ici, en restant appuyé sur C et D , le second message n’arrêtera pas de s’afficher, mais
nous sommes obligés de relâcher la touche A , puis de ré-appuyer pour ré-afficher le premier
message.
Nous pourrions rajouter à notre structure d’autres fonctionnalités telles que la gestion de certains
des évènements de la fenêtre ou la gestion des touches spéciales du clavier.
Notre structure est très intéressante. Elle nous permet de séparer la gestion des évènements
du reste du programme. On ne met à jour notre structure qu’au début de notre boucle. On se
contente ensuite de vérifier sur quelle touche il y a appui.
Notons qu’avant d’utiliser la structure, il faut l’initialiser en la remplissant de SDL_FALSE pour
que l’on ne puisse pas détecter d’évènements alors qu’en fait il n’y en a pas. Pour cela, nous
pouvons par exemple utiliser la fonction memset.
Maintenant que nous avons vu comment gérer les événements, nous pouvons enfin faire des
programmes interactifs.
116
9. Les évènements 1
Contenu masqué
Le principe est assez simple, il suffit de rappeler SDL_PollEvent tant qu’il n’y a pas d’évène-
ments, c’est-à-dire tant que la file est vide et donc tant que la valeur retournée est 0. Dans notre
fonction, nous n’aurons alors que cette ligne (et on peut y rajouter une petite pause).
1 while(!SDL_PollEvent(event))
2 SDL_Delay(20);
Ici, la seule chose que l’on pourrait nous reprocher est de ne pas gérer les erreurs. De plus, la
fonction que nous codons ici n’est pas tout à fait similaire à SDL_WaitEvent car cette dernière
implique que le programme ne soit plus exécuté tant qu’un évènement n’est pas rencontré.
Retourner au texte.
117
10. Conclusion
Ce tutoriel n’est pas encore fini et d’autres chapitres risquent bien de voir le jour. L’apprentissage
non plus n’est pas fini, il reste plein de choses à apprendre et l’univers de la SDL est très vaste.
Un grand merci à tous les membres qui ont apporté leur aide lors de la bêta, à Taurre qui a
fait un travail monstrueux pour valider ce tutoriel et surtout merci aux lecteurs!
118