Au sanglier codeur

Balance des couleurs et masquage

Introduction

Dans ce chapitre nous allons apprendre à réaliser une balance des couleurs et un masquage sur une surface. (D'où le titre ^^ )

Ces fonctionnalitées seront directement intégrées au code de CSurface, ce seront des méthodes de cette classe.

Récupérer un accès sur les pixels

Depuis le début du tuto on utilise sans arrêt des SDL_Surface.
Vous savez déjà qu'il s'agit d'un type de variable pouvant stocker une surface.

Mais que se cache vraiment derrière ce type ?

Nous n'allons pas chercher la réponse bien loin, une petite visite dans la doc et le tour est joué.

typedef struct SDL_Surface
{
	Uint32 flags;                           /* Read-only */
	SDL_PixelFormat *format;                /* Read-only */
	int w, h;                               /* Read-only */
	Uint16 pitch;                           /* Read-only */
	void *pixels;                           /* Read-write */
	SDL_Rect clip_rect;                     /* Read-only */
	int refcount;                           /* Read-mostly */
} SDL_Surface;


Nous n'allons pas tout expliqué mais uniquement ce qui est utile à la compréhension de la fonction de modification des pixels.

Cette fonction est directement tirée de la doc de la SDL.
Elle renvoie un pixel de la SDL_Surface passée en paramètre en fonction de sa position en X et en Y.

/*01*/    Uint32 getpixel(SDL_Surface *surface, int x, int y)
/*02*/    {
/*03*/        //On récupère la profondeur d'un pixel
/*04*/        int bpp = surface->format->BytesPerPixel;
/*05*/        //On récupère un pointeur vers le pixel qui nous intéresse.
/*06*/        Uint8 *p = (Uint8 *)surface->pixels + y * surface->pitch + x * bpp;
/*07*/        //Ce switch assure la portabilité entre les différentes profondeurs et architectures processeur.
/*08*/        switch(bpp)
/*09*/        {
/*10*/        case 1:
/*11*/            return *p;
/*12*/        break;
/*13*/        case 2:
/*14*/            return *(Uint16 *)p;
/*15*/        break;
/*16*/        case 3:
/*17*/            if(SDL_BYTEORDER == SDL_BIG_ENDIAN)
/*18*/                return p[0] << 16 | p[1] << 8 | p[2];
/*19*/            else
/*20*/                return p[0] | p[1] << 8 | p[2] << 16;
/*21*/        break;
/*22*/        case 4:
/*23*/            return *(Uint32 *)p;
/*24*/        break;
/*25*/        default:
/*26*/            return 0;       //Ne devrait pas arriver mais evite les warnings
/*27*/        break;
/*28*/        }
/*29*/    }


Vous remarquerez donc qu'un pixel est stocké dans une variable Uint32.

Si vous n'êtes pas au point sur les pixels ou que vous avez du mal à comprendre ce qui suit, je vous conseil la lecture du chapitre en annexe consacré justement aux pixels : Les pixels

Le principe du code de la fonction est d'aller chercher le pixel en fonction de sa position en X et Y sur la surface, mais comme vous l'aurez remarqué, le tableau n'est pas à 2 dimensions lui.
Comment faire alors pour retrouver sa position dans le tableau ?

Et bien c'est assez simple en fait, il suffit de connaître la manière dont les pixels sont rangés et la place qu'ils occupent.

La ligne 4 se charge de connaître la place qu'ils occupent, bpp contiendra le nombre d'octets.
Quand à leur agencement, il ne change jamais, le premier pixel stocké est celui le plus en haut à gauche, le suivant est celui immédiatement à sa droite, et ainsi de suite jusqu'à la fin de la ligne.
Le suivant sera alors celui le plus à gauche de la ligne juste en dessous. Et ça recommence jusqu'à la fin de l'image.

Il reste une dernière chose à voir : pitch.
pitch représente la profondeur d'une ligne horizontale de pixel de l'image. La formule pour le calculer est simple : pitch = w * BytesPerPixel.

L'unité du tableau de pixel n'est pas le pixel en lui même mais l'octet.
Si la profondeur d'un pixel est d'un octet alors une case du tableau contiendra un pixel, presque par "coup de chance".
Si la profondeur d'un pixel est de 32 bits, alors une case du tableau contiendra une composante du pixel.
Ce qui explique qu'il faille multiplier Y par le pitch (soit w * BytesPerPixel) et X par BytesPerPixel. (Ligne 6)

(surface->pixels + y * surface->pitch) donne (surface->pixels + y * w * BytesPerPixel).
Selon l'ordre des opérations, on commence par les multiplications.
pitch nous donne le nombre d'octets dans une ligne, on le multiplie par la position en Y du pixel et on ajoute le tout à la position du pixel de base, en haut à gauche
Résultat des courses : on se trouve sur la colonne la plus à gauche, sur la ligne du pixel à récupérer.

Il ne reste plus qu'à avancer du bon nombre de pixel en X et le tour est joué.
C'est précisement le rôle du reste de la ligne : + x * bpp;
Je rappelle qu'on multiplie par le nombre d'octet par pixel car les cases du tableau ne contiennent pas un pixel chacune mais un octet.

Il doit rester une chose qui doit vous turlupiner, ce sont ces casts de pointeur.
Cela vient du fait qu'il existe plusieurs profondeur de codage pour les pixels (nous ne vous apprenons rien, c'est le fameux BytesPerPixel).
Comme un pointeur pointe sur une zone équivalente à sa taille, on vérifie la taille des pixel pour adapter la taille du pointeur en conséquence.

