Finish a first draft

This commit is contained in:
Bradlee Speice 2024-12-14 16:55:54 -05:00
parent 9b1a3895d0
commit a05acf6748
8 changed files with 184 additions and 55 deletions

View File

@ -180,9 +180,23 @@ import FlamePost from "./FlamePost";
## Final transforms ## Final transforms
Our last step is to introduce a "final transform" ($F_{final}$) that is applied 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 regardless of which transform the chaos game selects. It works just like a normal transform
(composition of affine transform, variation blend, and post transform), (composition of affine transform, variation blend, and post transform),
but it doesn't change the chaos game state: but it doesn't change the chaos game state.
With that in place, our chaos game algorithm changes slightly:
$$
\begin{align*}
&(x, y) = \text{random point in the bi-unit square} \\
&\text{iterate } \{ \\
&\hspace{1cm} i = \text{random integer from 0 to } n - 1 \\
&\hspace{1cm} (x,y) = F_i(x,y) \\
&\hspace{1cm} (x_f,y_f) = F_{final}(x,y) \\
&\hspace{1cm} \text{plot}(x_f,y_f) \text{ if iterations} > 20 \\
\}
\end{align*}
$$
import chaosGameFinalSource from "!!raw-loader!./chaosGameFinal" import chaosGameFinalSource from "!!raw-loader!./chaosGameFinal"
@ -198,4 +212,4 @@ Variations are the fractal flame algorithm's first major innovation over previou
Blending variation functions and post/final transforms allows us to generate interesting images. 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 However, the images themselves are grainy and unappealing. In the next post, we'll clean up
the quality and add color. the image quality and add some color.

View File

@ -1,7 +1,7 @@
import React, {useContext, useEffect, useMemo, useRef, useState} from "react"; import React, {useContext, useEffect, useMemo, useRef, useState} from "react";
import * as params from "../src/params"; import * as params from "../src/params";
import {PainterContext} from "../src/Canvas"; import {PainterContext} from "../src/Canvas";
import {colorFromPalette} from "./paintColor"; import {colorFromPalette} from "./colorFromPalette";
import {chaosGameColor, Props as ChaosGameColorProps, TransformColor} from "./chaosGameColor"; import {chaosGameColor, Props as ChaosGameColorProps, TransformColor} from "./chaosGameColor";
import styles from "../src/css/styles.module.css"; import styles from "../src/css/styles.module.css";

View File

