mirror of
https://github.com/bspeice/speice.io
synced 2025-04-26 15:51:31 -04:00
Review draft
This commit is contained in:
parent
ced7827d0c
commit
361e476ede
@ -1,15 +1,14 @@
|
||||
---
|
||||
slug: 2025/03/playing-with-fire-camera
|
||||
title: "Playing with fire: The camera"
|
||||
date: 2025-03-07 12:00:00
|
||||
date: 2025-03-10 12:00:00
|
||||
authors: [bspeice]
|
||||
tags: []
|
||||
---
|
||||
|
||||
Something that bugged me while writing the first three articles on fractal flames were the constraints on
|
||||
output images. At the time, I had worked out how to render fractal flames by studying
|
||||
the source code of [Apophysis](https://sourceforge.net/projects/apophysis/)
|
||||
and [flam3](https://github.com/scottdraves/flam3). That was just enough to define a basic camera for displaying
|
||||
[Apophysis](https://sourceforge.net/projects/apophysis/) and [flam3](https://github.com/scottdraves/flam3); just enough to display images
|
||||
in a browser.
|
||||
|
||||
Having spent more time with fractal flames and computer graphics, it's time to implement
|
||||
@ -26,33 +25,30 @@ To review, the restrictions we've had so far:
|
||||
>
|
||||
> -- [The fractal flame algorithm](/2024/11/playing-with-fire)
|
||||
|
||||
There are a couple problems here:
|
||||
First, we've assumed that fractals get displayed in a square image. Ignoring aspect ratios simplifies
|
||||
the render process, but we don't usually want square images. It's possible to render a large
|
||||
square image and crop it to fit, but we'd rather render directly to the desired size.
|
||||
|
||||
First, the assumption that fractals get displayed in a square image. Ignoring aspect ratios simplifies
|
||||
the render process, but we usually don't want square images. As a workaround, you could render
|
||||
a large square image and crop it to fit an aspect ratio, but it's better to render the desired
|
||||
image size to start with.
|
||||
|
||||
Second, the assumption that fractals use the range $[0, 1]$. My statement above is an over-simplification;
|
||||
for Sierpinski's Gasket, the solution set is indeed defined on $[0, 1]$, but all other images in the series
|
||||
use a display range of $[-2, 2]$.
|
||||
Second, we've assumed that fractals have a pre-determined display range. For Sierpinski's Gasket,
|
||||
that was $[0, 1]$ (the reference parameters used a range of $[-2, 2]$). However, if we could
|
||||
control the display range, it would let us zoom in and out of the image.
|
||||
|
||||
## Parameters
|
||||
|
||||
For comparison, here are the camera controls available in Apophysis and [`flam3`](https://github.com/scottdraves/flam3/wiki/XML-File-Format):
|
||||
For comparison, the camera controls available in Apophysis offer a lot of flexibility:
|
||||
|
||||
<center></center>
|
||||
|
||||
There are four parameters yet to implement: position, rotation, zoom, and scale.
|
||||
The remaining parameters to implement are: position (X and Y), rotation, zoom, and scale.
|
||||
|
||||
### Position
|
||||
|
||||
Fractal flames normally use the origin as the center of an image. The position parameters (X and Y) move
|
||||
the center point, which effectively pans the image. A positive X position shifts the image left,
|
||||
and a negative X position shifts the image right. Similarly, a positive Y position shifts the image up,
|
||||
and a negative Y position shifts the image down.
|
||||
Fractal flames normally use $(0, 0)$ as the image center. The position parameters (X and Y) move
|
||||
the center point, which effectively pans the image. A positive X position shifts left, and a
|
||||
negative X position shifts right. Similarly, a positive Y position shifts up, and a negative
|
||||
Y position shifts the image down.
|
||||
|
||||
To apply the position, simply subtract the X and Y position from each point in the chaos game prior to plotting it:
|
||||
To apply the position parameters, simply subtract them from each point in the chaos game prior to plotting it:
|
||||
|
||||
```typescript
|
||||
[x, y] = [
|
||||
@ -63,9 +59,9 @@ To apply the position, simply subtract the X and Y position from each point in t
|
||||
|
||||
### Rotation
|
||||
|
||||
After the position parameters are applied, we can rotate the image around the (potentially shifted) center point.
|
||||
To do so, we'll go back to the [affine transformations](https://en.wikipedia.org/wiki/Affine_transformation)
|
||||
we've been using. Specifically, the rotation angle $\theta$ gives us a transform matrix we can apply to our point:
|
||||
After the position parameters, we can rotate the image around the (new) center point. To do so, we'll go back to the
|
||||
[affine transformations](https://en.wikipedia.org/wiki/Affine_transformation) we've been using so far.
|
||||
Specifically, the rotation angle $\theta$ gives us a transform matrix we can apply prior to plotting:
|
||||
|
||||
$$
|
||||
\begin{bmatrix}
|
||||
@ -79,7 +75,6 @@ y
|
||||
\end{bmatrix}
|
||||
$$
|
||||
|
||||
As a minor tweak, we also negate the rotation angle to match the behavior of Apophysis/`flam3`.
|
||||
|
||||
```typescript
|
||||
[x, y] = [
|
||||
@ -90,11 +85,17 @@ As a minor tweak, we also negate the rotation angle to match the behavior of Apo
|
||||
];
|
||||
```
|
||||
|
||||
:::note
|
||||
To match the behavior of Apophysis/`flam3`, we need to negate the rotation angle.
|
||||
:::
|
||||
|
||||
### Zoom
|
||||
|
||||
This parameter does what the name implies; zoom in and out of the image. Specifically, for a zoom parameter $z$,
|
||||
every point in the chaos game is scaled by $\text{pow}(2, z)$ prior to plotting. For example, if the point is $(1, 1)$,
|
||||
a zoom of 1 means we actually plot $(1, 1) \cdot \text{pow}(2, 1) = (2, 2)$.
|
||||
This parameter does what the name implies; zoom in and out of the image. To do this, we multiply
|
||||
the X and Y coordinates of each point by a zoom factor. For a zoom parameter $z$, the zoom factor
|
||||
will be $\text{pow}(2, z)$.
|
||||
|
||||
For example, if the current point is $(1, 1)$, a zoom parameter of 1 means we actually plot $(1, 1) \cdot \text{pow}(2, 1) = (2, 2)$.
|
||||
|
||||
```
|
||||
[x, y] = [
|
||||
@ -105,25 +106,32 @@ a zoom of 1 means we actually plot $(1, 1) \cdot \text{pow}(2, 1) = (2, 2)$.
|
||||
|
||||
:::info
|
||||
In addition to scaling the image, renderers also [scale the image quality](https://github.com/scottdraves/flam3/blob/f8b6c782012e4d922ef2cc2f0c2686b612c32504/rect.c#L796-L797)
|
||||
to compensate for the zoom parameter.
|
||||
to compensate for the reduced display range.
|
||||
:::
|
||||
|
||||
### Scale
|
||||
|
||||
Finally, we need to convert from fractal flame coordinates to individual pixels. The scale parameter defines
|
||||
how many pixels are in one unit of the fractal flame coordinate system. For example, if you open the
|
||||
[reference parameters](../params.flame) in a text editor, you'll see the following:
|
||||
how many pixels are in one unit of the fractal flame coordinate system, which gives us a mapping from one system
|
||||
to the other.
|
||||
|
||||
If you open the [reference parameters](../params.flame) in a text editor, you'll see the following:
|
||||
|
||||
```xml
|
||||
<flame name="final xform" size="600 600" center="0 0" scale="150">
|
||||
```
|
||||
|
||||
This says that the final image should be 600 pixels wide and 600 pixels tall, centered at the point $(0, 0)$,
|
||||
with 150 pixels per unit. Dividing 600 by 150 gives us an image that is 4 units wide and 4 units tall.
|
||||
And because the center is at $(0, 0)$, the final image is effectively looking at the range $[-2, 2]$ in the
|
||||
fractal coordinate system (as mentioned above).
|
||||
Here's what each element means:
|
||||
|
||||
To go from the fractal coordinate system to a pixel coordinate system, we multiply by the scale,
|
||||
- `size="600 600"`: The image should be 600 pixels wide and 600 pixels tall
|
||||
- `center="0 0"`: The image is centered at the point $(0, 0)$
|
||||
- `scale="150"`: The image has 150 pixels per unit
|
||||
|
||||
Let's break it down. Dividing the image width (600) by the image scale (150) gives us a value of 4.
|
||||
This means the image should be 4 units wide (same for the height). Because the center is at $(0, 0)$,
|
||||
the final image is effectively using the range $[-2, 2]$ in fractal coordinates.
|
||||
|
||||
Now, to go from fractal coordinates to pixel coordinates we multiply by the scale,
|
||||
then subtract half the image width and height:
|
||||
|
||||
```typescript
|
||||
@ -133,22 +141,23 @@ then subtract half the image width and height:
|
||||
]
|
||||
```
|
||||
|
||||
Scale can be used to implement a kind of "zoom" in images. If the reference parameters instead used `scale="300"`,
|
||||
Scale and zoom have similar effects on images. If the reference parameters used `scale="300"`,
|
||||
the same 600 pixels would instead be looking at the range $[-1, 1]$ in the fractal coordinate system.
|
||||
Using `zoom="1"` would accomplish the same result.
|
||||
|
||||
However, this also demonstrates the biggest problem with using scale: it's a parameter that only controls the output image.
|
||||
If the output image changed to `size="1200 1200"` and we kept `scale="150"`, the output image would
|
||||
be looking at the range $[-4, 4]$ - nothing but white space. Because, using the zoom parameter
|
||||
is the preferred way to zoom in and out of an image.
|
||||
However, this also demonstrates the biggest problem with using scale: it only controls the output image.
|
||||
For example, if the output image changed to `size="1200 1200"` and we kept `scale="150"`, it would
|
||||
have a display range of $[-4, 4]$. There would be a lot of extra white space. Because the zoom parameter
|
||||
has the same effect regardless of output image size, it is the preferred way to zoom in and out.
|
||||
|
||||
:::info
|
||||
One final note about the camera controls: every step in this process (position, rotation, zoom, scale)
|
||||
is an affine transformation. And because affine transformations can be chained together, it's possible to
|
||||
express all of our camera controls as a single transformation matrix. This is important for software optimization;
|
||||
rather than applying individual camera controls step-by-step, apply all of them at once.
|
||||
express all the camera controls as a single transformation matrix. This is important for software optimization;
|
||||
rather than applying parameters step-by-step, we can apply all of them at once.
|
||||
|
||||
Additionally, because the camera controls are an affine transformation, they could be implemented
|
||||
as a transform after the final transform. In practice though, it's helpful to control them separately.
|
||||
They could also be implemented as part of the final transform, but in practice, it's helpful
|
||||
to control them separately.
|
||||
:::
|
||||
|
||||
## Camera
|
||||
@ -160,9 +169,9 @@ import cameraSource from "!!raw-loader!./camera"
|
||||
|
||||
<CodeBlock language="typescript">{cameraSource}</CodeBlock>
|
||||
|
||||
For demonstration, the output image has a 4:3 aspect ratio, removing the previous restriction of a square image.
|
||||
In addition, the scale is automatically chosen to make sure the width of the image covers the range $[-2, 2]$.
|
||||
As a result of the aspect ratio, the image height effectively covers the range $[-1.5, 1.5]$.
|
||||
To demonstrate, this display has a 4:3 aspect ratio, removing the restriction of a square image.
|
||||
In addition, the scale is automatically chosen so the image width covers the range $[-2, 2]$.
|
||||
Because of the 4:3 aspect ratio, the image height now covers the range $[-1.5, 1.5]$.
|
||||
|
||||
import {SquareCanvas} from "../src/Canvas";
|
||||
import FlameCamera from "./FlameCamera";
|
||||
@ -171,9 +180,11 @@ import FlameCamera from "./FlameCamera";
|
||||
|
||||
## Summary
|
||||
|
||||
The fractal images so far relied on critical assumptions about the output format to make sure everything
|
||||
looked correct. However, we can implement a 2D "camera" with a series of affine transformations - going from
|
||||
the fractal flame coordinate system to pixel coordinates. Later implementations of fractal flame renderers like
|
||||
[Fractorium](http://fractorium.com/) operate in 3D, and have to implement the camera slightly differently.
|
||||
The previous fractal images relied on assumptions about the output format to make sure they looked correct.
|
||||
Now, we have much more control. We can implement a 2D "camera" as a series of affine transformations, going from
|
||||
fractal flame coordinates to pixel coordinates.
|
||||
|
||||
But for this blog series, it's nice to achieve feature parity with the existing code.
|
||||
More recent fractal flame renderers like [Fractorium](http://fractorium.com/) can also operate in 3D,
|
||||
and have to implement a more complex camera system to handle the extra dimension.
|
||||
|
||||
But for this blog series, it's nice to achieve feature parity with the reference implementation.
|
Loading…
Reference in New Issue
Block a user