mirror of
https://github.com/bspeice/speice.io
synced 2024-12-22 16:48:10 -05:00
Finish a first draft
This commit is contained in:
parent
9b1a3895d0
commit
a05acf6748
@ -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.
|
@ -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";
|
||||||
|
@ -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);
|
||||||
}
|
}
|
@ -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)
|
||||||
|
@ -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]];
|
||||||
|
}
|
@ -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.
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
export function mixColor(color1: number, color2: number, colorSpeed: number) {
|
||||||
|
return color1 * (1 - colorSpeed) + color2 * colorSpeed;
|
||||||
|
}
|
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user