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
|
||||
|
||||
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),
|
||||
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"
|
||||
|
||||
@ -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.
|
||||
|
||||
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 * as params from "../src/params";
|
||||
import {PainterContext} from "../src/Canvas";
|
||||
import {colorFromPalette} from "./paintColor";
|
||||
import {colorFromPalette} from "./colorFromPalette";
|
||||
import {chaosGameColor, Props as ChaosGameColorProps, TransformColor} from "./chaosGameColor";
|
||||
|
||||
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 {randomChoice} from "../src/randomChoice";
|
||||
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 step = 100_000;
|
||||
@ -13,51 +15,50 @@ export type TransformColor = {
|
||||
colorSpeed: number;
|
||||
}
|
||||
|
||||
function mixColor(color1: number, color2: number, colorSpeed: number) {
|
||||
return color1 * (1 - colorSpeed) + color2 * colorSpeed;
|
||||
}
|
||||
|
||||
export type Props = ChaosGameFinalProps & {
|
||||
palette: number[];
|
||||
colors: TransformColor[];
|
||||
finalColor: TransformColor;
|
||||
}
|
||||
export function* chaosGameColor({width, height, transforms, final, palette, colors, finalColor}: Props) {
|
||||
let currentColor = Math.random();
|
||||
const red = Array<number>(width * height).fill(0);
|
||||
const green = Array<number>(width * height).fill(0);
|
||||
const blue = Array<number>(width * height).fill(0);
|
||||
const alpha = Array<number>(width * height).fill(0);
|
||||
const imgRed = Array<number>(width * height).fill(0);
|
||||
const imgGreen = Array<number>(width * height).fill(0);
|
||||
const imgBlue = Array<number>(width * height).fill(0);
|
||||
const imgAlpha = Array<number>(width * height).fill(0);
|
||||
|
||||
let [x, y] = [randomBiUnit(), randomBiUnit()];
|
||||
let c = Math.random();
|
||||
|
||||
const iterations = width * height * quality;
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
const [transformIndex, transform] = randomChoice(transforms);
|
||||
[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);
|
||||
|
||||
if (i > 20) {
|
||||
const [pixelX, pixelY] = camera(finalX, finalY, width);
|
||||
const pixelIndex = histIndex(pixelX, pixelY, width, 1);
|
||||
|
||||
if (pixelIndex < 0 || pixelIndex >= alpha.length)
|
||||
if (pixelIndex < 0 || pixelIndex >= imgAlpha.length)
|
||||
continue;
|
||||
|
||||
const transformColor = colors[transformIndex];
|
||||
currentColor = mixColor(currentColor, transformColor.color, transformColor.colorSpeed);
|
||||
|
||||
const colorFinal = mixColor(currentColor, finalColor.color, finalColor.colorSpeed);
|
||||
const colorFinal = mixColor(c, finalColor.color, finalColor.colorSpeed);
|
||||
const [r, g, b] = colorFromPalette(palette, colorFinal);
|
||||
red[pixelIndex] += r;
|
||||
green[pixelIndex] += g;
|
||||
blue[pixelIndex] += b;
|
||||
alpha[pixelIndex] += 1;
|
||||
imgRed[pixelIndex] += r;
|
||||
imgGreen[pixelIndex] += g;
|
||||
imgBlue[pixelIndex] += b;
|
||||
imgAlpha[pixelIndex] += 1;
|
||||
}
|
||||
|
||||
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) {
|
||||
let iterations = quality * width * height;
|
||||
|
||||
// highlight-start
|
||||
const histogram = Array<number>(width * height).fill(0);
|
||||
// highlight-end
|
||||
|
||||
let [x, y] = [randomBiUnit(), randomBiUnit()];
|
||||
|
||||
@ -23,6 +25,7 @@ export function* chaosGameHistogram({width, height, transforms, final, paint}: P
|
||||
const [finalX, finalY] = final(x, y);
|
||||
|
||||
if (i > 20) {
|
||||
// highlight-start
|
||||
const [pixelX, pixelY] = camera(finalX, finalY, width);
|
||||
const hIndex = histIndex(pixelX, pixelY, width, 1);
|
||||
|
||||
@ -31,6 +34,7 @@ export function* chaosGameHistogram({width, height, transforms, final, paint}: P
|
||||
}
|
||||
|
||||
histogram[hIndex] += 1;
|
||||
// highlight-end
|
||||
}
|
||||
|
||||
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
|
||||
title: "Playing with fire: Log-density and color"
|
||||
title: "Playing with fire: Tone mapping and color"
|
||||
date: 2024-11-15 14:00:00
|
||||
authors: [bspeice]
|
||||
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"
|
||||
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 encounter each pixel during the chaos game. When the chaos game finishes,
|
||||
find the pixel we encountered most frequently. Finally, "paint" the image
|
||||
by setting each pixel's transparency to ratio of times encountered
|
||||
divided by the maximum value:
|
||||
We'll render the reference image again, but this time, count each time
|
||||
we encounter a pixel during the chaos game. This gives us a kind of "histogram"
|
||||
of the image:
|
||||
|
||||
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";
|
||||
|
||||
@ -39,22 +45,20 @@ import {SquareCanvas} from "../src/Canvas";
|
||||
import FlameHistogram from "./FlameHistogram";
|
||||
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,
|
||||
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 to paint the image improves the quality, it also leads to some parts vanishing entirely.
|
||||
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).
|
||||
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
|
||||
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
|
||||
a final image where everything is visible.
|
||||
|
||||
@ -80,33 +84,134 @@ import paintLogarithmicSource from "!!raw-loader!./paintLogarithmic"
|
||||
|
||||
import {paintLogarithmic} from './paintLogarithmic'
|
||||
|
||||
<SquareCanvas><FlameHistogram quality={15} paint={paintLogarithmic}/></SquareCanvas>
|
||||
<SquareCanvas><FlameHistogram paint={paintLogarithmic}/></SquareCanvas>
|
||||
|
||||
## Color
|
||||
|
||||
Finally, we'll spice things up with the last innovation introduced by
|
||||
the fractal flame algorithm: color. By including a color coordinate
|
||||
in the chaos game, we can illustrate the transforms that are responsible
|
||||
for each part of an image.
|
||||
Finally, we'll introduce the last innovation of the fractal flame algorithm: color.
|
||||
By including a color coordinate ($c$) in the chaos game, we can illustrate the transforms
|
||||
responsible for each part of the 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
|
||||
|
||||
Our first step is to define a color palette for the image. Fractal flames
|
||||
typically use a palette of 256 colors that transition smoothly
|
||||
from one to another. In the diagram below, each color in our palette is plotted
|
||||
on a small strip. Putting the strips side by side shows the palette for our image:
|
||||
Now, we need to map the color coordinate to a pixel color. Fractal flames typically use
|
||||
256 colors (each color has 3 values - red, green, blue) to define a palette.
|
||||
Then, the color coordinate becomes an index into the palette.
|
||||
|
||||
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 {PaletteBar} from "./FlameColor"
|
||||
|
||||
<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"
|
||||
|
||||
<CodeBlock language="typescript">{paintColorSource}</CodeBlock>
|
||||
|
||||
And now, at long last, a full-color fractal flame:
|
||||
|
||||
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] {
|
||||
const paletteIndex = Math.floor(colorIndex * (palette.length / 3)) * 3;
|
||||
return [palette[paletteIndex], palette[paletteIndex + 1], palette[paletteIndex + 2]];
|
||||
}
|
||||
|
||||
// hidden-start
|
||||
import {colorFromPalette} from "./colorFromPalette";
|
||||
// hidden-end
|
||||
export function paintColor(
|
||||
width: number,
|
||||
height: number,
|
||||
|
Loading…
Reference in New Issue
Block a user