Par Thomas WINLING, DEUG TI 2, Juin 1998
La création graphique en C constitue un aspect méconnu de la programmation. En effet, de par sa complexité apparente et vu le peu d'ouvrages qui lui sont consacrés, l'infographie est perçue comme une obscure branche, peu utile et peu utilisée, du langage C.
Pour mieux comprendre ces notions graphiques, il faut d'abord souligner leur lien avec le système de l'ordinateur : les performances graphiques découlant directement du matériel utilisé, elles ne seront pas équivalentes sur chaque machine. Ceci leur confère une propriété de spécificité qui, se rajoutant aux préjugés quant à la difficulté de dessiner en C, freine l'intérêt de ceux qui voudraient s'y initier.
Ces particularités mises à part, la programmation graphique ne diffère pas fondamentalement des méthodes classiques puisque la notion de récursivité, par exemple, y est importante.
Afin de faire découvrir et apprécier cet aspect de la programmation, je vous propose donc une vision globale des qualités artistiques que vous pourrez développer à l'aide du langage C ainsi que, en guise d'extrapolation, une approche des objets fractals, véritables curiosités mathématiques.
L'image affichée sur votre moniteur est générée point par point (affichage matriciel) grâce au faisceau d'électrons parcourant l'écran ligne par ligne. Le composant spécifique, chargé de fournir à l'écran divers signaux vidéo est (vous l'aurez deviner !) la carte graphique.
Pour chaque point, la carte cherche les informations relatives à la couleur dans la mémoire écran. A chaque pixel correspond un emplacement de cette mémoire qui concerne les données d'un point applicables d'un balayage à l'autre.
Il est important de comprendre le processus permettant au système graphique de déterminer les pixels qui doivent être modifier pour tracer, par exemple, une ligne à l'écran. Cela suppose tout d'abord l'existence d'une gestion non spécifique de la carte graphique par l'intermédiaire des fonctions graphiques de la toute puissante bibliothèque " GRAPHICS.LIB " (si vous ne l'incorporer pas dans votre programme, inutile d'espérer tracer même une ligne...). Ces nombreuses fonctions font elles-mêmes appel à un gestionnaire graphique, ou driver, dédié à la carte.
Ce mode de dessin, particulièrement facile (si !), se paie par une relative lenteur d'exécution et dans quelques cas, le programmeur, pour gagner en rapidité, devra trouver lui-même ses propres algorithmes (là, c'est beaucoup plus délicat).
La mémoire écran permet de stocker plusieurs fois le contenu d'un écran. On dit que la carte gère alors différentes pages et il suffit de passer de l'une à l'autre pour changer l'affichage au prochain balayage. L'intérêt ? C'est comme cela que vous obtenez de sympathiques effets d'animations.
Par ailleurs, les cartes graphiques sont capables de travailler avec plusieurs résolutions et un certain nombre de couleurs par pixel. Ces configurations répondent au doux nom de modes graphiques, eux-mêmes gérés par les drivers. L'utilisateur détermine un mode qui lui convient, en faisant le bon compromis entre résolution et couleurs.
Borland propose dix gestionnaires standards, intégrant en eux-mêmes plusieurs modes. Chaque gestionnaire " cible " un type de cartes spécifique. Vous trouverez la liste de ceux-ci dans l'aide du C++, à l'index graphics_modes. Les plus importants se trouvant dans le fichier EGAVGA.BGI et concernent les modes EGA-VGA se rapportant aux cartes les plus courantes actuellement. Si vous ne disposez pas d'une telle carte (remplacez-la d'ailleurs !), il faudra choisir un gestionnaire adapté avec toutes les surprises que cela entraîne à l'écran (j'espère que vous n'êtes pas détenteur d'une de ces antiques cartes CGA, véritables dinosaures de la micro).
Il convient bien sûr, dans un premier temps, d'inclure le fichier graphics.h à votre programme. La mise en marche de l'ordinateur place toutes les cartes graphiques en mode texte (25x80). Il faut alors passer en mode graphique par la fonction initgraph, qui charge également gestionnaires et modes graphiques :
void initgraph (int *graphdriver, int *graphmode, char
*pathtodriver)
*graphdriver : désigne la valeur du gestionnaire désiré
*graphmode : mode choisi
*pathtodriver : chaîne de caractères indiquant le chemin à suivre pour trouver le fichier d'extension BGI contenant le gestionnaire. Pour plus de confort, on supposera systématiquement que ces fichiers se trouvent dans le répertoire courant (sinon, empressez-vous de recopier le répertoire contenant ces gestionnaires dans votre répertoire de travail)
Exemple d'initialisation :
/* non directement exécutable */ #include "graphics.h" void main(void) { int gestionnaire=VGA, /* on cible une carte VGA */ mode=VGAHI; /* choix du mode 640x480,16 couleurs */ initgraph(&gestionnaire,&mode,"") ; /* suite du programme, l'écran est en mode graphique */ }
NB : l'argument *pathtodriver est normalement du type char, ce qui n'est pas le cas de la chaîne de caractères "" qui est une constante. Ceci n'engendre pas d'erreur dans la mesure où la fonction initgraph n'écrit pas dans la zone pointée par l'argument.
Il existe une astuce bien utile (même si elle peut vous générer des erreurs) qui permet de déterminer la carte rattachée au système si vous ne connaissez pas votre carte (quelle honte !) ou pour répondre à d'évidentes attentes de compatibilité entre ordinateurs. Il suffit d'affecter la valeur DETECT à l'argument *graphdriver.
Exemple de détection automatique :
/* non directement exécutable */ #include "graphics.h" void main(void) { int gestionnaire=DETECT, /* détection auto */ mode; /* on ne donne plus directement de valeur au mode */ initgraph(&gestionnaire,&mode,"") ; /* suite du programme, l'écran est en mode graphique */ }
On peut également déterminer la nature de la carte indépendamment de son initialisation par :
void detectgraph (int *graphdriver, int *graphmode)
Ceci pouvant vous permettre de gérer vous-mêmes l'initialisation graphique à la suite de cette fonction. D'ailleurs, si vous désirez en savoir un peu plus sur votre matériel, observez le programme CHECK (dont le listing est également disponible dans l'aide)...
#include"graphics.h" #include"conio.h" #include"stdlib.h" #include"stdio.h" char *dname[]={"detection demandée","CGA","MCGA","EGA","64 K EGA", "monochrome EGA","IBM 8514","Hercule monochrome","AT&T 6300 PC", "VGA","IBM 3270 PC"}; int main (void) { int gdriver, gmode, errorcode; detectgraph(&gdriver,&gmode); errorcode=graphresult(); if (errorcode!=grOk) { printf ("Graph error : %s\n",grapherrormsg(errorcode)); printf ("Press"); getch(); exit(1); } clrscr(); printf("Votre carte est du type %s, vous en avez de la chance!\n",dname[gdriver]); printf("\nAppuyez sur une touche..."); getch(); return 0; }
Si des erreurs graphiques surviennent dans votre programme (personne n'est parfait...), sachez qu'une variable interne contient la valeur affectée par le dernier appel fautif réalisé. Son accès n'étant pas direct, il faut le recours de la fonction graphresult :
int graphresult (void)
Cette fonction vous renverra un entier correspondant à un certain type dont vous trouverez la liste complète et les descriptions à l'index graphics_errors de l'aide. graphresult, une fois appelée, remet la variable interne à zéro, ce qui correspond en fait à l'absence d'erreur.
Si vous voulez connaître la nature de l'erreur, utilisez :
char *grapherrormsg (int errorcode)
Cette fonction vous fournira une description qui peut être utilisée de la sorte :
/* à incorporer */ int code ; if ((code=graphresult()) !=0) { printf ("Erreur : %s",grapherrormsg(code)) getch() ; exit (1) ; }
Une erreur vous fera alors sortir du programme.
Afin de repasser en mode texte et de rétablir efficacement la mémoire, il est nécessaire, une fois toutes les opérations graphiques effectuées, de " fermer " le système graphique par :
void closegraph (void)
Il est possible, pour diverses raisons obscures (afficher une grande quantité de texte pour une aide en ligne notamment) de réaliser une sortie temporaire par :
void restorecrtmode (void)
l'affichage texte étant en effet plus rapide que l'affichage en mode graphique (car vous pouvez toujours afficher du texte en mode graphique). Attention, cela ne vous dispense pas de toujours refermer le mode graphique, même si le programme n'y fait plus appel.
Le retour en mode graphique est réalisé avec :
void setgraphmode (int mode)
La gestion des couleurs constitue un problème lorsqu'on désire réaliser un programme "multicartes". En conséquence de quoi, il est conseillé de ne programmer des graphismes en couleurs qu'avec des cartes EGA, VGA ou de résolution et de nombre de couleurs supérieurs.
On appelle palette l'ensemble des couleurs susceptibles d'être utilisées simultanément à l'écran. Pour caractériser cette palette, il existe deux fonctions principales :
void far getmaxcolor (void)
qui retourne la valeur la plus élevée de la couleur utilisable (0 à 15 pour une carte VGA),
int far getpalettesize (void)
qui renvoie le nombre effectif de couleurs de la palette.
NB : ces deux fonctions sont du type far, elles forcent donc des appels et des retours longs.
Pour déterminer la couleur d'un traçage, il faut constituer les teintes de la palette et choisir parmi elles celle que l'on veut utiliser, grâce à son indice dans le tableau palette. Cette dernière valeur est appelée, en programmation graphique, la couleur.
Selon ce point de vue, la couleur de fond est toujours la teinte attribuée à la couleur 0. La bibliothèque graphique comprend une fonction destinée à gérer la sélection des teintes accessibles dans la palette :
void setpalette (int colornum, int color)
cette fonction permet d'attribuer la teinte color à la couleur colornum de la palette.
Pour le choix des couleurs, vous disposez de deux fonctions :
void setcolor (int color)
qui permet de choisir la couleur de tracé courante (pour des lignes notamment),
void setbkcolor (int color)
qui sélectionne la couleur de fond. Cette sélection consiste en fait à recopier dans la couleur 0 de la palette, la valeur de la teinte correspondant à la couleur color de la palette. Cependant, si l'argument color a pour valeur 0, c'est toujours la teinte noire qui est choisie.
Pour " effacer " l'écran, il faut évidemment attribuer à l'ensemble des pixels de l'écran la valeur de la couleur de fond. Pour obtenir ce résultat, il faut appeler la fonction cleardevice :
void far cleardevice (void)
Voici la liste des 16 couleurs VGA :
Indice de Palette (couleur) |
Teintes |
0 |
Noir |
1 |
Bleu |
2 |
Vert |
3 |
Cyan |
4 |
Rouge |
5 |
Magenta |
6 |
Brun |
7 |
Gris clair |
8 |
Gris foncé |
9 |
Bleu clair |
10 |
Vert clair |
11 |
Cyan clair |
12 |
Rouge clair |
13 |
Magenta clair |
14 |
Jaune |
15 |
Blanc |
L'origine du repère est située en haut et à gauche de l'écran, l'axe des ordonnées s'incrémentant de haut en bas. Cette orientation est à rapprocher de celle du mode texte où la première ligne est placée en haut de l'écran et reflète la direction effective du balayage d'électrons.
La valeur maximum sur chaque axe est égale à la résolution sur cet axe moins un. Pour connaître ces valeurs, le programmeur utilise les fonctions getmaxx et getmaxy :
int far getmaxx (void)
int far getmaxy (void)
qui retournent respectivement la valeur du maximum sur l'axe horizontal et vertical.
Le tracé d'un point à l'écran est réalisé à l'aide de la fonction putpixel :
void far putpixel (int xe, int ye, int color)
les deux premiers arguments désignent les coordonnées du point, le dernier argument est la couleur.
L'opération inverse du tracé d'un point est la lecture de la couleur d'un point. Cette opération est réalisée par l'appel de la fonction getpixel :
unsigned far getpixel (int xe, int ye)
En guise d'exemple, vous pourrez découvrir le programme DEPOT qui se base sur la seule utilisation des points en simulant la sédimentation de particules sur un substrat. Selon la puissance de votre ordinateur (un processeur de fréquence supérieure à 133 Mhz serait toutefois le bienvenu), les images demanderont un certain temps pour générer l'ensemble du dessin.
# include "graphics.h"# include "conio.h"# include "stdio.h"# include "stdlib.h" void main(void) { int gest=DETECT, mode, erreur, Xe,Ye,c, dx, maxXe,maxYe, nbr=0, test;
On définit le déplacement élémentaire sur X, les coordonnées maximales, le nombre de particules ainsi qu'une variable testant la fin de boucle.
initgraph(&gest,&mode,""); if ((erreur=graphresult())!=grOk) { printf("Erreur graphique : %d %s\n",graphresult(),grapherrormsg(erreur)); getch(); exit(1); } maxXe=getmaxx();maxYe=getmaxy(); c=getmaxcolor();
Initialistion graphique : comme c'est une application multicarte, on utilise getmaxx et getmaxy pour définir les limites de travail.
for (Xe=0;Xe<=maxXe;Xe++)putpixel(Xe,maxYe,c);
On trace l'ensemble de la dernière ligne suivant l'axe Oy (donc au bas de l'écran) pour commencer la simulation du dépôt.
do { Xe=maxXe/2; Ye=0; nbr++; putpixel(Xe,Ye,1);
Positionnement de la particule de départ, au milieu et en haut de l'écran.
do { dx=random(3)-1;
On choisit, aléatoirement, un déplacement en abscisse.
if ((test=getpixel(Xe+dx,Ye+1))==0)
Test de la présence, au niveau inférieure, d'un point (ou d'une particule) déjà affiché.
{ putpixel(Xe,Ye,0); Xe=Xe+dx; Ye++; putpixel(Xe,Ye,c); }
Si le test est négatif, on efface la particule pour la faire apparaître à un niveau encore inférieur (mais Ye est incrémenté !). Si le test est positif, la particule s'arrête.
}while((!test)&&(Xe<maxXe)&&(Xe>0)); }while(nbr<10000);
Cette boucle s'opère pour un certain nombre de particules.
getch(); closegraph(); }
Vous pouvez voir le résultat de ce programme.
Une autre méthode de gestion des points (qu'on ne pourra, vu sa lourdeur, développer) consiste à utiliser directement l'écriture en mémoire écran. Pour le principe, sachez qu'il faut calculer l'adresse de l'octet qui contient les informations concernées et déterminer dans cet octet les bits représentatifs de la couleur du point. Il faut ensuite y écrire sans modifier les autres bits...
Pour tracer des lignes, les fonctions dont vous disposez sont de deux types : les fonctions de traçage absolu et celles de traçage relatif.
Par traçage absolu, on entend en fait un traçage dans lequel les coordonnées de début et de fin de segment sont spécifiées de manière absolue par rapport au repère. Vous utiliserez alors la fonction line :
void line (int x1, int y1, int x2, int y2)
où x1, y1 sont les coordonnées du début du segment et x2, y2, celles de la fin du segment.
Utilisez par ailleurs setcolor pour déterminer la couleur de tracé, en la désignant par son indice et getcolor pour connaître la valeur courante. Par défaut, cette valeur est getmaxcolor, qui correspond à la couleur blanche.
Le fait de retracer une ligne en choisissant comme couleur celle du fond permet d'effacer une ligne. Le programme KOCH illustre cette méthode en vous proposant une approximation de la courbe.
Cette courbe est construite de la façon suivante : partant d'un segment de longueur 1, on en retire le 1/3 central que l'on remplace par un triangle sans base de coté 1/3. On remarque alors que cette courbe a une longueur de 4/3 au lieu des 3/3 du segment initial. On peut recommencer l'opération sur chacun des segments de longueur 1/3 et l'on obtient au final une courbe de longueur 16/9.
En répétant cette opération un nombre infini de fois, on construit une courbe de longueur infinie puisque à chaque étape, la longueur est multipliée par 4/3, la courbe restant toutefois bornée, enfermée dans un espace fini.
Elle possède en outre une propriété géométrique particulière : celle de similitude interne. Si nous considérons le 1/4 gauche de cette courbe et que nous le dilatons par une homothétie de facteur 3, nous élaborons en fait un objet identique à la courbe initiale.
Exemple : KOCH
#include"graphics.h" #include"conio.h" #include"stdlib.h" #include"stdio.h" #include"math.h" #include"dos.h" int table[1370][4]; void main (void) { int gest=DETECT,mode, erreur, tete,queue,dx,dy,i, x1,y1,x2,y2,x3,y3,x4,y4,x5,y5, maxXe,maxYe, couleur; float sin60,cos60; initgraph(&gest,&mode,""); if((erreur=graphresult())!=grOk) { printf("Erreur graph : %s\n",grapherrormsg(erreur)); getch();exit(1); } maxYe=getmaxy();maxXe=getmaxx(); couleur=getmaxcolor(); queue=0; tete=1; table[0][0]=0; table[0][1]=0; table[0][2]=maxXe; table[0][3]=0; line(0,0,maxXe,0); sin60=sin(M_PI/3.0); cos60=cos(M_PI/3.0); for(i=0;i<341;i++) { x1=table[queue][0]; y1=table[queue][1]; x5=table[queue][2]; y5=table[queue][3]; setcolor(BLACK); line(x1,y1,x5,y5);
Après avoir calculé une fois pour toutes les valeurs des angles sin60 et cos60, on détermine les valeurs de départ (celles du premier "triangle") et on affiche une ligne noire pour effacer le premier tracé qui est au départ toute la ligne horizontale du haut de l'écran. Vous pouvez d'ailleurs attribuer une autre couleur à cette ligne pour bien visualiser le procédé.
x2=(2*x1+x5)/3; y2=(2*y1+y5)/3; x4=(2*x5+x1)/3; y4=(2*y5+y1)/3; dx=x4-x2; dy=y4-y2; x3=x2+cos60*dx-cos60*dy; y3=y2+sin60*dx+cos60*dy; setcolor(couleur); line(x1,y1,x2,y2); line(x2,y2,x3,y3); line(x4,y4,x3,y3); line(x4,y4,x5,y5); table[tete][0]=x1; table[tete][1]=y1; table[tete][2]=x2; table[tete++][3]=y2; table[tete][0]=x2; table[tete][1]=y2; table[tete][2]=x3; table[tete++][3]=y3; table[tete][0]=x3; table[tete][1]=y3; table[tete][2]=x4; table[tete++][3]=y4; table[tete][0]=x4; table[tete][1]=y4; table[tete][2]=x5; table[tete++][3]=y5; queue++; delay(50); }
Après calcul des points caractéristiques (points d'intersection et sommets) et traçage des lignes correspondantes, on enregistre des points afin de s'en servir pour la prochaine itération.
getch(); closegraph(); printf("Gestionnaires : %s\n%s\n%s",gest); }
Vous pouvez voir le résultat de ce programme.
Il est fréquent que le début d'un segment que l'on veut tracer soit situé à la fin de celui que l'on vient de tracer (c'est une façon de dessiner plutôt logique et naturelle, non ?) repartant ainsi de la position actuelle. On définit ainsi un mode graphique relatif.
Le traçage depuis la position courante jusqu'à une position donnée en coordonnées absolues résulte de l'appel de la fonction lineto :
void lineto (int x, int y)
A l'opposé, le déplacement sans traçage jusqu'à une position donnée en coordonnées absolues est obtenu par l'appel de la fonction moveto :
void moveto (int x, int y)
les deux arguments désignant évidemment les coordonnées du point d'arrivée dans le repère écran.
Vous pouvez également tracer une ligne en termes de quantité de déplacement sur chacun des axes, relativement à la position actuelle, ce qui s'apparenterait à un vecteur : le déplacement avec traçage effectif requiert la fonction linerel :
void linerel (int dx, int dy)
celui sans traçage par moverel :
void moverel (int dx, int dy)
dx et dy représentant les coordonnées du vecteur relatif au déplacement.
Tracer un cercle n'est pas une opération aisée. En effet, en partant de la formule :
(x-a)2 + (y-b)2 = r2
où (a ; b) est le centre du cercle et r son rayon, on pourrait chercher à élaborer un algorithme respectant ces données cartésiennes car on assimile l'écran à un repère x,y orthonormé.
Avec une telle démarche, vous obtiendrez une belle ellipse, mais pas vraiment un cercle. En effet, le maillage même des pixels n'est pas carré mais plutôt rectangulaire. S'ajoutent à ce défaut les déformations résultant de l'imperfection de l'écran cathodique. Cependant, les cartes VGA (qui sont tout de même majoritaires) permettent un système quasiment carré.
La fonction circle de la bibliothèque donne ainsi la plupart du temps des résultats satisfaisants :
void circle (int x, int y, int radius)
elle trace, dans une couleur correspondant à la couleur courante, un cercle de centre x,y et de rayon radius.
Vous voulez juste une portion du gâteau ? Rien de plus simple, appelez arc :
arc (int x, int y, int stangle, int endangle, int radius)
dont le résultat est le traçage d'un arc de cercle de centre x,y de rayon radius, pour un angle stangle jusqu'à un angle endangle (défini en degrés et dans le sens trigonométrique).
La fonction que vous utiliserez lorsque vous voudrez générer une ellipse sera :
void ellipse (int x, int y, int stangle, int endangle, int xradius, int yradius)
On trace ainsi un arc d'ellipse (ou une ellipse complète) avec :
L'exemple du programme SPIRALE vous montrera qu'il est possible de dessiner une approximation d'une spirale.
Exemple : SPIRALE
#include"graphics.h"#include"conio.h" #include"stdlib.h" #include"stdio.h" #include"dos.h" #define PAS 3 void main(void) { int gest=DETECT, mode,i,j,k,kl, erreur, maxXe,maxYe,xc,yc; initgraph(&gest,&mode,""); if ((erreur=graphresult())!=grOk) { printf("Erreur graphique : %s\n",grapherrormsg(erreur)); getch(); exit(1); } delay(3000); maxYe=getmaxy(); maxXe=getmaxx(); xc=maxXe/2; yc=maxYe/2; i=0;
Déclaration des variables, initialisation graphique. Xc et Yc sont justement ensuite définis comme les coordonnées du centre graphique qui constitue également le point de la génération de l'ellipse.
do { delay(11*i); j=PAS*(i%2+1+i); k=PAS*((i+1)%2+1+i); kl=getmaxcolor()-random(14); setcolor(kl); ellipse(xc,yc,90*i,90+90*i,j,k); i++; }while ((j<xc)&&(k<yc));
On trace, à chaque secteur angulaire de 90°, un arc d'ellipse dont les longueurs des axes augmentent progressivement jusqu'à atteindre les valeurs limites de l'écran (xc et yc) grâce à la condition while ((j<xc)&&(k<yc)).La couleur change également aléatoirement à chaque 1/4. delay(11*i) ralentit progressivement l'affichage.
getch(); closegraph(); }
Dans votre périple infographique, vous allez certainement devoir traiter des ensembles de lignes comme un tout. Plusieurs fonctions graphiques existent pour vous aider à manier les polygones.
Pour obtenir cette figure simple, toujours dans la couleur courante, utilisez :
void rectangle (int left, int top, int right, int bottom)
left, top : position du coin supérieur gauche du rectangle
right, bottom : position du coin inférieur droit
Pour le polygone général, vous devez recourir à la fonction drawpoly :
void drawpoly (int numpoint, int *polypoint)
numpoint : nombre de sommets du polygone
polypoint : contient l'adresse d'un tableau d'entiers (que vous devez donc créer) qui constituent, deux par deux, les coordonnées d'un polygone que vous fermerez en donnant au dernier sommet les mêmes coordonnées que celles du premier
Les objets étudiés jusqu'à présent ne concernaient que des lignes ou des points sans introduire une quelconque notion de surface. Le remplissage n'est pas une opération aisée tant son algorithme est complexe à élaborer pour déterminer, à priori, les régions de l'écran à remplir.
L'algorithme le plus efficace se base sur un point, le germe, contenu dans la zone à remplir et colorie tous les points accessibles qui n'ont pas la couleur en question (et donc n'appartenant pas à la frontière délimitant la zone ciblée). Inutile de vous préciser que votre frontière doit être parfaitement fermée (vous ne remplirez jamais une passoire...)
Selon cette méthode graphique, vous disposez de :
void floodfill (int x, int y, int border)
x,y : coordonnées du germe
border : couleur des points de la frontière
Vous pouvez également jouer sur la couleur et le motif de remplissage à l'aide de :
void setfillstyle (int pattern, int color)
pattern prend l'une des valeurs de fill_patterns de graphics.h.
Description des motifs :
Valeurs de fill_patterns |
Motifs |
EMPTY_FILL |
couleur du fond |
SOLID_FILL |
couleur " color " |
LINE_FILL |
- - - |
LTSLASH_FILL |
/ / / |
SLASH_FILL |
/ / / en lignes minces |
BKSLASH_FILL |
\ \ \ en lignes minces |
LTBKSLASH_FILL |
\ \ \ |
HATCH_FILL |
hachures minces |
XHATCH_FILL |
hachure épaisses |
INTERLEAVE_FILL |
croisé |
WIDE_DOT_FILL |
points espacés |
CLOSE_DOT_FILL |
points serrés |
USER_FILL |
défini par l'utilisateur |
Pour un rectangle, vous disposez de deux fonctions :
void bar (int left, int top, int right, int bottom)
remplit un rectangle (vous connaissez les arguments utilisés),
void bar3d (int left, int top, int right, int bottom, int depth, int topflag)
vous permet un effet de perspective avec la profondeur depth. Si topflag est vrai (!= 0), la face supérieure de la barre est effectivement tracée, ce qui autorise l'empilage de plusieurs barres.
Les formes circulaires disposent elles aussi de leurs propres méthodes de remplissage :
void fillellipse (int x, int y, int xradius, int yradius)
vous remplira une ellipse et :
void sector (int x, int y, int stangle, int endangle, int xrad, int yrad)
remplit un secteur d'ellipse, avec les mêmes significations pour les arguments que ceux passés à ellipse.
Dans le cas simple du cercle ou d'un secteur angulaire de cercle, on peut utiliser :
void pieslice (int x, int y, int stangle, int endangle, int radius)
les paramètres étant les mêmes que pour la fonction arc.
Dans le cas général, une seule fonction suffit :
void fillpoly (int numpoint, int polypoint)
qui nécessite les mêmes paramètres que la fonction drawpoly.
Les transformations à deux dimensions sont souvent utilisées pour passer d'un système de coordonnées à un autre, permettant de manipuler ainsi vos objets graphiques.
Il existe ainsi quatre transformations simples (que l'on peut évidemment combiner à volonté) :
les translations, les rotations, les homothéties et les symétries axiales du plan. On supposera que de tels outils mathématiques sont connus (mettez-vous au travail dans le cas contraire).
Dans certains cas complexes, le calcul des coordonnées d'un point après de nombreuses transformations demande beaucoup de temps de calcul. Pour optimiser l'opération, vous l'aurez compris, il faut faire appel au calcul matriciel qui se résume à une seule matrice de transformation globale.
Ces matrices (elles sont carrées 3x3) sont représentées en programmation par des tableaux à deux dimensions. La matrice de départ (celle du point à traiter) sera quant à elle de type unicolonne, tout comme évidemment la matrice finale, désignant les nouvelles coordonnées du point. Du point de vue de l'opération à effectuer, nous multiplierons ces types de matrices pour parvenir au résultat attendu.
Exemple : en partant d'un point x,y nous allons passer à un repère en
combinant une translation de vecteur (6.5, 2.5), une rotation de 45° et une
homothétie de valeur : . Le passage de
coordonnées sera obtenu après l'application, dans l'ordre, de la translation
inverse (-6.5, -2.5), d'une rotation inverse de -45°, et d'une homothétie de
rapport
, soit
matriciellement :
On applique par la suite T pour obtenir les coordonnées finales x',y' du point transformé, écrites dans la matrice M' :
La programmation de ce type de méthodes s'effectue ainsi à l'aide de matrices que l'on applique aux points concernés pour aboutir au nouveau repère.
Les matrices vues précédemment ne constituent en fait qu'un outil de travail. Elles vont vous permettre en fait d'adapter les dimensions de l'objet à l'écran et de lui donner une orientation représentative.
En effet, à cause du repère initial de l'écran (il n'est pas vraiment intuitif...) vous devrez procédez à :
Par ailleurs, il existe d'autres fonctions (beaucoup plus complexes !) pour vous aider dans cette mise au point, faisant appel à des notions de clôture et de clipping qui ne seront développées plus en détails (pour les curieux, découvrez les fonctions : setviewport, getviewsettings et clearviewport.)
Les opérations présentées ici ne sont toutefois que des préparatifs à priori nécessaires, libre à vous ensuite de vous amuser en combinant les transformations les plus folles pour des graphismes d'esthètes.
Ahhh ! me direz vous, nous allons enfin passer aux choses sérieuses (car vous avez tout assimilé après une seule lecture rapide, à n'en pas douter !) Oui, on peut parler d'évolution dans la mesure où tous les objets abordés jusqu'à présent étaient à une ou deux dimensions (vous vous souvenez cependant de bar3d, non ?), ce qui est en soi une aberration car notre univers est bel et bien constitué de trois dimensions (voire plus pour certains). Le problème est donc posé : comment obtenir une image 2D convenable d'un objet réel bien volumineux ?
Une seule méthode s'impose, la projection orthogonale axonométrique. Cette méthode simple qui respecte les échelles (contrairement à l'utilisation des points de fuite) consiste à faire correspondre à chaque point 3D défini par ses coordonnées (x,y,z) un point sur l'écran 2D (défini par x',y') telles que toutes les lignes de projection soient orthogonales au plan de projection.
Seule information importante : la direction de projection par rapport au repère de l'objet car l'observateur est supposé se trouver " devant " l'objet et l'écran " derrière " l'objet.
Il reste un problème majeur à résoudre dans la mesure où il ne faut pas tracer les portions de l'image correspondant à une partie de l'objet cachée par lui-même.
Plusieurs méthodes, faisant notamment appel à des tables de valeurs (qui définissent en fait votre horizon) sont disponibles lorsque le tracé se base essentiellement sur des lignes mais nous allons étudier ici le cas des objets remplis où l'on procède " à l'envers " : l'objet est tracé en commençant par les portions de l'objet situées à l'arrière plan. Le masquage est alors automatique puisqu'un polygone rempli cache forcément les objets derrière lui. Le programme MONTAGNE vous présente cette méthode de traçage.
La montagne est définie sur un plan carré repéré par quatre points. A partir de ces premiers points, l'algorithme calcule 5 points supplémentaires : les points situés à mi-côté et le point situé au centre du carré, définissant ainsi quatre nouveaux carrés, sur lesquels on applique à nouveau l'algorithme :
Exemple : MONTAGNE
#include "graphics.h" #include "conio.h" #include "stdlib.h" #include "stdio.h" #include "math.h" #include "time.h" #include "dos.h" #define COTE 128 int frac [COTE+1][COTE+1]={0}; int face[5][2]; float rugosite; void setfrac(int,int,int,int); void main (void) { int gest=VGA, mode=VGAHI, erreur, i,j,n,nbr,ind,h, X1,Y1,X2,Y2; char c; initgraph(&gest,&mode,""); if ((erreur=graphresult())!=grOk) { printf("Erreur graphique : %s\n",grapherrormsg(erreur)); getch(); exit(1); } do { randomize(); rugosite=100.0+random(200); frac[0][COTE]=random(100)+100; frac[0][0]=-random(100); frac[COTE][COTE]=random(100)+100; frac[COTE][0]=-random(100); nbr=0; do nbr++; while (!((COTE>>nbr) & 1));
Initialisation des caractères aléatoires de l'ensemble du paysage. (COTE>>nbr) définit une condition relative aux bits de COTE.
for (n=1;n<=nbr;n++) { ind=COTE>>(n-1); for (i=0;i<COTE;i+=ind) for (j=0;j<COTE;j+=ind) setfrac(i,i+ind,j,j+ind); }
On génère alors progressivement le paysage...
for (i=0;i<=COTE;i++) for (j=0;j<=COTE;j++) if(frac[i][j]<0) frac[i][j]=0; cleardevice();
...génération des lacs, s'ils existent vu leur caractère aléatoire.
for (j=COTE-1;j>=0;j--) { for (i=0;i<COTE;i++) { X1=(int) (480.0/COTE*i); Y1=(int)((300.0/COTE*j)); X2=(int)(480.0/COTE*(i+1)); Y2=(int)((300.0/COTE*(j+1))); h=(frac[i][j]+frac[i+1][j]+frac[i+1][j+1]+frac[i][j+1])/4; if (h<=0) { setfillstyle(SOLID_FILL,EGA_LIGHTBLUE); setcolor(EGA_LIGHTBLUE); } else if (h<=10) { setfillstyle(SOLID_FILL,EGA_LIGHTGREEN); setcolor(EGA_LIGHTGRAY); } else if (h<=30) { setfillstyle(SOLID_FILL,EGA_GREEN); setcolor(EGA_LIGHTGRAY); } else if (h<=90) { setfillstyle(SOLID_FILL,EGA_LIGHTGRAY); setcolor(EGA_DARKGRAY); } else { setfillstyle(SOLID_FILL,EGA_WHITE); setcolor(EGA_LIGHTGRAY); }
Pour une image réaliste, les points d'altitude < 0 sont ramenés à 0 et coloriés en bleu ciel pour imiter l'eau. Les autres couleurs sont choisies d'après la hauteur des points (on va du vert au blanc). Vous pouvez modifier ces couleurs (une belle mer rouge sang...)
face[0][0]=X1+Y1/2; face[0][1]=479-(frac[i][j]+Y1/2); face[1][0]=X2+Y1/2; face[1][1]=479-(frac[i+1][j]+1/2); face[2][0]=X2+Y2/2; face[2][1]=479-(frac[i+1][j+1]+Y2/2); face[3][0]=X1+Y2/2; face[3][1]=479-(frac[i][j+1]+Y2/2); face[4][0]=X1+Y1/2; face[4][1]=479-(frac[i][j]+Y1/2); fillpoly(5,(int*)face); } } c=0; if (kbhit()) c=getch(); if (c==32) getch(); } while (c!=27); closegraph(); } void setfrac(int id,int ifi,int jd, int jf) { int dim,z1,z2,z3,z4,im,jm; dim=(int) (rugosite/COTE*(abs(ifi-id))); z1=frac[id][jd]; z2=frac[ifi][jd]; z3=frac[id][jf]; z4=frac[ifi][jf]; im=(id+ifi)/2; jm=(jd+jf)/2; frac[im][jd]=(z1+z2)/2+random(dim)-dim/2; frac[id][jm]=(z1+z3)/2+random(dim)-dim/2; frac[ifi][jm]=(z2+z4)/2+random(dim)-dim/2; frac[im][jf]=(z3+z4)/2+random(dim)-dim/2; frac[im][jm]=(z1+z2+z3+z4)/4+2*random(dim)-dim; }
La fonction setfrac détermine, en fonction de l'écart des indices, les points à tracer.
Vous pouvez voir le résultat de ce programme : écran 1 ou écran 2.
L'un des problèmes qui malmena à la fois géomètres et topographes fut la détermination exacte de la longueur des côtes et frontières des différentes nations. Le " grand " Mandelbrot (le succès a depuis longtemps atteint sa modestie !) ramena ce problème à un exemple d'une apparente simplicité. Dans l'un de ces articles, l'éminent et facétieux personnage s'interrogeait: " Quelle est la longueur de la côte de la Bretagne? ". Au premier abord, on eut pu croire que ce problème pouvait être résolu " simplement " en se munissant d'une bonne carte et en suivant le pourtour côtier avec un fil pour ensuite, à l'aide de la légende, en déduire un résultat. Cependant, après réflexion, on remarque que les détails on tendance à être omis sur une carte. La solution pourrait alors provenir de l'utilisation d'une carte plus détaillée. Las, en toute rigueur, la détermination devient impossible. Quelle est la carte susceptible de tenir compte de détails aussi précis que les galets, les grains de sables, et même les molécules ou les atomes ?
La conclusion de Mandelbrot, si elle fut peut-être choquante et originale, n'en fut pas moins la plus exacte, à savoir :
" La côte de la Bretagne a la même longueur que celle de Manhattan ou des Amériques. Elles sont toutes infinies. "
Evidemment, ce qui est valable pour les côtes l'est aussi pour la longueur d'une courbe de Koch comme pour toutes les courbes fractales. Si dans la pratique, des détails en deçà d'une certaine valeur, comme 100 mètres pour une carte, peuvent être ignorer, d'un point de vue purement mathématique un tel compromis laisse beaucoup à désirer. Puisque toutes les côtes comprenant des détails réels doivent avoir une même longueur (infinie), est-il possible de comparer de telles figures géométriques?
A nouveau, Mandelbrot étonne en répondant par l'affirmative mais par la même occasion, il déplace le problème de la mesure quantitative des longueurs à une nouvelle sorte de mesure qualitative fondée sur les échelles : la dimension fractale.
Pour pouvoir comprendre les dimensions fractales, nous devons éclaircir quelques points concernant la signification d 'une dimension. La plupart des personnes pensent avoir une connaissance tout à fait claire de ce concept. L'espace comprend trois dimensions. Les surfaces, comme une feuille de papier par exemple, ont deux dimensions. Une droite a une seule dimension. Enfin, un point ou un ensemble de points ont zéro dimension. Les dimensions rencontrées dans notre univers paraissent ainsi simples : 0,1,2 ou 3. Mais les choses ne sont pas toujours aussi simples : par exemple, quelle est la dimension d'une pelote de ficelle ? Vu de loin, la pelote ressemble à un point et a par conséquent zéro dimension. Néanmoins, à distance relativement proche (quelques mètres), on revient à la normale et la pelote a trois dimensions. Mais en s'en rapprochant très près, nous voyons un fil, tordu et enroulé. La pelote est dès lors constituée d'une ligne unidimensionnelle. Plus près cette ligne se transforme en une colonne d'épaisseur finie et la ficelle devient tridimensionnelle. Plus près encore, nous distinguons des fils qui s'enroulent pour former la ficelle, la pelote est redevenue unidimensionnelle.
Ainsi la " dimension effective " de la pelote passe de trois à un et inversement, et ceci sans cesse. Sa dimension apparente dépend de notre proximité. Nous voyons ainsi que la notion de dimension n'est pas aussi évidente qu'elle peut le paraître à première vue. Aucune des dimensions de la nature n'est donc plus évidente que celle-ci ; elles dépendent de notre point de vue. Mandelbrot va même jusqu'à affirmer fièrement que la relation inextricable qui existe entre objets et observateurs de la géométrie fractale se manifeste également au niveau de découvertes scientifiques importantes telle que la relativité, qui mirent à jour une interdépendance entre observateurs et observés. La mesure quantitative, sur laquelle repose la science est également défiée par ce fait. La longueur de la côte dépend de la quantité choisie comme mesure. Si la quantité est finalement relative, elle implique l'effacement de certains détails, elle s'avère être imprécise. Mandelbrot remplace une quantité, comme une longueur, par la mesure qualitative des dimensions fractales effectives, une mesure du degré relatif de complexité d'un objet.
Aussi déconcertant qu'il soit à première vue d'accepter que des objets puissent, dans la nature, admettre de telles " dimensions effectives ", ce concept permet de calculer la dimension fractale d'une côte et de découvrir qu'il s'agit d'un nombre fractionnel supérieur à 1. Lorsque la dimension fractale d'une courbe ou d'une côte est proche de 1, celle-ci est très régulière et ne comprend aucun petit détail. Plus on s'éloigne de 1, plus irrégulière ou chaotique est cette courbe.
Quel est le rapport entre, d'une part, irrégularité et détail et, d'autre part, dimension fractale ? Imaginez que l'on éparpille uniformément des grains de riz sur une carte. On disposerait, par exemple, de 10000 grains que l'on pourrait considérer comme caractérisant les deux dimensions de la carte. Une ligne droite tracée en travers de la page ne passe que par deux cents grains, seulement 2% des grains se trouvent donc sur la droite. La majeure partie d'entre eux se trouve dans d'autres régions du plan. Supposons maintenant que la ligne se tord et se courbe de manière à rencontrer de plus en plus de grains de riz, atteignant non seulement les grains mais également les points individuels du plan. Au fur et à mesure qu'augmente le nombre de ces points, il devient évident que la dimension de la ligne se rapproche plus de celle d'un plan que d'une ligne. En fait, les lignes fractales tordues ont une dimension fractionnelle, comme 1,2618, 1,1291 1,3652, etc.
L'une des courbes les plus particulières demeure celle créée par Peano. Cette courbe est devenue tellement irrégulière à des échelles infiniment petites que sa dimension est 2. En effet, cette courbe comprend un si grand nombre de torsions qu'elle passe par tous les points du plan. Néanmoins, malgré son extrême complexité de " contact ", elle ne se coupe jamais.
La dimension d'homothétie développée par Benoît Mandelbrot peut s'expliciter
par une remarque élémentaire. Si l'on divise un objet usuel S régulier (carré,
rectangle, triangle équilatéral, segment, cube...) en n objets s semblables à
l'objet initial S dans un rapport d'homothétie , alors
où d désigne la dimension usuelle de S.
Si un objet S peut être partagé en n objets s semblables à S dans le rapport
d'homothétie, l'unique solution d de l'équation est appelée
dimension d'homothétie de S. Ce nombre
n'est plus
nécessairement un entier.
Ainsi, pour la courbe de Koch, l'étape n+1 étant formée de 4 morceaux
semblables à ceux de l'étape n dans un rapport d'homothétie de , la
courbe a une dimension d'homothétie
Ou encore, pour la courbe de Peano, l'étape n+1 étant formée de 9 morceaux
semblables à ceux de l'étape n dans un rapport d'homothétie de , la
courbe a une dimension d'homothétie égale à
.
On s'intéresse ici à une fractale particulière sans aucun équivalent naturel comme les côtes bretonnes ou un chou-fleur (si ! votre chou-fleur est, de par sa structure, un peu fractal). Les représentations de l'ensemble sont en effet parmi les plus célèbres et les plus spectaculaires du domaine fractal.
Cette bizarrerie mathématique est constituée par l'ensemble des points du
plan complexe tels que la suite des termes de la série converge vers
0.
Ces points représentent l'ensemble de Mandelbrot que l'on colorie le plus souvent en noir, alors que les points qui divergent sont quant à eux coloriés selon leur vitesse de divergence.
Ce qui est remarquable, c'est que la frontière entre ces deux ensembles de points est de nature fractale. Il n'est pas possible de déterminer à priori l'appartenance d'un point à l'un ou l'autre ensemble, si ce n'est en calculant successivement les termes de la série jusqu'à un degré suffisant.
Pour le programme présenté, on se contente de calculer MAX termes (vous pouvez en mettre moins pour plus de rapidité) et on décide de l'appartenance du point. Cette méthode se justifie par la résolution (non infinie) de l'écran, car un pixel est en fait une aire, qui peut éventuellement être " à cheval " sur les deux ensembles.
D'un point de vue mathématique, l'ensemble de Mandelbrot est un cas particulier des ensembles de Julia.
Exemple : MANDEL # include <graphics.h> # include <conio.h> # include <stdio.h> # include <stdlib.h> # define MAX 200
200 représente le maximum de points calculés, c'est une constante définie en fonction des capacités de l'ordinateur (avec un proc. > 200 Mhz, MAX peut aller jusqu'à plusieurs centaines). Changez cette valeur pour de meilleurs résultats graphiques. Vous noterez également le nombre conséquent de variables, toutes nécessaires au bon déroulement d'un tel programme.
void main (void) { int gest=VGA, mode=VGAHI, erreur,couleur, xdr,xfr,ydr,yfr, i,j,k,m; float aa,bb,cc; double x,y,xc,yc,xold,yold,xd,xf,yd,yf,d,dx,dy; char c,c1,c2; unsigned taille; void *tampon; struct palettetype pal; puts("Entrez trois nombres :"); puts("Ils définiront la tendance (printemps-été 98!) de votre palette."); puts("Ces valeurs codent en fait les rapports entre le rouge, le vert et le bleu."); puts("\nExemple : 3 3 3 vous donnera une palette de gris très bien dégradée mais"); puts("l'écran ne sera pas très lumineux car tous ces gris sont proches du noir..."); puts("\n30 30 30 permet plus de contraste, tout en restant dans les gris.\t\t"); scanf("%f%f%f",&aa,&bb,&cc); initgraph(&gest,&mode,""); if ((erreur=graphresult())!=grOk) { printf("Erreur graphique : %s\n",grapherrormsg(erreur)); getch(); exit(1); } getpalette(&pal); for (i=0; i<pal.size; i++) setrgbpalette(pal.colors[i], i*aa, i*bb, i*cc);
Génération aléatoire d'une nouvelle palette : les trois couleurs principales y sont en effet paramètrées par i : changez les valeurs aa, bb et cc pour obtenir des dessins encore plus psychédéliques ou mieux, si vous en avez l'occasion programmez une rotation de la palette, une fois l'affichage des points calculé, pour animer votre ensemble de Mandelbrot.
xd=-2.0; xf=1.0; yd=-1.0; yf=1.0; d=0; m=MAX;
On définit ici les dimensions réelles initiales de l'écran. Etudiez la génération qui suit...
do { for (i=0;i<640;i++) { for (j=0;j<480;j++) { putpixel(i,j,15); xc=xold=xd+(xf-xd)/640.0*i; yc=yold=yd+(yf-yd)/480.0*j; k=0; do { x=xold*xold-yold*yold+xc; y=2.0*xold*yold+yc; d=x*x+y*y; k++; xold=x; yold=y; }while ((k<MAX)&&(d<4.0));
Ce bloc délimite la nature des points de l'écran une fois la série calculée. On y retrouve par ailleurs la condition d'arrêt des itérations (avant la limite MAX).
if (k==m) { do d*=2.0; while ((d<16.0)||(d==0.0)); couleur=0; }
On cherche alors l'appartenance à l'ensemble de Mandelbrot. Dans le cas positif, le point est colorié selon la couleur de fond qui ne sera pas systématiquement le noir grâce au choix de palettes que vous pouvez faire.
else couleur=k%16; putpixel(i,j,couleur);
Les points n'appartenant pas à l'ensemble sont en revanche coloriés par une couleur fonction de leur vitesse de divergence (k et d sont les valeurs déterminantes).
} if (kbhit()) { getch(); break; } } getch(); setwritemode(XOR_PUT); xdr=220; xfr=420; ydr=165; yfr=315; setcolor(15); rectangle(xdr,ydr,xfr,yfr);
On définit ici les dimensions d'un rectangle qu'on utilisera pour agrandir une partie du plan. setwritemode(XOR_PUT) permet le déplacement (ou le zoom) du rectangle. XOR_PUT est en fait un opérateur logique graphique qui, si on le répète deux fois, efface la ligne précédemment tracée et restaure l'écran. Ce qui donne l'illusion du déplacement du rectangle.
do { c1=getch(); c2=0; if (c1) switch (c1) { case '+': if (xfr>635)break; if (xdr<4) break; if (yfr>476) break; if (ydr<3) break; rectangle(xdr,ydr,xfr,yfr); xfr+=4; xdr-=4; xfr+=3; ydr-=3; rectangle(xdr,ydr,xfr,yfr); break; case '-': if ((xfr-xdr)<9) break; if ((yfr-ydr)<7) break; rectangle(xdr,ydr,xfr,yfr); xfr-=4; xdr+=4; yfr-=3; ydr+=3; rectangle(xdr,ydr,xfr,yfr); break; }
Lorsque vous appuyez sur + ou - vous effectuez un agrandissement ou une réduction du rectangle selon un pas de 4 en abscisse et de 3 en ordonnée.
else c2=getch(); switch (c2) { case 77 : if (xfr==639) break; rectangle(xdr,ydr,xfr,yfr); xfr++; xdr++; rectangle(xdr,ydr,xfr,yfr); break; case 75 : if (xdr==0) break; rectangle(xdr,ydr,xfr,yfr); xfr--; xdr--; rectangle(xdr,ydr,xfr,yfr); break; case 80 : if (yfr==479) break; rectangle(xdr,ydr,xfr,yfr); yfr++; ydr++; rectangle(xdr,ydr,xfr,yfr); break; case 72 : if (ydr==0) break; rectangle(xdr,ydr,xfr,yfr); yfr--; ydr--; rectangle(xdr,ydr,xfr,yfr); break; } } while ((c1!=13)&&(c1!=27));
C'est également le même principe de régénération pour le déplacement, mais le rectangle garde les dimensions que vous avez définies (avec + et -) pour effectuer des translations dans les quatre directions selon un pas de 1 pixel.
setwritemode(COPY_PUT); rectangle(xdr,ydr,xfr,yfr); setwritemode(XOR_PUT); dx=xd; dy=yd; xd=xd+(xf-xd)*xdr/640.0; xf=dx+(xf-dx)*xfr/640.0; yd=yd+(yf-yd)*ydr/480.0; yf=dy+(yf-dy)*yfr/480.0; m*=2; taille=imagesize(xdr,ydr,xfr,yfr); if (taille<60000) { tampon=malloc(taille); getimage(xdr,ydr,xfr,yfr,tampon); cleardevice(); putimage(640-(xfr-xdr)-1,480-(yfr-ydr)-1,tampon,0); free(tampon); }
En utilisant l'opérateur COPY_PUT, on reporte la zone agrandie au bas de l'écran.
else cleardevice(); }while (c1!=27); closegraph(); }
Vous pouvez voir le résultat de ce programme : écran 1 ou écran 2.
Gaston Julia publia en 1918 un livre qui lui valut alors un grand succès et une certaine popularité au sein de la communauté mathématicienne. Toutefois, même si Julia était un mathématicien célèbre dans les années 20, son travail demeura méconnu jusqu'au moment où Mandelbrot s'en servit lors de ses recherches au début des années 70.
En effet, recherchant dans les travaux de Julia une source de problèmes, Mandelbrot s'orienta à partir des recherches du mathématicien français (qui lui déplaisaient) vers ses propres expérimentations. Avec l'aide d'ordinateurs performants, Mandelbrot démontra que l'œuvre de Julia était source des plus belles fractales aujourd'hui connues. On peut ainsi dire que Julia découvrit il y a plus de soixante-dix ans des fractales superbes qui n'attendaient que d'être représentées grâce à la puissance de calcul de notre époque.
Pour comprendre ses résultats, un cycle d'études spécial fut organisé par Hubert Cremer à l'université de Berlin en 1925. Le summum de ces études fut évidemment un premier essai de visualisation de l'ensemble de Julia.
Les ensembles de Julia se trouvent dans le plan complexe. Ils sont indispensables à la compréhension des itérations de polynômes comme x2+c, que l'on prendra comme exemple. Une itération signifie que nous donnons c et choisissons une valeur pour x et obtenons ainsi x2+c. Maintenant nous substituons à x cette valeur et évaluons x2+c à nouveau, et ainsi de suite. Finalement, pour une valeur de c donnée, nous créons une séquence de nombre complexes :
x, x2+c, (x2+c)2+c, ((x2+c)2+c)2+c,...
Cette séquence doit avoir l'une des deux propriétés suivantes :
Les points qui conduisent au premier cas de figure sont appelés ensemble fuyant pour c, alors que les points de la deuxième situation sont appelés ensemble limité pour c. Ces deux ensembles ne sont pas vides. Par exemple, en choisissant c, puis pour une valeur de x suffisamment grande, nous obtenons :
x2+c > x
Ainsi, l'ensemble fuyant contient tous les points x ayant une très grande valeur. D'un autre côté, si nous choisissons x tel que :
x=x2+c
alors l'itération demeure stationnaire. En partant d'une telle valeur de x, la séquence produite par l'itération reste constante : x, x, x,... En d'autres mots, l'ensemble limité n 'est jamais égal à l'ensemble vide. Les ensembles couvrent une partie du plan complexe et sont complémentaires. Ainsi la limite du plan limité est-elle la même que celle du plan fuyant, c'est l'ensemble de Julia pour c.
L'exposé des différences fondamentale entre les ensembles de Julia et celui de Mandelbrot (dans leur définition tout du moins) font appel a de joyeuses notions mathématiques de polynômes quadratiques, d'ensembles connexes ou autres attracteurs et nécessiterait plusieurs pages. On se contentera ainsi d'écrire qu'elles concernent essentiellement le paramètre c.
On trouve dans les représentations des ensembles de Julia des structures qui se répètent. En fait, un ensemble de Julia peut être recouvert de copies de lui-même, mais ces copies sont obtenues par une transformation non linéaire, ce qui diffère par exemple de la courbe de Koch. Vous trouverez à la suite de ce texte quelques exemples d'ensembles, obtenus pour différentes valeurs de c, qui vous donneront un aperçu des propriétés géométriques fascinantes des ensembles fractals.
vous pouvez récupérer par ftp anonyme les sources des programmes proposés dans ce document sur ftp://www-ipst.u-strasbg.fr/pub/pat/. vous pouvez récupérer directement les sources (si vous voulez les compiler ou les modifier), ou récupérer directement les exécutables si vous n'avez pas le compilateur (vous devez décomprimer tout le fichier, y compris *.bgi).