Balance des couleurs

Déjà, qu'est-ce qu'une balance des couleurs ?
Il s'agit d'équilibrer (ou plutôt de modifier l'équilibre existant) les 3 composantes de couleurs de l'image. (rouge, vert et bleu)

Par exemple :

L'image originale On a un peu forcé sur le rouge On a un peu forcé sur le bleu

L'image originale se trouve tout à gauche, puis les valeurs de modification de chaque composantes sont indiquées sur les 2 suivantes. (Les images sont clicables)

Comme vous l'aurez peut être deviné, nous allons passer 3 valeurs en paramètre à la fonction, et ces valeurs seront ajoutées aux valeurs actuelles des composantes pour chaque pixel.
Ce sont ces valeurs que nous avons indiqué entre parenthèses sur les images.

Le principe de l'algorithme utilisé pour ce faire est simplissime :

Pour chaque pixel de la surface
|---récupérer le pixel
|---Récupérer les composantes dans des variables séparées
|---Pour chaque composante
|---|---ajouter la valeur de la balance à la composante associée
|---Fin pour
|---Refaire un pixel avec les nouvelles composantes
|---Remplacer l'ancien pixel source par le nouveau
Fin pour

Et voila le code source correspondant :

/* 01 */  Uint32 pixSource;
/* 02 */  Uint8 sR, sG, sB, sA;
/* 03 */  SDL_LockSurface(m_baseSurf);
/* 04 */  //On passe tous les pixels en revue
/* 05 */  for (int y = 0; y < m_baseSurf->h; ++y)
/* 06 */  {
/* 07 */      for (int x = 0; x < m_baseSurf->w; ++x)
/* 08 */      {
/* 09 */          //On recupere le pixel de l'image source.
/* 10 */          pixSource = getPixel(x,y);
/* 11 */          //On recupere les composantes du pixel.
/* 12 */        	  SDL_GetRGBA(pixSource, m_baseSurf->format,
/* 13 */                      &sR, &sG, &sB, &sA);
/* 14 */          //Ces 8 lignes servent a  eviter les depassements.
/* 15 */          sR = addChannel(sR, rModif);
/* 16 */          sG = addChannel(sG, gModif);
/* 17 */          sB = addChannel(sB, bModif);
/* 18 */        	  //On remet tout dans le pixel.
/* 19 */          pixSource = SDL_MapRGBA(m_baseSurf->format,
/* 20 */                                  sR, sG, sB, sA);
/* 21 */          setPixel(x, y, pixSource);
/* 22 */      }
/* 23 */  }
/* 24 */  SDL_UnlockSurface(m_baseSurf);

Analysons le fonctionnement de cette fonction :

A la première ligne, nous créons une variable Uint32 qui va contenir le pixel à modifier, puis à la ligne suivante les 4 variables qui vont nous permettre de travailler sur les composantes de ce pixel.

La ligne 3 "locke" la surface, ce qui signifie que nous allons pouvoir modifier directement le tableau de pixels associé à la surface.

Les lignes 5 et 7 sont les boucles qui vont nous permettre de parcourir tout le tableau, pour chaque ligne de la surface (y), on parcourt toutes les colonnes (x).
Et on parcourt toutes les lignes de la surface avec la boucle ligne 5.

On entame les choses sérieuses à la ligne 10, où on récupère un accès vers le pixel désigné par notre position dans les boucles.
On copie ce pixel dans notre variable de la ligne 1 puis on en extrait la valeur de chaque composante dans les variables créées à la ligne 2 grâce à la fonction SDL_GetRGBA.

Les 3 lignes suivantes font appel à une fonction qui ajoute les 2 valeurs passées en paramètre en vérifiant que le résultat ne dépasse pas 255 et ne soit pas inférieur à 0.

Maintenant que nous avons fait les modifications sur les composantes, il faut nous en servir pour reconstituer le pixel.
C'est le rôle des lignes 19-20 qui utilisent une fonction toute faîte de la SDL pour cela.

La dernière chose à faire est de remplacer le pixel d'origine, celui qui est dans le tableau, et qui sera affiché à l'écran.
Une fois que c'est fait, on "délocke" la surface et c'est bon. :)

Masquage

Jetez un oeil au screen ci-dessous si vous ne voyez pas bien en quoi consiste un masquage.

L'image originale Le masque L'image masquée

