libimg: Transformation et composition d'images pour gint
Posté le 10/03/2020 16:08
Ce topic fait partie de la série de topics du fxSDK.
Salut ! Juste à temps pour le
CPC #26, voici une bibliothèque pour gint permettant de jouer avec les images sur Graph mono et Graph 90 Ça a été demandé par KikooDX (
ici,
ici).
Un petit aperçu de ce qu'on peut faire.
Voilà les fonctionnalités principales :
• Gestion complète de la transparence, composition alpha.
• Miroir horizontal et vertical, rotation de 90/180/270 degrés, agrandissement par facteur entier.
• Éclaircissement et assombrissement, fondu au blanc, recoloriage avec une couleur unie.
• Presque toutes les transformations peuvent être utilisées en place.
• Système de positionnement et références à des sous-images très flexible.
La suite de ce topic est un tutoriel d'utilisation directement traduit du README du dépôt, qui est en anglais. Enjoy!
Conversion des images et rendu VRAM
La première chose à faire pour utiliser libimg est de convertir les images au format
img_t. Ça se fait en sélectionnant le type
libimg-image dans les paramètres du fichier.
IMG.sprite.png = type:libimg-image
L'image fraîchement convertie peut être utilisée depuis le code C en donnant la déclaration externe qui convient, comme pour tout ce qui est converti par fxconv. Elle est prête à l'emploi dès que la définition de
img_t et des fonctions de la bibliothèque, qui sont fournies par
<libimg.h>, sont incluses.
#include <libimg.h>
extern img_t const sprite;
Faites attention au fait que
les images converties sont en lecture seule, comme toujours quand c'est converti à la compilation. Donc on peut pas la modifier ou la transformer en-place ; j'y reviendrai. Pour se rappeller que l'image est en lecture seule, j'ai ajouté le mot-clé
const ; c'est entièrement optionnel.
Pour afficher cette superbe image dans la VRAM, utilisez
img_render_vram(). Sur les Graph mono type Graph 35+E, c'est la seule façon de dessiner dans la VRAM, car tristement la VRAM n'a pas le même format qu'une
img_t. Sur Graph 90+E, des méthodes plus pétées sont disponibles.
img_render_vram(sprite, x, y);
img_render_vram(), comme toutes les autres fonctions de la bibliothèque, tient compte des pixels transparents. Seuls les pixels opaques sont copiés, ce qui permet de combiner facilement des images en les dessinant juste les unes après les autres.
Si vous avez une image en niveaux de gris, utilisez
img_render_vram_gray() qui va afficher correctement les pixels gris (avec les mêmes paramètres).
img_render_vram() se contentera de faire une approximation des gris par du noir et blanc.
Créer et détruire de nouvelles images
Puisque notre image convertie est en lecture seule, on ne peut pas faire grand-chose d'autre pour l'instant. Si on veut s'amuser avec les transformations et les animations, on doit créer des images dans lesquelles on peut véritablement écrire. Il y a deux moyens de le faire : soit en créant des nouvelles images vides, soit en dupliquant une image existante.
img_create() crée une nouvelle image non initialisée. La valeur (couleur) des pixels est aléatoire dans une nouvelle image, on verra plus tard comment utiliser
img_fill() pour leur donner une couleur fixe. Parfois initialiser l'image n'est pas nécessaire et ne ferait que réduire les performances, donc libimg essaie de ne pas vous gêner et ne le fait pas automatiquement.
img_t new_32x48_image = img_create(32, 48);
img_copy() crée une copie d'une image existante. La copie est de la même taille que la source, mais tous les pixels sont dupliqués. On peut toujours écrire dans une copie, même si la source est en lecture seule.
img_t copy_of_sprite = img_copy(sprite);
Bien sûr, la création de nouvelles images
peut échouer. Ces deux fonctions utilisent
malloc() pour obtenir de la mémoire en interne, et
malloc() échoue s'il n'y a pas assez de mémoire disponible. Dans cette situation,
img_create() et
img_copy() renvoie des
images nulles, qui sont comme le pointeur
NULL pour les images. Pour savoir si une image est nulle, utilisez
img_null().
if(img_null(copy_of_sprite)) {
/* Argh, copy has failed! */
}
else {
/* Everything is fine, let's go! */
}
Une image nulle n'a pas de pixels, et ne peut pas être lue ou modifiée. Mais vous pouvez quand même donner des images nulles aux fonctions de la libimg ; elles s'en rendront compte et ne feront rien ou renverront d'autres images nulles. Pas de risque de crash avec ça.
Évidemment, puisqu'il y a un
malloc(), il doit y avoir un
free(). Les images crées par
img_create() et
img_copy() doivent être libérées une fois que vous n'en avez plus besoin. Appelez
img_destroy() pour les libérer.
img_destroy(new_32x48_image);
img_destroy(copy_of_sprite);
Remarquez que les images converties comme
sprite, les images nulles et les sous-images (présentées plus tard !) n'ont pas besoin d'être détruites car elles ne sont pas créées avec
malloc(). Mais pour vous simplifier la vie,
img_destroy() est programmé pour les détecter, donc en cas de doute libérez tout ce dont vous n'avez plus besoin et vous n'aurez pas de problème.
Transformations basiques
Il est temps de s'amuser avec nos superbes images. On va commencer par renverser horizontalement notre sprite pour simuler une animation de marche dans l'autre sens. Pour ça, on a besoin d'une image de la bonne taille qu'on va remplir de blanc. C'est important de remplir parce que le sprite a des pixels transparents, donc si on ne remplit pas la nouvelle image, certains des pixels aléatoires seront visibles autour du sprite transformé.
#include <gint/display.h>
img_t flipped_sprite = img_create(sprite.width, sprite.height);
img_fill(flipped_sprite, C_WHITE);
Cet exemple en profite pour démontrer quelques éléments importants :
• La taille d'une image peut être obtenue en lisant ses attributs
width et
height.
• La fonction
img_fill() remplace tous les pixels d'une image par une couleur uniforme.
• Les couleurs utilisées dans libimg sont les même que celles utilisées dans gint, à l'exception de la transparent sur Graph 90+E, qui est
0x0001. J'y reviendrai très vite.
Maintenant qu'on a une image blanche avec la même taille que
sprite, on peut se lancer et y générer le miroir horizontal de
sprite :
img_hflip(sprite, flipped_sprite);
Et voilà. Maintenant
img_render_vram(flipped_sprite, x, y) affiche la version renversée. Tant que
flipped_sprite n'est pas libéré, il peut être affiché autant de fois qu'on veut. (Remarquez que la transformation n'aurait pas marché si
flipped_sprite était en lecture seule. En particulier, on ne pourrait pas utiliser
sprite comme la cible d'une transformation).
Mais attendez, puisque
flipped_sprite était blanc à l'origine, on vient de faire un gros carré blanc tout moche sur l'écran. Ce qu'on voulait en fait c'est un fond
transparent. On peut utiliser la couleur spéciale
IMG_ALPHA pour l'obtenir.
/* Rend flipped_sprite complètement transparent : */
img_fill(flipped_sprite, IMG_ALPHA);
Sur les Graph mono, les couleurs de la libimg sont exactement les mêmes que les couleurs de gint, donc
IMG_ALPHA est la même chose que
C_NONE. Sur Graph 90+E, les choses sont un peu plus compliquées. Les couleurs sont au format RGB565, et tous les entiers de 16 bits sont des couleurs RGB565 valides, donc il n'y a pas de valeur disponible pour représenter la transparence. libimg résoud ce dilemme en décretant que
0x0001 représente la transparence. Cela signifie que la couleur
0x0001 n'est pas disponible dans les images de type
img_t. Cette couleur a été choisie parce qu'elle est extrêmement sombre, indistinguable du noir, et qu'il est facile de comparer un nombre à 1 donc c'est un poil plus rapide dans le code. Retenez qu'il faut vraiment utiliser
IMG_ALPHA pour obtenir de la transparence dans libimg.
Puisque remplir une image avec des pixels transparents est une opération courante, un raccourci appelé
img_clear() est fourni pour le faire.
/* Rend aussi flipped_sprite complètement transparent : */
img_clear(flipped_sprite);
Le code complet appelle maintenant
img_create(),
img_clear() puis
img_hflip() pour transformer. C'est aussi une séquence courante, donc un raccourci appelé
img_hflip_create() a été conçu pour aller plus vite.
img_t flipped_sprite = img_hflip_create(sprite);
Toutes les transformations fonctionnent sur ce modèle. Cela comprend:
•
img_hflip() et
img_vflip() qui renverse horizontalement et verticalement.
•
img_rotate() qui tourne de 0, 90, 180 ou 270 degrés.
•
img_upscale() qui multiple la taille d'un facteur entier.
•
img_dye() qui remplace tous les pixels opaques par une couleur unie.
•
img_ligthen(),
img_whiten() et
img_darken() qui jouent sur la luminosité.
On peut aussi commencer à mentionner des conventions utiles de la bibliothèque :
• Toutes les transformations prennent la source en la destination en premiers arguments, et ensuite le reste des paramètres comme l'angle de rotation.
• Toutes les transformations ont une variante
_create() qui ne prend pas de destination en argument, et en crée une avec juste la bonne taille, puis la retourne après la transformation.
Sous-surfaces et positionnement
Jusqu'ici on n'a transformé des images que vers des destinations de la même taille, ou si vous avez essayé la rotation ou l'agrandissement, de tailles similaires. Imaginons qu'on veut créer une spritesheet avec à la fois le sprite original et la version renversée. On a un problème parce que
img_hflip() ne nous permet pas de dire où placer le résultat transformé dans l'image de destination. En fait,
img_hflip() place toujours le résultat dans le coin haut gauche.
C'est parce que la libimg possède un système de positionnement plus puissant que de rajouter des paramètres. Ce système est construit autour de l'idée d'
extraire des références à des sous-images.
Prenons directement un exemple. On va créer une spritesheet de 32x16 avec un sprite de 16x16 sur la gauche et un sur la droite.
img_t spritesheet = img_create(32, 16);
Le sprite de droite est à la position (16,0) dans
spritesheet. Si on veut l'utiliser souvent ou appliquer beaucoup de transformations, on va devoir répéter ces coordonnées de nombreuses fois, ce qui est casse-pieds et source d'erreur. À la place, on peut utiliser la fonction
img_sub() pour obtenir une référence vers la moitié droite de
spritesheet.
img_t right_sprite = img_sub(spritesheet, 16, 0, 16, 16);
img_sub() prend cinq paramètres : l'image source, et la position et taille de la sous-image qui nous intéresse, sous la forme
x,
y,
w et
h. Le sprite de droite commence à
x=16 et
y=0, et est de largeur
w=16 et hauteur
h=16.
img_sub() ne crée pas une nouvelle image. Elle donne simplement une nouvelle vue sur les mêmes pixels. Modifier
right_sprite affecterait totalement la moitié droite de
spritesheet. D'ailleurs, on n'a qu'à faire ça.
img_fill(right_sprite, C_BLACK);
Et juste comme ça, on vient de remplir
uniquement la moitié de
spritesheet. Appeler
img_fill(spritesheet, C_BLACK) en aurait rempli
la totalité. Voyez comment le système de positionnement rend
img_fill() aussi polyvalent qu'une fonction de remplissage de rectangles.
Avec tout ça sous la main, on peut maintenant créer notre spritesheet :
/* Copie le sprite normal dans la moitié gauche */
img_render(sprite, spritesheet);
/* Renverse le sprite sur la moitié droite */
img_hflip(sprite, right_sprite);
Bien sûr, ce système marche avec toutes les transformations. Notez cependant que si l'image ou sous-image donnée comme destination doit être
au moins aussi grande que le résultat de la transformation. Si la destination est plus petite, la transformation ne fera rien.
La sous-image ne contient pas nouveaux pixels, donc elle n'a pas besoin d'être détruite. Vous pouvez quand même appeler
img_destroy() dessus et il ne se passera rien. Mais si on n'a pas besoin de la détruire, on n'a pas besoin de la stocker dans une variable. Et donc on peut renverser le sprite de cette façon :
img_hflip(sprite, img_sub(spritesheet, 16, 0, 16, 16));
Ça se lit « renverse
sprite horizontalement, et écris le résultat dans
spritesheet, à la position (16,0) dans un rectangle de taille 16x16 ».
Ici on sait que la taille du rectangle
doit être 16x16, parce que c'est la taille du sprite. Donc on peut s'en passer en indiquant seulement une largeur et une hauteur de -1. Ça créera une référence qui commencera à la positon (16,0) et ira jusqu'au coin en bas à droite de
spritesheet.
img_hflip() ne modifiera quand même que les premiers 16x16 pixels dans le coin haut gauche, donc ce n'est pas grave si la référence est plus grande que nécessaire.
img_hflip(sprite, img_sub(spritesheet, 16, 0, -1, -1));
Si vous avez l'impression que c'est une construction commune, vous avez parfaitement raison ! Et donc un raccourci a été créé pour aller plus vite. Au lieu de spécifier une largeur et une hauteur de -1, on peut appeler
img_at() qui les spécifiera à notre place.
img_at(img, x, y) est la même chose que
img_sub(img, x, y, -1, -1). Donc la transformation devient :
img_hflip(sprite, img_at(spritehseet, 16, 0));
Ce qui se lit « renverse
sprite horizontalement et écris le résultat dans
spritesheet, à la position (16,0) ». C'est beaucoup plus court que tout à l'heure, et comme la référence à la sous-image n'a pas besoin d'être libérée, on ne risque pas de créer de fuite de mémoire.
Les références aux sous-images sont un outil puissant et elles peuvent être utilisées d'un bon nombre de façons. Mais il y a une règle d'or :
détruire l'original détruit aussi les sous-images. Une fois que vous avez appelé
img_destroy(spritesheet), toutes les sous-images, y compris
right_sprite, deviennent
invalides et toute utilisation serait ue erreur. Gardez toujours un oeil sur les originaux !
Comme exemple de la polyvalence des références aux sous-images, remarques qu'on peut utiliser
img_sub() sur la
source de la transformation pour n'en transformer qu'une partie !
J'ai utilisé
img_render() sans expliquer ce qu'elle faisait, donc c'est un bon moment pour y jeter un oeil.
Rendu direc sur la VRAM (Graph 90+E)
Sur Graph mono, la VRAM n'a pas le même format qu'une
img_t, donc on ne peut pas utiliser la VRAM comme la destination d'une transformation. On ne peut qu'utiliser
img_render_vram() pour copier une image vers la VRAM une fois qu'elle a été préparée.
Mais sur Graph 90+E, la VRAM a bel et bien le même format qu'une
img_t. Pour en obtenir une référence, utilisez
img_vram() :
img_t vram_as_an_image = img_vram();
Comme ce n'est qu'une référence, il n'y a pas besoin de la détruire avec
img_destroy(). Le faire quand même ne produit aucun effet, donc en cas de doute, libérez toujours toutes les images après utilisation.
Cette référence vers la VRAM nous permet de transformer directement vers la VRAM sans passer par des images temporaires. Par exemple, on peut écrire :
img_hflip(sprite, img_at(img_vram(), x, y));
De la même façon, les deux appels suivants reùplissent un rectangle de la VRAM (bien que
drect() soit plus rapide) :
#include <gint/display.h>
drect(x, y, w, h, color);
img_fill(img_sub(img_vram(), x, y, w, h), color);
C'est ici que la fonction
img_render() commence à briller. Cette fonction copie l'image source vers l'image destination sans la transformer. Contrairement aux transformations, elle supporte le clipping, donc si la destination est plus petite que la source, elle copie uniquement les pixels visibles. (Les transformations paniquent et ne font rien si cette situation se présente.) En fait, sur Graph 90+E la fonction
img_render_vram() n'est qu'un raccourci pour :
img_render(src, img_at(img_vram(), x, y));
img_render() est utilisée le plus souvent de ette façon, pour copier (faire le rendu) d'une image vers la VRAM. D'où son nom.
Transformations en-place
Certaines transformations peuvent transformer une image sans utiliser d'image destination auxiliaire. L'image source est écrasée durant la transformation, donc l'original est perdu mais il n'y a pas besoin de mémoire supplémentaire. Ça s'appelle une
transformation en-place.
Il n'est possible de transformer en-place que si l'image transformée est de la même taille que l'image source. Dans la version actuelle, cela inclut toutes les transformations, sauf les rotations par 90 et 270 degrés quand l'image n'est pas carrée, et l'agrandissement par une facteur non trivial.
Pour exécuter une transformation en-place, utilisez la source comme destination :
img_t sprite_copy = img_copy(sprite);
img_hflip(sprite_copy, sprite_copy);
Il est également possible d'utiliser une sous-image d'une image comme source et une autre sous-image comme cible. L'appel ci-dessous renverse horizontalement la moitié gauche de
spritesheet dans la moitié droite :
img_hflip(img_sub(spritesheet, 0, 0, 16, 16), img_at(spritesheet, 16, 0));
On peut aussi faire ça directement sur la VRAM sur Graph 90+E puisque la VRAM n'est qu'une
img_t déguisée.
Remarquez cependant qu'on ne peut pas transformer si la source et la cible se recouvrent partiellement (ie. elles ne sont pas disjointes même elles en sont pas non plus la même zone). Aucune transformation ne le supporte et le résultats sera toujours faux !
Accès aux pixels et transformations personnalisées
Toute image peut être lue manuellement pour connaître la couleur des pixels, et également écrite si elle n'est pas en lecture seule.
img_t est une stucture avec une description de l'image et les attributs utiles suivants :
•
img.width et
img.height sont la largeur et la hauteur de l'image, respectivement.
•
img.stride est le nombre de pixels entre deux lignes. Souvent c'est plus grand que
img.width !
•
img.pixels est le tableau des pixels.
Le format est ligne par ligne (
row-major order) avec espacement (
stride). Voici à quoi une image de taille 8x3 avec un espacement de 12 ressemble en mémoire. Les nombres sur le diagramme correspondent aux indices dans
img.pixels.
<------ img.width ------>
+-------------------------+-------------+
| 0 1 2 3 4 5 6 7 | . . . . | ^
| 12 13 14 15 16 18 18 19 | . . . . | img.height
| 24 25 26 27 28 29 etc | . . . . | v
+-------------------------+-------------+
<------------ img.stride ------------->
Par exemple, le troisième pixel de la deuxième ligne est
img.pixels[13]. Remarquez qu'il faut ajouter
img.stride pour descendre d'une ligne, et non pas
img.width. Par ailleurs, les indices représentés par des points, comme
pixels[8],
ne font pas partie de l'image et ne doivent en aucun cas être lus.
La raison derrière ce format avec espacement est pour supporter les références aux sous-images. Quand on crée une spritesheet de 32x16, l'espacement est de 32 et donc il n'y a pas de vide, comme on pourrait s'y atendre. Mais quand on extrait une moitié de 16x16, même si on ne regarde plus que la moitié des lignes, il y a quand même 32 pixels entre chaque ! C'est pour ça que l'espacement d'une image est souvent plus grand que sa largeur.
Cela signifie aussi que les images ne sont pas continues dans la mémoire, donc il n'est pas possible de remplacer tous les pixels en un seul appel à
memcpy().
Voici comment une fonction fait pour itérer sur tous les pixels d'une image. Le code suivant remplace tous les pixels transparents par du blanc :
img_pixel_t *px = img.pixels;
for(int y = 0; y < img.height; y++)
{
for(int x = 0; x < img.width; x++)
{
/* Et on fait ce qu'on veut avec px[x] */
if(px[x] == IMG_ALPHA) px[x] = C_WHITE;
}
px += img.stride;
}
Les pixels sont de type
img_pixel_t. La façon de les manipuler dépend de la plateforme visée :
• Sur Graph mono,
img_pixel_t est
uint8_t et ses valeurs sont les couleurs de
<gint/display.h>. La couleur transparente
IMG_ALPHA est égale à
C_NONE.
• Sur Graph 90+E,
img_pixel_t est
uint16_t et ses valeurs sont les couleurs RGB565. Les couleurs opaques de
<gint/display.h> peuvent être utilisées, à l'exception de
IMG_ALPHA = 0x0001 qui représente la transparence. Remarquez que
IMG_ALPHA n'est
pas égal à
C_NONE.
Citer : Posté le 20/06/2024 16:51 | #
Comment on le spécifie dans les métadonnées ? ^^'
- Kirby's DreamLand : Gobe , Gobe , Gobe !!!
- L'invasion Seanchans : Détruit la flotte ennemis a bord du "Danseur des vagues".
Citer : Posté le 20/06/2024 16:58 | #
Remplace build par install. Il faut installer aussi.
section: .data
Citer : Posté le 20/06/2024 17:10 | #
Merci beaucoup
Je viens de réussir à hflip l'image !
int j1 = l + i*layers + y * lw * layers;
int j2 = l + (lw-i-1)*layers + y * lw * layers;
J'ai rajouté le y dans la formule de calcul de j1 et j2 aussi
- Kirby's DreamLand : Gobe , Gobe , Gobe !!!
- L'invasion Seanchans : Détruit la flotte ennemis a bord du "Danseur des vagues".
Citer : Posté le 20/06/2024 17:20 | #
Oups, bien vu l'erreur !
Note que dans le shift je me suis trompé. Si l'image a une taille qui n'est pas multiple de 32 il y a initialement des zéros à droite. Après le flip les zéros sont à gauche, donc j'ai tout décalé vers la gauche. Mais j'ai oublié de transférer les bits perdus de chaque entier de 32 bits que je décale vers l'entier de 32 bits précédent.
Tu peux ignorer cette étape si ça te dérange pas que le padding soit à gauche après le flip.
Citer : Posté le 20/06/2024 17:25 | #
Oui, oui, j'ai vu ^^"
C'est pas un gros soucis à prendre en compte.
J'ai trouvé un moyen pour monitorer les fps de mon programme avec libprof.
Une idée de comment mesurer l'espace mémoire utilisé durant l'execution du programme ?
- Kirby's DreamLand : Gobe , Gobe , Gobe !!!
- L'invasion Seanchans : Détruit la flotte ennemis a bord du "Danseur des vagues".
Citer : Posté le 20/06/2024 17:27 | #
Pour la partie dynamique tu peux connaître l'espace utilisé/libre dans l'arène malloc() gérée par gint comme ceci :
kmalloc_gint_stats_t *stats = kmalloc_get_gint_stats(kmalloc_get_arena("_uram"));
stats->free_memory;
stats->used_memory;
Plus de détails dans kmalloc.h.
Si cette arène est pleine gint passe sur le malloc() système pour lequel tu ne peux malheureusement pas avoir de statistiques. Donc ce n'est une valeur précise que tant que l'arène est pas pleine.
Citer : Posté le 20/06/2024 17:33 | #
Super merci
- Kirby's DreamLand : Gobe , Gobe , Gobe !!!
- L'invasion Seanchans : Détruit la flotte ennemis a bord du "Danseur des vagues".