Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96e373b6e0 | ||
|
|
dc7beedcb1 | ||
|
|
cac558a1fb | ||
|
|
4e61121c75 | ||
|
|
a9a9a247ab |
@@ -66,18 +66,6 @@ pixel is a handful of `f32` complex multiplies.
|
|||||||
`compute_reference`/`compute_set_reference`: iterate the chosen formula at
|
`compute_reference`/`compute_set_reference`: iterate the chosen formula at
|
||||||
high precision on the CPU, emitting `Z_n` as `f32` pairs — that's the
|
high precision on the CPU, emitting `Z_n` as `f32` pairs — that's the
|
||||||
reference orbit the GPU perturbs from.
|
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
|
||||||
@@ -87,8 +75,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 (from `common.wgsl`) must match the
|
abs-based ones. A `KIND_*` constant here must match the matching
|
||||||
matching `FractalKind` variant's discriminant exactly.
|
`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
|
||||||
@@ -117,10 +105,9 @@ pixel is a handful of `f32` complex multiplies.
|
|||||||
### Adding a new `FractalKind`
|
### Adding a new `FractalKind`
|
||||||
|
|
||||||
Touches, in order: `reference.rs` (enum variant + CPU iteration formula, and a
|
Touches, in order: `reference.rs` (enum variant + CPU iteration formula, and a
|
||||||
test comparing against a naive `f64` iteration), `common.wgsl` (matching
|
test comparing against a naive `f64` iteration), `mandelbrot.wgsl` (matching
|
||||||
`KIND_*` const), `mandelbrot.wgsl` (matching `advance_delta`/`fprime` arms),
|
`KIND_*` const + `advance_delta`/`fprime` arms), `buddhabrot.wgsl` (matching
|
||||||
`buddhabrot.wgsl` (matching arm in `advance()`, if the kind makes sense as a
|
arm in `advance()`, if the kind makes sense as a Buddhabrot), `renderer.rs`
|
||||||
Buddhabrot), `renderer.rs`
|
|
||||||
`Uniforms` (only if the kind needs a new per-kind constant, e.g. Phoenix's
|
`Uniforms` (only if the kind needs a new per-kind constant, e.g. Phoenix's
|
||||||
`phoenix_p`), `share.rs` (encode/decode string tag), `app.rs` (`KINDS` label,
|
`phoenix_p`), `share.rs` (encode/decode string tag), `app.rs` (`KINDS` label,
|
||||||
`JULIA_PRESETS`/`SET_PRESETS` slot, `default_view_for` entry, and optionally a
|
`JULIA_PRESETS`/`SET_PRESETS` slot, `default_view_for` entry, and optionally a
|
||||||
|
|||||||
+2
-12
@@ -791,13 +791,6 @@ impl FractalApp {
|
|||||||
&self.reference
|
&self.reference
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The configured shadow-style lights, for headless export's `ExportRender`
|
|
||||||
/// (which has no `FractalCallback` to source them from).
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
pub(crate) fn lights(&self) -> &[Light] {
|
|
||||||
&self.lights
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Recompute the reference orbit when needed. Native: dispatch to a worker
|
/// Recompute the reference orbit when needed. Native: dispatch to a worker
|
||||||
/// thread and pick up completed results. Web: compute inline.
|
/// thread and pick up completed results. Web: compute inline.
|
||||||
fn ensure_reference(&mut self) {
|
fn ensure_reference(&mut self) {
|
||||||
@@ -1031,7 +1024,6 @@ impl FractalApp {
|
|||||||
renderer.export_handles()
|
renderer.export_handles()
|
||||||
};
|
};
|
||||||
let reference = Arc::clone(&self.reference);
|
let reference = Arc::clone(&self.reference);
|
||||||
let lights = self.lights.clone();
|
|
||||||
|
|
||||||
let shared = Arc::new(Mutex::new(ExportShared {
|
let shared = Arc::new(Mutex::new(ExportShared {
|
||||||
fraction: 0.0,
|
fraction: 0.0,
|
||||||
@@ -1058,7 +1050,6 @@ impl FractalApp {
|
|||||||
h,
|
h,
|
||||||
uniforms,
|
uniforms,
|
||||||
reference.as_slice(),
|
reference.as_slice(),
|
||||||
&lights,
|
|
||||||
);
|
);
|
||||||
let sh = Arc::clone(&shared);
|
let sh = Arc::clone(&shared);
|
||||||
let png =
|
let png =
|
||||||
@@ -1088,7 +1079,6 @@ impl FractalApp {
|
|||||||
h,
|
h,
|
||||||
uniforms,
|
uniforms,
|
||||||
reference.as_slice(),
|
reference.as_slice(),
|
||||||
&lights,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Render tile by tile, awaiting each submission so the browser
|
// Render tile by tile, awaiting each submission so the browser
|
||||||
@@ -2065,10 +2055,10 @@ impl FractalApp {
|
|||||||
dx -= PAN_SPEED_PX * dt;
|
dx -= PAN_SPEED_PX * dt;
|
||||||
}
|
}
|
||||||
if down {
|
if down {
|
||||||
dy -= PAN_SPEED_PX * dt;
|
dy += PAN_SPEED_PX * dt;
|
||||||
}
|
}
|
||||||
if up {
|
if up {
|
||||||
dy += PAN_SPEED_PX * dt;
|
dy -= PAN_SPEED_PX * dt;
|
||||||
}
|
}
|
||||||
if dx != 0.0 || dy != 0.0 {
|
if dx != 0.0 || dy != 0.0 {
|
||||||
self.view.pan_pixels(dx, dy, height_px);
|
self.view.pan_pixels(dx, dy, height_px);
|
||||||
|
|||||||
@@ -120,13 +120,7 @@ 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(
|
source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/buddhabrot.wgsl").into()),
|
||||||
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 {
|
||||||
|
|||||||
+3
-58
@@ -164,14 +164,7 @@ 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(
|
source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/mandelbrot.wgsl").into()),
|
||||||
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 {
|
||||||
@@ -218,19 +211,6 @@ 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,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -246,10 +226,6 @@ 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(),
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -316,14 +292,7 @@ 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(
|
source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/colorize.wgsl").into()),
|
||||||
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 {
|
||||||
@@ -397,13 +366,7 @@ 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(
|
source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/blit.wgsl").into()),
|
||||||
concat!(
|
|
||||||
include_str!("../shaders/common.wgsl"),
|
|
||||||
include_str!("../shaders/blit.wgsl"),
|
|
||||||
)
|
|
||||||
.into(),
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let blit_bind_group_layout =
|
let blit_bind_group_layout =
|
||||||
@@ -629,7 +592,6 @@ 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"),
|
||||||
@@ -650,19 +612,6 @@ 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,
|
||||||
@@ -675,10 +624,6 @@ 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(),
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ pub fn run(cli: Cli) -> Result<(), String> {
|
|||||||
height,
|
height,
|
||||||
uniforms,
|
uniforms,
|
||||||
app.reference_points(),
|
app.reference_points(),
|
||||||
app.lights(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
eprintln!("rendering {width}×{height}…");
|
eprintln!("rendering {width}×{height}…");
|
||||||
|
|||||||
@@ -13,7 +13,12 @@ struct VsOut {
|
|||||||
|
|
||||||
@vertex
|
@vertex
|
||||||
fn vs_main(@builtin(vertex_index) idx: u32) -> VsOut {
|
fn vs_main(@builtin(vertex_index) idx: u32) -> VsOut {
|
||||||
let p = fullscreen_triangle_pos(idx);
|
var verts = array<vec2<f32>, 3>(
|
||||||
|
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
|
||||||
|
|||||||
@@ -62,6 +62,17 @@ 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;
|
||||||
|
const KIND_COMPLEX_MULTIBROT: u32 = 9u;
|
||||||
|
|
||||||
@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>>;
|
||||||
@@ -82,6 +93,10 @@ 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) {
|
||||||
@@ -90,6 +105,21 @@ fn complex_pow(z: vec2<f32>, p: u32) -> vec2<f32> {
|
|||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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));
|
||||||
|
}
|
||||||
|
|
||||||
// One iteration step z_n -> z_{n+1} for the current kind. `zp` is the
|
// One iteration step z_n -> z_{n+1} for the current kind. `zp` is the
|
||||||
// previous iterate (z_{n-1}), used only by the Phoenix two-term recurrence.
|
// previous iterate (z_{n-1}), used only by the Phoenix two-term recurrence.
|
||||||
// Must match `FractalKind` in reference.rs (the direct, non-perturbative form
|
// Must match `FractalKind` in reference.rs (the direct, non-perturbative form
|
||||||
@@ -221,7 +251,12 @@ 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> {
|
||||||
return vec4<f32>(fullscreen_triangle_pos(idx), 0.0, 1.0);
|
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 vec4<f32>(verts[idx], 0.0, 1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@fragment
|
@fragment
|
||||||
|
|||||||
+131
-10
@@ -10,28 +10,148 @@
|
|||||||
// 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>,
|
||||||
|
complex_power: 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> {
|
||||||
return vec4<f32>(fullscreen_triangle_pos(idx), 0.0, 1.0);
|
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 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 {
|
||||||
let x = i32(pos.x);
|
if textureLoad(data_tex, vec2<i32>(i32(pos.x), i32(pos.y)), 0).b != 0. {
|
||||||
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 h0 = textureLoad(data_tex, vec2<i32>(x, y), 0).g;
|
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 h1 = textureLoad(data_tex, vec2<i32>(x + 1, y), 0).g;
|
|
||||||
let h2 = textureLoad(data_tex, vec2<i32>(x, y + 1), 0).g;
|
let normal = normalize(cross(d[1] - d[0], d[2] - d[0]));
|
||||||
let normal = normal_from_heights(h0, h1, h2);
|
|
||||||
return vec4<f32>(shadow_color(normal), 1.0);
|
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 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);
|
||||||
@@ -39,7 +159,8 @@ 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;
|
||||||
|
|
||||||
var col = classic_color(ci, de);
|
let t = fract(ci * u.color_scale + u.color_offset);
|
||||||
|
var col = palette(u.palette_id, t) * sqrt(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);
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
// 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;
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
// 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;
|
|
||||||
}
|
|
||||||
+112
-60
@@ -12,11 +12,50 @@
|
|||||||
// 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>,
|
||||||
|
// 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,
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
@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>,
|
||||||
@@ -26,7 +65,12 @@ struct VsOut {
|
|||||||
|
|
||||||
@vertex
|
@vertex
|
||||||
fn vs_main(@builtin(vertex_index) idx: u32) -> VsOut {
|
fn vs_main(@builtin(vertex_index) idx: u32) -> VsOut {
|
||||||
let ndc = fullscreen_triangle_pos(idx);
|
var verts = array<vec2<f32>, 3>(
|
||||||
|
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.
|
||||||
@@ -34,6 +78,11 @@ 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);
|
||||||
@@ -45,6 +94,21 @@ fn cdiv(a: vec2<f32>, b: vec2<f32>) -> vec2<f32> {
|
|||||||
return vec2<f32>(a.x * b.x + a.y * b.y, a.y * b.x - a.x * b.y) / d;
|
return vec2<f32>(a.x * b.x + a.y * b.y, a.y * b.x - a.x * b.y) / d;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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));
|
||||||
|
}
|
||||||
|
|
||||||
// |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.
|
||||||
@@ -92,25 +156,18 @@ fn multibrot_delta(z: vec2<f32>, e: vec2<f32>, p: u32) -> vec2<f32> {
|
|||||||
const COMPLEX_MULTIBROT_TERMS: u32 = 16u;
|
const COMPLEX_MULTIBROT_TERMS: u32 = 16u;
|
||||||
|
|
||||||
// Perturbation delta for z -> z^p with a complex p: (Z+e)^p - Z^p.
|
// Perturbation delta for z -> z^p with a complex p: (Z+e)^p - Z^p.
|
||||||
//
|
// = Z^p * ((1+w)^p - 1), w = e/Z, expanded as a Taylor series in w (never
|
||||||
// When |e| << |Z| (the common case: it's the whole reason perturbation
|
// forming 1+w, which would round tiny w away in f32 — the same reason
|
||||||
// works), forming Z+e directly would round e away in f32, so instead expand
|
// `multibrot_delta` never forms Z+e directly). Series: (1+w)^p - 1 =
|
||||||
// = 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
|
// 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`
|
// incrementally: C(p,k) = C(p,k-1) * (p-(k-1)) / k.
|
||||||
// (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
|
// Z ~ 0 (the reference start, X_0 = 0 for Mandelbrot) makes w singular; there
|
||||||
// singular), e is *not* small relative to Z — that's normal perturbation
|
// (0+e)^p - 0^p = e^p exactly, so that case is handled directly via `cpow`.
|
||||||
// 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> {
|
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
|
if dot(z, z) < 1e-20 {
|
||||||
// the `< 0.25` test below and falls through to the direct branch.
|
return cpow(e, p);
|
||||||
let w2 = dot(e, e) / dot(z, z);
|
}
|
||||||
if w2 < 0.25 {
|
|
||||||
let w = cdiv(e, z);
|
let w = cdiv(e, z);
|
||||||
var wk = vec2<f32>(1.0, 0.0); // w^0
|
var wk = vec2<f32>(1.0, 0.0); // w^0
|
||||||
var coef = vec2<f32>(1.0, 0.0); // C(p,0)
|
var coef = vec2<f32>(1.0, 0.0); // C(p,0)
|
||||||
@@ -122,8 +179,6 @@ fn complex_multibrot_delta(z: vec2<f32>, e: vec2<f32>, p: vec2<f32>) -> vec2<f32
|
|||||||
}
|
}
|
||||||
return cmul(cpow(z, p), acc);
|
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
|
||||||
@@ -194,6 +249,26 @@ fn fprime(z: vec2<f32>) -> vec2<f32> {
|
|||||||
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
|
||||||
@@ -332,15 +407,22 @@ 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);
|
||||||
}
|
}
|
||||||
return classic_color(s.ci, s.de);
|
let t = fract(s.ci * u.color_scale + u.color_offset);
|
||||||
|
return palette(u.palette_id, t) * s.de;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Supersampled escape data at one point: average (ci, DE factor) over the
|
// Iteration pass: write per-pixel escape data (color-independent) so a colour
|
||||||
// AA grid's escaped sub-samples, plus the fraction that landed in the
|
// change is remapped by the cheap colourise pass without re-iterating.
|
||||||
// interior. Shared by `fs_data` (writes it straight to the data texture) and
|
// R = ci (palette parameter), G = DE factor, B = interior fraction (for AA).
|
||||||
// `fs_color`'s shadow branch (used both at the pixel and at its two
|
// AA is grid-supersampled here; the interior fraction lets the colourise pass
|
||||||
// neighbours, to build a DE height field without a texture round-trip).
|
// anti-alias the set boundary (blend toward black) after the fact.
|
||||||
fn aggregate_sample(base: vec2<f32>, dx: vec2<f32>, dy: vec2<f32>, px: f32) -> vec3<f32> {
|
@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));
|
||||||
|
|
||||||
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;
|
||||||
@@ -362,22 +444,7 @@ fn aggregate_sample(base: vec2<f32>, dx: vec2<f32>, dy: vec2<f32>, px: f32) -> v
|
|||||||
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 vec3<f32>(ci_avg, de_avg, interior_frac);
|
return vec4<f32>(ci_avg, de_avg, interior_frac, 1.0);
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
||||||
@@ -390,21 +457,6 @@ 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);
|
||||||
|
|||||||
+2
-22
@@ -21,44 +21,24 @@ fn validate(name: &str, src: &str) {
|
|||||||
fn mandelbrot_shader_is_valid() {
|
fn mandelbrot_shader_is_valid() {
|
||||||
validate(
|
validate(
|
||||||
"mandelbrot.wgsl",
|
"mandelbrot.wgsl",
|
||||||
concat!(
|
|
||||||
include_str!("../src/shaders/common.wgsl"),
|
|
||||||
include_str!("../src/shaders/iterate_uniforms.wgsl"),
|
|
||||||
include_str!("../src/shaders/mandelbrot.wgsl"),
|
include_str!("../src/shaders/mandelbrot.wgsl"),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn colorize_shader_is_valid() {
|
fn colorize_shader_is_valid() {
|
||||||
validate(
|
validate("colorize.wgsl", include_str!("../src/shaders/colorize.wgsl"));
|
||||||
"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(
|
validate("blit.wgsl", include_str!("../src/shaders/blit.wgsl"));
|
||||||
"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",
|
||||||
concat!(
|
|
||||||
include_str!("../src/shaders/common.wgsl"),
|
|
||||||
include_str!("../src/shaders/buddhabrot.wgsl"),
|
include_str!("../src/shaders/buddhabrot.wgsl"),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user