Le principe est qu'on masque certaines composantes d'une image pour les remplacer par celles d'une autre.
En l'occurrence, nous masquons la composante bleu de la première image par celle de la 2eme.
Ce qui explique que le texte soit visible en bleu sur l'image finale (tout à droite) et que le fond de celle ci soit devenue jaune.
Etant blanc-gris à la base, si on masque sa composante bleu à 0 (le noir du fond de l'image masque (celle du milieu)), il ne reste que les composantes vertes et rouges qui font du jaune quand elles sont mélangées.

Pour pour identifier les composantes qu'il faut masquer, nous allons passer 4 booléens à la fonction.
L'algorithme pour réaliser cela est sensiblement identique à celui de la balance des couleurs :

Pour chaque pixel de la surface
|---Récupérer le pixel de la surface source
|---Récupérer le pixel correspondant du masque
|---Extraire les composantes du pixel source
|---Extraire les composantes du pixel du masque
|---Pour chaque composante du pixel source
|---|---Masquer avec la composante du masque si le paramètre l'indique
|---Fin pour
|---Refaire un pixel avec les nouvelles composantes
|---Remplacer l'ancien pixel source par le nouveau
Fin pour

Voyons maintenant le code correspondant :

/* 01 */void CSurface::mask(CSurface &mask, bool isRMask,
/* 02 */                    bool isGMask, bool isBMask, bool isAMask)
/* 03 */{
/* 04 */  if ((m_position.w != mask.getPos().w) || (m_position.h != mask.getPos().h))
/* 05 */      CInternalLogger::getInstance()->logToFile("Dimensions are not the same");
/* 06 */  else
/* 07 */  {
/* 08 */      Uint32 pixSource, pixMask;
/* 09 */      Uint8 sR, sG, sB, sA, mR, mG, mB, mA;
/* 10 */      SDL_LockSurface(m_baseSurf);
/* 11 */      SDL_LockSurface(mask.getSurface());
/* 12 */      //On passe tous les pixels en revue.
/* 13 */      for (int y = 0; y < m_baseSurf->h; ++y)
/* 14 */      {
/* 15 */          for (int x = 0; x < m_baseSurf->w; ++x)
/* 16 */          {
/* 17 */              pixSource = getPixel(x,y);
/* 18 */              pixMask = mask.getPixel(x,y);
/* 19 */              SDL_GetRGBA(pixSource, m_baseSurf->format,
/* 20 */                          &sR, &sG, &sB, &sA);
/* 21 */              SDL_GetRGBA(pixMask, mask.getSurface()->format,
/* 22 */                          &mR, &mG, &mB, &mA);
/* 23 */              //On masque les composantes du pixel source en fonction des parametres.
/* 24 */              sR = (isRMask) ? mR : sR;
/* 25 */              sG = (isGMask) ? mG : sG;
/* 26 */              sB = (isBMask) ? mB : sB;
/* 27 */              sA = (isAMask) ? mA : sA;
/* 28 */              pixSource = SDL_MapRGBA(m_baseSurf->format,
/* 29 */                                      sR, sG, sB, sA);
/* 30 */              setPixel(x, y, pixSource);
/* 31 */          }
/* 32 */      }
/* 33 */      SDL_UnlockSurface(m_baseSurf);
/* 34 */      SDL_UnlockSurface(mask.getSurface());
/* 35 */  }
/* 36 */}

Le code est un peu plus long que pour la balance des couleurs car il y a 2 surfaces à gérer cette fois.
Et d'ailleurs si ces 2 surfaces ne font pas exactement les mêmes dimensions, tout tombe à l'eau. C'est pourquoi on le vérifie dès le début (lignes 4-5) et on inscrit le problème dans le logger interne si jamais il y en a un.

On retrouve les mêmes déclaration de variables lignes 8 et 9, sauf qu'elles sont doubles.
De même, on locke les surfaces (lignes 10 - 11) et on fait 2 boucles imbriquées (lignes 13 - 15) pour parcourir tous les pixels.
On continue en récupérant les pixels de chaque surface (lignes 17 - 18) et en en extrayant les composantes (lignes 19 à 22).

Et c'est là que ça devient intéressant, des lignes 24 à 27 :
Ces 4 lignes sont des ternaires qui vont nous permettre de masquer les composantes du pixel source en fonction des paramètres de la fonction.
Par exemple la ligne 24 donne ceci en français : si isRMask est true, sR = mR, sinon sR = sR.
C'est la même chose qu'un if-elsif classique mais en plus court. :)

Une fois que c'est fait, on recompose un pixel (lignes 28 - 29) et on le met à la place de l'ancien pixel source (ligne 30).
Il ne reste plus qu'à délocker les surfaces (lignes 33 - 34) et le tour est joué. ;)

Conclusion

La balance des couleurs et le masquage sont des fonctionnalitées relativement "spécialisées". Nous entendons par là quelles ne vont pas servir sans arrêts dans le jeu.
Néanmoins elles permettent de réaliser de très jolies choses, par exemple de très beaux menus, et méritent par conséquent qu'on prenne la peine de les coder. ^^


Valid XHTML 1.0 Strict - Le Sanglier Codeur, par GuilOooo & Kevin Leonhart - Remonter en haut - Valid CSS !