Proofreading

This commit is contained in:
Bradlee Speice 2024-12-16 21:29:03 -05:00
parent a6194763d1
commit 37e2992865
4 changed files with 99 additions and 107 deletions

View File

@ -22,15 +22,14 @@ import banner from '../banner.png'
<!-- truncate --> <!-- truncate -->
I don't remember exactly when I first learned about fractal flames, but I do remember being entranced by the images they created. I don't remember when exactly I first learned about fractal flames, but I do remember being entranced by the images they created.
I also remember their unique appeal to my young engineering mind; this was an art form I could participate in. I also remember their unique appeal to my young engineering mind; this was an art form I could participate in.
The original [Fractal Flame Algorithm paper](https://flam3.com/flame_draves.pdf) describing their structure was too much The [Fractal Flame Algorithm paper](https://flam3.com/flame_draves.pdf) describing their structure was too much
for me to handle at the time (I was ~12 years old), so I was content to play around and enjoy the pictures. for me to handle at the time (I was ~12 years old), so I was content to play around and enjoy the pictures.
But the desire to understand it stuck around. Now, with a graduate degree under my belt, I want to revisit it But the desire to understand it stuck around. Now, with a graduate degree under my belt, I wanted to revisit it.
and try to make some progress.
This guide is my attempt to explain how fractal flames work in a way that younger me &mdash; and others interested in the art &mdash; This guide is my attempt to explain how fractal flames work so that younger me &mdash; and others interested in the art &mdash;
can understand without too much prior knowledge. can understand without too much prior knowledge.
--- ---
@ -42,7 +41,7 @@ This post covers section 2 of the Fractal Flame Algorithm paper
::: :::
As mentioned, fractal flames are a type of "[iterated function system](https://en.wikipedia.org/wiki/Iterated_function_system)," As mentioned, fractal flames are a type of "[iterated function system](https://en.wikipedia.org/wiki/Iterated_function_system),"
or IFS. The formula for an IFS is short, but takes some time to unpack: or IFS. The formula for an IFS is short, but takes some time to work through:
$$ $$
S = \bigcup_{i=0}^{n-1} F_i(S) S = \bigcup_{i=0}^{n-1} F_i(S)
@ -52,7 +51,7 @@ $$
First, $S$. $S$ is the set of points in two dimensions (in math terms, $S \in \mathbb{R}^2$) First, $S$. $S$ is the set of points in two dimensions (in math terms, $S \in \mathbb{R}^2$)
that represent a "solution" of some kind to our equation. that represent a "solution" of some kind to our equation.
Our goal is to find all points in $S$, plot them, and display that image. Our goal is to find all the points in $S$, plot them, and display that image.
For example, if we say $S = \{(0,0), (1, 1), (2, 2)\}$, there are three points to plot: For example, if we say $S = \{(0,0), (1, 1), (2, 2)\}$, there are three points to plot:
@ -67,20 +66,17 @@ export const simpleData = [
<VictoryScatter data={simpleData} size={5} style={{data: {fill: "blue"}}}/> <VictoryScatter data={simpleData} size={5} style={{data: {fill: "blue"}}}/>
</VictoryChart> </VictoryChart>
However, this is a pretty boring image. With fractal flames, rather than listing individual points, With fractal flames, rather than listing individual points, we use functions to describe the solution.
we use functions to describe which points are part of the solution. This means there are an infinite This means there are an infinite number of points, but if we find _enough_ points to plot, we get a nice picture.
number of points, but if we find _enough_ points to plot, we'll end up with a nice picture. And if the functions change, the solution also changes, and we get something new.
And if the functions change, the solution also changes, and we get a new picture.
### Transform functions ### Transform functions
Second, the $F_i(S)$ functions, also known as "transforms." Second, the $F_i(S)$ functions, also known as "transforms."
At their most basic, each $F_i$ takes in a 2-dimensional point and gives back a new point Each transform takes in a 2-dimensional point and gives a new point back
(in math terms, $F_i \in \mathbb{R}^2 \rightarrow \mathbb{R}^2$). (in math terms, $F_i \in \mathbb{R}^2 \rightarrow \mathbb{R}^2$).
While you could theoretically use any function, we'll start with a specific kind of function While you could theoretically use any function, we'll focus on a specific kind of function
called an "[affine transformation](https://en.wikipedia.org/wiki/Affine_transformation)." called an "[affine transformation](https://en.wikipedia.org/wiki/Affine_transformation)." Every transform uses the same formula:
The general form of an affine transformation is:
$$ $$
F_i(a_i x + b_i y + c_i, d_i x + e_i y + f_i) F_i(a_i x + b_i y + c_i, d_i x + e_i y + f_i)
@ -91,8 +87,8 @@ import CodeBlock from '@theme/CodeBlock'
<CodeBlock language="typescript">{transformSource}</CodeBlock> <CodeBlock language="typescript">{transformSource}</CodeBlock>
The parameters ($a_i$, $b_i$, etc.) are values we get to choose. The parameters ($a_i$, $b_i$, etc.) are values we choose.
For example, we can represent a "shift" function like this: For example, we can define a "shift" function like this:
$$ $$
\begin{align*} \begin{align*}
@ -106,7 +102,7 @@ F_{shift}(x, y) &= (1 \cdot x + 0.5, 1 \cdot y + 1.5)
\end{align*} \end{align*}
$$ $$
Applying this function to our original points will give us a new set of points: Applying this transform to the original points gives us a new set of points:
import {applyCoefs} from "../src/transform" import {applyCoefs} from "../src/transform"
@ -139,7 +135,7 @@ $$
S = \bigcup_{i=0}^{n-1} F_i(S) S = \bigcup_{i=0}^{n-1} F_i(S)
$$ $$
Or, to put it in English, we might say: Or, in English, we might say:
> Our solution, $S$, is the union of all sets produced by applying each function, $F_i$, > Our solution, $S$, is the union of all sets produced by applying each function, $F_i$,
> to points in the solution. > to points in the solution.
@ -153,17 +149,13 @@ defining the mathematics of iterated function systems:
> Furthermore, $S$ is compact and is the closure of the set of fixed points $s_{i_1...i_p}$ > Furthermore, $S$ is compact and is the closure of the set of fixed points $s_{i_1...i_p}$
> of finite compositions $F_{i_1...i_p}$ of members of $F$. > of finite compositions $F_{i_1...i_p}$ of members of $F$.
:::note
I've tweaked the conventions of that paper slightly to match the Fractal Flame paper
:::
Before your eyes glaze over, let's unpack this: Before your eyes glaze over, let's unpack this:
- **Furthermore, $S$ is [compact](https://en.wikipedia.org/wiki/Compact_space)...**: All points in our solution will be in a finite range - **Furthermore, $S$ is [compact](https://en.wikipedia.org/wiki/Compact_space)...**: All points in our solution will be in a finite range
- **...and is the [closure](https://en.wikipedia.org/wiki/Closure_(mathematics)) of the set of [fixed points](https://en.wikipedia.org/wiki/Fixed_point_(mathematics))**: - **...and is the [closure](https://en.wikipedia.org/wiki/Closure_(mathematics)) of the set of [fixed points](https://en.wikipedia.org/wiki/Fixed_point_(mathematics))**:
Applying our functions to points in the solution will give us other points that are in the solution Applying our functions to points in the solution will give us other points that are in the solution
- **...of finite compositions $F_{i_1...i_p}$ of members of $F$**: By composing our functions (that is, - **...of finite compositions $F_{i_1...i_p}$ of members of $F$**: By composing our functions (that is,
using the output of one function as input to the next function), we will arrive at the points in the solution using the output of one function as input to the next), we will arrive at the points in the solution
Thus, by applying the functions to fixed points of our system, we will find the other points we care about. Thus, by applying the functions to fixed points of our system, we will find the other points we care about.
@ -177,11 +169,11 @@ Thus, by applying the functions to fixed points of our system, we will find the
algorithm demonstrates, we only need functions to be contractive _on average_. At worst, the system will algorithm demonstrates, we only need functions to be contractive _on average_. At worst, the system will
degenerate and produce a bad image. degenerate and produce a bad image.
Second, we're focused on $\mathbb{R}^2$ because we're generating images, but the Hutchinson paper Second, we're focused on $\mathbb{R}^2$ because we're generating images, but the math
allows for arbitrary dimensions; you could also have 3-dimensional fractal flames. allows for arbitrary dimensions; you could also have 3-dimensional fractal flames.
Finally, there's a close relationship between fractal flames and [attractors](https://en.wikipedia.org/wiki/Attractor). Finally, there's a close relationship between fractal flames and [attractors](https://en.wikipedia.org/wiki/Attractor).
Specifically, the fixed points of $S$ act as attractors for the chaos game (explained in more detail below). Specifically, the fixed points of $S$ act as attractors for the chaos game (explained below).
</details> </details>
This is still a bit vague, so let's work through an example. This is still a bit vague, so let's work through an example.
@ -200,7 +192,7 @@ $$
### The chaos game ### The chaos game
Next, how do we find the "fixed points" mentioned earlier? The paper lays out an algorithm called the "[chaos game](https://en.wikipedia.org/wiki/Chaos_game)" Now, how do we find the "fixed points" mentioned earlier? The paper lays out an algorithm called the "[chaos game](https://en.wikipedia.org/wiki/Chaos_game)"
that gives us points in the solution: that gives us points in the solution:
$$ $$
@ -220,8 +212,8 @@ The chaos game algorithm is effectively the "finite compositions of $F_{i_1..i_p
Let's turn this into code, one piece at a time. Let's turn this into code, one piece at a time.
First, we need to generate some random numbers. The "bi-unit square" is the range $[-1, 1]$, To start, we need to generate some random numbers. The "bi-unit square" is the range $[-1, 1]$,
so we generate a random point using an existing API: and we can do this using an existing API:
import biunitSource from '!!raw-loader!../src/randomBiUnit' import biunitSource from '!!raw-loader!../src/randomBiUnit'
@ -235,14 +227,14 @@ import randintSource from '!!raw-loader!../src/randomInteger'
### Plotting ### Plotting
Finally, implementing the `plot` function. This blog series is designed to be interactive, Finally, implementing the `plot` function. This blog series is interactive,
so everything displays directly in the browser. As an alternative, so everything displays directly in the browser. As an alternative,
software like `flam3` and Apophysis can "plot" by saving an image to disk. software like `flam3` and Apophysis can "plot" by saving an image to disk.
To show the results, we'll use the [Canvas API](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API). To see the results, we'll use the [Canvas API](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API).
This allows us to manipulate individual pixels in an image and display it on screen. This allows us to manipulate individual pixels in an image and show it on screen.
First, we need to convert from Fractal Flame coordinates to pixel coordinates. First, we need to convert from fractal flame coordinates to pixel coordinates.
To simplify things, we'll assume that we're plotting a square image To simplify things, we'll assume that we're plotting a square image
with range $[0, 1]$ for both $x$ and $y$: with range $[0, 1]$ for both $x$ and $y$:
@ -251,7 +243,7 @@ import cameraSource from "!!raw-loader!./cameraGasket"
<CodeBlock language="typescript">{cameraSource}</CodeBlock> <CodeBlock language="typescript">{cameraSource}</CodeBlock>
Next, we'll store the pixel data in an [`ImageData` object](https://developer.mozilla.org/en-US/docs/Web/API/ImageData). Next, we'll store the pixel data in an [`ImageData` object](https://developer.mozilla.org/en-US/docs/Web/API/ImageData).
Each pixel in the image on screen has a corresponding index in the `data` array. Each pixel on screen has a corresponding index in the `data` array.
To plot a point, we set that pixel to be black: To plot a point, we set that pixel to be black:
import plotSource from '!!raw-loader!./plot' import plotSource from '!!raw-loader!./plot'
@ -277,25 +269,22 @@ import chaosGameSource from '!!raw-loader!./chaosGame'
### Weights ### Weights
There's one last step before we finish the introduction. So far, each function $F_i$ has There's one last step before we finish the introduction. So far, each transform has
the same chance of being chosen. We can change that by introducing a "weight" ($w_i$) the same chance of being picked in the chaos game.
to each transform in the chaos game: We can change that by giving them a "weight" ($w_i$) instead:
import randomChoiceSource from '!!raw-loader!../src/randomChoice' import randomChoiceSource from '!!raw-loader!../src/randomChoice'
<CodeBlock language={'typescript'}>{randomChoiceSource}</CodeBlock> <CodeBlock language={'typescript'}>{randomChoiceSource}</CodeBlock>
If we let the chaos game run forever, these weights wouldn't matter.
But because the iteration count is limited, changing the weights
means we don't plot some parts of the image:
import chaosGameWeightedSource from "!!raw-loader!./chaosGameWeighted"; import chaosGameWeightedSource from "!!raw-loader!./chaosGameWeighted";
<CodeBlock language={'typescript'}>{chaosGameWeightedSource}</CodeBlock> <CodeBlock language={'typescript'}>{chaosGameWeightedSource}</CodeBlock>
For Sierpinski's Gasket, we start with equal weighting,
but changing the transform weights affects how often
the chaos game "visits" parts of the image. If the chaos
were to run forever, we'd get the same image;
but because the iteration count is limited,
some parts may be missing below.
:::tip :::tip
Double-click the image if you want to save a copy! Double-click the image if you want to save a copy!
::: :::
@ -308,7 +297,7 @@ import {SquareCanvas} from "../src/Canvas";
## Summary ## Summary
Studying the foundations of fractal flames is challenging, Studying the foundations of fractal flames is challenging,
but we now have an understanding of both the mathematics but we now have an understanding of the mathematics
and implementations of iterated function systems. and the implementation of iterated function systems.
In the next post, we'll study the first innovation of fractal flames: variations. In the next post, we'll look at the first innovation of fractal flame algorithm: variations.

View File

@ -6,31 +6,30 @@ authors: [bspeice]
tags: [] tags: []
--- ---
Now that we have a basic chaos game in place, it's time to spice things up. Variations create the Now that we've learned about the chaos game, it's time to spice things up. Variations create the
shapes and patterns that fractal flames are known for. shapes and patterns that fractal flames are known for.
<!-- truncate --> <!-- truncate -->
:::info :::info
This post uses [reference parameters](../params.flame) to demonstrate the fractal flame algorithm. This post uses [reference parameters](../params.flame) to demonstrate the fractal flame algorithm.
If you're interested in tweaking the parameters, or generating your own art, [Apophysis](https://sourceforge.net/projects/apophysis/) If you're interested in tweaking the parameters, or creating your own, [Apophysis](https://sourceforge.net/projects/apophysis/)
can load that file to play around with! can load that file.
::: :::
## Variations ## Variations
:::note :::note
This post covers section 3 for the Fractal Flame Algorithm paper This post covers section 3 of the Fractal Flame Algorithm paper
::: :::
import CodeBlock from '@theme/CodeBlock' import CodeBlock from '@theme/CodeBlock'
We previously introduced transforms as the "functions" of an "iterated function system," and showed how We previously introduced transforms as the "functions" of an "iterated function system," and showed how
playing the chaos game leads to an image of Sierpinski's Gasket. Even though we used simple functions, playing the chaos game gives us an image of Sierpinski's Gasket. Even though we used simple functions,
the image it generates is exciting. But it's still not nearly as exciting as the images the Fractal Flame the image it generates is intriguing. But what would happen if we used something more complex?
algorithm is known for.
This leads us to the first big innovation of the Fractal Flame algorithm: adding non-linear functions This leads us to the first big innovation of the fractal flame algorithm: adding non-linear functions
after the affine transform. These functions are called "variations": after the affine transform. These functions are called "variations":
$$ $$
@ -44,14 +43,14 @@ import variationSource from '!!raw-loader!../src/variation'
Just like transforms, variations ($V_j$) are functions that take in $(x, y)$ coordinates Just like transforms, variations ($V_j$) are functions that take in $(x, y)$ coordinates
and give back new $(x, y)$ coordinates. and give back new $(x, y)$ coordinates.
However, the sky is the limit for what happens between input and output. However, the sky is the limit for what happens between input and output.
The Fractal Flame paper lists 49 different variation functions, The Fractal Flame paper lists 49 variation functions,
and the official `flam3` implementation supports [98 different variations](https://github.com/scottdraves/flam3/blob/7fb50c82e90e051f00efcc3123d0e06de26594b2/variations.c). and the official `flam3` implementation supports [98 different variations](https://github.com/scottdraves/flam3/blob/7fb50c82e90e051f00efcc3123d0e06de26594b2/variations.c).
To draw our reference image, we'll focus on four variations: To draw our reference image, we'll focus on just four:
### Linear (variation 0) ### Linear (variation 0)
This variation is dead simple: just return the $x$ and $y$ coordinates as-is. This variation is dead simple: return the $x$ and $y$ coordinates as-is.
$$ $$
V_0(x,y) = (x,y) V_0(x,y) = (x,y)
@ -63,12 +62,12 @@ import linearSrc from '!!raw-loader!../src/linear'
:::tip :::tip
In a way, we've already been using this variation! The transforms that define Sierpinski's Gasket In a way, we've already been using this variation! The transforms that define Sierpinski's Gasket
apply the affine coefficients to the input point, and use that as the output point. apply the affine coefficients to the input point and use that as the output.
::: :::
### Julia (variation 13) ### Julia (variation 13)
This variation is a good example of the non-linear functions we can use. It uses both trigonometry This variation is a good example of a non-linear function. It uses both trigonometry
and probability to produce interesting shapes: and probability to produce interesting shapes:
$$ $$
@ -93,7 +92,7 @@ import juliaSrc from '!!raw-loader!../src/julia'
### Popcorn (variation 17) ### Popcorn (variation 17)
Some variations rely on knowing the transform's affine coefficients; they're called "dependent variations." Some variations rely on knowing the transform's affine coefficients; they're called "dependent variations."
For the popcorn variation, we use $c$ and $f$: For this variation, we use $c$ and $f$:
$$ $$
V_{17}(x,y) = (x + c\ \text{sin}(\text{tan }3y), y + f\ \text{sin}(\text{tan }3x)) V_{17}(x,y) = (x + c\ \text{sin}(\text{tan }3y), y + f\ \text{sin}(\text{tan }3x))
@ -105,8 +104,8 @@ import popcornSrc from '!!raw-loader!../src/popcorn'
### PDJ (variation 24) ### PDJ (variation 24)
Some variations have extra parameters; they're called "parametric variations." Some variations have extra parameters we can choose; they're called "parametric variations."
For the PDJ variation, there are four extra parameters we can choose: For the PDJ variation, there are four extra parameters:
$$ $$
p_1 = \text{pdj.a} \hspace{0.1cm} p_2 = \text{pdj.b} \hspace{0.1cm} p_3 = \text{pdj.c} \hspace{0.1cm} p_4 = \text{pdj.d} \\ p_1 = \text{pdj.a} \hspace{0.1cm} p_2 = \text{pdj.b} \hspace{0.1cm} p_3 = \text{pdj.c} \hspace{0.1cm} p_4 = \text{pdj.d} \\
@ -133,11 +132,11 @@ import blendSource from "!!raw-loader!../src/blend";
<CodeBlock language={'typescript'}>{blendSource}</CodeBlock> <CodeBlock language={'typescript'}>{blendSource}</CodeBlock>
With that in place, we have enough to render a first fractal flame. We'll use the same With that in place, we have enough to render a fractal flame. We'll use the same
chaos game as before, but our new transforms and variations produce a dramatically different image: chaos game as before, but the new transforms and variations produce a dramatically different image:
:::tip :::tip
Try using the variation weight sliders to figure out which parts of the image each transform controls. Try using the variation weights to figure out which parts of the image each transform controls.
::: :::
import {SquareCanvas} from "../src/Canvas"; import {SquareCanvas} from "../src/Canvas";
@ -147,9 +146,9 @@ import FlameBlend from "./FlameBlend";
## Post transforms ## Post transforms
Next, we'll introduce a second affine transform, applied _after_ variation blending. This is called a "post transform." Next, we'll introduce a second affine transform applied _after_ variation blending. This is called a "post transform."
We'll use some new variables, but the post transform function should look familiar: We'll use some new variables, but the post transform should look familiar:
$$ $$
\begin{align*} \begin{align*}
@ -163,7 +162,7 @@ import postSource from '!!raw-loader!./post'
<CodeBlock language="typescript">{postSource}</CodeBlock> <CodeBlock language="typescript">{postSource}</CodeBlock>
The image below uses the same transforms/variations as the previous fractal flame, The image below uses the same transforms/variations as the previous fractal flame,
but allows modifying the post-transform coefficients: but allows changing the post-transform coefficients:
<details> <details>
<summary>If you want to test your understanding...</summary> <summary>If you want to test your understanding...</summary>
@ -178,12 +177,12 @@ import FlamePost from "./FlamePost";
## Final transforms ## Final transforms
Our last step is to introduce a "final transform" ($F_{final}$) that is applied The last step is to introduce a "final transform" ($F_{final}$) that is applied
regardless of which transform the chaos game selects. It works just like a normal transform regardless of which regular transform ($F_i$) the chaos game selects.
(composition of affine transform, variation blend, and post transform), It's just like a normal transform (composition of affine transform, variation blend, and post transform),
but it doesn't change the chaos game state. but it doesn't affect the chaos game state.
With that in place, our chaos game algorithm changes slightly: After adding the final transform, our chaos game algorithm looks like this:
$$ $$
\begin{align*} \begin{align*}
@ -201,6 +200,9 @@ import chaosGameFinalSource from "!!raw-loader!./chaosGameFinal"
<CodeBlock language="typescript">{chaosGameFinalSource}</CodeBlock> <CodeBlock language="typescript">{chaosGameFinalSource}</CodeBlock>
This image uses the same normal/post transforms as above, but allows modifying
the coefficients and variations of the final transform:
import FlameFinal from "./FlameFinal"; import FlameFinal from "./FlameFinal";
<SquareCanvas name={"flame_final"}><FlameFinal/></SquareCanvas> <SquareCanvas name={"flame_final"}><FlameFinal/></SquareCanvas>
@ -208,7 +210,7 @@ import FlameFinal from "./FlameFinal";
## Summary ## Summary
Variations are the fractal flame algorithm's first major innovation. Variations are the fractal flame algorithm's first major innovation.
By blending variation functions, and post/final transforms, we generate unique images. By blending variation functions and post/final transforms, we generate unique images.
However, the images themselves are grainy and unappealing. In the next post, we'll clean up However, these images are grainy and unappealing. In the next post, we'll clean up
the image quality and add some color. the image quality and add some color.

View File

@ -6,7 +6,7 @@ authors: [bspeice]
tags: [] tags: []
--- ---
So far, our `plot()` function has been fairly simple; map a fractal flame coordinate to a specific pixel, So far, our `plot()` function has been fairly simple: map a fractal flame coordinate to a specific pixel,
and color in that pixel. This works well for simple function systems (like Sierpinski's Gasket), and color in that pixel. This works well for simple function systems (like Sierpinski's Gasket),
but more complex systems (like the reference parameters) produce grainy images. but more complex systems (like the reference parameters) produce grainy images.
@ -20,21 +20,20 @@ In this post, we'll refine the image quality and add color to really make things
This post covers sections 4 and 5 of the Fractal Flame Algorithm paper This post covers sections 4 and 5 of the Fractal Flame Algorithm paper
::: :::
One problem with the existing chaos game is that we waste work One problem with the current chaos game algorithm is that we waste work
by treating pixels as a binary "on" (opaque) or "off" (transparent). because pixels are either "on" (opaque) or "off" (transparent).
If the chaos game encounters the same location twice, nothing actually changes. If the chaos game encounters the same pixel twice, nothing changes.
To demonstrate how much work is wasted, we'll render the reference image again. To demonstrate how much work is wasted, we'll count each time the chaos game
However, we'll also count each time the chaos game encounters a pixel. visits a pixel while iterating. This gives us a kind of image "histogram":
This gives us a kind of image "histogram":
import chaosGameHistogramSource from "!!raw-loader!./chaosGameHistogram" import chaosGameHistogramSource from "!!raw-loader!./chaosGameHistogram"
<CodeBlock language="typescript">{chaosGameHistogramSource}</CodeBlock> <CodeBlock language="typescript">{chaosGameHistogramSource}</CodeBlock>
When the chaos game finishes, we find the pixel encountered most frequently. When the chaos game finishes, we find the pixel encountered most often.
Finally, we "paint" the image by setting each pixel's alpha value (transparency) Finally, we "paint" the image by setting each pixel's alpha (transparency) value
to the ratio of times encountered divided by the maximum: to the ratio of times visited divided by the maximum:
import CodeBlock from "@theme/CodeBlock"; import CodeBlock from "@theme/CodeBlock";
@ -50,22 +49,22 @@ import {paintLinear} from "./paintLinear";
## Tone mapping ## Tone mapping
While using a histogram reduces the "graininess" of the image, it also leads to some parts vanishing entirely. While using a histogram reduces the "graining," it also leads to some parts vanishing entirely.
In the reference parameters, the outer circle is preserved, but the interior appears to be missing! In the reference parameters, the outer circle is still there, but the interior is gone!
To fix this, we'll introduce the second major innovation of the fractal flame algorithm: [tone mapping](https://en.wikipedia.org/wiki/Tone_mapping). To fix this, we'll introduce the second major innovation of the fractal flame algorithm: [tone mapping](https://en.wikipedia.org/wiki/Tone_mapping).
This is a technique used in computer graphics to compensate for differences in how This is a technique used in computer graphics to compensate for differences in how
computers represent brightness, and how people see brightness. computers represent brightness, and how people actually see brightness.
As a concrete example, high dynamic range (HDR) photography uses this technique to capture As a concrete example, high-dynamic-range (HDR) photography uses this technique to capture
nice images of scenes with a wide range of brightnesses. To take a picture of something dark, scenes with a wide range of brightnesses. To take a picture of something dark,
you need a long exposure time. However, long exposures lead to "hot spots" (sections that are pure white). you need a long exposure time. However, long exposures lead to "hot spots" (sections that are pure white).
By taking multiple pictures with different exposure times, we can combine them to create By taking multiple pictures with different exposure times, we can combine them to create
a final image where everything is visible. a final image where everything is visible.
In fractal flames, this "tone map" is accomplished by scaling brightness according to the _logarithm_ In fractal flames, this "tone map" is accomplished by scaling brightness according to the _logarithm_
of how many times we encounter a pixel. This way, "cold spots" (pixels the chaos game visits infrequently) of how many times we encounter a pixel. This way, "cold spots" (pixels the chaos game visits infrequently)
will still be visible, and "hot spots" (pixels the chaos game visits frequently) won't wash out. are still visible, and "hot spots" (pixels the chaos game visits frequently) won't wash out.
<details> <details>
<summary>Log-scale vibrancy also explains fractal flames appear to be 3D...</summary> <summary>Log-scale vibrancy also explains fractal flames appear to be 3D...</summary>
@ -89,7 +88,7 @@ import {paintLogarithmic} from './paintLogarithmic'
## Color ## Color
Finally, we'll introduce the last innovation of the fractal flame algorithm: color. Now we'll introduce the last innovation of the fractal flame algorithm: color.
By including a third coordinate ($c$) in the chaos game, we can illustrate the transforms By including a third coordinate ($c$) in the chaos game, we can illustrate the transforms
responsible for the image. responsible for the image.
@ -100,11 +99,12 @@ Color in a fractal flame is continuous on the range $[0, 1]$. This is important
- It helps blend colors together in the final image. Slight changes in the color value lead to - It helps blend colors together in the final image. Slight changes in the color value lead to
slight changes in the actual color slight changes in the actual color
- It allows us to swap in new color palettes easily. We're free to choose what actual colors - It allows us to swap in new color palettes easily. We're free to choose what actual colors
each color value represents each value represents
We'll give each transform a color value ($c_i$) in the $[0, 1]$ range. We'll give each transform a color value ($c_i$) in the $[0, 1]$ range.
The final transform gets a value too ($c_f$).
Then, at each step in the chaos game, we'll set the current color Then, at each step in the chaos game, we'll set the current color
by blending it with the previous color and the current transform: by blending it with the previous color:
$$ $$
\begin{align*} \begin{align*}
@ -123,13 +123,13 @@ $$
### Color speed ### Color speed
:::warning :::warning
Color speed as a concept isn't introduced in the Fractal Flame Algorithm paper. Color speed isn't introduced in the Fractal Flame Algorithm paper.
It is included here because [`flam3` implements it](https://github.com/scottdraves/flam3/blob/7fb50c82e90e051f00efcc3123d0e06de26594b2/variations.c#L2140), It is included here because [`flam3` implements it](https://github.com/scottdraves/flam3/blob/7fb50c82e90e051f00efcc3123d0e06de26594b2/variations.c#L2140),
and because it's fun to play with. and because it's fun to play with.
::: :::
Next, we'll add a parameter to each transform that controls how much it affects the current color. Next, we'll add a parameter to each transform that controls how much it changes the current color.
This is known as the "color speed" ($s_i$): This is known as the "color speed" ($s_i$):
$$ $$
@ -155,7 +155,7 @@ There's one small complication: the color coordinate is continuous, but the pale
uses discrete colors. How do we handle situations where the color coordinate is uses discrete colors. How do we handle situations where the color coordinate is
"in between" the colors of our palette? "in between" the colors of our palette?
One way is to use a step function. In the code below, we multiply the color coordinate One way to handle this is a step function. In the code below, we multiply the color coordinate
by the number of colors in the palette, then truncate that value. This gives us a discrete index: by the number of colors in the palette, then truncate that value. This gives us a discrete index:
import colorFromPaletteSource from "!!raw-loader!./colorFromPalette"; import colorFromPaletteSource from "!!raw-loader!./colorFromPalette";
@ -169,7 +169,7 @@ import colorFromPaletteSource from "!!raw-loader!./colorFromPalette";
For example, `flam3` uses [linear interpolation](https://github.com/scottdraves/flam3/blob/7fb50c82e90e051f00efcc3123d0e06de26594b2/rect.c#L483-L486) For example, `flam3` uses [linear interpolation](https://github.com/scottdraves/flam3/blob/7fb50c82e90e051f00efcc3123d0e06de26594b2/rect.c#L483-L486)
</details> </details>
In the diagram below, each color in our palette is plotted on a small vertical strip. In the diagram below, each color in the palette is plotted on a small vertical strip.
Putting the strips side by side shows the full palette used by the reference parameters: Putting the strips side by side shows the full palette used by the reference parameters:
import * as params from "../src/params" import * as params from "../src/params"
@ -179,8 +179,9 @@ import {PaletteBar} from "./FlameColor"
### Plotting ### Plotting
We're now ready to plot our $(x_f,y_f,c_f)$ coordinates. After translating from color coordinate ($c_f$) We're now ready to plot our $(x_f,y_f,c_f)$ coordinates. This time, we'll use a histogram
to RGB value, add that to the image histogram: for each color channel (red, green, blue, alpha). After translating from color coordinate ($c_f$)
to RGB value, add that to the histogram:
import chaosGameColorSource from "!!raw-loader!./chaosGameColor" import chaosGameColorSource from "!!raw-loader!./chaosGameColor"
@ -208,14 +209,14 @@ brightness/transparency to reduce the visual "graining" of previous images.
Next, introducing a third coordinate to the chaos game makes color images possible, Next, introducing a third coordinate to the chaos game makes color images possible,
the third major innovation of the fractal flame algorithm. Using a continuous the third major innovation of the fractal flame algorithm. Using a continuous
color scale and color palette adds a splash of color to our transforms. color scale and color palette adds a splash of excitement to the image.
The Fractal Flame Algorithm paper does go on to describe more techniques The Fractal Flame Algorithm paper goes on to describe more techniques
not covered here. For example, Image quality can be improved with density estimation not covered here. For example, image quality can be improved with density estimation
and filtering. New parameters can be generated by "mutating" existing and filtering. New parameters can be generated by "mutating" existing
fractal flames. And fractal flames can even be animated to produce videos! fractal flames. And fractal flames can even be animated to produce videos!
That said, I think this is a good place to wrap up. We were able to go from That said, I think this is a good place to wrap up. We went from
an introduction to the mathematics of fractal systems all the way to an introduction to the mathematics of fractal systems all the way to
generating full-color images. Fractal flames are a challenging topic, generating full-color images. Fractal flames are a challenging topic,
but it's extremely rewarding to learn more about how they work. but it's extremely rewarding to learn about how they work.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 557 KiB

After

Width:  |  Height:  |  Size: 418 KiB