Compare commits
11
Commits
ab8ef03ec4
...
993185795f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
993185795f | ||
|
|
456d744ac6 | ||
|
|
90d917ede0 | ||
|
|
5a81247d76 | ||
|
|
d84e752e15 | ||
|
|
eb041c22c5 | ||
|
|
06b52fe954 | ||
|
|
c7d687c107 | ||
|
|
c2b38ea21b | ||
|
|
16a916d35a | ||
|
|
45654c0846 |
@@ -31,10 +31,16 @@ cargo install wasm-bindgen-cli --version 0.2.128 # must match the wasm-bindgen
|
|||||||
python3 -m http.server -d dist 8080
|
python3 -m http.server -d dist 8080
|
||||||
```
|
```
|
||||||
|
|
||||||
Native debug env vars (see `src/app.rs`, near the top of `FractalApp::new`):
|
Native CLI flags (`src/cli.rs`, applied in `FractalApp::apply_cli`): `--kind`,
|
||||||
`MANDEL_KIND`, `MANDEL_POWER`, `MANDEL_JULIA="re,im"`, `MANDEL_SHARE="<fragment>"`,
|
`--power`, `--julia re,im`, `--phoenix-p re,im`, `--lambda-l re,im`,
|
||||||
`MANDEL_VIEW="re,im,half_height[,iterations]"`, `MANDEL_DE=1`,
|
`--palette`, `--share <fragment>`,
|
||||||
`MANDEL_BUDDHABROT=1`, `MANDEL_EXPORT=1` (+ `MANDEL_EXPORT_PATH=out.png`).
|
`--view re,im,half_height[,iterations]`, `--de`, `--buddhabrot`,
|
||||||
|
`--buddha-palette`. `--headless` (`src/headless.rs`) skips the window
|
||||||
|
entirely: it builds the same view from the other flags, creates its own
|
||||||
|
offscreen wgpu device, and renders straight to a PNG (`--width`/`--height`,
|
||||||
|
default 1920×1080, `--export-path out.png`) without needing a GPU-backed
|
||||||
|
window/event loop. Not yet supported with `--buddhabrot`. Run
|
||||||
|
`mandelbrot --help` for the full list.
|
||||||
|
|
||||||
There's no GPU in most sandboxes: `cargo check`/`cargo test --test shader_valid`
|
There's no GPU in most sandboxes: `cargo check`/`cargo test --test shader_valid`
|
||||||
are the fast, headless way to validate a change. `cargo test` also runs but
|
are the fast, headless way to validate a change. `cargo test` also runs but
|
||||||
@@ -56,11 +62,27 @@ pixel is a handful of `f32` complex multiplies.
|
|||||||
- `src/view.rs` — `ViewState`; center is arbitrary-precision `FBig` (`Big`
|
- `src/view.rs` — `ViewState`; center is arbitrary-precision `FBig` (`Big`
|
||||||
type alias), pixel scale stays `f64` (still in-range at 10³⁰×). Precision
|
type alias), pixel scale stays `f64` (still in-range at 10³⁰×). Precision
|
||||||
(bits) scales with zoom depth (`precision_for`).
|
(bits) scales with zoom depth (`precision_for`).
|
||||||
- `src/fractal/reference.rs` — `FractalKind` enum (Mandelbrot, Burning Ship,
|
- `src/fractal/kind.rs` — the `FractalKind` enum (Mandelbrot, Burning Ship,
|
||||||
Tricorn, Multibrot, Celtic, Perpendicular, Buffalo, Phoenix, Lambda) and
|
Tricorn, Multibrot, Celtic, Perpendicular, Buffalo, Phoenix, Lambda,
|
||||||
`compute_reference`/`compute_set_reference`: iterate the chosen formula at
|
Complex Multibrot) plus everything that only needs to switch on it:
|
||||||
high precision on the CPU, emitting `Z_n` as `f32` pairs — that's the
|
`label`/`description`/`formula` (UI text), `share_tag`/`from_share_tag`
|
||||||
reference orbit the GPU perturbs from.
|
(share-link encoding), `default_set_view` (per-kind starting view), and the
|
||||||
|
`ALL` array used to enumerate every kind.
|
||||||
|
- `src/fractal/reference.rs` — `compute_reference`/`compute_set_reference`:
|
||||||
|
iterate the chosen formula at high precision on the CPU, emitting `Z_n` as
|
||||||
|
`f32` pairs — that's the reference orbit the GPU perturbs from.
|
||||||
|
- `src/shaders/*.wgsl` — none of these are standalone WGSL modules; WGSL has
|
||||||
|
no `#include`, so each is compiled by concatenating plain-text fragments
|
||||||
|
with `concat!`/`include_str!` at the `create_shader_module` call site (see
|
||||||
|
`renderer.rs`, `buddhabrot.rs`, and `tests/shader_valid.rs`, which must
|
||||||
|
concatenate the same pieces to validate what actually gets built).
|
||||||
|
`common.wgsl` (fullscreen-triangle vertex helper, `cmul`/`cpow`, `KIND_*`
|
||||||
|
constants) is prepended to every shader. `iterate_uniforms.wgsl` (the
|
||||||
|
perturbation-pipeline `Uniforms` struct + `palette()`) is additionally
|
||||||
|
prepended to `mandelbrot.wgsl` and `colorize.wgsl`, which share that layout.
|
||||||
|
Because there's no namespacing, a definition must live in exactly one file
|
||||||
|
among those concatenated together for a given shader — don't redefine a
|
||||||
|
`common.wgsl`/`iterate_uniforms.wgsl` symbol locally.
|
||||||
- `src/shaders/mandelbrot.wgsl` — the perturbation fragment shader.
|
- `src/shaders/mandelbrot.wgsl` — the perturbation fragment shader.
|
||||||
`advance_delta(z, e)` is the per-kind delta step (`z` = reference point,
|
`advance_delta(z, e)` is the per-kind delta step (`z` = reference point,
|
||||||
`e` = current delta); the caller adds `step_add` (= `dc`) afterward — this
|
`e` = current delta); the caller adds `step_add` (= `dc`) afterward — this
|
||||||
@@ -70,8 +92,8 @@ pixel is a handful of `f32` complex multiplies.
|
|||||||
reference data since the orbit point alone wouldn't be enough to recover an
|
reference data since the orbit point alone wouldn't be enough to recover an
|
||||||
exact delta). `fprime(z)` is the derivative used for distance-estimation
|
exact delta). `fprime(z)` is the derivative used for distance-estimation
|
||||||
(DE) shading; exact for holomorphic kinds, an approximation (`~2Z`) for the
|
(DE) shading; exact for holomorphic kinds, an approximation (`~2Z`) for the
|
||||||
abs-based ones. A `KIND_*` constant here must match the matching
|
abs-based ones. A `KIND_*` constant (from `common.wgsl`) must match the
|
||||||
`FractalKind` variant's discriminant exactly.
|
matching `FractalKind` variant's discriminant exactly.
|
||||||
- `src/fractal/renderer.rs` — `FractalRenderer` (wgpu pipelines, uniform +
|
- `src/fractal/renderer.rs` — `FractalRenderer` (wgpu pipelines, uniform +
|
||||||
storage buffers, bind groups), `Uniforms` (repr(C) layout that must match
|
storage buffers, bind groups), `Uniforms` (repr(C) layout that must match
|
||||||
the WGSL `Uniforms` struct field-for-field, including padding), and
|
the WGSL `Uniforms` struct field-for-field, including padding), and
|
||||||
@@ -89,28 +111,31 @@ pixel is a handful of `f32` complex multiplies.
|
|||||||
`should_request`/`ensure_reference` (decide when the reference is stale and
|
`should_request`/`ensure_reference` (decide when the reference is stale and
|
||||||
dispatch/collect it), `make_uniforms` (assemble the per-frame `Uniforms`),
|
dispatch/collect it), `make_uniforms` (assemble the per-frame `Uniforms`),
|
||||||
`tick_animations` (drives the "morph c/p/λ" and auto-zoom animations),
|
`tick_animations` (drives the "morph c/p/λ" and auto-zoom animations),
|
||||||
`default_view_for` (per-kind starting view). `KINDS`, `JULIA_PRESETS`, and
|
`default_view_for` (wraps `FractalKind::default_set_view`, adding the
|
||||||
`SET_PRESETS` are sized as `[T; FractalKind::<last variant> as usize + 1]` —
|
kind-independent Julia case). `JULIA_PRESETS` and `SET_PRESETS` are sized as
|
||||||
adding a new `FractalKind` means bumping all three (and adding an empty
|
`[T; FractalKind::<last variant> as usize + 1]` — adding a new `FractalKind`
|
||||||
`&[]` slot to the two preset arrays if the kind has none).
|
means bumping both (and adding an empty `&[]` slot to each if the kind has
|
||||||
|
none), plus adding it to `FractalKind::ALL` in `kind.rs`.
|
||||||
- `src/fractal/share.rs` — `ShareState`: encodes the full view (mode, kind,
|
- `src/fractal/share.rs` — `ShareState`: encodes the full view (mode, kind,
|
||||||
full-precision decimal center, zoom, iterations, per-kind constants,
|
full-precision decimal center, zoom, iterations, per-kind constants,
|
||||||
coloring) as a `#`-fragment URL for bookmarking/sharing deep-zoom locations.
|
coloring) as a `#`-fragment URL for bookmarking/sharing deep-zoom locations.
|
||||||
|
|
||||||
### Adding a new `FractalKind`
|
### Adding a new `FractalKind`
|
||||||
|
|
||||||
Touches, in order: `reference.rs` (enum variant + CPU iteration formula, and a
|
Touches, in order: `kind.rs` (enum variant + `ALL` slot + `label`/
|
||||||
test comparing against a naive `f64` iteration), `mandelbrot.wgsl` (matching
|
`description`/`formula`/`share_tag`/`from_share_tag`/`default_set_view`
|
||||||
`KIND_*` const + `advance_delta`/`fprime` arms), `buddhabrot.wgsl` (matching
|
arms), `reference.rs` (CPU iteration formula arm, and a test comparing
|
||||||
arm in `advance()`, if the kind makes sense as a Buddhabrot), `renderer.rs`
|
against a naive `f64` iteration), `common.wgsl` (matching `KIND_*` const),
|
||||||
`Uniforms` (only if the kind needs a new per-kind constant, e.g. Phoenix's
|
`mandelbrot.wgsl` (matching `advance_delta`/`fprime` arms), `buddhabrot.wgsl`
|
||||||
`phoenix_p`), `share.rs` (encode/decode string tag), `app.rs` (`KINDS` label,
|
(matching arm in `advance()`, if the kind makes sense as a Buddhabrot),
|
||||||
`JULIA_PRESETS`/`SET_PRESETS` slot, `default_view_for` entry, and optionally a
|
`renderer.rs` `Uniforms` (only if the kind needs a new per-kind constant,
|
||||||
UI control for its constant + an animation toggle, following the
|
e.g. Phoenix's `phoenix_p`), `app.rs` (`JULIA_PRESETS`/`SET_PRESETS` slot,
|
||||||
Phoenix/Lambda pattern). If `c` doesn't enter the formula additively (e.g. a
|
and optionally a UI control for its constant + an animation toggle,
|
||||||
rational map with `c` in a denominator), the `advance_delta`/`step_add` split
|
following the Phoenix/Lambda pattern). If `c` doesn't enter the formula
|
||||||
doesn't work — that needs its own step function plus extra per-step reference
|
additively (e.g. a rational map with `c` in a denominator), the
|
||||||
data uploaded in a second GPU buffer alongside the orbit.
|
`advance_delta`/`step_add` split doesn't work — that needs its own step
|
||||||
|
function plus extra per-step reference data uploaded in a second GPU buffer
|
||||||
|
alongside the orbit.
|
||||||
|
|
||||||
### Buddhabrot is a separate pipeline
|
### Buddhabrot is a separate pipeline
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ png = "0.18.1"
|
|||||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
env_logger = "0.11.11"
|
env_logger = "0.11.11"
|
||||||
clap = { version = "4.5.51", features = ["derive"] }
|
clap = { version = "4.5.51", features = ["derive"] }
|
||||||
|
pollster = "1.0.1"
|
||||||
|
|
||||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
futures-channel = { version = "0.3.34", default-features = false, features = ["alloc", "std"] }
|
futures-channel = { version = "0.3.34", default-features = false, features = ["alloc", "std"] }
|
||||||
|
|||||||
+507
-158
File diff suppressed because it is too large
Load Diff
+34
-5
@@ -17,10 +17,26 @@ pub struct Cli {
|
|||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub power: Option<u32>,
|
pub power: Option<u32>,
|
||||||
|
|
||||||
|
/// Complex exponent for the Complex Multibrot kind (z -> z^power + c).
|
||||||
|
#[arg(long, value_name = "RE,IM")]
|
||||||
|
pub complex_power: Option<String>,
|
||||||
|
|
||||||
/// Start in Julia mode with this seed constant.
|
/// Start in Julia mode with this seed constant.
|
||||||
#[arg(long, value_name = "RE,IM")]
|
#[arg(long, value_name = "RE,IM")]
|
||||||
pub julia: Option<String>,
|
pub julia: Option<String>,
|
||||||
|
|
||||||
|
/// Coloring palette index.
|
||||||
|
#[arg(long, value_name = "INDEX")]
|
||||||
|
pub palette: Option<u32>,
|
||||||
|
|
||||||
|
/// Phoenix constant p for the Phoenix kind (z -> z^2 + c + p*z_prev).
|
||||||
|
#[arg(long, value_name = "RE,IM")]
|
||||||
|
pub phoenix_p: Option<String>,
|
||||||
|
|
||||||
|
/// Lambda constant λ for the Lambda kind (z -> λ*z*(1 - z)).
|
||||||
|
#[arg(long, value_name = "RE,IM")]
|
||||||
|
pub lambda_l: Option<String>,
|
||||||
|
|
||||||
/// Restore a view from a share-link fragment (the part after '#').
|
/// Restore a view from a share-link fragment (the part after '#').
|
||||||
#[arg(long, value_name = "FRAGMENT")]
|
#[arg(long, value_name = "FRAGMENT")]
|
||||||
pub share: Option<String>,
|
pub share: Option<String>,
|
||||||
@@ -41,13 +57,23 @@ pub struct Cli {
|
|||||||
#[arg(long, value_name = "INDEX")]
|
#[arg(long, value_name = "INDEX")]
|
||||||
pub buddha_palette: Option<u32>,
|
pub buddha_palette: Option<u32>,
|
||||||
|
|
||||||
/// Render a PNG export on startup.
|
/// Output path for --headless (default: fractal-<timestamp>.png).
|
||||||
#[arg(long)]
|
|
||||||
pub export: bool,
|
|
||||||
|
|
||||||
/// Output path for --export (default: fractal-<timestamp>.png).
|
|
||||||
#[arg(long, value_name = "PATH")]
|
#[arg(long, value_name = "PATH")]
|
||||||
pub export_path: Option<String>,
|
pub export_path: Option<String>,
|
||||||
|
|
||||||
|
/// Run without opening a window: render the current view to a PNG and
|
||||||
|
/// exit. Combine with --kind/--julia/--share/--view etc. to pick what to
|
||||||
|
/// render. Not yet supported with --buddhabrot.
|
||||||
|
#[arg(long)]
|
||||||
|
pub headless: bool,
|
||||||
|
|
||||||
|
/// Output image width in pixels (--headless only).
|
||||||
|
#[arg(long, value_name = "PX", default_value_t = 1920)]
|
||||||
|
pub width: u32,
|
||||||
|
|
||||||
|
/// Output image height in pixels (--headless only).
|
||||||
|
#[arg(long, value_name = "PX", default_value_t = 1080)]
|
||||||
|
pub height: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, ValueEnum)]
|
#[derive(Copy, Clone, Debug, ValueEnum)]
|
||||||
@@ -65,6 +91,8 @@ pub enum KindArg {
|
|||||||
Buffalo,
|
Buffalo,
|
||||||
Phoenix,
|
Phoenix,
|
||||||
Lambda,
|
Lambda,
|
||||||
|
#[value(alias = "cmulti")]
|
||||||
|
ComplexMultibrot,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<KindArg> for FractalKind {
|
impl From<KindArg> for FractalKind {
|
||||||
@@ -79,6 +107,7 @@ impl From<KindArg> for FractalKind {
|
|||||||
KindArg::Buffalo => FractalKind::Buffalo,
|
KindArg::Buffalo => FractalKind::Buffalo,
|
||||||
KindArg::Phoenix => FractalKind::Phoenix,
|
KindArg::Phoenix => FractalKind::Phoenix,
|
||||||
KindArg::Lambda => FractalKind::Lambda,
|
KindArg::Lambda => FractalKind::Lambda,
|
||||||
|
KindArg::ComplexMultibrot => FractalKind::ComplexMultibrot,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,11 @@ pub struct BuddhabrotUniforms {
|
|||||||
/// (yellow core, blue halo), 2 = grayscale. Display-only, like `exposure`
|
/// (yellow core, blue halo), 2 = grayscale. Display-only, like `exposure`
|
||||||
/// — excluded from `ContentKey` so changing it doesn't reset accumulation.
|
/// — excluded from `ContentKey` so changing it doesn't reset accumulation.
|
||||||
pub palette: u32,
|
pub palette: u32,
|
||||||
pub _pad: [u32; 3],
|
/// Padding so `complex_power` (a vec2, 8-byte aligned in the shader)
|
||||||
|
/// starts on an 8-byte boundary.
|
||||||
|
pub _pad0: u32,
|
||||||
|
/// Complex exponent for the Complex Multibrot kind; ignored by other kinds.
|
||||||
|
pub complex_power: [f32; 2],
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The subset of `BuddhabrotUniforms` that determines the *content* of the
|
/// The subset of `BuddhabrotUniforms` that determines the *content* of the
|
||||||
@@ -62,6 +66,7 @@ struct ContentKey {
|
|||||||
bailout_sq: f32,
|
bailout_sq: f32,
|
||||||
kind: u32,
|
kind: u32,
|
||||||
power: u32,
|
power: u32,
|
||||||
|
complex_power: [f32; 2],
|
||||||
r_cap: u32,
|
r_cap: u32,
|
||||||
g_cap: u32,
|
g_cap: u32,
|
||||||
b_cap: u32,
|
b_cap: u32,
|
||||||
@@ -78,6 +83,7 @@ impl From<&BuddhabrotUniforms> for ContentKey {
|
|||||||
bailout_sq: u.bailout_sq,
|
bailout_sq: u.bailout_sq,
|
||||||
kind: u.kind,
|
kind: u.kind,
|
||||||
power: u.power,
|
power: u.power,
|
||||||
|
complex_power: u.complex_power,
|
||||||
r_cap: u.r_cap,
|
r_cap: u.r_cap,
|
||||||
g_cap: u.g_cap,
|
g_cap: u.g_cap,
|
||||||
b_cap: u.b_cap,
|
b_cap: u.b_cap,
|
||||||
@@ -114,7 +120,13 @@ impl BuddhabrotRenderer {
|
|||||||
pub fn new(device: &wgpu::Device, target_format: wgpu::TextureFormat) -> Self {
|
pub fn new(device: &wgpu::Device, target_format: wgpu::TextureFormat) -> Self {
|
||||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||||
label: Some("buddhabrot"),
|
label: Some("buddhabrot"),
|
||||||
source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/buddhabrot.wgsl").into()),
|
source: wgpu::ShaderSource::Wgsl(
|
||||||
|
concat!(
|
||||||
|
include_str!("../shaders/common.wgsl"),
|
||||||
|
include_str!("../shaders/buddhabrot.wgsl"),
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
//! `FractalKind`: the enum selecting which iteration formula is in use, plus
|
||||||
|
//! everything that only needs to switch on it (UI label/description/formula
|
||||||
|
//! text, share-link tag, default parameter-plane view). The CPU/GPU orbit
|
||||||
|
//! math itself lives in `reference.rs` (CPU reference orbit) and
|
||||||
|
//! `shaders/mandelbrot.wgsl` (GPU perturbation delta) since both must also
|
||||||
|
//! stay in sync with `common.wgsl`'s `KIND_*` constants.
|
||||||
|
|
||||||
|
/// The iteration formula. Must be kept in sync with `advance_delta` and the
|
||||||
|
/// `KIND_*` constants in the shader.
|
||||||
|
#[repr(u8)]
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
pub enum FractalKind {
|
||||||
|
/// `z -> z^2 + c`.
|
||||||
|
Mandelbrot = 0,
|
||||||
|
/// `z -> (|Re z| + i|Im z|)^2 + c`.
|
||||||
|
BurningShip = 1,
|
||||||
|
/// `z -> conj(z)^2 + c` (the Mandelbar).
|
||||||
|
Tricorn = 2,
|
||||||
|
/// `z -> z^power + c` (power >= 2).
|
||||||
|
Multibrot = 3,
|
||||||
|
/// `z -> |Re(z^2)| + i·Im(z^2) + c` (abs on the real output of the square).
|
||||||
|
Celtic = 4,
|
||||||
|
/// `z -> (x^2 - y^2) - 2·x·|y|·i + c` (abs on the imaginary input).
|
||||||
|
Perpendicular = 5,
|
||||||
|
/// `z -> |Re(z^2)| - |Im(z^2)|·i + c` (abs on both outputs).
|
||||||
|
Buffalo = 6,
|
||||||
|
/// `z -> z^2 + c + p·z_{n-1}` (two-term recurrence; `p` is `phoenix_p`).
|
||||||
|
Phoenix = 7,
|
||||||
|
/// `z -> lambda·z(1 - z)` (logistic map).
|
||||||
|
Lambda = 8,
|
||||||
|
/// `z -> z^power + c`, where `power` is a complex constant (the
|
||||||
|
/// `complex_power` argument), via the principal branch `z^p = exp(p·ln z)`.
|
||||||
|
ComplexMultibrot = 9,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FractalKind {
|
||||||
|
/// Every kind, in declaration/discriminant order. Sized arrays keyed by
|
||||||
|
/// `kind as usize` (`JULIA_PRESETS`, `SET_PRESETS`) must have one slot per
|
||||||
|
/// entry here.
|
||||||
|
pub const ALL: [FractalKind; 10] = [
|
||||||
|
FractalKind::Mandelbrot,
|
||||||
|
FractalKind::BurningShip,
|
||||||
|
FractalKind::Tricorn,
|
||||||
|
FractalKind::Multibrot,
|
||||||
|
FractalKind::Celtic,
|
||||||
|
FractalKind::Perpendicular,
|
||||||
|
FractalKind::Buffalo,
|
||||||
|
FractalKind::Phoenix,
|
||||||
|
FractalKind::Lambda,
|
||||||
|
FractalKind::ComplexMultibrot,
|
||||||
|
];
|
||||||
|
|
||||||
|
pub fn description(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
FractalKind::Mandelbrot => {
|
||||||
|
"The Mandelbrot set is the most famous fractal set, obtained with the simplest escape-time formula. This set represents all Julia fractals: each points of the Mandelbrot set is related to a specific Julia fractal."
|
||||||
|
}
|
||||||
|
FractalKind::BurningShip => {
|
||||||
|
"A variation of the famous Mandelbrot set, using absolute values on the real and imaginary part of each iterations."
|
||||||
|
}
|
||||||
|
FractalKind::Tricorn => {
|
||||||
|
"The Tricorn set is obtained using the same formula as the Mandelbrot set, taking the complex conjugate of the previous iteration."
|
||||||
|
}
|
||||||
|
FractalKind::Multibrot => {
|
||||||
|
"Multibrot use the same formula as the Mandelbrot set, with a bigger exposant."
|
||||||
|
}
|
||||||
|
FractalKind::Celtic => "",
|
||||||
|
FractalKind::Perpendicular => "",
|
||||||
|
FractalKind::Buffalo => "",
|
||||||
|
FractalKind::Phoenix => "",
|
||||||
|
FractalKind::Lambda => "",
|
||||||
|
FractalKind::ComplexMultibrot => {
|
||||||
|
"Like Multibrot, but the exponent itself is a complex number instead of a plain integer, via z^p = exp(p·ln z)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// UI label for this kind (combo box / info panel heading).
|
||||||
|
pub fn label(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
FractalKind::Mandelbrot => "Mandelbrot",
|
||||||
|
FractalKind::BurningShip => "Burning Ship",
|
||||||
|
FractalKind::Tricorn => "Tricorn",
|
||||||
|
FractalKind::Multibrot => "Multibrot",
|
||||||
|
FractalKind::Celtic => "Celtic",
|
||||||
|
FractalKind::Perpendicular => "Perpendicular",
|
||||||
|
FractalKind::Buffalo => "Buffalo",
|
||||||
|
FractalKind::Phoenix => "Phoenix",
|
||||||
|
FractalKind::Lambda => "Lambda",
|
||||||
|
FractalKind::ComplexMultibrot => "Complex Multibrot",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The iteration formula in human-readable notation (mirrors the doc
|
||||||
|
/// comments on the variants above). `power` is only used by Multibrot;
|
||||||
|
/// `complex_power` only by Complex Multibrot.
|
||||||
|
pub fn formula(&self, power: u32, complex_power: (f64, f64)) -> String {
|
||||||
|
match self {
|
||||||
|
FractalKind::Mandelbrot => "z = z² + c".to_string(),
|
||||||
|
FractalKind::BurningShip => "z = (|Re(z)| + i|Im(z)|)² + c".to_string(),
|
||||||
|
FractalKind::Tricorn => "z = conj(z)² + c".to_string(),
|
||||||
|
FractalKind::Multibrot => format!("z = z^{power} + c"),
|
||||||
|
FractalKind::Celtic => "z = |Re(z²)| + i·Im(z²) + c".to_string(),
|
||||||
|
FractalKind::Perpendicular => "z = (x² − y²) − 2x|y|i + c".to_string(),
|
||||||
|
FractalKind::Buffalo => "z = |Re(z²)| − i|Im(z²)| + c".to_string(),
|
||||||
|
FractalKind::Phoenix => "z = z² + c + p·z_prev".to_string(),
|
||||||
|
FractalKind::Lambda => "z = λ·z(1 − z)".to_string(),
|
||||||
|
FractalKind::ComplexMultibrot => {
|
||||||
|
format!("z = z^({:.3}{:+.3}i) + c", complex_power.0, complex_power.1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Short tag used to identify this kind in a share-link fragment.
|
||||||
|
pub fn share_tag(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
FractalKind::Mandelbrot => "mandel",
|
||||||
|
FractalKind::BurningShip => "burning",
|
||||||
|
FractalKind::Tricorn => "tricorn",
|
||||||
|
FractalKind::Multibrot => "multi",
|
||||||
|
FractalKind::Celtic => "celtic",
|
||||||
|
FractalKind::Perpendicular => "perp",
|
||||||
|
FractalKind::Buffalo => "buffalo",
|
||||||
|
FractalKind::Phoenix => "phoenix",
|
||||||
|
FractalKind::Lambda => "lambda",
|
||||||
|
FractalKind::ComplexMultibrot => "cmulti",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Inverse of `share_tag`; unknown tags fall back to `None` so the caller
|
||||||
|
/// can decide the default (matches historical share-link behavior).
|
||||||
|
pub fn from_share_tag(tag: &str) -> Option<FractalKind> {
|
||||||
|
Some(match tag {
|
||||||
|
"mandel" => FractalKind::Mandelbrot,
|
||||||
|
"burning" => FractalKind::BurningShip,
|
||||||
|
"tricorn" => FractalKind::Tricorn,
|
||||||
|
"multi" => FractalKind::Multibrot,
|
||||||
|
"celtic" => FractalKind::Celtic,
|
||||||
|
"perp" => FractalKind::Perpendicular,
|
||||||
|
"buffalo" => FractalKind::Buffalo,
|
||||||
|
"phoenix" => FractalKind::Phoenix,
|
||||||
|
"lambda" => FractalKind::Lambda,
|
||||||
|
"cmulti" => FractalKind::ComplexMultibrot,
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Default parameter-plane (Mandelbrot-mode) view for this kind, as
|
||||||
|
/// `(center_re, center_im, half_height)`. The Julia (dynamical) plane
|
||||||
|
/// doesn't vary by kind, so it isn't covered here.
|
||||||
|
pub fn default_set_view(&self) -> (f64, f64, f64) {
|
||||||
|
match self {
|
||||||
|
FractalKind::Mandelbrot => (-0.5, 0.0, 1.25),
|
||||||
|
FractalKind::BurningShip => (-0.5, -0.5, 1.3),
|
||||||
|
FractalKind::Tricorn => (-0.25, 0.0, 1.7),
|
||||||
|
FractalKind::Multibrot => (0.0, 0.0, 1.5),
|
||||||
|
FractalKind::Celtic => (-0.5, 0.0, 1.6),
|
||||||
|
FractalKind::Perpendicular => (-0.5, 0.0, 1.5),
|
||||||
|
FractalKind::Buffalo => (-0.5, 0.5, 1.5),
|
||||||
|
FractalKind::Phoenix => (-0.5, 0.0, 1.5),
|
||||||
|
FractalKind::Lambda => (-0.5, 0.0, 2.4),
|
||||||
|
FractalKind::ComplexMultibrot => (0.0, 0.0, 1.5),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
-5
@@ -2,14 +2,17 @@
|
|||||||
//! egui paint callback.
|
//! egui paint callback.
|
||||||
|
|
||||||
pub mod buddhabrot;
|
pub mod buddhabrot;
|
||||||
|
pub mod kind;
|
||||||
pub mod reference;
|
pub mod reference;
|
||||||
pub mod renderer;
|
pub mod renderer;
|
||||||
pub mod share;
|
pub mod share;
|
||||||
|
|
||||||
pub use buddhabrot::{BuddhabrotCallback, BuddhabrotRenderer, BuddhabrotUniforms};
|
pub use buddhabrot::{BuddhabrotCallback, BuddhabrotRenderer, BuddhabrotUniforms};
|
||||||
pub use reference::{FractalKind, compute_reference, compute_set_reference};
|
pub use kind::FractalKind;
|
||||||
pub use renderer::{
|
pub use reference::{compute_reference, compute_set_reference};
|
||||||
ExportRender, FractalCallback, FractalRenderer, MAX_REF_POINTS, Uniforms,
|
#[cfg(target_arch = "wasm32")]
|
||||||
encode_png_with_progress,
|
pub use renderer::encode_png_with_progress;
|
||||||
};
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub use renderer::export_to_png_blocking;
|
||||||
|
pub use renderer::{ExportRender, FractalCallback, FractalRenderer, MAX_REF_POINTS, Uniforms};
|
||||||
pub use share::ShareState;
|
pub use share::ShareState;
|
||||||
|
|||||||
+112
-28
@@ -10,33 +10,9 @@
|
|||||||
//! * Mandelbrot-set: `z0 = 0`, `c = view center` (the c-plane point per pixel).
|
//! * Mandelbrot-set: `z0 = 0`, `c = view center` (the c-plane point per pixel).
|
||||||
//! * Julia-set: `z0 = view center`, `c = fractal constant` (fixed per view).
|
//! * Julia-set: `z0 = view center`, `c = fractal constant` (fixed per view).
|
||||||
|
|
||||||
|
use super::kind::FractalKind;
|
||||||
use crate::view::{Big, big_from_f64};
|
use crate::view::{Big, big_from_f64};
|
||||||
|
|
||||||
/// The iteration formula. Must be kept in sync with `advance_delta` and the
|
|
||||||
/// `KIND_*` constants in the shader.
|
|
||||||
#[repr(u8)]
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
|
||||||
pub enum FractalKind {
|
|
||||||
/// `z -> z^2 + c`.
|
|
||||||
Mandelbrot = 0,
|
|
||||||
/// `z -> (|Re z| + i|Im z|)^2 + c`.
|
|
||||||
BurningShip = 1,
|
|
||||||
/// `z -> conj(z)^2 + c` (the Mandelbar).
|
|
||||||
Tricorn = 2,
|
|
||||||
/// `z -> z^power + c` (power >= 2).
|
|
||||||
Multibrot = 3,
|
|
||||||
/// `z -> |Re(z^2)| + i·Im(z^2) + c` (abs on the real output of the square).
|
|
||||||
Celtic = 4,
|
|
||||||
/// `z -> (x^2 - y^2) - 2·x·|y|·i + c` (abs on the imaginary input).
|
|
||||||
Perpendicular = 5,
|
|
||||||
/// `z -> |Re(z^2)| - |Im(z^2)|·i + c` (abs on both outputs).
|
|
||||||
Buffalo = 6,
|
|
||||||
/// `z -> z^2 + c + p·z_{n-1}` (two-term recurrence; `p` is `phoenix_p`).
|
|
||||||
Phoenix = 7,
|
|
||||||
/// `z -> lambda·z(1 - z)` (logistic map).
|
|
||||||
Lambda = 8,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reference orbit escapes once |Z|^2 exceeds this. Kept larger than the pixel
|
/// Reference orbit escapes once |Z|^2 exceeds this. Kept larger than the pixel
|
||||||
/// bailout so pixels escaping alongside the reference can still reach their
|
/// bailout so pixels escaping alongside the reference can still reach their
|
||||||
/// bailout before the stored orbit runs out.
|
/// bailout before the stored orbit runs out.
|
||||||
@@ -57,6 +33,7 @@ pub fn compute_reference(
|
|||||||
power: u32,
|
power: u32,
|
||||||
phoenix_p: (f64, f64),
|
phoenix_p: (f64, f64),
|
||||||
lambda_l: (f64, f64),
|
lambda_l: (f64, f64),
|
||||||
|
complex_power: (f64, f64),
|
||||||
) -> Vec<[f32; 2]> {
|
) -> Vec<[f32; 2]> {
|
||||||
let cr = c_re.clone().with_precision(precision).value();
|
let cr = c_re.clone().with_precision(precision).value();
|
||||||
let ci = c_im.clone().with_precision(precision).value();
|
let ci = c_im.clone().with_precision(precision).value();
|
||||||
@@ -72,6 +49,9 @@ pub fn compute_reference(
|
|||||||
// Lambda distortion constant `l` (a small fixed complex number).
|
// Lambda distortion constant `l` (a small fixed complex number).
|
||||||
let lr = big_from_f64(lambda_l.0, precision);
|
let lr = big_from_f64(lambda_l.0, precision);
|
||||||
let li = big_from_f64(lambda_l.1, precision);
|
let li = big_from_f64(lambda_l.1, precision);
|
||||||
|
// Complex Multibrot exponent (a fixed complex number).
|
||||||
|
let cpow_re = big_from_f64(complex_power.0, precision);
|
||||||
|
let cpow_im = big_from_f64(complex_power.1, precision);
|
||||||
|
|
||||||
let mut points: Vec<[f32; 2]> = Vec::with_capacity(max_iter as usize + 1);
|
let mut points: Vec<[f32; 2]> = Vec::with_capacity(max_iter as usize + 1);
|
||||||
|
|
||||||
@@ -142,6 +122,10 @@ pub fn compute_reference(
|
|||||||
let lzi = &lr * &zi + &li * &zr;
|
let lzi = &lr * &zi + &li * &zr;
|
||||||
(&lzr * &re2 - &lzi * &im2, re2 * lzi + lzr * im2)
|
(&lzr * &re2 - &lzi * &im2, re2 * lzi + lzr * im2)
|
||||||
}
|
}
|
||||||
|
FractalKind::ComplexMultibrot => {
|
||||||
|
let (pr, pi) = complex_pow_complex(&zr, &zi, &cpow_re, &cpow_im, precision);
|
||||||
|
(pr + &cr, pi + &ci)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Shift the previous iterate (only the Phoenix arm reads it).
|
// Shift the previous iterate (only the Phoenix arm reads it).
|
||||||
@@ -178,6 +162,32 @@ fn complex_pow(zr: &Big, zi: &Big, power: u32, precision: usize) -> (Big, Big) {
|
|||||||
(rr, ri)
|
(rr, ri)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `true` if `x` is (numerically) zero. The f64 check is exact for a true
|
||||||
|
/// zero; only matters here to special-case `ln(0)`.
|
||||||
|
fn is_big_zero(x: &Big) -> bool {
|
||||||
|
x.to_f64().value() == 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `(zr + i zi)^(pr + i pi)` for a complex exponent, via the principal branch
|
||||||
|
/// `z^p = exp(p·ln z)` where `ln z = ln|z| + i·arg(z)`. Used by
|
||||||
|
/// `ComplexMultibrot`; must be kept in sync with the shader's `cpow`.
|
||||||
|
/// `z = 0` is special-cased to `0` (the formula's `ln(0)` would otherwise
|
||||||
|
/// panic; this is the correct limit for the `Re(p) > 0` region the UI
|
||||||
|
/// exposes).
|
||||||
|
fn complex_pow_complex(zr: &Big, zi: &Big, pr: &Big, pi: &Big, precision: usize) -> (Big, Big) {
|
||||||
|
if is_big_zero(zr) && is_big_zero(zi) {
|
||||||
|
return (big_zero(precision), big_zero(precision));
|
||||||
|
}
|
||||||
|
let r2 = &zr.sqr() + &zi.sqr();
|
||||||
|
let ln_r = r2.ln() >> 1; // 0.5 * ln(r2) = ln(sqrt(r2)); exact halving.
|
||||||
|
let theta = zi.atan2(zr);
|
||||||
|
let exp_re = (pr * &ln_r - pi * &theta).with_precision(precision).value();
|
||||||
|
let exp_im = (pr * &theta + pi * &ln_r).with_precision(precision).value();
|
||||||
|
let mag = exp_re.exp();
|
||||||
|
let (sin_a, cos_a) = exp_im.sin_cos();
|
||||||
|
(&mag * &cos_a, &mag * &sin_a)
|
||||||
|
}
|
||||||
|
|
||||||
/// Convenience: parameter-plane ("Mandelbrot-set") reference (`z0 = 0`,
|
/// Convenience: parameter-plane ("Mandelbrot-set") reference (`z0 = 0`,
|
||||||
/// `c = center`) for any `kind`.
|
/// `c = center`) for any `kind`.
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
@@ -190,10 +200,21 @@ pub fn compute_set_reference(
|
|||||||
power: u32,
|
power: u32,
|
||||||
phoenix_p: (f64, f64),
|
phoenix_p: (f64, f64),
|
||||||
lambda_l: (f64, f64),
|
lambda_l: (f64, f64),
|
||||||
|
complex_power: (f64, f64),
|
||||||
) -> Vec<[f32; 2]> {
|
) -> Vec<[f32; 2]> {
|
||||||
let zero = big_zero(precision);
|
let zero = big_zero(precision);
|
||||||
compute_reference(
|
compute_reference(
|
||||||
&zero, &zero, center_re, center_im, max_iter, precision, kind, power, phoenix_p, lambda_l,
|
&zero,
|
||||||
|
&zero,
|
||||||
|
center_re,
|
||||||
|
center_im,
|
||||||
|
max_iter,
|
||||||
|
precision,
|
||||||
|
kind,
|
||||||
|
power,
|
||||||
|
phoenix_p,
|
||||||
|
lambda_l,
|
||||||
|
complex_power,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,6 +237,7 @@ mod tests {
|
|||||||
2,
|
2,
|
||||||
(0.0, 0.0),
|
(0.0, 0.0),
|
||||||
(0.0, 0.0),
|
(0.0, 0.0),
|
||||||
|
(0.0, 0.0),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Independent naive f64 orbit.
|
// Independent naive f64 orbit.
|
||||||
@@ -255,6 +277,7 @@ mod tests {
|
|||||||
2,
|
2,
|
||||||
(0.0, 0.0),
|
(0.0, 0.0),
|
||||||
(0.0, 0.0),
|
(0.0, 0.0),
|
||||||
|
(0.0, 0.0),
|
||||||
);
|
);
|
||||||
assert_eq!(points.len(), 501, "interior orbit should not escape");
|
assert_eq!(points.len(), 501, "interior orbit should not escape");
|
||||||
}
|
}
|
||||||
@@ -273,6 +296,7 @@ mod tests {
|
|||||||
2,
|
2,
|
||||||
(0.0, 0.0),
|
(0.0, 0.0),
|
||||||
(0.0, 0.0),
|
(0.0, 0.0),
|
||||||
|
(0.0, 0.0),
|
||||||
);
|
);
|
||||||
|
|
||||||
let (c_re, c_im) = (-1.75_f64, -0.03_f64);
|
let (c_re, c_im) = (-1.75_f64, -0.03_f64);
|
||||||
@@ -302,6 +326,7 @@ mod tests {
|
|||||||
3,
|
3,
|
||||||
(0.0, 0.0),
|
(0.0, 0.0),
|
||||||
(0.0, 0.0),
|
(0.0, 0.0),
|
||||||
|
(0.0, 0.0),
|
||||||
);
|
);
|
||||||
|
|
||||||
let (c_re, c_im) = (0.3_f64, 0.2_f64);
|
let (c_re, c_im) = (0.3_f64, 0.2_f64);
|
||||||
@@ -337,6 +362,7 @@ mod tests {
|
|||||||
2,
|
2,
|
||||||
(0.0, 0.0),
|
(0.0, 0.0),
|
||||||
(0.0, 0.0),
|
(0.0, 0.0),
|
||||||
|
(0.0, 0.0),
|
||||||
);
|
);
|
||||||
|
|
||||||
let (mut zr, mut zi) = (0.15_f64, -0.1_f64);
|
let (mut zr, mut zi) = (0.15_f64, -0.1_f64);
|
||||||
@@ -366,6 +392,7 @@ mod tests {
|
|||||||
2,
|
2,
|
||||||
(0.0, 0.0),
|
(0.0, 0.0),
|
||||||
(0.0, 0.0),
|
(0.0, 0.0),
|
||||||
|
(0.0, 0.0),
|
||||||
);
|
);
|
||||||
|
|
||||||
let (c_re, c_im) = (-0.6_f64, 0.4_f64);
|
let (c_re, c_im) = (-0.6_f64, 0.4_f64);
|
||||||
@@ -396,6 +423,7 @@ mod tests {
|
|||||||
2,
|
2,
|
||||||
(0.0, 0.0),
|
(0.0, 0.0),
|
||||||
(0.0, 0.0),
|
(0.0, 0.0),
|
||||||
|
(0.0, 0.0),
|
||||||
);
|
);
|
||||||
|
|
||||||
let (c_re, c_im) = (-0.7_f64, -0.2_f64);
|
let (c_re, c_im) = (-0.7_f64, -0.2_f64);
|
||||||
@@ -426,6 +454,7 @@ mod tests {
|
|||||||
2,
|
2,
|
||||||
(0.0, 0.0),
|
(0.0, 0.0),
|
||||||
(0.0, 0.0),
|
(0.0, 0.0),
|
||||||
|
(0.0, 0.0),
|
||||||
);
|
);
|
||||||
|
|
||||||
let (c_re, c_im) = (-1.2_f64, -0.35_f64);
|
let (c_re, c_im) = (-1.2_f64, -0.35_f64);
|
||||||
@@ -448,8 +477,17 @@ mod tests {
|
|||||||
let cr = Big::try_from(0.5667_f64).unwrap();
|
let cr = Big::try_from(0.5667_f64).unwrap();
|
||||||
let ci = Big::try_from(0.0_f64).unwrap();
|
let ci = Big::try_from(0.0_f64).unwrap();
|
||||||
let p = (-0.5_f64, 0.0_f64);
|
let p = (-0.5_f64, 0.0_f64);
|
||||||
let points =
|
let points = compute_set_reference(
|
||||||
compute_set_reference(&cr, &ci, 60, 200, FractalKind::Phoenix, 2, p, (0.0, 0.0));
|
&cr,
|
||||||
|
&ci,
|
||||||
|
60,
|
||||||
|
200,
|
||||||
|
FractalKind::Phoenix,
|
||||||
|
2,
|
||||||
|
p,
|
||||||
|
(0.0, 0.0),
|
||||||
|
(0.0, 0.0),
|
||||||
|
);
|
||||||
|
|
||||||
let (c_re, c_im) = (0.5667_f64, 0.0_f64);
|
let (c_re, c_im) = (0.5667_f64, 0.0_f64);
|
||||||
let (mut zr, mut zi) = (0.0_f64, 0.0_f64);
|
let (mut zr, mut zi) = (0.0_f64, 0.0_f64);
|
||||||
@@ -469,4 +507,50 @@ mod tests {
|
|||||||
zi = nzi;
|
zi = nzi;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Complex Multibrot (power 2.5 + 0.3i) reference matches a naive f64
|
||||||
|
/// iteration of `z^p = exp(p·ln z)`.
|
||||||
|
#[test]
|
||||||
|
fn complex_multibrot_reference_matches_naive_f64() {
|
||||||
|
let cr = Big::try_from(0.1_f64).unwrap();
|
||||||
|
let ci = Big::try_from(-0.2_f64).unwrap();
|
||||||
|
let power = (2.5_f64, 0.3_f64);
|
||||||
|
let points = compute_set_reference(
|
||||||
|
&cr,
|
||||||
|
&ci,
|
||||||
|
60,
|
||||||
|
200,
|
||||||
|
FractalKind::ComplexMultibrot,
|
||||||
|
2,
|
||||||
|
(0.0, 0.0),
|
||||||
|
(0.0, 0.0),
|
||||||
|
power,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Naive f64 complex power via z^p = exp(p * ln z), ln z = ln|z| + i*arg(z).
|
||||||
|
fn naive_cpow(zr: f64, zi: f64, pr: f64, pi: f64) -> (f64, f64) {
|
||||||
|
if zr == 0.0 && zi == 0.0 {
|
||||||
|
return (0.0, 0.0);
|
||||||
|
}
|
||||||
|
let ln_r = 0.5 * (zr * zr + zi * zi).ln();
|
||||||
|
let theta = zi.atan2(zr);
|
||||||
|
let exp_re = pr * ln_r - pi * theta;
|
||||||
|
let exp_im = pr * theta + pi * ln_r;
|
||||||
|
let mag = exp_re.exp();
|
||||||
|
(mag * exp_im.cos(), mag * exp_im.sin())
|
||||||
|
}
|
||||||
|
|
||||||
|
let (c_re, c_im) = (0.1_f64, -0.2_f64);
|
||||||
|
let (mut zr, mut zi) = (0.0_f64, 0.0_f64);
|
||||||
|
for point in &points {
|
||||||
|
let tol = 1e-4 * (1.0 + zr.abs().max(zi.abs()));
|
||||||
|
assert!((point[0] as f64 - zr).abs() < tol, "re: {point:?} vs {zr}");
|
||||||
|
assert!((point[1] as f64 - zi).abs() < tol, "im: {point:?} vs {zi}");
|
||||||
|
let (pr, pi) = naive_cpow(zr, zi, power.0, power.1);
|
||||||
|
let nzr = pr + c_re;
|
||||||
|
let nzi = pi + c_im;
|
||||||
|
zr = nzr;
|
||||||
|
zi = nzi;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+115
-3
@@ -37,6 +37,7 @@ fn geom_differs(a: &Uniforms, b: &Uniforms) -> bool {
|
|||||||
|| a.aa_level != b.aa_level
|
|| a.aa_level != b.aa_level
|
||||||
|| a.kind != b.kind
|
|| a.kind != b.kind
|
||||||
|| a.power != b.power
|
|| a.power != b.power
|
||||||
|
|| a.complex_power != b.complex_power
|
||||||
|| a.dc_offset != b.dc_offset
|
|| a.dc_offset != b.dc_offset
|
||||||
|| a.phoenix_p != b.phoenix_p
|
|| a.phoenix_p != b.phoenix_p
|
||||||
|| a.de_coloring != b.de_coloring
|
|| a.de_coloring != b.de_coloring
|
||||||
@@ -87,6 +88,9 @@ pub struct Uniforms {
|
|||||||
/// Distortion constant `l` for the Lambda map (`l·z(1 - z)`);
|
/// Distortion constant `l` for the Lambda map (`l·z(1 - z)`);
|
||||||
/// ignored by other kinds.
|
/// ignored by other kinds.
|
||||||
pub lambda_l: [f32; 2],
|
pub lambda_l: [f32; 2],
|
||||||
|
/// Complex exponent for the Complex Multibrot kind (`z^power + c`);
|
||||||
|
/// ignored by other kinds.
|
||||||
|
pub complex_power: [f32; 2],
|
||||||
/// 0 = escape-time coloring, 1 = distance-estimation shading.
|
/// 0 = escape-time coloring, 1 = distance-estimation shading.
|
||||||
pub de_coloring: u32,
|
pub de_coloring: u32,
|
||||||
// 0 = classic colors, 1 = shadows
|
// 0 = classic colors, 1 = shadows
|
||||||
@@ -160,7 +164,14 @@ impl FractalRenderer {
|
|||||||
pub fn new(device: &wgpu::Device, target_format: wgpu::TextureFormat) -> Self {
|
pub fn new(device: &wgpu::Device, target_format: wgpu::TextureFormat) -> Self {
|
||||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||||
label: Some("mandelbrot"),
|
label: Some("mandelbrot"),
|
||||||
source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/mandelbrot.wgsl").into()),
|
source: wgpu::ShaderSource::Wgsl(
|
||||||
|
concat!(
|
||||||
|
include_str!("../shaders/common.wgsl"),
|
||||||
|
include_str!("../shaders/iterate_uniforms.wgsl"),
|
||||||
|
include_str!("../shaders/mandelbrot.wgsl"),
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
@@ -207,6 +218,19 @@ impl FractalRenderer {
|
|||||||
},
|
},
|
||||||
count: None,
|
count: None,
|
||||||
},
|
},
|
||||||
|
// Only read by the export pipeline's shadow branch (`fs_color`
|
||||||
|
// with the custom-lights palette); the iterate pipeline
|
||||||
|
// (`fs_data`) ignores it, but both pipelines share this layout.
|
||||||
|
wgpu::BindGroupLayoutEntry {
|
||||||
|
binding: 2,
|
||||||
|
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||||
|
ty: wgpu::BindingType::Buffer {
|
||||||
|
ty: wgpu::BufferBindingType::Uniform,
|
||||||
|
has_dynamic_offset: false,
|
||||||
|
min_binding_size: None,
|
||||||
|
},
|
||||||
|
count: None,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -222,6 +246,10 @@ impl FractalRenderer {
|
|||||||
binding: 1,
|
binding: 1,
|
||||||
resource: ref_buffer.as_entire_binding(),
|
resource: ref_buffer.as_entire_binding(),
|
||||||
},
|
},
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 2,
|
||||||
|
resource: lights_buffer.as_entire_binding(),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -288,7 +316,14 @@ impl FractalRenderer {
|
|||||||
// Colourise pass: data texture + colour uniforms → colour texture.
|
// Colourise pass: data texture + colour uniforms → colour texture.
|
||||||
let colorize_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
let colorize_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||||
label: Some("colorize"),
|
label: Some("colorize"),
|
||||||
source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/colorize.wgsl").into()),
|
source: wgpu::ShaderSource::Wgsl(
|
||||||
|
concat!(
|
||||||
|
include_str!("../shaders/common.wgsl"),
|
||||||
|
include_str!("../shaders/iterate_uniforms.wgsl"),
|
||||||
|
include_str!("../shaders/colorize.wgsl"),
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
),
|
||||||
});
|
});
|
||||||
let colorize_bind_group_layout =
|
let colorize_bind_group_layout =
|
||||||
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||||
@@ -362,7 +397,13 @@ impl FractalRenderer {
|
|||||||
// Blit pipeline: samples the cache texture onto egui's surface.
|
// Blit pipeline: samples the cache texture onto egui's surface.
|
||||||
let blit_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
let blit_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||||
label: Some("blit"),
|
label: Some("blit"),
|
||||||
source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/blit.wgsl").into()),
|
source: wgpu::ShaderSource::Wgsl(
|
||||||
|
concat!(
|
||||||
|
include_str!("../shaders/common.wgsl"),
|
||||||
|
include_str!("../shaders/blit.wgsl"),
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
let blit_bind_group_layout =
|
let blit_bind_group_layout =
|
||||||
@@ -588,6 +629,7 @@ impl ExportRender {
|
|||||||
height: u32,
|
height: u32,
|
||||||
uniforms: Uniforms,
|
uniforms: Uniforms,
|
||||||
reference: &[[f32; 2]],
|
reference: &[[f32; 2]],
|
||||||
|
lights: &[Light],
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
label: Some("export uniforms"),
|
label: Some("export uniforms"),
|
||||||
@@ -608,6 +650,19 @@ impl ExportRender {
|
|||||||
queue.write_buffer(&ref_buffer, 0, bytemuck::cast_slice(&reference[..count]));
|
queue.write_buffer(&ref_buffer, 0, bytemuck::cast_slice(&reference[..count]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only read by the shadow branch's custom-lights palette; harmless
|
||||||
|
// (zeroed) for every other coloring mode.
|
||||||
|
let lights_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
|
label: Some("export lights"),
|
||||||
|
size: (MAX_LIGHT_COUNT * std::mem::size_of::<Light>()) as u64,
|
||||||
|
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||||
|
mapped_at_creation: false,
|
||||||
|
});
|
||||||
|
let mut light_bytes = [0u8; size_of::<Light>() * MAX_LIGHT_COUNT];
|
||||||
|
let n = lights.len().min(MAX_LIGHT_COUNT);
|
||||||
|
light_bytes[..n * size_of::<Light>()].copy_from_slice(bytemuck::cast_slice(&lights[..n]));
|
||||||
|
queue.write_buffer(&lights_buffer, 0, &light_bytes);
|
||||||
|
|
||||||
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||||
label: Some("export bind group"),
|
label: Some("export bind group"),
|
||||||
layout: bind_group_layout,
|
layout: bind_group_layout,
|
||||||
@@ -620,6 +675,10 @@ impl ExportRender {
|
|||||||
binding: 1,
|
binding: 1,
|
||||||
resource: ref_buffer.as_entire_binding(),
|
resource: ref_buffer.as_entire_binding(),
|
||||||
},
|
},
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 2,
|
||||||
|
resource: lights_buffer.as_entire_binding(),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -757,6 +816,59 @@ impl ExportRender {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Render `er` tile by tile (blocking on the GPU after each tile so progress
|
||||||
|
/// reflects real work), read it back, and encode the result as PNG bytes.
|
||||||
|
/// Blocks the calling thread throughout, so it's only for native targets:
|
||||||
|
/// the UI export path runs it on a background thread, headless rendering
|
||||||
|
/// runs it directly since it has no frame loop to share a thread with.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub fn export_to_png_blocking(
|
||||||
|
device: &wgpu::Device,
|
||||||
|
queue: &wgpu::Queue,
|
||||||
|
er: &ExportRender,
|
||||||
|
mut on_progress: impl FnMut(&'static str, f32),
|
||||||
|
) -> Vec<u8> {
|
||||||
|
// Progress budget: rendering fills [0, RENDER_END], encoding the rest.
|
||||||
|
const RENDER_END: f32 = 0.6;
|
||||||
|
|
||||||
|
for t in 0..er.tiles {
|
||||||
|
er.render_tile(device, queue, t);
|
||||||
|
let _ = device.poll(wgpu::PollType::Wait {
|
||||||
|
submission_index: None,
|
||||||
|
timeout: None,
|
||||||
|
});
|
||||||
|
let done = (t + 1) as f32 / er.tiles as f32;
|
||||||
|
on_progress("Rendering", RENDER_END * done);
|
||||||
|
}
|
||||||
|
er.copy_to_readback(device, queue);
|
||||||
|
|
||||||
|
let (tx, rx) = std::sync::mpsc::channel();
|
||||||
|
er.readback()
|
||||||
|
.slice(..)
|
||||||
|
.map_async(wgpu::MapMode::Read, move |res| {
|
||||||
|
let _ = tx.send(res);
|
||||||
|
});
|
||||||
|
let _ = device.poll(wgpu::PollType::Wait {
|
||||||
|
submission_index: None,
|
||||||
|
timeout: None,
|
||||||
|
});
|
||||||
|
let _ = rx.recv();
|
||||||
|
|
||||||
|
on_progress("Encoding", RENDER_END);
|
||||||
|
let png = {
|
||||||
|
let data = er
|
||||||
|
.readback()
|
||||||
|
.slice(..)
|
||||||
|
.get_mapped_range()
|
||||||
|
.expect("map readback buffer");
|
||||||
|
encode_png_with_progress(&data, er.width, er.height, er.padded_bpr, er.swap_rb, |f| {
|
||||||
|
on_progress("Encoding", RENDER_END + (0.97 - RENDER_END) * f)
|
||||||
|
})
|
||||||
|
};
|
||||||
|
er.readback().unmap();
|
||||||
|
png
|
||||||
|
}
|
||||||
|
|
||||||
/// Convert a padded BGRA/RGBA readback into tightly-packed RGBA8 and encode it
|
/// Convert a padded BGRA/RGBA readback into tightly-packed RGBA8 and encode it
|
||||||
/// as PNG bytes, reporting progress in `[0, 1]` via `on_progress` as rows are
|
/// as PNG bytes, reporting progress in `[0, 1]` via `on_progress` as rows are
|
||||||
/// streamed to the compressor (encoding is the slow, subdividable phase).
|
/// streamed to the compressor (encoding is the slow, subdividable phase).
|
||||||
|
|||||||
+16
-28
@@ -25,6 +25,8 @@ pub struct ShareState {
|
|||||||
pub phoenix_p: (f64, f64),
|
pub phoenix_p: (f64, f64),
|
||||||
/// Distortion constant for the Lambda kind (ignored by others).
|
/// Distortion constant for the Lambda kind (ignored by others).
|
||||||
pub lambda_l: (f64, f64),
|
pub lambda_l: (f64, f64),
|
||||||
|
/// Complex exponent for the Complex Multibrot kind (ignored by others).
|
||||||
|
pub complex_power: (f64, f64),
|
||||||
pub color_scale: f32,
|
pub color_scale: f32,
|
||||||
pub color_offset: f32,
|
pub color_offset: f32,
|
||||||
/// Palette index (`palette_id` in the shader).
|
/// Palette index (`palette_id` in the shader).
|
||||||
@@ -37,20 +39,7 @@ impl ShareState {
|
|||||||
pub fn encode(&self) -> String {
|
pub fn encode(&self) -> String {
|
||||||
let mut s = String::new();
|
let mut s = String::new();
|
||||||
s.push_str(if self.julia { "m=j" } else { "m=m" });
|
s.push_str(if self.julia { "m=j" } else { "m=m" });
|
||||||
s.push_str(&format!(
|
s.push_str(&format!("&f={}", self.kind.share_tag()));
|
||||||
"&f={}",
|
|
||||||
match self.kind {
|
|
||||||
FractalKind::Mandelbrot => "mandel",
|
|
||||||
FractalKind::BurningShip => "burning",
|
|
||||||
FractalKind::Multibrot => "multi",
|
|
||||||
FractalKind::Tricorn => "tricorn",
|
|
||||||
FractalKind::Celtic => "celtic",
|
|
||||||
FractalKind::Perpendicular => "perp",
|
|
||||||
FractalKind::Buffalo => "buffalo",
|
|
||||||
FractalKind::Phoenix => "phoenix",
|
|
||||||
FractalKind::Lambda => "lambda",
|
|
||||||
}
|
|
||||||
));
|
|
||||||
s.push_str(&format!("&pw={}", self.power));
|
s.push_str(&format!("&pw={}", self.power));
|
||||||
s.push_str(&format!(
|
s.push_str(&format!(
|
||||||
"&re={}&im={}&hh={}&it={}",
|
"&re={}&im={}&hh={}&it={}",
|
||||||
@@ -60,8 +49,12 @@ impl ShareState {
|
|||||||
s.push_str(&format!("&px={}&py={}", self.phoenix_p.0, self.phoenix_p.1));
|
s.push_str(&format!("&px={}&py={}", self.phoenix_p.0, self.phoenix_p.1));
|
||||||
s.push_str(&format!("&lx={}&ly={}", self.lambda_l.0, self.lambda_l.1));
|
s.push_str(&format!("&lx={}&ly={}", self.lambda_l.0, self.lambda_l.1));
|
||||||
s.push_str(&format!(
|
s.push_str(&format!(
|
||||||
"&cs={}&co={}&pal={}",
|
"&cpr={}&cpi={}",
|
||||||
self.color_scale, self.color_offset, self.palette
|
self.complex_power.0, self.complex_power.1
|
||||||
|
));
|
||||||
|
s.push_str(&format!(
|
||||||
|
"&cs={}&co={}&pal={}&spal={}",
|
||||||
|
self.color_scale, self.color_offset, self.palette, self.shadow_palette
|
||||||
));
|
));
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
@@ -79,18 +72,7 @@ impl ShareState {
|
|||||||
julia: map.get("m").map(|m| *m == "j").unwrap_or(false),
|
julia: map.get("m").map(|m| *m == "j").unwrap_or(false),
|
||||||
kind: map
|
kind: map
|
||||||
.get("f")
|
.get("f")
|
||||||
.map(|f| match *f {
|
.and_then(|f| FractalKind::from_share_tag(f))
|
||||||
"mandel" => FractalKind::Mandelbrot,
|
|
||||||
"multi" => FractalKind::Multibrot,
|
|
||||||
"burning" => FractalKind::BurningShip,
|
|
||||||
"tricorn" => FractalKind::Tricorn,
|
|
||||||
"celtic" => FractalKind::Celtic,
|
|
||||||
"perp" => FractalKind::Perpendicular,
|
|
||||||
"buffalo" => FractalKind::Buffalo,
|
|
||||||
"phoenix" => FractalKind::Phoenix,
|
|
||||||
"lambda" => FractalKind::Lambda,
|
|
||||||
_ => FractalKind::Mandelbrot,
|
|
||||||
})
|
|
||||||
.unwrap_or(FractalKind::Mandelbrot),
|
.unwrap_or(FractalKind::Mandelbrot),
|
||||||
power: map.get("pw").and_then(|s| s.parse().ok()).unwrap_or(2),
|
power: map.get("pw").and_then(|s| s.parse().ok()).unwrap_or(2),
|
||||||
center_re: (*map.get("re")?).to_string(),
|
center_re: (*map.get("re")?).to_string(),
|
||||||
@@ -109,6 +91,10 @@ impl ShareState {
|
|||||||
map.get("lx").and_then(|s| s.parse().ok()).unwrap_or(-0.5),
|
map.get("lx").and_then(|s| s.parse().ok()).unwrap_or(-0.5),
|
||||||
map.get("ly").and_then(|s| s.parse().ok()).unwrap_or(0.0),
|
map.get("ly").and_then(|s| s.parse().ok()).unwrap_or(0.0),
|
||||||
),
|
),
|
||||||
|
complex_power: (
|
||||||
|
map.get("cpr").and_then(|s| s.parse().ok()).unwrap_or(2.0),
|
||||||
|
map.get("cpi").and_then(|s| s.parse().ok()).unwrap_or(0.0),
|
||||||
|
),
|
||||||
color_scale: map.get("cs").and_then(|s| s.parse().ok()).unwrap_or(0.02),
|
color_scale: map.get("cs").and_then(|s| s.parse().ok()).unwrap_or(0.02),
|
||||||
color_offset: map.get("co").and_then(|s| s.parse().ok()).unwrap_or(0.0),
|
color_offset: map.get("co").and_then(|s| s.parse().ok()).unwrap_or(0.0),
|
||||||
palette: map.get("pal").and_then(|s| s.parse().ok()).unwrap_or(0),
|
palette: map.get("pal").and_then(|s| s.parse().ok()).unwrap_or(0),
|
||||||
@@ -134,6 +120,7 @@ mod tests {
|
|||||||
julia_c: (-0.123, 0.745),
|
julia_c: (-0.123, 0.745),
|
||||||
phoenix_p: (-0.5, 0.1),
|
phoenix_p: (-0.5, 0.1),
|
||||||
lambda_l: (-0.5, 0.0),
|
lambda_l: (-0.5, 0.0),
|
||||||
|
complex_power: (2.5, 0.3),
|
||||||
color_scale: 0.02,
|
color_scale: 0.02,
|
||||||
color_offset: 0.25,
|
color_offset: 0.25,
|
||||||
palette: 3,
|
palette: 3,
|
||||||
@@ -149,6 +136,7 @@ mod tests {
|
|||||||
assert_eq!(d.iterations, s.iterations);
|
assert_eq!(d.iterations, s.iterations);
|
||||||
assert_eq!(d.julia_c, s.julia_c);
|
assert_eq!(d.julia_c, s.julia_c);
|
||||||
assert_eq!(d.phoenix_p, s.phoenix_p);
|
assert_eq!(d.phoenix_p, s.phoenix_p);
|
||||||
|
assert_eq!(d.complex_power, s.complex_power);
|
||||||
assert_eq!(d.palette, s.palette);
|
assert_eq!(d.palette, s.palette);
|
||||||
assert_eq!(d.shadow_palette, s.shadow_palette);
|
assert_eq!(d.shadow_palette, s.shadow_palette);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
// Headless PNG rendering: parse the CLI, build the exact same view/state the
|
||||||
|
// windowed app would from it, then render straight to a file. No window, no
|
||||||
|
// event loop, no worker-thread debounce (nothing to debounce for a one-shot
|
||||||
|
// render); it just creates its own wgpu device, computes the reference orbit
|
||||||
|
// once, and renders through the same `ExportRender` path the "Export PNG"
|
||||||
|
// button uses.
|
||||||
|
|
||||||
|
use eframe::egui_wgpu::wgpu;
|
||||||
|
|
||||||
|
use crate::app::{FractalApp, unix_timestamp};
|
||||||
|
use crate::cli::Cli;
|
||||||
|
use crate::fractal::{ExportRender, FractalRenderer, export_to_png_blocking};
|
||||||
|
|
||||||
|
/// Cap on the output image dimension (px), to stay within GPU texture limits.
|
||||||
|
const MAX_DIM: u32 = 8192 * 16;
|
||||||
|
|
||||||
|
pub fn run(cli: Cli) -> Result<(), String> {
|
||||||
|
if cli.buddhabrot {
|
||||||
|
return Err("headless mode doesn't support --buddhabrot yet".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let width = cli.width.clamp(16, MAX_DIM);
|
||||||
|
let height = cli.height.clamp(16, MAX_DIM);
|
||||||
|
let export_path = cli
|
||||||
|
.export_path
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| format!("fractal-{}.png", unix_timestamp()));
|
||||||
|
|
||||||
|
let mut app = FractalApp::default_state();
|
||||||
|
app.apply_cli(cli);
|
||||||
|
|
||||||
|
eprintln!("computing reference orbit…");
|
||||||
|
app.compute_reference_blocking();
|
||||||
|
|
||||||
|
let (device, queue) = pollster::block_on(request_device())?;
|
||||||
|
let format = wgpu::TextureFormat::Bgra8Unorm;
|
||||||
|
let renderer = FractalRenderer::new(&device, format);
|
||||||
|
let (pipeline, bind_group_layout, format) = renderer.export_handles();
|
||||||
|
|
||||||
|
let uniforms = app.make_uniforms(width as f64 / height as f64);
|
||||||
|
let er = ExportRender::new(
|
||||||
|
&device,
|
||||||
|
&queue,
|
||||||
|
pipeline,
|
||||||
|
&bind_group_layout,
|
||||||
|
format,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
uniforms,
|
||||||
|
app.reference_points(),
|
||||||
|
app.lights(),
|
||||||
|
);
|
||||||
|
|
||||||
|
eprintln!("rendering {width}×{height}…");
|
||||||
|
let png = export_to_png_blocking(&device, &queue, &er, |phase, fraction| {
|
||||||
|
eprint!("\r{phase} {:>3.0}%", fraction * 100.0);
|
||||||
|
});
|
||||||
|
eprintln!();
|
||||||
|
|
||||||
|
std::fs::write(&export_path, &png).map_err(|e| format!("save failed: {e}"))?;
|
||||||
|
println!("saved {export_path} ({width}×{height})");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set up a wgpu device with no surface/window attached, matching the limits
|
||||||
|
/// `main::wgpu_options` requests for the windowed app (the fractal fragment
|
||||||
|
/// shader needs storage buffers, which downlevel/WebGL-style limits disallow).
|
||||||
|
async fn request_device() -> Result<(wgpu::Device, wgpu::Queue), String> {
|
||||||
|
let instance = wgpu::Instance::default();
|
||||||
|
let adapter = instance
|
||||||
|
.request_adapter(&wgpu::RequestAdapterOptions::default())
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("no compatible GPU adapter: {e}"))?;
|
||||||
|
adapter
|
||||||
|
.request_device(&wgpu::DeviceDescriptor {
|
||||||
|
label: Some("headless fractal device"),
|
||||||
|
required_features: wgpu::Features::empty(),
|
||||||
|
required_limits: adapter.limits(),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("failed to create device: {e}"))
|
||||||
|
}
|
||||||
+15
@@ -16,6 +16,8 @@ mod view;
|
|||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
mod cli;
|
mod cli;
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
mod headless;
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
mod worker;
|
mod worker;
|
||||||
|
|
||||||
use app::FractalApp;
|
use app::FractalApp;
|
||||||
@@ -49,11 +51,24 @@ fn wgpu_options() -> eframe::egui_wgpu::WgpuConfiguration {
|
|||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
fn main() -> eframe::Result {
|
fn main() -> eframe::Result {
|
||||||
|
use clap::Parser as _;
|
||||||
|
|
||||||
env_logger::builder()
|
env_logger::builder()
|
||||||
.filter_level(log::LevelFilter::Info)
|
.filter_level(log::LevelFilter::Info)
|
||||||
.parse_default_env()
|
.parse_default_env()
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
|
let cli = cli::Cli::parse();
|
||||||
|
if cli.headless {
|
||||||
|
return match headless::run(cli) {
|
||||||
|
Ok(()) => Ok(()),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("error: {e}");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
let native_options = eframe::NativeOptions {
|
let native_options = eframe::NativeOptions {
|
||||||
renderer: eframe::Renderer::Wgpu,
|
renderer: eframe::Renderer::Wgpu,
|
||||||
wgpu_options: wgpu_options(),
|
wgpu_options: wgpu_options(),
|
||||||
|
|||||||
@@ -13,12 +13,7 @@ struct VsOut {
|
|||||||
|
|
||||||
@vertex
|
@vertex
|
||||||
fn vs_main(@builtin(vertex_index) idx: u32) -> VsOut {
|
fn vs_main(@builtin(vertex_index) idx: u32) -> VsOut {
|
||||||
var verts = array<vec2<f32>, 3>(
|
let p = fullscreen_triangle_pos(idx);
|
||||||
vec2<f32>(-1.0, -1.0),
|
|
||||||
vec2<f32>(3.0, -1.0),
|
|
||||||
vec2<f32>(-1.0, 3.0),
|
|
||||||
);
|
|
||||||
let p = verts[idx];
|
|
||||||
var out: VsOut;
|
var out: VsOut;
|
||||||
out.pos = vec4<f32>(p, 0.0, 1.0);
|
out.pos = vec4<f32>(p, 0.0, 1.0);
|
||||||
// Map NDC to texture UV. v is flipped so the cache's top row (rendered at
|
// Map NDC to texture UV. v is flipped so the cache's top row (rendered at
|
||||||
|
|||||||
+11
-27
@@ -47,30 +47,21 @@ struct Uniforms {
|
|||||||
// Tonemap colour style: 0 = classic (R/G/B = raw caps), 1 = nebula
|
// Tonemap colour style: 0 = classic (R/G/B = raw caps), 1 = nebula
|
||||||
// (yellow core, blue halo), 2 = grayscale.
|
// (yellow core, blue halo), 2 = grayscale.
|
||||||
palette: u32,
|
palette: u32,
|
||||||
// Padding to a 16-byte multiple. NOT vec3<u32> — that type aligns to 16
|
// Padding so `complex_power` (a vec2, 8-byte aligned) starts on an
|
||||||
// bytes in WGSL (unlike Rust's `[u32; 3]`, which aligns to 4), which
|
// 8-byte boundary. NOT vec3<u32> — that type aligns to 16 bytes in WGSL
|
||||||
// silently added 32 bytes instead of 16 and mismatched the Rust struct's
|
// (unlike Rust's `[u32; 3]`, which aligns to 4), which silently added 32
|
||||||
// size (a wgpu validation error at dispatch time: "size 96 where the
|
// bytes instead of 16 and mismatched the Rust struct's size (a wgpu
|
||||||
// shader expects 112").
|
// validation error at dispatch time: "size 96 where the shader expects
|
||||||
|
// 112").
|
||||||
_pad0: u32,
|
_pad0: u32,
|
||||||
_pad1: u32,
|
// Complex exponent for the Complex Multibrot kind; unused by other kinds.
|
||||||
_pad2: u32,
|
complex_power: vec2<f32>,
|
||||||
};
|
};
|
||||||
|
|
||||||
const PALETTE_NEBULA: u32 = 0u;
|
const PALETTE_NEBULA: u32 = 0u;
|
||||||
const PALETTE_YELLOW: u32 = 1u;
|
const PALETTE_YELLOW: u32 = 1u;
|
||||||
const PALETTE_GRAYSCALE: u32 = 2u;
|
const PALETTE_GRAYSCALE: u32 = 2u;
|
||||||
|
|
||||||
const KIND_MANDELBROT: u32 = 0u;
|
|
||||||
const KIND_BURNING_SHIP: u32 = 1u;
|
|
||||||
const KIND_TRICORN: u32 = 2u;
|
|
||||||
const KIND_MULTIBROT: u32 = 3u;
|
|
||||||
const KIND_CELTIC: u32 = 4u;
|
|
||||||
const KIND_PERPENDICULAR: u32 = 5u;
|
|
||||||
const KIND_BUFFALO: u32 = 6u;
|
|
||||||
const KIND_PHOENIX: u32 = 7u;
|
|
||||||
const KIND_LAMBDA: u32 = 8u;
|
|
||||||
|
|
||||||
@group(0) @binding(0) var<uniform> u: Uniforms;
|
@group(0) @binding(0) var<uniform> u: Uniforms;
|
||||||
// Compute pass: read-write atomic histogram (3 planes of width*height, R/G/B).
|
// Compute pass: read-write atomic histogram (3 planes of width*height, R/G/B).
|
||||||
@group(0) @binding(1) var<storage, read_write> histogram: array<atomic<u32>>;
|
@group(0) @binding(1) var<storage, read_write> histogram: array<atomic<u32>>;
|
||||||
@@ -91,10 +82,6 @@ fn rand01(seed: u32) -> f32 {
|
|||||||
return f32(hash_u32(seed)) * (1.0 / 4294967295.0);
|
return f32(hash_u32(seed)) * (1.0 / 4294967295.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cmul(a: vec2<f32>, b: vec2<f32>) -> vec2<f32> {
|
|
||||||
return vec2<f32>(a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn complex_pow(z: vec2<f32>, p: u32) -> vec2<f32> {
|
fn complex_pow(z: vec2<f32>, p: u32) -> vec2<f32> {
|
||||||
var r = vec2<f32>(1.0, 0.0);
|
var r = vec2<f32>(1.0, 0.0);
|
||||||
for (var i: u32 = 0u; i < p; i = i + 1u) {
|
for (var i: u32 = 0u; i < p; i = i + 1u) {
|
||||||
@@ -126,6 +113,8 @@ fn advance(z: vec2<f32>, zp: vec2<f32>, c: vec2<f32>) -> vec2<f32> {
|
|||||||
} else if u.kind == KIND_LAMBDA {
|
} else if u.kind == KIND_LAMBDA {
|
||||||
// l * z * (1 - z); c is unused (see file doc comment above).
|
// l * z * (1 - z); c is unused (see file doc comment above).
|
||||||
return cmul(u.lambda_l, cmul(z, vec2<f32>(1.0 - z.x, -z.y)));
|
return cmul(u.lambda_l, cmul(z, vec2<f32>(1.0 - z.x, -z.y)));
|
||||||
|
} else if u.kind == KIND_COMPLEX_MULTIBROT {
|
||||||
|
return cpow(z, u.complex_power) + c;
|
||||||
}
|
}
|
||||||
return vec2<f32>(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y) + c; // Mandelbrot
|
return vec2<f32>(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y) + c; // Mandelbrot
|
||||||
}
|
}
|
||||||
@@ -232,12 +221,7 @@ fn cs_main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|||||||
|
|
||||||
@vertex
|
@vertex
|
||||||
fn vs_main(@builtin(vertex_index) idx: u32) -> @builtin(position) vec4<f32> {
|
fn vs_main(@builtin(vertex_index) idx: u32) -> @builtin(position) vec4<f32> {
|
||||||
var verts = array<vec2<f32>, 3>(
|
return vec4<f32>(fullscreen_triangle_pos(idx), 0.0, 1.0);
|
||||||
vec2<f32>(-1.0, -1.0),
|
|
||||||
vec2<f32>(3.0, -1.0),
|
|
||||||
vec2<f32>(-1.0, 3.0),
|
|
||||||
);
|
|
||||||
return vec4<f32>(verts[idx], 0.0, 1.0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@fragment
|
@fragment
|
||||||
|
|||||||
+10
-130
@@ -10,147 +10,28 @@
|
|||||||
// at the fragment's integer pixel coordinate (nearest — iteration data must not
|
// at the fragment's integer pixel coordinate (nearest — iteration data must not
|
||||||
// be linearly filtered across escape boundaries).
|
// be linearly filtered across escape boundaries).
|
||||||
|
|
||||||
// Must match `Uniforms` in mandelbrot.wgsl / the Rust `Uniforms` struct.
|
|
||||||
struct Uniforms {
|
|
||||||
span: vec2<f32>,
|
|
||||||
max_iter: u32,
|
|
||||||
ref_len: u32,
|
|
||||||
color_offset: f32,
|
|
||||||
color_scale: f32,
|
|
||||||
bailout_sq: f32,
|
|
||||||
is_julia: u32,
|
|
||||||
palette_id: u32,
|
|
||||||
shadow_palette_id: u32,
|
|
||||||
aa_level: u32,
|
|
||||||
kind: u32,
|
|
||||||
power: u32,
|
|
||||||
dc_offset: vec2<f32>,
|
|
||||||
phoenix_p: vec2<f32>,
|
|
||||||
lambda_l: vec2<f32>,
|
|
||||||
de_coloring: u32,
|
|
||||||
shadow: u32,
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Light {
|
|
||||||
azimuth: f32,
|
|
||||||
altitude: f32,
|
|
||||||
color: u32,
|
|
||||||
_pad: u32
|
|
||||||
};
|
|
||||||
|
|
||||||
@group(0) @binding(0) var<uniform> u: Uniforms;
|
@group(0) @binding(0) var<uniform> u: Uniforms;
|
||||||
@group(0) @binding(1) var data_tex: texture_2d<f32>;
|
@group(0) @binding(1) var data_tex: texture_2d<f32>;
|
||||||
@group(0) @binding(2) var<uniform> lights: array<Light, 16>;
|
@group(0) @binding(2) var<uniform> lights: array<Light, 16>;
|
||||||
|
|
||||||
// Smooth cyclic palettes (Inigo Quilez cosine palettes). Must match the palette
|
|
||||||
// in mandelbrot.wgsl.
|
|
||||||
fn palette(id: u32, t: f32) -> vec3<f32> {
|
|
||||||
if id == 4u {
|
|
||||||
return vec3<f32>(t, t, t); // grayscale
|
|
||||||
}
|
|
||||||
let a = vec3<f32>(0.5, 0.5, 0.5);
|
|
||||||
let b = vec3<f32>(0.5, 0.5, 0.5);
|
|
||||||
var c = vec3<f32>(1.0, 1.0, 1.0);
|
|
||||||
var d = vec3<f32>(0.00, 0.10, 0.20); // 0: amber / blue
|
|
||||||
if id == 1u {
|
|
||||||
d = vec3<f32>(0.00, 0.33, 0.67); // rainbow
|
|
||||||
} else if id == 2u {
|
|
||||||
d = vec3<f32>(0.30, 0.20, 0.20); // warm ember
|
|
||||||
} else if id == 3u {
|
|
||||||
c = vec3<f32>(1.0, 1.0, 0.5);
|
|
||||||
d = vec3<f32>(0.80, 0.90, 0.30); // lime / magenta
|
|
||||||
}
|
|
||||||
return a + b * cos(6.28318530718 * (c * t + d));
|
|
||||||
}
|
|
||||||
|
|
||||||
@vertex
|
@vertex
|
||||||
fn vs_main(@builtin(vertex_index) idx: u32) -> @builtin(position) vec4<f32> {
|
fn vs_main(@builtin(vertex_index) idx: u32) -> @builtin(position) vec4<f32> {
|
||||||
var verts = array<vec2<f32>, 3>(
|
return vec4<f32>(fullscreen_triangle_pos(idx), 0.0, 1.0);
|
||||||
vec2<f32>(-1.0, -1.0),
|
|
||||||
vec2<f32>(3.0, -1.0),
|
|
||||||
vec2<f32>(-1.0, 3.0),
|
|
||||||
);
|
|
||||||
return vec4<f32>(verts[idx], 0.0, 1.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load(x: i32, y: i32) -> vec3<f32> {
|
|
||||||
let dist = textureLoad(data_tex, vec2<i32>(x, y), 0).g;
|
|
||||||
return vec3<f32>(f32(x), f32(y), dist);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compute_light(normal: vec3<f32>, light: vec3<f32>) -> vec3<f32> {
|
|
||||||
return vec3<f32>(max(0., dot(normal, normalize(light))));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn uncharted2tonemap(x: vec3<f32>) -> vec3<f32> {
|
|
||||||
let A = 0.15; // Shoulder strength
|
|
||||||
let B = 0.50; // Linear strength
|
|
||||||
let C = 0.10; // Linear angle
|
|
||||||
let D = 0.20; // Toe strength
|
|
||||||
let E = 0.02; // Toe numerator / shoarder angle/etc.
|
|
||||||
let F = 0.30; // Toe denominator
|
|
||||||
|
|
||||||
return ((x * (A * x + C * B) + D * E) / (x * (A * x + B) + D * F)) - E / F;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn filmic(color: vec3<f32>, white_point: f32) -> vec3<f32> {
|
|
||||||
let exposure_bias = 2.0;
|
|
||||||
let curr = uncharted2tonemap(color * exposure_bias);
|
|
||||||
|
|
||||||
// Valeur blanche maximale de référence
|
|
||||||
let white_scale = vec3(1.0) / uncharted2tonemap(vec3(white_point));
|
|
||||||
return curr * white_scale;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn s(color: vec3<f32>, k: f32, c: f32) -> vec3<f32> {
|
|
||||||
return 1. / (1. + exp(-k * (color - c)));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn contrast(color: vec3<f32>, k: f32, c: f32) -> vec3<f32> {
|
|
||||||
let color_c = s(color, k, c);
|
|
||||||
|
|
||||||
return (color_c - s(vec3<f32>(0), k, c)) / (s(vec3<f32>(1), k, c) - s(vec3<f32>(0), k, c));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@fragment
|
@fragment
|
||||||
fn fs_main(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
|
fn fs_main(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
|
||||||
if u.shadow != 0u {
|
if u.shadow != 0u {
|
||||||
if textureLoad(data_tex, vec2<i32>(i32(pos.x), i32(pos.y)), 0).b != 0. {
|
let x = i32(pos.x);
|
||||||
|
let y = i32(pos.y);
|
||||||
|
if textureLoad(data_tex, vec2<i32>(x, y), 0).b != 0. {
|
||||||
return vec4<f32>(0.1, 0.1, 0.1, 1.0);
|
return vec4<f32>(0.1, 0.1, 0.1, 1.0);
|
||||||
} else {
|
} else {
|
||||||
let d = array<vec3<f32>, 3>(load(i32(pos.x), i32(pos.y)), load(i32(pos.x + 1), i32(pos.y)), load(i32(pos.x), i32(pos.y + 1)));
|
let h0 = textureLoad(data_tex, vec2<i32>(x, y), 0).g;
|
||||||
|
let h1 = textureLoad(data_tex, vec2<i32>(x + 1, y), 0).g;
|
||||||
let normal = normalize(cross(d[1] - d[0], d[2] - d[0]));
|
let h2 = textureLoad(data_tex, vec2<i32>(x, y + 1), 0).g;
|
||||||
|
let normal = normal_from_heights(h0, h1, h2);
|
||||||
var color: vec3<f32>;
|
return vec4<f32>(shadow_color(normal), 1.0);
|
||||||
if u.shadow_palette_id == 0u {
|
|
||||||
color = compute_light(normal,vec3<f32>(.5, .5, .5)) + vec3<f32>(0.58, 0.85, 1.) * 0.2;
|
|
||||||
|
|
||||||
color = filmic(color, 2.5);
|
|
||||||
color = contrast(color, 4., 0.67);
|
|
||||||
} else if u.shadow_palette_id == 1u {
|
|
||||||
color = compute_light(normal, vec3<f32>(0., .5, .5)) * vec3<f32>(1., 0.5, 0.5) + compute_light(normal, vec3<f32>(0.5, 0., .5)) * vec3<f32>(0.5, 1., 1.);
|
|
||||||
|
|
||||||
color = filmic(color, 4.2);
|
|
||||||
} else {
|
|
||||||
color = vec3<f32>(0);
|
|
||||||
var light_count = 0;
|
|
||||||
for (var i = 0u ; i < 16; i++) {
|
|
||||||
let light_color = unpack4x8unorm(lights[i].color);
|
|
||||||
if any(light_color != vec4<f32>(0)) {
|
|
||||||
light_count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
color += compute_light(normal, vec3<f32>(
|
|
||||||
cos(lights[i].azimuth) * cos(lights[i].altitude),
|
|
||||||
sin(lights[i].azimuth) * cos(lights[i].altitude),
|
|
||||||
sin(lights[i].altitude))) * light_color.xyz * light_color.a;
|
|
||||||
}
|
|
||||||
|
|
||||||
color = filmic(color, 1. + f32(light_count));
|
|
||||||
}
|
|
||||||
|
|
||||||
return vec4<f32>(color, 1.0);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let d = textureLoad(data_tex, vec2<i32>(i32(pos.x), i32(pos.y)), 0);
|
let d = textureLoad(data_tex, vec2<i32>(i32(pos.x), i32(pos.y)), 0);
|
||||||
@@ -158,8 +39,7 @@ fn fs_main(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
|
|||||||
let de = d.g;
|
let de = d.g;
|
||||||
let interior_frac = d.b;
|
let interior_frac = d.b;
|
||||||
|
|
||||||
let t = fract(ci * u.color_scale + u.color_offset);
|
var col = classic_color(ci, de);
|
||||||
var col = palette(u.palette_id, t) * de;
|
|
||||||
// Anti-alias the set boundary: fade toward black by the fraction of the
|
// Anti-alias the set boundary: fade toward black by the fraction of the
|
||||||
// pixel's sub-samples that landed in the interior.
|
// pixel's sub-samples that landed in the interior.
|
||||||
col = col * (1.0 - interior_frac);
|
col = col * (1.0 - interior_frac);
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
// Shared helpers, concatenated into every shader at build time via
|
||||||
|
// `concat!`/`include_str!` (see renderer.rs / buddhabrot.rs). Keep this file
|
||||||
|
// free of anything that differs between pipelines (e.g. a `Uniforms` struct —
|
||||||
|
// mandelbrot/colorize and buddhabrot each have their own shape) since every
|
||||||
|
// shader gets the whole thing spliced in.
|
||||||
|
|
||||||
|
// Fullscreen triangle vertex position: one triangle that covers the whole
|
||||||
|
// viewport (cheaper than a quad's two), shared by every full-screen vertex
|
||||||
|
// shader in this project.
|
||||||
|
fn fullscreen_triangle_pos(idx: u32) -> vec2<f32> {
|
||||||
|
var verts = array<vec2<f32>, 3>(
|
||||||
|
vec2<f32>(-1.0, -1.0),
|
||||||
|
vec2<f32>(3.0, -1.0),
|
||||||
|
vec2<f32>(-1.0, 3.0),
|
||||||
|
);
|
||||||
|
return verts[idx];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complex multiply.
|
||||||
|
fn cmul(a: vec2<f32>, b: vec2<f32>) -> vec2<f32> {
|
||||||
|
return vec2<f32>(a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x);
|
||||||
|
}
|
||||||
|
|
||||||
|
// z^p for a complex exponent p, via the principal branch z^p = exp(p * ln z),
|
||||||
|
// ln z = ln|z| + i*arg(z). z = 0 maps to 0 (the correct limit for the
|
||||||
|
// Re(p) > 0 region the UI exposes; ln(0) would otherwise be -inf).
|
||||||
|
fn cpow(z: vec2<f32>, p: vec2<f32>) -> vec2<f32> {
|
||||||
|
let r2 = dot(z, z);
|
||||||
|
if r2 < 1e-30 {
|
||||||
|
return vec2<f32>(0.0, 0.0);
|
||||||
|
}
|
||||||
|
let ln_r = 0.5 * log(r2);
|
||||||
|
let theta = atan2(z.y, z.x);
|
||||||
|
let mag = exp(p.x * ln_r - p.y * theta);
|
||||||
|
let ang = p.x * theta + p.y * ln_r;
|
||||||
|
return mag * vec2<f32>(cos(ang), sin(ang));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iteration formula selector, shared by the perturbation (mandelbrot.wgsl)
|
||||||
|
// and direct (buddhabrot.wgsl) iteration paths. Must match `FractalKind` in
|
||||||
|
// reference.rs.
|
||||||
|
const KIND_MANDELBROT: u32 = 0u;
|
||||||
|
const KIND_BURNING_SHIP: u32 = 1u;
|
||||||
|
const KIND_TRICORN: u32 = 2u;
|
||||||
|
const KIND_MULTIBROT: u32 = 3u;
|
||||||
|
const KIND_CELTIC: u32 = 4u;
|
||||||
|
const KIND_PERPENDICULAR: u32 = 5u;
|
||||||
|
const KIND_BUFFALO: u32 = 6u;
|
||||||
|
const KIND_PHOENIX: u32 = 7u;
|
||||||
|
const KIND_LAMBDA: u32 = 8u;
|
||||||
|
const KIND_COMPLEX_MULTIBROT: u32 = 9u;
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
// Shared by mandelbrot.wgsl (writes the per-pixel data texture) and
|
||||||
|
// colorize.wgsl (reads it): the iteration pass and the colour remap pass
|
||||||
|
// must agree on both the uniform layout and the palette function.
|
||||||
|
|
||||||
|
// Must match the Rust `Uniforms` struct in renderer.rs field-for-field,
|
||||||
|
// including padding.
|
||||||
|
struct Uniforms {
|
||||||
|
span: vec2<f32>,
|
||||||
|
max_iter: u32,
|
||||||
|
ref_len: u32,
|
||||||
|
color_offset: f32,
|
||||||
|
color_scale: f32,
|
||||||
|
bailout_sq: f32,
|
||||||
|
is_julia: u32,
|
||||||
|
palette_id: u32,
|
||||||
|
shadow_palette_id: u32,
|
||||||
|
aa_level: u32,
|
||||||
|
// Iteration formula (see the KIND_* constants in common.wgsl).
|
||||||
|
kind: u32,
|
||||||
|
// Exponent for the Multibrot kind.
|
||||||
|
power: u32,
|
||||||
|
dc_offset: vec2<f32>,
|
||||||
|
// Distortion constant p for the Phoenix map (z^2 + c + p*z_{n-1}); unused
|
||||||
|
// by other kinds. Placed by dc_offset so both vec2s stay 8-byte aligned.
|
||||||
|
phoenix_p: vec2<f32>,
|
||||||
|
// Distortion constant l for the Lambda map (l*z(1 - z_{n-1})); unused
|
||||||
|
// by other kinds.
|
||||||
|
lambda_l: vec2<f32>,
|
||||||
|
// Complex exponent for the Complex Multibrot kind (z^power + c); unused
|
||||||
|
// by other kinds.
|
||||||
|
complex_power: vec2<f32>,
|
||||||
|
// 0 = escape-time coloring, 1 = distance-estimation shading.
|
||||||
|
de_coloring: u32,
|
||||||
|
// 0 = classic colors, 1 = shadows
|
||||||
|
shadow: u32,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Smooth cyclic palettes (Inigo Quilez cosine palettes), selected by id.
|
||||||
|
fn palette(id: u32, t: f32) -> vec3<f32> {
|
||||||
|
if id == 4u {
|
||||||
|
return vec3<f32>(t, t, t); // grayscale
|
||||||
|
}
|
||||||
|
let a = vec3<f32>(0.5, 0.5, 0.5);
|
||||||
|
let b = vec3<f32>(0.5, 0.5, 0.5);
|
||||||
|
var c = vec3<f32>(1.0, 1.0, 1.0);
|
||||||
|
var d = vec3<f32>(0.00, 0.10, 0.20); // 0: amber / blue
|
||||||
|
if id == 1u {
|
||||||
|
d = vec3<f32>(0.00, 0.33, 0.67); // rainbow
|
||||||
|
} else if id == 2u {
|
||||||
|
d = vec3<f32>(0.30, 0.20, 0.20); // warm ember
|
||||||
|
} else if id == 3u {
|
||||||
|
c = vec3<f32>(1.0, 1.0, 0.5);
|
||||||
|
d = vec3<f32>(0.80, 0.90, 0.30); // lime / magenta
|
||||||
|
}
|
||||||
|
return a + b * cos(6.28318530718 * (c * t + d));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Classic (non-shadow) escape colouring: palette lookup at the smoothed
|
||||||
|
// iteration count `ci`, darkened by the distance-estimate factor `de`
|
||||||
|
// (sqrt-compressed so the darkening falls off more gently near the
|
||||||
|
// boundary). Shared by the colourise pass's classic branch (colorize.wgsl,
|
||||||
|
// applied to an already-averaged data texel) and the PNG-export pass
|
||||||
|
// (mandelbrot.wgsl's `fs_color`, applied per sub-sample pre-AA) — the two
|
||||||
|
// places a fully escaped point is turned into a final pixel colour.
|
||||||
|
fn classic_color(ci: f32, de: f32) -> vec3<f32> {
|
||||||
|
let t = fract(ci * u.color_scale + u.color_offset);
|
||||||
|
return palette(u.palette_id, t) * sqrt(de);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A single directional/point light, set by the UI's light list. `color`'s
|
||||||
|
// alpha channel doubles as intensity (see `shadow_color`'s use of
|
||||||
|
// `light_color.a`). Each shader that binds a `lights: array<Light, 16>`
|
||||||
|
// uniform (colorize.wgsl, mandelbrot.wgsl's export shadow path) uses this
|
||||||
|
// same layout.
|
||||||
|
struct Light {
|
||||||
|
azimuth: f32,
|
||||||
|
altitude: f32,
|
||||||
|
color: u32,
|
||||||
|
_pad: u32,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn compute_light(normal: vec3<f32>, light: vec3<f32>) -> vec3<f32> {
|
||||||
|
return vec3<f32>(max(0., dot(normal, normalize(light))));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn uncharted2tonemap(x: vec3<f32>) -> vec3<f32> {
|
||||||
|
let A = 0.15; // Shoulder strength
|
||||||
|
let B = 0.50; // Linear strength
|
||||||
|
let C = 0.10; // Linear angle
|
||||||
|
let D = 0.20; // Toe strength
|
||||||
|
let E = 0.02; // Toe numerator / shoarder angle/etc.
|
||||||
|
let F = 0.30; // Toe denominator
|
||||||
|
|
||||||
|
return ((x * (A * x + C * B) + D * E) / (x * (A * x + B) + D * F)) - E / F;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn filmic(color: vec3<f32>, white_point: f32) -> vec3<f32> {
|
||||||
|
let exposure_bias = 2.0;
|
||||||
|
let curr = uncharted2tonemap(color * exposure_bias);
|
||||||
|
|
||||||
|
// Valeur blanche maximale de référence
|
||||||
|
let white_scale = vec3(1.0) / uncharted2tonemap(vec3(white_point));
|
||||||
|
return curr * white_scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn s(color: vec3<f32>, k: f32, c: f32) -> vec3<f32> {
|
||||||
|
return 1. / (1. + exp(-k * (color - c)));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn contrast(color: vec3<f32>, k: f32, c: f32) -> vec3<f32> {
|
||||||
|
let color_c = s(color, k, c);
|
||||||
|
|
||||||
|
return (color_c - s(vec3<f32>(0), k, c)) / (s(vec3<f32>(1), k, c) - s(vec3<f32>(0), k, c));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Surface normal from three height samples (`h0` at the pixel, `h1` one pixel
|
||||||
|
// to the right, `h2` one pixel down), treating DE as a height field. Only the
|
||||||
|
// differences matter, so callers don't need to pass pixel coordinates — a
|
||||||
|
// texture-backed caller (colorize.wgsl) and a live-sampled caller
|
||||||
|
// (mandelbrot.wgsl's export shadow path) can share this.
|
||||||
|
fn normal_from_heights(h0: f32, h1: f32, h2: f32) -> vec3<f32> {
|
||||||
|
let d0 = vec3<f32>(0.0, 0.0, h0);
|
||||||
|
let d1 = vec3<f32>(1.0, 0.0, h1);
|
||||||
|
let d2 = vec3<f32>(0.0, 1.0, h2);
|
||||||
|
return normalize(cross(d1 - d0, d2 - d0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shade a DE-derived surface normal per `u.shadow_palette_id`: 0 = grayscale
|
||||||
|
// key light, 1 = red/blue two-tone, 2 = the user's custom `lights` list.
|
||||||
|
// Shared by the interactive shadow pass (colorize.wgsl) and the PNG-export
|
||||||
|
// shadow path (mandelbrot.wgsl's `fs_color`), which must render identically.
|
||||||
|
fn shadow_color(normal: vec3<f32>) -> vec3<f32> {
|
||||||
|
var color: vec3<f32>;
|
||||||
|
if u.shadow_palette_id == 0u {
|
||||||
|
color = compute_light(normal, vec3<f32>(.5, .5, .5)) + vec3<f32>(0.58, 0.85, 1.) * 0.2;
|
||||||
|
|
||||||
|
color = filmic(color, 2.5);
|
||||||
|
color = contrast(color, 4., 0.67);
|
||||||
|
} else if u.shadow_palette_id == 1u {
|
||||||
|
color = compute_light(normal, vec3<f32>(0., .5, .5)) * vec3<f32>(1., 0.5, 0.5) + compute_light(normal, vec3<f32>(0.5, 0., .5)) * vec3<f32>(0.5, 1., 1.);
|
||||||
|
|
||||||
|
color = filmic(color, 4.2);
|
||||||
|
} else {
|
||||||
|
color = vec3<f32>(0);
|
||||||
|
var light_count = 0;
|
||||||
|
for (var i = 0u; i < 16; i++) {
|
||||||
|
let light_color = unpack4x8unorm(lights[i].color);
|
||||||
|
if any(light_color != vec4<f32>(0)) {
|
||||||
|
light_count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
color += compute_light(normal, vec3<f32>(
|
||||||
|
cos(lights[i].azimuth) * cos(lights[i].altitude),
|
||||||
|
sin(lights[i].azimuth) * cos(lights[i].altitude),
|
||||||
|
sin(lights[i].altitude))) * light_color.xyz * light_color.a;
|
||||||
|
}
|
||||||
|
|
||||||
|
color = filmic(color, 1. + f32(light_count));
|
||||||
|
}
|
||||||
|
return color;
|
||||||
|
}
|
||||||
+94
-84
@@ -12,46 +12,11 @@
|
|||||||
// the reference index to 0 and carry the full value as the new delta (valid
|
// the reference index to 0 and carry the full value as the new delta (valid
|
||||||
// because X_0 = 0).
|
// because X_0 = 0).
|
||||||
|
|
||||||
struct Uniforms {
|
|
||||||
span: vec2<f32>,
|
|
||||||
max_iter: u32,
|
|
||||||
ref_len: u32,
|
|
||||||
color_offset: f32,
|
|
||||||
color_scale: f32,
|
|
||||||
bailout_sq: f32,
|
|
||||||
is_julia: u32,
|
|
||||||
palette_id: u32,
|
|
||||||
shadow_palette_id: u32,
|
|
||||||
aa_level: u32,
|
|
||||||
// Iteration formula (see the KIND_* constants below).
|
|
||||||
kind: u32,
|
|
||||||
// Exponent for the Multibrot kind.
|
|
||||||
power: u32,
|
|
||||||
dc_offset: vec2<f32>,
|
|
||||||
// Distortion constant p for the Phoenix map (z^2 + c + p*z_{n-1}); unused
|
|
||||||
// by other kinds. Placed by dc_offset so both vec2s stay 8-byte aligned.
|
|
||||||
phoenix_p: vec2<f32>,
|
|
||||||
// Distortion constant l for the Lambda map (l*z(1 - z_{n-1})); unused
|
|
||||||
// by other kinds.
|
|
||||||
lambda_l: vec2<f32>,
|
|
||||||
// 0 = escape-time coloring, 1 = distance-estimation shading.
|
|
||||||
de_coloring: u32,
|
|
||||||
// 0 = classic colors, 1 = shadows
|
|
||||||
shadow: u32,
|
|
||||||
};
|
|
||||||
|
|
||||||
const KIND_MANDELBROT: u32 = 0u;
|
|
||||||
const KIND_BURNING_SHIP: u32 = 1u;
|
|
||||||
const KIND_TRICORN: u32 = 2u;
|
|
||||||
const KIND_MULTIBROT: u32 = 3u;
|
|
||||||
const KIND_CELTIC: u32 = 4u;
|
|
||||||
const KIND_PERPENDICULAR: u32 = 5u;
|
|
||||||
const KIND_BUFFALO: u32 = 6u;
|
|
||||||
const KIND_PHOENIX: u32 = 7u;
|
|
||||||
const KIND_LAMBDA: u32 = 8u;
|
|
||||||
|
|
||||||
@group(0) @binding(0) var<uniform> u: Uniforms;
|
@group(0) @binding(0) var<uniform> u: Uniforms;
|
||||||
@group(0) @binding(1) var<storage, read> ref_orbit: array<vec2<f32>>;
|
@group(0) @binding(1) var<storage, read> ref_orbit: array<vec2<f32>>;
|
||||||
|
// Only read by `fs_color`'s shadow branch (custom-lights palette); the
|
||||||
|
// iteration pass (`fs_data`) never touches it.
|
||||||
|
@group(0) @binding(2) var<uniform> lights: array<Light, 16>;
|
||||||
|
|
||||||
struct VsOut {
|
struct VsOut {
|
||||||
@builtin(position) pos: vec4<f32>,
|
@builtin(position) pos: vec4<f32>,
|
||||||
@@ -61,12 +26,7 @@ struct VsOut {
|
|||||||
|
|
||||||
@vertex
|
@vertex
|
||||||
fn vs_main(@builtin(vertex_index) idx: u32) -> VsOut {
|
fn vs_main(@builtin(vertex_index) idx: u32) -> VsOut {
|
||||||
var verts = array<vec2<f32>, 3>(
|
let ndc = fullscreen_triangle_pos(idx);
|
||||||
vec2<f32>(-1.0, -1.0),
|
|
||||||
vec2<f32>(3.0, -1.0),
|
|
||||||
vec2<f32>(-1.0, 3.0),
|
|
||||||
);
|
|
||||||
let ndc = verts[idx];
|
|
||||||
var out: VsOut;
|
var out: VsOut;
|
||||||
out.pos = vec4<f32>(ndc, 0.0, 1.0);
|
out.pos = vec4<f32>(ndc, 0.0, 1.0);
|
||||||
// Flip y so +imaginary points up the screen.
|
// Flip y so +imaginary points up the screen.
|
||||||
@@ -74,16 +34,17 @@ fn vs_main(@builtin(vertex_index) idx: u32) -> VsOut {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Complex multiply.
|
|
||||||
fn cmul(a: vec2<f32>, b: vec2<f32>) -> vec2<f32> {
|
|
||||||
return vec2<f32>(a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Complex conjugate.
|
// Complex conjugate.
|
||||||
fn conj(a: vec2<f32>) -> vec2<f32> {
|
fn conj(a: vec2<f32>) -> vec2<f32> {
|
||||||
return vec2<f32>(a.x, -a.y);
|
return vec2<f32>(a.x, -a.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Complex division a / b.
|
||||||
|
fn cdiv(a: vec2<f32>, b: vec2<f32>) -> vec2<f32> {
|
||||||
|
let d = dot(b, b);
|
||||||
|
return vec2<f32>(a.x * b.x + a.y * b.y, a.y * b.x - a.x * b.y) / d;
|
||||||
|
}
|
||||||
|
|
||||||
// |c + d| - |c|, evaluated exactly (no catastrophic cancellation even when the
|
// |c + d| - |c|, evaluated exactly (no catastrophic cancellation even when the
|
||||||
// sum crosses zero). This is what makes the Burning Ship delta correct through
|
// sum crosses zero). This is what makes the Burning Ship delta correct through
|
||||||
// the sign flips that happen all along the axes, where the ship's detail lives.
|
// the sign flips that happen all along the axes, where the ship's detail lives.
|
||||||
@@ -123,6 +84,47 @@ fn multibrot_delta(z: vec2<f32>, e: vec2<f32>, p: u32) -> vec2<f32> {
|
|||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Number of terms kept in `complex_multibrot_delta`'s series. Truncation, not
|
||||||
|
// exactness: unlike `multibrot_delta` (a finite binomial sum for an integer
|
||||||
|
// power), a complex power has no finite expansion, so this converges rather
|
||||||
|
// than terminates. Fine as long as perturbation's usual invariant (|e| << |z|,
|
||||||
|
// kept true by rebasing) holds, since each extra term is O(w^k) smaller.
|
||||||
|
const COMPLEX_MULTIBROT_TERMS: u32 = 16u;
|
||||||
|
|
||||||
|
// Perturbation delta for z -> z^p with a complex p: (Z+e)^p - Z^p.
|
||||||
|
//
|
||||||
|
// When |e| << |Z| (the common case: it's the whole reason perturbation
|
||||||
|
// works), forming Z+e directly would round e away in f32, so instead expand
|
||||||
|
// = Z^p * ((1+w)^p - 1), w = e/Z, as a Taylor series in w: (1+w)^p - 1 =
|
||||||
|
// sum_{k=1}^N C(p,k) w^k, with the complex binomial coefficient built up
|
||||||
|
// incrementally: C(p,k) = C(p,k-1) * (p-(k-1)) / k. Unlike `multibrot_delta`
|
||||||
|
// (a finite binomial sum for an integer power), this only *converges* — and
|
||||||
|
// only for |w| < 1 — rather than terminating exactly.
|
||||||
|
//
|
||||||
|
// Right after a rebase (or near a reference point close to zero, where w is
|
||||||
|
// singular), e is *not* small relative to Z — that's normal perturbation
|
||||||
|
// dynamics, not a deep-zoom edge case — and the series above would diverge.
|
||||||
|
// But forming Z+e directly is numerically safe exactly there (e isn't many
|
||||||
|
// orders of magnitude smaller than Z), so fall back to a plain subtraction.
|
||||||
|
fn complex_multibrot_delta(z: vec2<f32>, e: vec2<f32>, p: vec2<f32>) -> vec2<f32> {
|
||||||
|
// |w|^2 = |e|^2 / |Z|^2; inf or nan (Z ~ 0, or both ~ 0) correctly fails
|
||||||
|
// the `< 0.25` test below and falls through to the direct branch.
|
||||||
|
let w2 = dot(e, e) / dot(z, z);
|
||||||
|
if w2 < 0.25 {
|
||||||
|
let w = cdiv(e, z);
|
||||||
|
var wk = vec2<f32>(1.0, 0.0); // w^0
|
||||||
|
var coef = vec2<f32>(1.0, 0.0); // C(p,0)
|
||||||
|
var acc = vec2<f32>(0.0, 0.0);
|
||||||
|
for (var k: u32 = 1u; k <= COMPLEX_MULTIBROT_TERMS; k = k + 1u) {
|
||||||
|
coef = cdiv(cmul(coef, p - vec2<f32>(f32(k - 1u), 0.0)), vec2<f32>(f32(k), 0.0));
|
||||||
|
wk = cmul(wk, w);
|
||||||
|
acc = acc + cmul(coef, wk);
|
||||||
|
}
|
||||||
|
return cmul(cpow(z, p), acc);
|
||||||
|
}
|
||||||
|
return cpow(z + e, p) - cpow(z, p);
|
||||||
|
}
|
||||||
|
|
||||||
// One perturbation step of the current fractal's delta: e -> f(Z+e) - f(Z),
|
// One perturbation step of the current fractal's delta: e -> f(Z+e) - f(Z),
|
||||||
// where `z` is the reference orbit value X_m. `step_add` (dc) is added by the
|
// where `z` is the reference orbit value X_m. `step_add` (dc) is added by the
|
||||||
// caller. Must match `FractalKind` on the CPU side.
|
// caller. Must match `FractalKind` on the CPU side.
|
||||||
@@ -163,6 +165,8 @@ fn advance_delta(z: vec2<f32>, e: vec2<f32>) -> vec2<f32> {
|
|||||||
// Lambda map: z^{n+1} = λ·z·(1-z). Delta: e = λ·e·(1-2z-e).
|
// Lambda map: z^{n+1} = λ·z·(1-z). Delta: e = λ·e·(1-2z-e).
|
||||||
let one_minus_2z_minus_e = vec2<f32>(1.0 - 2.0 * z.x - e.x, -2.0 * z.y - e.y);
|
let one_minus_2z_minus_e = vec2<f32>(1.0 - 2.0 * z.x - e.x, -2.0 * z.y - e.y);
|
||||||
return cmul(u.lambda_l, cmul(e, one_minus_2z_minus_e));
|
return cmul(u.lambda_l, cmul(e, one_minus_2z_minus_e));
|
||||||
|
} else if u.kind == KIND_COMPLEX_MULTIBROT {
|
||||||
|
return complex_multibrot_delta(z, e, u.complex_power);
|
||||||
}
|
}
|
||||||
return 2.0 * cmul(z, e) + cmul(e, e); // Mandelbrot (and Phoenix square part)
|
return 2.0 * cmul(z, e) + cmul(e, e); // Mandelbrot (and Phoenix square part)
|
||||||
}
|
}
|
||||||
@@ -183,30 +187,13 @@ fn fprime(z: vec2<f32>) -> vec2<f32> {
|
|||||||
} else if u.kind == KIND_LAMBDA {
|
} else if u.kind == KIND_LAMBDA {
|
||||||
// Lambda: f'(z) = λ·(1-2z).
|
// Lambda: f'(z) = λ·(1-2z).
|
||||||
return cmul(u.lambda_l, vec2<f32>(1.0 - 2.0 * z.x, -2.0 * z.y));
|
return cmul(u.lambda_l, vec2<f32>(1.0 - 2.0 * z.x, -2.0 * z.y));
|
||||||
|
} else if u.kind == KIND_COMPLEX_MULTIBROT {
|
||||||
|
// f'(z) = p * z^(p-1).
|
||||||
|
return cmul(u.complex_power, cpow(z, u.complex_power - vec2<f32>(1.0, 0.0)));
|
||||||
}
|
}
|
||||||
return 2.0 * z;
|
return 2.0 * z;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Smooth cyclic palettes (Inigo Quilez cosine palettes), selected by id.
|
|
||||||
fn palette(id: u32, t: f32) -> vec3<f32> {
|
|
||||||
if id == 4u {
|
|
||||||
return vec3<f32>(t, t, t); // grayscale
|
|
||||||
}
|
|
||||||
let a = vec3<f32>(0.5, 0.5, 0.5);
|
|
||||||
let b = vec3<f32>(0.5, 0.5, 0.5);
|
|
||||||
var c = vec3<f32>(1.0, 1.0, 1.0);
|
|
||||||
var d = vec3<f32>(0.00, 0.10, 0.20); // 0: amber / blue
|
|
||||||
if id == 1u {
|
|
||||||
d = vec3<f32>(0.00, 0.33, 0.67); // rainbow
|
|
||||||
} else if id == 2u {
|
|
||||||
d = vec3<f32>(0.30, 0.20, 0.20); // warm ember
|
|
||||||
} else if id == 3u {
|
|
||||||
c = vec3<f32>(1.0, 1.0, 0.5);
|
|
||||||
d = vec3<f32>(0.80, 0.90, 0.30); // lime / magenta
|
|
||||||
}
|
|
||||||
return a + b * cos(6.28318530718 * (c * t + d));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Escape data for one sample: `ci` is the (color-independent) palette parameter,
|
// Escape data for one sample: `ci` is the (color-independent) palette parameter,
|
||||||
// `de` the distance-estimate darkening factor in [0,1], `escaped` false for the
|
// `de` the distance-estimate darkening factor in [0,1], `escaped` false for the
|
||||||
// interior of the set. Splitting iteration from coloring lets a colour change be
|
// interior of the set. Splitting iteration from coloring lets a colour change be
|
||||||
@@ -345,22 +332,15 @@ fn color_sample(s: Sample) -> vec3<f32> {
|
|||||||
if !s.escaped {
|
if !s.escaped {
|
||||||
return vec3<f32>(0.0, 0.0, 0.0);
|
return vec3<f32>(0.0, 0.0, 0.0);
|
||||||
}
|
}
|
||||||
let t = fract(s.ci * u.color_scale + u.color_offset);
|
return classic_color(s.ci, s.de);
|
||||||
return palette(u.palette_id, t) * s.de;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Iteration pass: write per-pixel escape data (color-independent) so a colour
|
// Supersampled escape data at one point: average (ci, DE factor) over the
|
||||||
// change is remapped by the cheap colourise pass without re-iterating.
|
// AA grid's escaped sub-samples, plus the fraction that landed in the
|
||||||
// R = ci (palette parameter), G = DE factor, B = interior fraction (for AA).
|
// interior. Shared by `fs_data` (writes it straight to the data texture) and
|
||||||
// AA is grid-supersampled here; the interior fraction lets the colourise pass
|
// `fs_color`'s shadow branch (used both at the pixel and at its two
|
||||||
// anti-alias the set boundary (blend toward black) after the fact.
|
// neighbours, to build a DE height field without a texture round-trip).
|
||||||
@fragment
|
fn aggregate_sample(base: vec2<f32>, dx: vec2<f32>, dy: vec2<f32>, px: f32) -> vec3<f32> {
|
||||||
fn fs_data(in: VsOut) -> @location(0) vec4<f32> {
|
|
||||||
let base = in.centered * u.span + u.dc_offset;
|
|
||||||
let dx = dpdx(base);
|
|
||||||
let dy = dpdy(base);
|
|
||||||
let px = length(abs(dx) + abs(dy));
|
|
||||||
|
|
||||||
let aa = max(u.aa_level, 1u);
|
let aa = max(u.aa_level, 1u);
|
||||||
let inv = 1.0 / f32(aa);
|
let inv = 1.0 / f32(aa);
|
||||||
var ci_sum = 0.0;
|
var ci_sum = 0.0;
|
||||||
@@ -382,7 +362,22 @@ fn fs_data(in: VsOut) -> @location(0) vec4<f32> {
|
|||||||
let ci_avg = select(0.0, ci_sum / f32(escaped_n), escaped_n > 0u);
|
let ci_avg = select(0.0, ci_sum / f32(escaped_n), escaped_n > 0u);
|
||||||
let de_avg = select(1.0, de_sum / f32(escaped_n), escaped_n > 0u);
|
let de_avg = select(1.0, de_sum / f32(escaped_n), escaped_n > 0u);
|
||||||
let interior_frac = 1.0 - f32(escaped_n) / total;
|
let interior_frac = 1.0 - f32(escaped_n) / total;
|
||||||
return vec4<f32>(ci_avg, de_avg, interior_frac, 1.0);
|
return vec3<f32>(ci_avg, de_avg, interior_frac);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iteration pass: write per-pixel escape data (color-independent) so a colour
|
||||||
|
// change is remapped by the cheap colourise pass without re-iterating.
|
||||||
|
// R = ci (palette parameter), G = DE factor, B = interior fraction (for AA).
|
||||||
|
// AA is grid-supersampled here; the interior fraction lets the colourise pass
|
||||||
|
// anti-alias the set boundary (blend toward black) after the fact.
|
||||||
|
@fragment
|
||||||
|
fn fs_data(in: VsOut) -> @location(0) vec4<f32> {
|
||||||
|
let base = in.centered * u.span + u.dc_offset;
|
||||||
|
let dx = dpdx(base);
|
||||||
|
let dy = dpdy(base);
|
||||||
|
let px = length(abs(dx) + abs(dy));
|
||||||
|
|
||||||
|
return vec4<f32>(aggregate_sample(base, dx, dy, px), 1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Combined iterate + colour in a single pass, for PNG export (which never needs
|
// Combined iterate + colour in a single pass, for PNG export (which never needs
|
||||||
@@ -395,6 +390,21 @@ fn fs_color(in: VsOut) -> @location(0) vec4<f32> {
|
|||||||
let dy = dpdy(base);
|
let dy = dpdy(base);
|
||||||
let px = length(abs(dx) + abs(dy));
|
let px = length(abs(dx) + abs(dy));
|
||||||
|
|
||||||
|
if u.shadow != 0u {
|
||||||
|
// No data texture to sample neighbours from (this pass never runs
|
||||||
|
// one), so build the same DE height field colorize.wgsl reads from
|
||||||
|
// the texture by aggregating live, at the pixel and its two
|
||||||
|
// neighbours a `dx`/`dy` step away.
|
||||||
|
let here = aggregate_sample(base, dx, dy, px);
|
||||||
|
if here.z != 0.0 {
|
||||||
|
return vec4<f32>(0.1, 0.1, 0.1, 1.0);
|
||||||
|
}
|
||||||
|
let right = aggregate_sample(base + dx, dx, dy, px);
|
||||||
|
let down = aggregate_sample(base + dy, dx, dy, px);
|
||||||
|
let normal = normal_from_heights(here.y, right.y, down.y);
|
||||||
|
return vec4<f32>(shadow_color(normal), 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
let aa = max(u.aa_level, 1u);
|
let aa = max(u.aa_level, 1u);
|
||||||
let inv = 1.0 / f32(aa);
|
let inv = 1.0 / f32(aa);
|
||||||
var acc = vec3<f32>(0.0, 0.0, 0.0);
|
var acc = vec3<f32>(0.0, 0.0, 0.0);
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ pub struct RefRequest {
|
|||||||
pub phoenix_p: (f64, f64),
|
pub phoenix_p: (f64, f64),
|
||||||
/// Distortion constant for the Lambda map (ignored by other kinds).
|
/// Distortion constant for the Lambda map (ignored by other kinds).
|
||||||
pub lambda_l: (f64, f64),
|
pub lambda_l: (f64, f64),
|
||||||
|
/// Complex exponent for the Complex Multibrot kind (ignored by other kinds).
|
||||||
|
pub complex_power: (f64, f64),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct RefResult {
|
pub struct RefResult {
|
||||||
@@ -107,6 +109,7 @@ fn compute(req: &RefRequest) -> Vec<[f32; 2]> {
|
|||||||
req.power,
|
req.power,
|
||||||
req.phoenix_p,
|
req.phoenix_p,
|
||||||
req.lambda_l,
|
req.lambda_l,
|
||||||
|
req.complex_power,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
compute_set_reference(
|
compute_set_reference(
|
||||||
@@ -118,6 +121,7 @@ fn compute(req: &RefRequest) -> Vec<[f32; 2]> {
|
|||||||
req.power,
|
req.power,
|
||||||
req.phoenix_p,
|
req.phoenix_p,
|
||||||
req.lambda_l,
|
req.lambda_l,
|
||||||
|
req.complex_power,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-4
@@ -21,24 +21,44 @@ fn validate(name: &str, src: &str) {
|
|||||||
fn mandelbrot_shader_is_valid() {
|
fn mandelbrot_shader_is_valid() {
|
||||||
validate(
|
validate(
|
||||||
"mandelbrot.wgsl",
|
"mandelbrot.wgsl",
|
||||||
include_str!("../src/shaders/mandelbrot.wgsl"),
|
concat!(
|
||||||
|
include_str!("../src/shaders/common.wgsl"),
|
||||||
|
include_str!("../src/shaders/iterate_uniforms.wgsl"),
|
||||||
|
include_str!("../src/shaders/mandelbrot.wgsl"),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn colorize_shader_is_valid() {
|
fn colorize_shader_is_valid() {
|
||||||
validate("colorize.wgsl", include_str!("../src/shaders/colorize.wgsl"));
|
validate(
|
||||||
|
"colorize.wgsl",
|
||||||
|
concat!(
|
||||||
|
include_str!("../src/shaders/common.wgsl"),
|
||||||
|
include_str!("../src/shaders/iterate_uniforms.wgsl"),
|
||||||
|
include_str!("../src/shaders/colorize.wgsl"),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn blit_shader_is_valid() {
|
fn blit_shader_is_valid() {
|
||||||
validate("blit.wgsl", include_str!("../src/shaders/blit.wgsl"));
|
validate(
|
||||||
|
"blit.wgsl",
|
||||||
|
concat!(
|
||||||
|
include_str!("../src/shaders/common.wgsl"),
|
||||||
|
include_str!("../src/shaders/blit.wgsl"),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn buddhabrot_shader_is_valid() {
|
fn buddhabrot_shader_is_valid() {
|
||||||
validate(
|
validate(
|
||||||
"buddhabrot.wgsl",
|
"buddhabrot.wgsl",
|
||||||
include_str!("../src/shaders/buddhabrot.wgsl"),
|
concat!(
|
||||||
|
include_str!("../src/shaders/common.wgsl"),
|
||||||
|
include_str!("../src/shaders/buddhabrot.wgsl"),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user