Générateur d'images fractales (2)

05022009 In: Pas à pas

Cet article est le deuxième d'une série. Je vous recommande de commencer par le premier article, et même de le laisser ouvert pour pouvoir suivre au mieux celui-ci.

Cette fois-ci, nous entrons dans le vif du sujet, avec une première version du générateur. C'est parti !

Mise en place du projet

  • Créez un projet de type Cocoa Document-Based.
    Notre application devra tôt ou tard enregistrer et charger des documents.

  • Créez un fichier source de type Objective-C NSView subclass, appelé CFRMandelbrotView.
    Cette vue personnalisée effectuera le calcul et l'affichage.

  • Ouvrez MyDocument.nib dans Interface Builder. Dans la fenêtre Window se trouve un texte. Effacez-le.

  • Glissez une Custom View dans la fenêtre. Vous la trouverez dans la palette Library > Cocoa > Views & Cells.

  • Dans l'onglet View Size de l'Inspecteur, réglez les dimensions (W et H) de la vue à 400 x 300 pixels.

  • Dans l'onglet View Identity mettez la classe à CFRMandelbrotView.

Enregistrez, et revenez à XCode.
Pour simplifier, c'est la vue qui effectuera le calcul. Nous brisons le paradigme MVC, mais ce n'est pas bien grave pour ce programme d'essai.

Gestion des nombres complexes

Le langage Objective-C ne gère pas les nombres complexes. Nous allons devoir coder cela nous même. Pour commencer, déclarons dans CFRMandelbrotView.h un type "nombre complexe", basé sur une structure:

typedef struct Complexe_s
{
    double reel;    // Partie réelle
    double imag;    // Partie imaginaire
} Complexe_t;

Ajoutez également le prototype de la fonction qui permet d'élever un nombre complexe au carré:

Complexe_t Carre(Complexe_t z);

Passons maintenant à CFRMandelbrotView.m, pour y écrire la fonction:

Complexe_t Carre(Complexe_t z)
{
    Complexe_t carre;

    carre.reel = z.reel*z.reel - z.imag*z.imag;
    carre.imag = 2.0 * z.reel * z.imag;

    return carre;
}

Génération et affichage

Nous allons maintenant écrire la méthode -drawRect:. Cette méthode est appelée à chaque fois qu'il faut rafraîchir la vue. En pratique, dans ce premier programme, elle ne sera appelée que lors du premier affichage.

Création de la bitmap

Les bitmaps sont représentées par la classe NSBitmapRep. Créons une instance et initialisons-là:

NSBitmapImageRep* bitmapRep = [[NSBitmapImageRep alloc]
    initWithBitmapDataPlanes:NULL
    pixelsWide:400
    pixelsHigh:300
    bitsPerSample:8
    samplesPerPixel:1
    hasAlpha:NO
    isPlanar:NO
    colorSpaceName:NSDeviceWhiteColorSpace
    bytesPerRow:0
    bitsPerPixel:8];

Voilà une méthode avec beaucoup de paramètres !

  • Le premier paramètre est mis à NULL. Nous voulons que la méthode réserve la mémoire pour la bitmap elle-même.
  • pixelsWide et pixelsHigh sont les dimensions de la bitmap, en pixels.
  • samplesPerPixel (échantillons/pixel)
    Un échantillon est une composante de la couleur. Par exemple, une image RVB possède trois échantillons: un Rouge, un Bleu et un Vert. Comme nous générons une image en niveaux de gris, il n'y a qu'un échantillon par pixel.
  • bitsPerSample (bits/échantillon)
    Nous générons une image en 256 niveau de gris, ce qui requiert 8 bits/échantillon.
  • hasAlpha
    Indique si l'image possède une couche alpha (opacité). Dans notre cas, non.
  • isPlanar
    Il est possible de gérer plusieurs couches de couleurs séparément (une couche rouge, une verte, une bleu, une alpha). Pas dans notre cas.
  • colorSpaceName (nom de l'espace de couleur)
    Nous utilisons l'espace de couleur NSDeviceWhiteColorSpace, qui nous permet d'avoir la valeur maximale blanche. Vous pourrez essayer avec NSDeviceBlackColorSpace si vous voulez.
  • bytesPerRow (octets par ligne) Nous mettons à 0 pour que la méthode calcule cette valeur elle-même en fonction des autres paramètres.
  • bitsPerPixel
    C'est toujours 8 bits/pixel.

Calcul de l'ensemble

// Parcourir tous les points de la bitmap
double x, y;
for(x = 0; x < 400; x++)
{
    for(y = 0; y < 300; y++)
    {

Nous parcourons tous les pixels de la bitmap.

        // Convertir les coordonnées
        Complexe_t c;
        c.reel =  x/100.0 - 2;
        c.imag = -y/100.0 + 1.5;

Nous convertissons les coordonnées x et y du repère de la bitmap vers celui du plan mathématique.

        // Initialiser z[0]
        Complexe_t z = {0.0, 0.0};

Le calcul de chaque pixel commence par la mise à zéro de c.

        NSUInteger n;
        for(n=0; n < MAX_ITERATIONS; n++)
        {
            // z[n+1] = z[n+1]^2 + c
            Complexe_t zCarre = Carre(z);
            z.reel = zCarre.reel + c.reel;
            z.imag = zCarre.imag + c.imag;

            // La suite diverge si |z| > 2
            double moduleAuCarre = z.reel*z.reel + z.imag*z.imag;
            if(moduleAuCarre > 4.0)
                break;
        }

Nous recherchons ensuite combien il faut d'itérations pour que la suite diverge. Remarquez une optimisation ici: au lieu de tester si le module
√(a^2 + b^2) > 2
nous testons si
(a^2 + b^2) > 4

une racine carrée étant un calcul très coûteux en temps machine. Le code reste toutefois peu efficace: son amélioration est prévue dans un prochain article. La priorité est ici donnée à sa lisibilité.

        // Donner le niveau de gris au pixel
        NSUInteger nuance = n * 255 / MAX_ITERATIONS;

J'ai remanié la formule donnée dans l'article précédent. La division est faite en dernier, ce qui permet de faire tous les calculs sur des entiers (ce qui évite les casts et accélère un peu).

        [bitmapRep setPixel:&nuance atX:x y:y];
    }
}

Et enfin, nous écrivons la nuance dans la bitmap. Vous remarquerez que le premier paramètre de setPixel: est un tableau d'entiers. Étant donné que nous travaillons avec un seul échantillon par pixel, le tableau n'a qu'un seul indice, et il revient donc au même de passer un pointeur sur nuance.

Affichage

Affichons la bitmap dans la vue:

// Afficher la bitmap
[bitmapRep drawAtPoint:NSMakePoint(0,0)];

Rappelez-vous que l'origine de la vue se trouve dans le coin inférieur gauche.

Et finalement, il ne faut pas oublier de libérer la mémoire:

[bitmapRep release];

Le résultat

Lancez le programme. La fenêtre doit s'ouvrir avec l'ensemble dessiné.

L'ensemble généré

C'est beau !

La suite

Essayez différentes valeurs de MAX_ITERATIONS. Le rendu sera différent. Nous ajouterons bientôt des contrôles pour changer les paramètres du calcul.

Pour l'instant, la nature fractale de l'ensemble ne saute pas aux yeux, mais en observant attentivement, on retrouve tout de même un peu la forme de la "tête" sur les deux "ailes".
Une des améliorations à apporter sera justement la possibilité de se déplacer dans le plan, en particulier, de zoomer.

Le projet XCode complet à télécharger.

Renaud Pradenc
Céroce.com

Articles similaires