@ -3,7 +3,9 @@ import {Props as ChaosGameFinalProps} from "../2-transforms/chaosGameFinal";
import {randomBiUnit} from "../src/randomBiUnit"; import {randomBiUnit} from "../src/randomBiUnit";
import {randomChoice} from "../src/randomChoice"; import {randomChoice} from "../src/randomChoice";
import {camera, histIndex} from "../src/camera"; import {camera, histIndex} from "../src/camera";
import {colorFromPalette, paintColor} from "./paintColor"; import {colorFromPalette} from "./colorFromPalette";
import {mixColor} from "./mixColor";
import {paintColor} from "./paintColor";
const quality = 15; const quality = 15;
const step = 100_000; const step = 100_000;
@ -13,51 +15,50 @@ export type TransformColor = {
colorSpeed: number; colorSpeed: number;
} }
function mixColor(color1: number, color2: number, colorSpeed: number) {
return color1 * (1 - colorSpeed) + color2 * colorSpeed;
}
export type Props = ChaosGameFinalProps & { export type Props = ChaosGameFinalProps & {
palette: number[]; palette: number[];
colors: TransformColor[]; colors: TransformColor[];
finalColor: TransformColor; finalColor: TransformColor;
} }
export function* chaosGameColor({width, height, transforms, final, palette, colors, finalColor}: Props) { export function* chaosGameColor({width, height, transforms, final, palette, colors, finalColor}: Props) {
let currentColor = Math.random(); const imgRed = Array<number>(width * height).fill(0);
const red = Array<number>(width * height).fill(0); const imgGreen = Array<number>(width * height).fill(0);
const green = Array<number>(width * height).fill(0); const imgBlue = Array<number>(width * height).fill(0);
const blue = Array<number>(width * height).fill(0); const imgAlpha = Array<number>(width * height).fill(0);
const alpha = Array<number>(width * height).fill(0);
let [x, y] = [randomBiUnit(), randomBiUnit()]; let [x, y] = [randomBiUnit(), randomBiUnit()];
let c = Math.random();
const iterations = width * height * quality; const iterations = width * height * quality;
for (let i = 0; i < iterations; i++) { for (let i = 0; i < iterations; i++) {
const [transformIndex, transform] = randomChoice(transforms); const [transformIndex, transform] = randomChoice(transforms);
[x, y] = transform(x, y); [x, y] = transform(x, y);
// highlight-start
const transformColor = colors[transformIndex];
c = mixColor(c, transformColor.color, transformColor.colorSpeed);
// highlight-end
const [finalX, finalY] = final(x, y); const [finalX, finalY] = final(x, y);
if (i > 20) { if (i > 20) {
const [pixelX, pixelY] = camera(finalX, finalY, width); const [pixelX, pixelY] = camera(finalX, finalY, width);
const pixelIndex = histIndex(pixelX, pixelY, width, 1); const pixelIndex = histIndex(pixelX, pixelY, width, 1);
if (pixelIndex < 0 || pixelIndex >= alpha.length) if (pixelIndex < 0 || pixelIndex >= imgAlpha.length)
continue; continue;
const transformColor = colors[transformIndex]; const colorFinal = mixColor(c, finalColor.color, finalColor.colorSpeed);
currentColor = mixColor(currentColor, transformColor.color, transformColor.colorSpeed);
const colorFinal = mixColor(currentColor, finalColor.color, finalColor.colorSpeed);
const [r, g, b] = colorFromPalette(palette, colorFinal); const [r, g, b] = colorFromPalette(palette, colorFinal);
red[pixelIndex] += r; imgRed[pixelIndex] += r;
green[pixelIndex] += g; imgGreen[pixelIndex] += g;
blue[pixelIndex] += b; imgBlue[pixelIndex] += b;
alpha[pixelIndex] += 1; imgAlpha[pixelIndex] += 1;
} }
if (i % step === 0) if (i % step === 0)
yield paintColor(width, height, red, green, blue, alpha); yield paintColor(width, height, imgRed, imgGreen, imgBlue, imgAlpha);
} }
yield paintColor(width, height, red, green, blue, alpha); yield paintColor(width, height, imgRed, imgGreen, imgBlue, imgAlpha);
} }

View File

