diff --git a/blog/2024-11-15-playing-with-fire/1-introduction/index.mdx b/blog/2024-11-15-playing-with-fire/1-introduction/index.mdx index 9489018..6ca0ff9 100644 --- a/blog/2024-11-15-playing-with-fire/1-introduction/index.mdx +++ b/blog/2024-11-15-playing-with-fire/1-introduction/index.mdx @@ -22,15 +22,14 @@ import banner from '../banner.png' -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. -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. -But the desire to understand it stuck around. Now, with a graduate degree under my belt, I want to revisit it -and try to make some progress. +But the desire to understand it stuck around. Now, with a graduate degree under my belt, I wanted to revisit it. -This guide is my attempt to explain how fractal flames work in a way that younger me — and others interested in the art — +This guide is my attempt to explain how fractal flames work so that younger me — and others interested in the art — 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)," -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) @@ -52,7 +51,7 @@ $$ 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. -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: @@ -67,20 +66,17 @@ export const simpleData = [ -However, this is a pretty boring image. With fractal flames, rather than listing individual points, -we use functions to describe which points are part of the solution. This means there are an infinite -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 a new picture. +With fractal flames, rather than listing individual points, we use functions to describe the solution. +This means there are an infinite number of points, but if we find _enough_ points to plot, we get a nice picture. +And if the functions change, the solution also changes, and we get something new. ### Transform functions 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$). -While you could theoretically use any function, we'll start with a specific kind of function -called an "[affine transformation](https://en.wikipedia.org/wiki/Affine_transformation)." - -The general form of an affine transformation is: +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)." Every transform uses the same formula: $$ 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' {transformSource} -The parameters ($a_i$, $b_i$, etc.) are values we get to choose. -For example, we can represent a "shift" function like this: +The parameters ($a_i$, $b_i$, etc.) are values we choose. +For example, we can define a "shift" function like this: $$ \begin{align*} @@ -106,7 +102,7 @@ F_{shift}(x, y) &= (1 \cdot x + 0.5, 1 \cdot y + 1.5) \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" @@ -139,7 +135,7 @@ $$ 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$, > 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}$ > 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: - **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))**: 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, - 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. @@ -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 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. 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). This is still a bit vague, so let's work through an example. @@ -200,7 +192,7 @@ $$ ### 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: $$ @@ -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. -First, 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: +To start, we need to generate some random numbers. The "bi-unit square" is the range $[-1, 1]$, +and we can do this using an existing API: import biunitSource from '!!raw-loader!../src/randomBiUnit' @@ -235,14 +227,14 @@ import randintSource from '!!raw-loader!../src/randomInteger' ### 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, 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). -This allows us to manipulate individual pixels in an image and display it on screen. +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 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 with range $[0, 1]$ for both $x$ and $y$: @@ -251,7 +243,7 @@ import cameraSource from "!!raw-loader!./cameraGasket" {cameraSource} 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: import plotSource from '!!raw-loader!./plot' @@ -277,25 +269,22 @@ import chaosGameSource from '!!raw-loader!./chaosGame' ### Weights -There's one last step before we finish the introduction. So far, each function $F_i$ has -the same chance of being chosen. We can change that by introducing a "weight" ($w_i$) -to each transform in the chaos game: +There's one last step before we finish the introduction. So far, each transform has +the same chance of being picked in the chaos game. +We can change that by giving them a "weight" ($w_i$) instead: import randomChoiceSource from '!!raw-loader!../src/randomChoice' {randomChoiceSource} +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"; {chaosGameWeightedSource} -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 Double-click the image if you want to save a copy! ::: @@ -308,7 +297,7 @@ import {SquareCanvas} from "../src/Canvas"; ## Summary Studying the foundations of fractal flames is challenging, -but we now have an understanding of both the mathematics -and implementations of iterated function systems. +but we now have an understanding of the mathematics +and the implementation of iterated function systems. -In the next post, we'll study the first innovation of fractal flames: variations. \ No newline at end of file +In the next post, we'll look at the first innovation of fractal flame algorithm: variations. \ No newline at end of file diff --git a/blog/2024-11-15-playing-with-fire/2-transforms/index.mdx b/blog/2024-11-15-playing-with-fire/2-transforms/index.mdx index 8c64c7a..c21fd66 100644 --- a/blog/2024-11-15-playing-with-fire/2-transforms/index.mdx +++ b/blog/2024-11-15-playing-with-fire/2-transforms/index.mdx @@ -6,31 +6,30 @@ authors: [bspeice] 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. :::info 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/) -can load that file to play around with! +If you're interested in tweaking the parameters, or creating your own, [Apophysis](https://sourceforge.net/projects/apophysis/) +can load that file. ::: ## Variations :::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' 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, -the image it generates is exciting. But it's still not nearly as exciting as the images the Fractal Flame -algorithm is known for. +playing the chaos game gives us an image of Sierpinski's Gasket. Even though we used simple functions, +the image it generates is intriguing. But what would happen if we used something more complex? -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": $$ @@ -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 and give back new $(x, y)$ coordinates. 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). -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) -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) @@ -63,12 +62,12 @@ import linearSrc from '!!raw-loader!../src/linear' :::tip 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) -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: $$ @@ -93,7 +92,7 @@ import juliaSrc from '!!raw-loader!../src/julia' ### Popcorn (variation 17) 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)) @@ -105,8 +104,8 @@ import popcornSrc from '!!raw-loader!../src/popcorn' ### PDJ (variation 24) -Some variations have extra parameters; they're called "parametric variations." -For the PDJ variation, there are four extra parameters we can choose: +Some variations have extra parameters we can choose; they're called "parametric variations." +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} \\ @@ -133,11 +132,11 @@ import blendSource from "!!raw-loader!../src/blend"; {blendSource} -With that in place, we have enough to render a first fractal flame. We'll use the same -chaos game as before, but our new transforms and variations produce a dramatically different image: +With that in place, we have enough to render a fractal flame. We'll use the same +chaos game as before, but the new transforms and variations produce a dramatically different image: :::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"; @@ -147,9 +146,9 @@ import FlameBlend from "./FlameBlend"; ## 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*} @@ -163,7 +162,7 @@ import postSource from '!!raw-loader!./post' {postSource} 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:
If you want to test your understanding... @@ -178,12 +177,12 @@ import FlamePost from "./FlamePost"; ## Final transforms -Our 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 -(composition of affine transform, variation blend, and post transform), -but it doesn't change the chaos game state. +The last step is to introduce a "final transform" ($F_{final}$) that is applied +regardless of which regular transform ($F_i$) the chaos game selects. +It's just like a normal transform (composition of affine transform, variation blend, and post transform), +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*} @@ -201,6 +200,9 @@ import chaosGameFinalSource from "!!raw-loader!./chaosGameFinal" {chaosGameFinalSource} +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"; @@ -208,7 +210,7 @@ import FlameFinal from "./FlameFinal"; ## Summary 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. \ No newline at end of file diff --git a/blog/2024-11-15-playing-with-fire/3-log-density/index.mdx b/blog/2024-11-15-playing-with-fire/3-log-density/index.mdx index 0254a4f..7faa251 100644 --- a/blog/2024-11-15-playing-with-fire/3-log-density/index.mdx +++ b/blog/2024-11-15-playing-with-fire/3-log-density/index.mdx @@ -6,7 +6,7 @@ authors: [bspeice] 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), 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 ::: -One problem with the existing chaos game is that we waste work -by treating pixels as a binary "on" (opaque) or "off" (transparent). -If the chaos game encounters the same location twice, nothing actually changes. +One problem with the current chaos game algorithm is that we waste work +because pixels are either "on" (opaque) or "off" (transparent). +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. -However, we'll also count each time the chaos game encounters a pixel. -This gives us a kind of image "histogram": +To demonstrate how much work is wasted, we'll count each time the chaos game +visits a pixel while iterating. This gives us a kind of image "histogram": import chaosGameHistogramSource from "!!raw-loader!./chaosGameHistogram" {chaosGameHistogramSource} -When the chaos game finishes, we find the pixel encountered most frequently. -Finally, we "paint" the image by setting each pixel's alpha value (transparency) -to the ratio of times encountered divided by the maximum: +When the chaos game finishes, we find the pixel encountered most often. +Finally, we "paint" the image by setting each pixel's alpha (transparency) value +to the ratio of times visited divided by the maximum: import CodeBlock from "@theme/CodeBlock"; @@ -50,22 +49,22 @@ import {paintLinear} from "./paintLinear"; ## Tone mapping -While using a histogram reduces the "graininess" of the image, it also leads to some parts vanishing entirely. -In the reference parameters, the outer circle is preserved, but the interior appears to be missing! +While using a histogram reduces the "graining," it also leads to some parts vanishing entirely. +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). 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 -nice images of scenes with a wide range of brightnesses. To take a picture of something dark, +As a concrete example, high-dynamic-range (HDR) photography uses this technique to capture +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). By taking multiple pictures with different exposure times, we can combine them to create a final image where everything is visible. 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) -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.
Log-scale vibrancy also explains fractal flames appear to be 3D... @@ -89,7 +88,7 @@ import {paintLogarithmic} from './paintLogarithmic' ## 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 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 slight changes in the actual color - 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. +The final transform gets a value too ($c_f$). 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*} @@ -123,13 +123,13 @@ $$ ### Color speed :::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), 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$): $$ @@ -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 "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: 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)
-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: import * as params from "../src/params" @@ -179,8 +179,9 @@ import {PaletteBar} from "./FlameColor" ### Plotting -We're now ready to plot our $(x_f,y_f,c_f)$ coordinates. After translating from color coordinate ($c_f$) -to RGB value, add that to the image histogram: +We're now ready to plot our $(x_f,y_f,c_f)$ coordinates. This time, we'll use a 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" @@ -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, 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 -not covered here. For example, Image quality can be improved with density estimation +The Fractal Flame Algorithm paper goes on to describe more techniques +not covered here. For example, image quality can be improved with density estimation and filtering. New parameters can be generated by "mutating" existing 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 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. diff --git a/blog/2024-11-15-playing-with-fire/banner.png b/blog/2024-11-15-playing-with-fire/banner.png index 7ebc14c..8349b47 100644 Binary files a/blog/2024-11-15-playing-with-fire/banner.png and b/blog/2024-11-15-playing-with-fire/banner.png differ