LibGD : affichage de cercles et d’arcs…

Aujourd'hui je devais changer la routines d'affichage de notre fonction "SoftRectangle", qui affiche des rectangles à bord arrondis, pour passer d'un affichage d'une couleur donnée à un affichage en utilisant un XOR (histoire que les contours marchent quelle que soit la couleur de fond).
Comme on utilisait de façon extensive les routines d'affichage de la libGD, ce n'était pas super compatible. Je me suis dit, un peu naïvement, que j'allais récupérer les sources, extraire les routines utilisées, et changer le pixel plot par du XOR... Et bien finalement quand j'ai vu tout ça j'ai préféré le refaire ^^
En fait, déjà, toutes les routines de dessin sont 100% génériques. Comprendre par là qu'un des paramètres d'une image est son type (truecolor, palette, etc...), et que bien entendu la façon d'écrire un pixel en dépend complètement. Du coup, quand on trace une ligne, pour chaque pixel à afficher on va utiliser la routine générique qui va regarder le type et appeler la bonne macro correspondante... Je ne sais pas si le compilo arrive à faire son boulot à fond pour optimiser tout ça, mais j'espère, sinon le résultat ne doit pas être terrible niveau perfs :s
Bref, je disais ça surtout pour la routine qui permet de tracer les arcs de cercle. Là j'ai eu un peu peur, car au final la routine ne fait que parcourir les angles possibles, degré par degré, et trace des lignes entre les différents points pour former le cercle... Forcément, pour mon XOR ce n'était pas terrible car chaque pixel était allumé/éteint une bonne dizaine de fois, donc c'était un poil trop aléatoire... La bonne optimisation que j'ai vue est l'utilisation d'une LUT en lien et place des cosinus/sinus, mais par contre pour ne pas dépasser, chaque accès dans la LUT nécessite un petit calcul de modulo 360 qui ne doit pas être gratuit :/
Bon, j'ai voulu faire mieux, à savoir un truc qui marchera dans mon cas et qui tournera, pour le coup, vraiment bien. Je me suis donc tourné vers Monseigneur Bresenham et son algorithme de tracé d'arc de cercle. Je ne vais pas rentrer trop dans les détails, mais en gros on ne va afficher les pixels qu'une fois, et en plus histoire d'aller super vite, pour chaque calcul de position, on l'utilise 8 fois (de façon assez logique on a 1 fois par cadran, donc ça fait déjà 4 fois, et en fait dans chaque cadran on peut imaginer partir d'un sens ou de l'autre (symétrie "en diagonale"), ce qui multiplie par 2
).
procédure tracerOctant (entier rayon, entier x_centre, entier y_centre) déclarer entier x, y, m ; x ←0 ; y ← rayon ; // on se place en haut du cercle m ←5-4*rayon ; // initialisation Tant que x <= y // tant qu'on est dans le second octant tracerPixel( x+x_centre , y+y_centre ) ; tracerPixel( y+x_centre , x+y_centre ) ; tracerPixel( -x+x_centre , y+y_centre ) ; tracerPixel( -y+x_centre , x+y_centre ) ; tracerPixel( x+x_centre , -y+y_centre ) ; tracerPixel( y+x_centre , -x+y_centre ) ; tracerPixel( -x+x_centre , -y+y_centre ) ; tracerPixel( -y+x_centre , -x+y_centre ) ; si m > 0 alors //choix du point F y ← y - 1 ; m ← m-8*y ; fin si ; x ← x+1 ; m ← m + 8*x+4 ; fin tant que ; fin de procédure ;
Là ça fait des cercles complets alors que je voulais juste faire des cadrans vite fait, mais en fait il m'a suffit de rajouter en paramètre la largeur et la hauteur de la boite à créer, et de rajouter des +w et des +h là où il faut (en gros en imagine qu'on dessine de base le cadran en haut à gauche, du coup les cadrans avec un x positif on doit leur ajouter +w, et les cadrans avec un y positif on leur ajoute +h...
Avec ça on obtient de façon rapide et efficace le tracé des 4 quart de cercle à moindre frais
Cet algo ne présente qu'une limite, qui fait que je vais probablement voir pour le remplacer par un autre : Si on affiche 2 cercles concentriques dans le but de faire un tracé 2 fois plus épais... on a des trous ! Il n'a pas été fait pour ça, et donc il faudra alors se tourner vers un autre algo, tel quel algorithme de tracé de cercle d'Andres.
Pixel plot, optimisation 2
Après un premier billet sur le pixel plot en alpha un poil technique, je reviens pour un billet un peu plus simple mais toujours très efficace. Cette optimisation peut, de plus, être combinée à la précédente pour améliorer d'autant plus la vitesse d'affichage.
Version naïve
Voici une boucle d'affichage, version "naïve" :
for (int y=0; y<height; y++){
for (int x=0; x<width; x++){
if (alpha[x+y*width] == MAX_ALPHA){
dst[dest_x + x + (dest_y + y) * dest_width] = color[x + y*width];
} else if (alpha[x+y*width] > 0) {
idem mais avec le code alpha
}
}
}
J'ai vraiment mis la version de base, sans utiliser de pointeurs ni rien. Idéalement il faudrait calculer les pointeurs de la source et de la destination juste avant la ligne 2, et ensuite faire du pointer++, mais ce n'est pas le sujet de ce billet.
On voit ici plusieurs qu'afin de ne pas utiliser le code alpha pour rien (car il reste lent pour rien quand on n'en a pas besoin), on a en fait 3 cas de figures :
- Pixel plein, on affiche bêtement
- Pixel avec alpha, on affiche avec le code lent
- Alpha = 0, on ne fait rien
Si on a une image avec beaucoup de vide, on va passer beaucoup de temps à boucler et faire des if pour rien. Si on a une image qui ne contient que des pixels pleins, pareil, la perte de temps est monstrueuse. Il faudrait donc pouvoir améliorer ce comportement.
RLE power
En fait, ce dont on aurait besoin pour améliorer la vitesse, c'est de ne pas vérifier systématiquement le niveau d'alpha des pixels à afficher. Si on savait à l'avance, autant que possible, ce qu'on doit faire, alors on peut gagner pas mal de temps.
Je trouve qu'il est important d'avoir de bonnes notions des algos qui existent dans un peu tous les domaines, car in fine on ne sait jamais où et quand des versions analogues pourraient servir. Ici on va reprendre le principe de la compression RLE, et la mettre à profit pour l'affichage ^^
La compression RLE (Run Length Encoding) est une des compression les plus basiques qui soit, très facile à comprendre : en gros, l'idée est que si dans une image on a souvent les mêmes pixels à la suite, alors plutôt que d'écrire chaque pixel dans le fichier, on pourrait écrire un pixel, puis le nombre de fois qu'il apparait. Ca marche particulièrement bien pour des images avec peu de couleurs (palettes 2, 4, ou 16 couleurs par exemple), moins bien pour le reste puisqu'on a beaucoup plus de variations, mais le débat n'est pas là ^^
Ici, en plus de notre tableau avec les couleurs et de celui avec l'alpha (qui peuvent être en fait un seul et même tableau), on va donc se rajouter un tableau de type RLE. Ce qu'il va contenir, c'est le nombre de pixels à la suite qui ont le même type d'alpha. Par type, j'entends les 3 catégories qu'on a énoncé précédemment : pixel opaque, pixel semi-transparent, ou pixel invisible.
Grâce à ça, on fait un premier test pour savoir le type de pixel qu'on va afficher, mais au lieu d'en afficher un seul, on va introduire une nouvelle boucle dans le if qui va permettre d'afficher très rapidement tous les pixels du même type.
- Pour les pixels opaque, on peut même envisager de faire un memcpy si on a suffisamment de pixels opaques à la suite. Le code devient alors super simple et rapide.
- Pour les pixels semi-transparents, pas de miracle, mais on économisera pour chaque pixel le if...
- Pour les pixels invisible/transparents, là c'est génial, car on peut directement tous les sauter d'un coup. Et là, quand on a des images avec pas mal de vide (des icones par exemple), le gain est assez monstrueux.
Je ne poste pas de code (sauf si vous me le demandez) pour illustrer ça, car il y a pas mal de façon de faire et que pour que ce soit beau et rapide il faudrait passer par des pointeurs dans tous les sens (contrairement à la version naïve).
Discussion
Cette optimisation est-elle toujours efficace, ou alors peut-elle être mise à défaut ? De façon assez logique, on voit que si dans une image on a une alternance très rapide du type de niveau d'alpha (par exemple un damier de pixels), alors on perd pas mal de temps : au lieu d'avoir juste 1 if dans la boucle, on va avoir un if plus une boucle dedans (qui n'aura aucun intérêt pour 1 pixel), et du coup on y perd un peu. Ca reste relativement rare, mais ça pourrait arriver si on a énormément de toutes petites images à afficher. Dès que les images sont de taille correcte (plus que 8x8, en gros), alors le gain l'emporte largement.
Le gain est aussi d'autant plus important que le nombre de pixels semi-transparents est faible. Ceci est du au fait que le gain est plus massif pour les pixels opaque et transparents que pour les semi-transparents. Mais même avec beaucoup de pixels semi-transparents, on y gagne quand même.
Quant à comment implémenter le calcul du RLE, il y a une façon relativement simple et efficace de le faire : quand on précalcule les niveaux d'alpha, on va partir pour chaque ligne de la droite plutôt que de la gauche. On met la valeur du RLE à 1. Et là, l'algo est tout con :
- Si le pixel a le même type d'alpha, on incrémente la valeur du RLE et on la stocke. Si on ne prend que 8 octets pour stocker le RLE d'un pixel, alors il faut vérifier qu'on ne dépasse pas 255 ^^ (sinon on bloque à 255)
- Si le pixel a une valeur différente, on met à 1
Pas grand chose de plus à dire. Ah si, il faut préciser qu'on perd quand même encore quelques octets par image (selon la taille de l'image), donc c'est un choix à faire entre utilisation CPU et utilisation de RAM.
On peut en fait optimiser la place que ça prend en RAM (c'est le choix que j'avais fait sur PocketPC), mais ce n'est pas gratuit : au lieu de stocker la valeur de RLE pour chaque pixel, on peut se contenter de les stocker uniquement pour le premier pixel d'un type d'alpha donné, comme dans la compression RLE d'origine. Le gain de place peut être important, mais les conséquences sur le code sont assez importantes : si on a une image partiellement hors de l'écran, on ne va pas pouvoir clipper directement en lisant le bon pixel, et on va devoir en fait faire comme si on partait du début de l'image, dérouler le RLE, mais sans faire l'affichage. Sur de grosses images partiellement hors de l'écran on va perdre pas mal de temps comme ça, et la routine d'affichage devient inutilement complexe (pour gérer la portion hors de l'écran), alors qu'avec la version décrite ci-dessus le code reste très simple et compact.
L'idée derrière cette optimisation, comme beaucoup d'autre, est au final de précalculer un maximum de choses au chargement, de stocker des informations sur ces précalcule, pour les réutiliser intelligemment par la suite.
Pixel plot alpha optimisé
Bon, je n'ai pas eu le temps de faire un billet hier, j'espère me rattraper aujourd'hui avec une astuce que j'avais trouvée assez géniale pour écrire en 15bit des pixels avec niveaux d'alpha de façon super rapide. Je l'avais utilisé il y a quelques années sur PocketPC et les performances étaient hallucinantes.
Dithering : méthode de Sierra
Un livre électronique a un gros défaut : son écran monochrome, qui ne présente que peu de niveaux de gris. Hier je bossais donc sur améliorer le rendu (en terme de vitesse et de qualité), sachant que le résultat final n'est codé que sur 3bits (8 niveaux de gris).
Je vais donc vous présenter la méthode de dithering qu'on a utiliser pour avoir un meilleur rendu : celle de Sierra. Petit schéma pour illustrer le but du jeu :
Image d'origine![]() |
Méthode de Sierra![]() |