@ -13,7 +13,9 @@ export type Props = ChaosGameFinalProps & {
export function* chaosGameHistogram({width, height, transforms, final, paint}: Props) { export function* chaosGameHistogram({width, height, transforms, final, paint}: Props) {
let iterations = quality * width * height; let iterations = quality * width * height;
// highlight-start
const histogram = Array<number>(width * height).fill(0); const histogram = Array<number>(width * height).fill(0);
// highlight-end
let [x, y] = [randomBiUnit(), randomBiUnit()]; let [x, y] = [randomBiUnit(), randomBiUnit()];
@ -23,6 +25,7 @@ export function* chaosGameHistogram({width, height, transforms, final, paint}: P
const [finalX, finalY] = final(x, y); const [finalX, finalY] = final(x, y);
if (i > 20) { if (i > 20) {
// highlight-start
const [pixelX, pixelY] = camera(finalX, finalY, width); const [pixelX, pixelY] = camera(finalX, finalY, width);
const hIndex = histIndex(pixelX, pixelY, width, 1); const hIndex = histIndex(pixelX, pixelY, width, 1);
@ -31,6 +34,7 @@ export function* chaosGameHistogram({width, height, transforms, final, paint}: P
} }
histogram[hIndex] += 1; histogram[hIndex] += 1;
// highlight-end
} }
if (i % step === 0) if (i % step === 0)

View File

@ -0,0 +1,4 @@
export function colorFromPalette(palette: number[], colorIndex: number): [number, number, number] {
const paletteIndex = Math.floor(colorIndex * (palette.length / 3)) * 3;
return [palette[paletteIndex], palette[paletteIndex + 1], palette[paletteIndex + 2]];
}

View File

@ -1,6 +1,6 @@
--- ---
slug: 2024/11/playing-with-fire-log-density slug: 2024/11/playing-with-fire-log-density
title: "Playing with fire: Log-density and color" title: "Playing with fire: Tone mapping and color"
date: 2024-11-15 14:00:00 date: 2024-11-15 14:00:00
authors: [bspeice] authors: [bspeice]
tags: [] tags: []
@ -23,11 +23,17 @@ This post covers sections 4 and 5 of the Fractal Flame Algorithm paper
To start, it's worth demonstrating how much work is actually "wasted" To start, it's worth demonstrating how much work is actually "wasted"
when we treat pixels as a binary "on" (opaque) or "off" (transparent). when we treat pixels as a binary "on" (opaque) or "off" (transparent).
We'll render the reference image again, but this time, track each time We'll render the reference image again, but this time, count each time
we encounter each pixel during the chaos game. When the chaos game finishes, we encounter a pixel during the chaos game. This gives us a kind of "histogram"
find the pixel we encountered most frequently. Finally, "paint" the image of the image:
by setting each pixel's transparency to ratio of times encountered
divided by the maximum value: import chaosGameHistogramSource from "!!raw-loader!./chaosGameHistogram"
<CodeBlock language="typescript">{chaosGameHistogramSource}</CodeBlock>
When the chaos game finishes, find the pixel we encountered most frequently.
Finally, "paint" the image by setting each pixel's alpha value (transparency)
to the ratio of times encountered, divided by the maximum value:
import CodeBlock from "@theme/CodeBlock"; import CodeBlock from "@theme/CodeBlock";
@ -39,22 +45,20 @@ import {SquareCanvas} from "../src/Canvas";
import FlameHistogram from "./FlameHistogram"; import FlameHistogram from "./FlameHistogram";
import {paintLinear} from "./paintLinear"; import {paintLinear} from "./paintLinear";
<SquareCanvas><FlameHistogram quality={15} paint={paintLinear}/></SquareCanvas> <SquareCanvas><FlameHistogram paint={paintLinear}/></SquareCanvas>
## Log display ## Tone mapping
While using a histogram to paint the image improves the quality, While using a histogram to paint the image improves the quality, it also leads to some parts vanishing entirely.
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 preserved, but the interior appears to be missing!
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 color, and how color is perceived by people. computers represent color, and how people see color.
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 wide brightness ranges. To take a picture of something dark, nice images of scenes with wide brightness ranges. To take a picture of something dark,
you need a long exposure time. However, long exposures can lead to images that "wash out" and become pure white. you need a long exposure time. However, long exposures can lead to "hot spots" in images that are pure white.
By taking multiple pictures using different exposure times, we can combine them to create By taking multiple pictures using different exposure times, we can combine them to create
a final image where everything is visible. a final image where everything is visible.
@ -80,33 +84,134 @@ import paintLogarithmicSource from "!!raw-loader!./paintLogarithmic"
import {paintLogarithmic} from './paintLogarithmic' import {paintLogarithmic} from './paintLogarithmic'
<SquareCanvas><FlameHistogram quality={15} paint={paintLogarithmic}/></SquareCanvas> <SquareCanvas><FlameHistogram paint={paintLogarithmic}/></SquareCanvas>
## Color ## Color
Finally, we'll spice things up with the last innovation introduced by Finally, we'll introduce the last innovation of the fractal flame algorithm: color.
the fractal flame algorithm: color. By including a color coordinate By including a color coordinate ($c$) in the chaos game, we can illustrate the transforms
in the chaos game, we can illustrate the transforms that are responsible responsible for each part of the image.
for each part of an image.
### Color coordinate
Color in a fractal flame uses a range of $[0, 1]$. This is important for two reasons:
- It helps blend colors together in the final image
- It allows us to swap in new color palettes easily
We'll give each transform a color value ($c_i$) in the $[0, 1]$ range.
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:
$$
\begin{align*}
&(x, y) = \text{random point in the bi-unit square} \\
&c = \text{random point from [0,1]} \\
&\text{iterate } \{ \\
&\hspace{1cm} i = \text{random integer from 0 to } n - 1 \\
&\hspace{1cm} (x,y) = F_i(x,y) \\
&\hspace{1cm} (x_f,y_f) = F_{final}(x,y) \\
&\hspace{1cm} c = (c + c_i) / 2 \\
&\hspace{1cm} \text{plot}(x_f,y_f,c_f) \text{ if iterations} > 20 \\
\}
\end{align*}
$$
### Color speed
:::warning
Color speed as a concept 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.
This is known as the "color speed" ($s_i$):
$$
c = c \cdot (1 - s_i) + c_i \cdot s_i
$$
import mixColorSource from "!!raw-loader!./mixColor"
<CodeBlock language="typescript">{mixColorSource}</CodeBlock>
Color speed values work just like transform weights. A value of 1
means we take the transform color and ignore the previous color state.
Similarly, a value of 0 means we keep the current color state and ignore the
transform color.
### Palette ### Palette
Our first step is to define a color palette for the image. Fractal flames Now, we need to map the color coordinate to a pixel color. Fractal flames typically use
typically use a palette of 256 colors that transition smoothly 256 colors (each color has 3 values - red, green, blue) to define a palette.
from one to another. In the diagram below, each color in our palette is plotted Then, the color coordinate becomes an index into the palette.
on a small strip. Putting the strips side by side shows the palette for our image:
There's one small complication: the color coordinate is continuous, but the palette
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
by the number of colors in the palette, then truncate that value. This gives us a discrete index:
import colorFromPaletteSource from "!!raw-loader!./colorFromPalette";
<CodeBlock language="typescript">{colorFromPaletteSource}</CodeBlock>
<details>
<summary>As an alternative...</summary>
...you could also interpolate between colors in the palette.
For example: [`flam3` code](https://github.com/scottdraves/flam3/blob/7fb50c82e90e051f00efcc3123d0e06de26594b2/rect.c#L483-L486)
</details>
In the diagram below, each color in our palette is plotted on a small vertical strip.
Putting the strips side by side shows the palette used by our reference image:
import * as params from "../src/params" import * as params from "../src/params"
import {PaletteBar} from "./FlameColor" import {PaletteBar} from "./FlameColor"
<PaletteBar height="40" palette={params.palette}/> <PaletteBar height="40" palette={params.palette}/>
### Color coordinate ### 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 value to the image histogram:
import chaosGameColorSource from "!!raw-loader!./chaosGameColor"
<CodeBlock language="typescript">{chaosGameColorSource}</CodeBlock>
Finally, painting the image. With tone mapping, logarithms scale the image brightness to match
how it is perceived. When using color, we scale each color channel by the alpha channel:
import paintColorSource from "!!raw-loader!./paintColor" import paintColorSource from "!!raw-loader!./paintColor"
<CodeBlock language="typescript">{paintColorSource}</CodeBlock> <CodeBlock language="typescript">{paintColorSource}</CodeBlock>
And now, at long last, a full-color fractal flame:
import FlameColor from "./FlameColor"; import FlameColor from "./FlameColor";
<SquareCanvas><FlameColor quality={15}/></SquareCanvas> <SquareCanvas><FlameColor/></SquareCanvas>
## Summary
Tone mapping is the second major innovation of the fractal flame algorithm.
By tracking how often the chaos game encounters each pixel, we can adjust
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.
The Fractal Flame Algorithm paper goes on to describe more techniques
not covered here. Image quality can be improved with density estimation
and filtering. New parameters can be generated by "mutating" existing
fractal flames. 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
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.

View File

@ -0,0 +1,3 @@
export function mixColor(color1: number, color2: number, colorSpeed: number) {
return color1 * (1 - colorSpeed) + color2 * colorSpeed;
}

View File

@ -1,8 +1,6 @@
export function colorFromPalette(palette: number[], colorIndex: number): [number, number, number] { // hidden-start
const paletteIndex = Math.floor(colorIndex * (palette.length / 3)) * 3; import {colorFromPalette} from "./colorFromPalette";
return [palette[paletteIndex], palette[paletteIndex + 1], palette[paletteIndex + 2]]; // hidden-end
}
export function paintColor( export function paintColor(
width: number, width: number,
height: number, height: number,