--- slug: 2024/11/playing-with-fire-transforms title: "Playing with fire: Transforms and variations" date: 2024-11-15 13:00:00 authors: [bspeice] tags: [] --- Now that we have a basic chaos game in place, it's time to spice things up. Variations create the shapes and patterns that fractal flames are known for. :::info This post uses a set of [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 and you can try tweaking things yourself! ::: ## Variations :::note This post covers section 3 for 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. This leads us to the first big innovation of the Fractal Flame algorithm: applying non-linear functions after the affine transform has happened. These functions are called "variations": $$ F_i(x, y) = V_j(a_i \cdot x + b_i \cdot y + c_i, d_i \cdot x + e_i \cdot y + f_i) $$ import variationSource from '!!raw-loader!../src/variation' {variationSource} Just like transforms, variations ($V_j$) are functions that map $(x, y)$ coordinates to new coordinates. However, the sky is the limit for what we can do in between input and output. The Fractal Flame paper lists 49 different 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: ### Linear (variation 0) This variation is dead simple: just return the $x$ and $y$ coordinates as-is. $$ V_0(x,y) = (x,y) $$ import linearSrc from '!!raw-loader!../src/linear' {linearSrc} :::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. ::: ### Julia (variation 13) This variation is a good example of the non-linear functions we can use. It uses both trigonometry and probability to produce interesting shapes: TODO: Connection with the julia set? $$ \begin{align*} r &= \sqrt{x^2 + y^2} \\ \theta &= \text{arctan}(x / y) \\ \Omega &= \left\{ \begin{array}{lr} 0 \hspace{0.4cm} \text{w.p. } 0.5 \\ \pi \hspace{0.4cm} \text{w.p. } 0.5 \\ \end{array} \right\} \\ V_{13}(x, y) &= \sqrt{r} \cdot (\text{cos} ( \theta / 2 + \Omega ), \text{sin} ( \theta / 2 + \Omega )) \end{align*} $$ import juliaSrc from '!!raw-loader!../src/julia' {juliaSrc} ### Popcorn (variation 17) Some variations rely on knowing their transform's affine coefficients; they're known as "dependent variations." For the popcorn variation, we use $c$ and $f$: $$ V_{17}(x,y) = (x + c \cdot \text{sin}(\text{tan }3y), y + f \cdot \text{sin}(\text{tan }3x)) $$ import popcornSrc from '!!raw-loader!../src/popcorn' {popcornSrc} ### PDJ (variation 24) Some variations have extra parameters we can choose; these are known as "parametric variations." For the PDJ variation, there are four extra parameters we can choose: $$ p_1 = \text{pdj.a} \hspace{0.2cm} p_2 = \text{pdj.b} \hspace{0.2cm} p_3 = \text{pdj.c} \hspace{0.2cm} p_4 = \text{pdj.d} \\ V_{24} = (\text{sin}(p_1 \cdot y) - \text{cos}(p_2 \cdot x), \text{sin}(p_3 \cdot x) - \text{cos}(p_4 \cdot y)) $$ import pdjSrc from '!!raw-loader!../src/pdj' {pdjSrc} ## Blending Now, one variation is fun, but we can also combine variations in a process called "blending." Each variation receives the same $x$ and $y$ inputs, and we add together each variation's $x$ and $y$ outputs. We'll also give each variation a weight (called $v_{ij}$) that changes how much it contributes to the transform: $$ F_i(x,y) = \sum_{j} v_{ij} V_j(a_i \cdot x + b_i \cdot y + c_i, \hspace{0.2cm} d_i \cdot x + e_i \cdot y + f_i) $$ The formula looks intimidating, but it's not hard to implement: 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: :::tip Try using the variation weight sliders to figure out which parts of the image each transform controls. ::: import {SquareCanvas} from "../src/Canvas"; import FlameBlend from "./FlameBlend"; ## Post transforms Next, we'll introduce a second affine transform, known as a post transform, that is applied _after_ variation blending. We'll use some new variables, but the post transform function should look familiar: $$ \begin{align*} P_i(x, y) &= (\alpha_i x + \beta_i y + \gamma_i, \delta_i x + \epsilon_i y + \zeta_i) \\ F_i(x, y) &= P_i\left(\sum_{j} v_{ij} V_j(x, y)\right) \end{align*} $$ 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.
If you want to test your understanding... Challenge 1: What post-transform coefficients will give us the previous image? Challenge 2: What post-transform coefficients will give us a _mirrored_ image?
import FlamePost from "./FlamePost"; ## Final transforms Our last step is to introduce a "final transform" ($F_{final}$) that is applied regardless of which transform function we're using. 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: import chaosGameFinalSource from "!!raw-loader!./chaosGameFinal" {chaosGameFinalSource} import FlameFinal from "./FlameFinal"; ## Summary Variations are the fractal flame algorithm's first major innovation over previous IFS's. Blending variation functions and post/final transforms allows us to generate interesting images. However, the images themselves are grainy and unappealing. In the next post, we'll clean up the quality and add color.