Flame Maker – Etape 4 – Dessinateur Flame couleur
Introduction
Le but de cette étape est d'améliorer deux aspects du programme de l'étape précédente :
- L'algorithme de dessin, afin de lui faire produire des images en couleur.
- La construction des fractales Flame, afin de pouvoir les construire de manière incrémentale via un bâtisseur.
Avant de décrire la version couleur de l'algorithme de dessin, il importe d'examiner comment caractériser les couleurs.
Couleur
Introduction
Il existe de nombreuses manières de caractériser une couleur. Toutefois, en raison du fonctionnement de la vision humaine, la plupart de ces caractérisations sont composées d'un triplet de nombres réels.
Pour ce projet, la représentation appelée RGB (pour red, green, blue) sera utilisée. Elle consiste à caractériser une couleur par un triplet de nombres donnant respectivement la composante rouge, verte et bleue de la couleur. Chacune de ces composantes est un nombre réel compris entre 0 et 1 (inclus), où 0 représente l'absence totale de cette composante, et 1 sa présence maximale.
Par exemple, le noir est représentée par le triplet (0,0,0), le blanc par (1,1,1), le rouge pur par (1,0,0), le vert pur par (0,1,0) et ainsi de suite.
Mélange
Il est possible de mélanger deux couleurs pour en obtenir une nouvelle. Pour mélanger deux couleurs \(C\) et \(C^\prime\) avec une proportion \(p\) de la première (et donc \(1-p\) de la seconde), chaque composante de la couleur finale \(D\) est obtenue ainsi : \[ D_{r,g,b} = p\,C_{r,g,b} + (1 - p)\,C^\prime_{r,g,b} \] Par exemple, en mélangeant du vert pur (0,1,0) et du blanc (1,1,1), avec une proportion de 60% pour le vert, on obtient la couleur (0.4, 1, 0.4), soit un vert clair.
Encodage entier
La représentation d'une couleur sous la forme d'un triplet de réels est précise et agréable à utiliser, mais gourmande en mémoire et en temps de calcul. Pour cette raison, en informatique, les couleurs sont souvent représentées par de petits nombres entiers groupés dans un entier plus grand.
Par exemple, on peut représenter chaque composante par un entier compris entre 0 (correspondant à 0) et 255 (correspondant à 1), qui occupe 8 bits. Ensuite, les trois composantes peuvent être combinées en un seul entier de 24 bits, p.ex. en plaçant la composante rouge dans les bits 23 à 16, la composante bleue verte dans les bits 15 à 8 et la composante verte bleue dans les bits 7 à 0.
Pour des raisons qu'il serait trop long d'expliquer ici, lorsqu'on encode une composante réelle d'une couleur en un entier, il faut le faire au moyen d'un relation non linéaire. On dit généralement que l'on gamma-encode cette valeur. Pour ce projet, la formule de gamma-encodage de la norme sRGB sera utilisée. Elle est définie ainsi : \[ c_s = \begin{cases} 12.92\, c & \mbox{si } c \le 0.0031308\\ 1.055\, c^{1/2.4} - 0.055 & \mbox{sinon} \end{cases} \] où \(c_s\) est la composante encodée, et \(c\) la composante originale1.
Cette formule produit des valeurs comprises entre 0 et 1, qu'il faut encore multiplier par 255 puis arrondir pour obtenir les valeurs entières dans la plage souhaitée.
Mise en œuvre Java
La classe Color
du paquetage ch.epfl.flamemaker.color
, non modifiable, modélise une couleur. Elle est équipée du constructeur public suivant :
Color(double r, double g, double b)
, qui construit une couleur en fonction des composantes rouge, verte et bleue données. Lève l'exceptionIllegalArgumentException
si l'une des composantes est invalide.
Cette classe possède de plus les méthodes suivantes :
double red()
, qui retourne la composante rouge de la couleur.double green()
, qui retourne la composante verte de la couleur.double blue()
, qui retourne la composante bleue de la couleur.Color mixWith(Color that, double proportion)
, qui retourne la couleur obtenue en mélangeant la couleur représentée par le récepteur, en proportion donnée, avec la couleurthat
. Lève l'exceptionIllegalArgumentException
si la proportion donnée n'est pas comprise dans l'intervalle [0;1].int asPackedRGB()
, qui retourne la couleur encodée dans un entier, comme décrit plus haut (c-à-d où chaque composante occupe 8 bits et où la rouge occupe les bits 23 à 16, la verte les bits 15 à 8 et la bleue les bits 7 à 0). Notez que cette méthode n'est pas utile pour cette étape, mais le sera plus tard.
La classe Color
possède de plus la méthode statique suivante :
int sRGBEncode(double v, int max)
, qui retourne la valeurv
gamma-encodée au moyen de la formule sRGB donnée plus haut, puis transformée en un entier compris entre 0 (qui représente 0) etmax
(qui représente 1).
Finalement, la classe Color
possède plusieurs champs statiques non modifiables contenant des couleurs de base. Ces champs, nommés BLACK
, WHITE
, RED
, GREEN
et BLUE
, contiennent les couleurs auxquelles on s'attend.
Conseil de programmation : pour écrire la méthode asPackedRGB
, aidez-vous des opérateurs de décalage et des opérateurs bit-à-bit de Java pour placer les valeurs de 8 bits dans l'entier retourné. Notez qu'un entier Java de type int
possède 32 bits en tout.
Dessin
Introduction
Lors du passage des fractales IFS aux fractales Flame, l'algorithme de dessin avait été amélioré pour produire des images en niveaux de gris. Cette étape améliore encore l'algorithme pour produire des images en couleur.
Produire de telles images implique de trouver une technique de coloriage des fractales qui donne des résultats esthétiquement satisfaisants. Celle présentée ci-dessous se base pour cela sur l'historique des transformations qui ont été appliquées par l'algorithme du chaos.
Algorithme du chaos en couleur
Avant de présenter la version améliorée de l'algorithme du chaos, il importe de préciser que celui-ci ne manipule pas directement des couleurs, mais des index de couleur. Un index de couleur est un nombre réel compris entre 0 et 1 (inclus) qui n'est transformé en véritable couleur qu'au moment du dessin, au moyen d'une palette, qui associe une couleur à un index.
Pour obtenir une version couleur de l'algorithme du chaos, on commence par associer à chaque transformation \(F_i\) caractérisant la fractale un index de couleur \(C_i\). Ensuite, on augmente l'algorithme pour associer à chaque point \(p_j\) calculé un index de couleur \(c_j\) qui est la moyenne de l'index de couleur du point précédent et l'index de couleur associé à la transformation \(F_{i_j}\). En d'autres termes, la version colorée de l'algorithme du chaos calcule deux suites de valeurs, celle des points \(p_0, \ldots, p_m\) comme précédemment, et celle des index de couleur \(c_0,\ldots,c_m\), définie ainsi : \[ \begin{array}{ll} p_0 = (0,0) & c_0 = 0\\ p_1 = F_{i_1}(p_0) & c_1 = \tfrac{1}{2}(C_{i_1} + c_0)\\ p_2 = F_{i_2}(p_1) & c_2 = \tfrac{1}{2}(C_{i_2} + c_1)\\ p_3 = F_{i_3}(p_2) & c_3 = \tfrac{1}{2}(C_{i_3} + c_2)\\ \ldots & \ldots\\ p_m = F_{i_m}(p_{m-1}) & c_m = \tfrac{1}{2}(C_{i_m} + c_{m-1}) \end{array} \] En développant l'équation pour l'index de couleur \(c_j\), on obtient la formule suivante : \[ c_j = \sum_{k=1}^{j}{\frac{C_{i_k}}{2^{j+1-k}}} \] qui montre que l'index de couleur associé à un point dépend essentiellement des dernières transformations appliquées, les pondérations diminuant de manière exponentielle lorsqu'on « remonte dans le temps ».
Accumulation
Comme pour l'étape précédente, les points calculés par l'algorithme du chaos sont stockés dans un accumulateur. L'index de couleur d'une case de l'accumulateur est défini comme étant la moyenne de l'index de couleur des points qu'elle contient. Cette règle implique qu'une case ne contenant aucun point n'a pas d'index de couleur associé. Il faut dès lors savoir comment colorier une telle case dans l'image finale. De manière plus générale, on peut se demander comment utiliser le nombre de points contenus par une case lors de son coloriage. Pour mémoire, ce nombre de points déterminait la luminosité de la case (ou plutôt, pour être précis, du pixel de l'image correspondant) lors de l'étape précédente.
L'idée est d'ajouter un paramètre à l'algorithme de dessin, la couleur de fond. Cette couleur de fond est mélangée avec la couleur propre de la case, à savoir celle obtenue grâce à son index de couleur, via la palette. La proportion de la couleur propre dans le mélange est déterminée par le nombre de points contenus par la case de l'accumulateur, au moyen de la formule logarithmique de l'étape précédente.
Ainsi, une case ne contenant aucun point est coloriée avec la couleur de fond, car la proportion de la couleur propre à utiliser est nulle. A l'inverse, une case qui contient le nombre maximal de points est coloriée uniquement avec sa couleur propre. Et toutes les cases contenant un nombre de points entre ces deux valeurs sont coloriées par mélange entre la couleur de fond et la couleur propre de la case.
Index de couleur des transformations
Il reste encore à savoir quels index de couleur attribuer aux transformations, c-à-d quelles valeurs \(C_i\) utiliser. Une solution serait de laisser l'utilisateur définir ces index à sa guise. Toutefois, afin de simplifier l'interface utilisateur au maximum, une technique différente est utilisée ici. Elle consiste à attribuer automatiquement les index aux transformations, de manière à utiliser au mieux toute l'étendue de la palette. Les index \(C_i\) sont pour ce faire définis ainsi : \[ C_0 = 0,\\ C_1 = 1,\\ C_2 = \tfrac{1}{2},\\ C_3 = \tfrac{1}{4},\ C_4 = \tfrac{3}{4},\\ C_5 = \tfrac{1}{8},\ C_6 = \tfrac{3}{8},\ C_7 = \tfrac{5}{8},\ C_8 = \tfrac{7}{8},\\ C_9 = \tfrac{1}{16},\ C_{10} = \tfrac{3}{16},\ C_{11} = \tfrac{5}{16},\ \ldots,\ C_{14} = \tfrac{11}{16},\ C_{15} = \tfrac{13}{16},\ C_{16} = \tfrac{15}{16},\\ C_{17} = \tfrac{1}{32},\ \ldots \] Il existe une formule mathématique donnant la valeur de \(C_j\) pour \(j \ge 2\), mais sa découverte est laissée en exercice. A noter que vous devez absolument la découvrir, puis la programmer, puisque votre programme doit fonctionner avec un nombre quelconque de transformations !
Mise en œuvre Java
Fractale
La classe Flame
de l'étape précédente doit être adaptée afin que la méthode compute
ne calcule plus seulement la série des points mais également celle des couleurs, et stocke celles-ci dans l'accumulateur.
Accumulateur
La classe FlameAccumulator
de l'étape précédente, et son bâtisseur FlameAccumulator.Builder
, doivent être adaptés pour gérer la couleur.
Les adaptations à effectuer à FlameAccumulator
sont minimes. Un nouvel argument doit être ajouté à son constructeur, qui est un tableau de réels contenant la somme des index de couleur de chaque case, grâce à laquelle l'index de couleur moyen de la case peut être déterminé. Le constructeur a alors le profil suivant :
FlameAccumulator(int[][] hitCount, double[][] colorIndexSum)
et il va de soi que le second tableau doit être copié en profondeur, exactement comme le premier.
De plus, une méthode doit être ajoutée à la classe, afin de pouvoir calculer la couleur d'un point. Elle se présente ainsi :
Color color(Palette palette, Color background, int x, int y)
, qui retourne la couleur de la case de l'accumulateur aux coordonnées données. La couleur est calculée selon la technique décrite plus haut. Lève l'exceptionIndexOutOfBoundsException
si l'une des deux coordonnées est invalide.
La classe du bâtisseur, FlameAccumulator.Builder
, doit bien entendu être également adaptée, afin que sa méthode hit
prenne aussi la couleur du point calculé en argument.
Palette
L'interface Palette
du paquetage ch.epfl.flamemaker.color
modélise les palettes. Elle est équipée de la seule méthode suivante :
Color colorForIndex(double index)
, qui retourne la couleur associée à l'index de couleur donné. Lève l'exceptionIllegalArgumentException
si l'index est invalide.
Cette interface est implémentée par les deux classes décrites ci-dessous, qui mettent en œuvre deux types de palettes.
Palette interpolante
Le premier type de palette, modélisé par la classe InterpolatedPalette
(du paquetage ch.epfl.flamemaker.color
), interpole entre plusieurs couleurs. Son constructeur accepte une liste de couleurs (de type List<Color>
) et produit une palette qui interpole entre les couleurs de cette liste. C'est-à-dire que la palette fait correspondre la première couleur de la liste à l'index 0 et la dernière couleur de la liste à l'index 1. Les autres couleurs sont uniformément répartie dans l'intervalle [0;1], et pour les index qui tombent entre deux de ces couleurs, celle-ci sont mélangées de manière appropriée.
Il va de soi qu'une telle palette a besoin d'au moins deux couleurs pour être bien définie, donc le constructeur doit lever une exception en cas de liste trop courte.
Par exemple, une palette interpolant entre les couleurs rouge (1,0,0), vert (0,1,0) et bleu (0,0,1) dans cet ordre produit :
- la couleur rouge pour l'index 0,
- la couleur vert pour l'index 0.5,
- la couleur bleu pour l'index 1,
- la couleur (0.5, 0.5, 0) pour l'index 0.25, car ce dernier se situe à mi-chemin entre celui correpondant à la couleur rouge et celui correpondant à la couleur vert,
- et ainsi de suite.
Cette palette est présentée dans la figure ci-dessous.
La palette interpolant entre les couleurs rouge, vert et bleu
Palette aléatoire
Le second type de palette, modélisé par la classe RandomPalette
(du paquetage ch.epfl.flamemaker.color
), se comporte comme une palette interpolante mais les couleurs qui la composent sont choisies de manière aléatoire.
Le constructeur de cette classe prend donc un entier en argument, spécifiant le nombre de couleurs à choisir aléatoirement. Pour les mêmes raisons que celles mentionnées ci-dessus, ce nombre doit être supérieur ou égal à 2 pour que la palette soit bien définie. Le constructeur doit donc lever une exception si tel n'est pas le cas.
Attention : ne faites pas hériter RandomPalette
de InterpolatedPalette
car cela n'est pas propre. Par contre, n'hésitez pas à utiliser InterpolatedPalette
pour mettre en œuvre RandomPalette
.
Images PPM
Introduction
Les images PGM utilisées lors de l'étape précédente sont en niveau de gris et ne conviennent donc pas à cette étape. Heureusement, une généralisation des images PGM, appelé PPM, permet de représenter des images en couleur.
Format de fichier
Le format PPM est structuré de manière similaire aux formats PBM et PGM, à savoir :
- La première ligne contient la chaîne
P3
qui identifie ce type de fichiers. - La seconde ligne contient la largeur et la hauteur de l'image, séparés par une espace.
- La troisième ligne contient la valeur maximale de chacune des composantes couleur. Un bon choix pour cette valeur maximale est 100.
- Les lignes qui suivent contiennent les lignes de l'image. Chaque ligne est constituée d'une suite de triplets d'entiers, encodant (dans l'ordre) la composante rouge, verte et bleue des pixels. Les éléments du triplets et les triplets eux-mêmes sont séparés par une ou plusieurs espaces.
Bâtisseur de fractale
Introduction
La classe modélisant les fractales Flame n'est pas modifiable. Cela est approprié à ce stade du projet, car on désire uniquement dessiner des fractales prédéfinies, et pas en construire de manière incrémentale.
Plus tard, il sera toutefois nécessaire de pouvoir construire des fractales Flame interactivement, puisque c'est exactement le but du programme Flame Maker. Pour cette raison, il importe de fournir un bâtisseur de fractale Flame.
Une fractale Flame étant caractérisée par une séquence de transformations Flame, il faut également fournir un bâtisseur pour ces transformations.
Mise en œuvre Java
La classe Flame.Builder
(imbriquée statiquement dans la classe Flame
) permet de bâtir une fractale Flame de manière incrémentale. Ce bâtisseur possède un unique constructeur :
Builder(Flame flame)
, qui construit un bâtisseur de fractale Flame, initialisé avec la fractale donnée.
Ce bâtisseur possède de plus les méthodes suivantes :
int transformationCount()
, qui retourne le nombre de transformations Flame caractérisant la fractale en cours de construction.void addTransformation(FlameTransformation transformation)
, qui ajoute la transformation Flame à la fractale, en dernière position.AffineTransformation affineTransformation(int index)
, qui retourne la composante affine de la transformation Flame d'index donné.void setAffineTransformation(int index, AffineTransformation newTransformation)
, qui change la composante affine de la transformation Flame d'index donné.double variationWeight(int index, Variation variation)
, qui retourne le poids de la variation donnée pour la transformation Flame d'index donné.void setVariationWeight(int index, Variation variation, double newWeight)
, qui change le poids de la variation donnée pour la transformation Flame d'index donné.void removeTransformation(int index)
, qui supprime la transformation Flame d'index donné.Flame build()
, qui construit et retourne la fractale Flame.
Bien entendu, toutes les méthodes ci-dessus qui prennent un index en argument doivent lever l'exception IndexOutOfBoundsException
si cet index est invalide.
Pour pouvoir écrire le bâtisseur décrit ci-dessus, il faut avoir la possibilité de bâtir également des transformations Flame de manière incrémentale. Le moyen le plus simple de faire cela est de définir une classe FlameTransformation.Builder
(imbriquée statiquement dans la classe FlameTransformation
) et de l'équiper des méthodes nécessaires.
Test
Comme pour les étapes précédentes, une manière de tester votre programme consiste à lui faire générer des fractales connues et vérifier que vous obtenez l'image attendue.
Pour tester votre classe de bâtisseur, vous pouvez de plus en créer un à partir d'une des fractales connues et appeler les méthodes nécessaires pour la transformer en une autre fractale connue.
Les images ci-dessous ont été obtenues avec les mêmes paramètres que pour l'étape précédente, sur un fond noir et coloriées au moyen de la palette interpolant entre les couleurs rouge, vert et bleu. Les composantes couleur ont été gamma-encodées avant d'être transformées en entiers.
Shark fin
Shark fin
Turbulence
Turbulence
Résumé
Pour cette étape, vous devez :
- Ecrire les classes et interfaces
Color
,Palette
,InterpolatedPalette
etRandomPalette
du paquetagech.epfl.flamemaker.color
en fonction des spécifications ci-dessus. - Augmenter les classes
Flame
,FlameAccumulator
etFlameAccumulator.Builder
afin de permettre la production d'images couleur, en utilisant l'algorithme décrit plus haut. - Ecrire une classe principale (nommée p.ex.
FlamePPMMaker
), équipée d'une méthodemain
qui calcule les fractales Flame de test décrites ci-dessus et produise les images correspondantes dans des fichiers au format PPM. - Vérifier que ces images correspondent à celles données ci-dessus et corriger les éventuels problèmes.
- Ecrire les classes
Flame.Builder
etFlameTransformation.Builder
en fonction des spécifications ci-dessus, vérifier qu'elles se comportent correctement et corriger les éventuels problèmes.
Mises à jour
- 05/03: Les composantes vertes et bleues sont maintenant dans le bon ordre dans l'encodage entier des couleurs.
- 05/03: Le paquetage dans lequel placer l'interface
Palette
et les classes qui l'implémentent est maintenant le bon, à savoirch.epfl.flamemaker.color
. - 06/03: Le nom du bâtisseur de transformation Flame est corrigé dans le résumé ci-dessus.
Notes
1 A noter que cette fonction est quasi-identique à la fonction plus simple suivante : \[ c_s = c^{1/2.2} \] La première est toutefois préférée à la seconde pour des questions de stabilité numérique avec de très petites valeurs de \(c\).