Compare commits
2
Commits
main
...
58eb443e56
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58eb443e56 | ||
|
|
2cd06c4219 |
+2
-1
@@ -26,7 +26,8 @@ web-sys = { version = "0.3.105", features = ["Window", "Location", "Url", "UrlSe
|
|||||||
# Release: optimize hard (fractal math is hot).
|
# Release: optimize hard (fractal math is hot).
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
codegen-units = 1
|
# codegen-units = 1
|
||||||
|
debug = true
|
||||||
|
|
||||||
# Dev: keep our own crate debuggable, but optimize dependencies (dashu, wgpu,
|
# Dev: keep our own crate debuggable, but optimize dependencies (dashu, wgpu,
|
||||||
# egui) so the explorer is actually interactive during development.
|
# egui) so the explorer is actually interactive during development.
|
||||||
|
|||||||
+75
-23
@@ -10,6 +10,7 @@ use crate::fractal::{
|
|||||||
};
|
};
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
use crate::fractal::{compute_reference, compute_set_reference};
|
use crate::fractal::{compute_reference, compute_set_reference};
|
||||||
|
use crate::lights::Light;
|
||||||
use crate::view::{
|
use crate::view::{
|
||||||
Big, DEFAULT_HALF_HEIGHT, ViewState, big_from_decimal_str, big_from_f64, big_to_decimal_str,
|
Big, DEFAULT_HALF_HEIGHT, ViewState, big_from_decimal_str, big_from_f64, big_to_decimal_str,
|
||||||
precision_for,
|
precision_for,
|
||||||
@@ -29,6 +30,8 @@ const INTERACT_DOWNSCALE: u32 = 2;
|
|||||||
const INTERACT_SETTLE: f64 = 0.12;
|
const INTERACT_SETTLE: f64 = 0.12;
|
||||||
/// Palette names; index maps to `palette_id` in the shader.
|
/// Palette names; index maps to `palette_id` in the shader.
|
||||||
const PALETTE_NAMES: &[&str] = &["Amber", "Rainbow", "Ember", "Lime", "Grayscale"];
|
const PALETTE_NAMES: &[&str] = &["Amber", "Rainbow", "Ember", "Lime", "Grayscale"];
|
||||||
|
/// Shadow palette names; index maps to `palette_id` in the shader.
|
||||||
|
const SHADOW_PALETTE_NAMES: &[&str] = &["Grayscale", "Red & Blue", "Custom lights"];
|
||||||
/// Buddhabrot tonemap style names; index maps to `BuddhabrotUniforms::palette`.
|
/// Buddhabrot tonemap style names; index maps to `BuddhabrotUniforms::palette`.
|
||||||
const BUDDHA_PALETTE_NAMES: &[&str] = &["Nebula", "Yellow", "Grayscale"];
|
const BUDDHA_PALETTE_NAMES: &[&str] = &["Nebula", "Yellow", "Grayscale"];
|
||||||
|
|
||||||
@@ -121,7 +124,7 @@ const SET_PRESETS: [&[SetPreset]; FractalKind::Lambda as usize + 1] = [
|
|||||||
"-0.7436438870371587",
|
"-0.7436438870371587",
|
||||||
"0.1318259042053",
|
"0.1318259042053",
|
||||||
8.0e-8,
|
8.0e-8,
|
||||||
10000,
|
2000,
|
||||||
None,
|
None,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -262,11 +265,17 @@ pub struct FractalApp {
|
|||||||
color_scale: f32,
|
color_scale: f32,
|
||||||
color_offset: f32,
|
color_offset: f32,
|
||||||
palette: u32,
|
palette: u32,
|
||||||
|
shadow_palette: u32,
|
||||||
/// Supersample each pixel 2×2 for smoother edges (costs ~4× fragment work).
|
/// Supersample each pixel 2×2 for smoother edges (costs ~4× fragment work).
|
||||||
antialias: bool,
|
antialias: bool,
|
||||||
/// Distance-estimation shading: darkens toward the set boundary using the
|
/// Distance-estimation shading: darkens toward the set boundary using the
|
||||||
/// orbit derivative, giving crisp filaments at deep zoom instead of speckle.
|
/// orbit derivative, giving crisp filaments at deep zoom instead of speckle.
|
||||||
de_coloring: bool,
|
de_coloring: bool,
|
||||||
|
// Use shadow coloring
|
||||||
|
shadow: bool,
|
||||||
|
|
||||||
|
/// List of enabled lights in the world
|
||||||
|
lights: Vec<Light>,
|
||||||
|
|
||||||
/// Render as a Buddhabrot (Monte-Carlo orbit-density histogram) instead of
|
/// Render as a Buddhabrot (Monte-Carlo orbit-density histogram) instead of
|
||||||
/// the ordinary escape-time set. Plain f32 view — no deep zoom, no
|
/// the ordinary escape-time set. Plain f32 view — no deep zoom, no
|
||||||
@@ -401,8 +410,11 @@ impl FractalApp {
|
|||||||
color_scale: 0.15,
|
color_scale: 0.15,
|
||||||
color_offset: 0.0,
|
color_offset: 0.0,
|
||||||
palette: 0,
|
palette: 0,
|
||||||
|
shadow_palette: 0,
|
||||||
antialias: false,
|
antialias: false,
|
||||||
de_coloring: false,
|
de_coloring: false,
|
||||||
|
shadow: false,
|
||||||
|
lights: vec![Light::default()],
|
||||||
buddhabrot: false,
|
buddhabrot: false,
|
||||||
buddha_r_cap: 50,
|
buddha_r_cap: 50,
|
||||||
buddha_g_cap: 500,
|
buddha_g_cap: 500,
|
||||||
@@ -576,6 +588,7 @@ impl FractalApp {
|
|||||||
color_scale: self.color_scale,
|
color_scale: self.color_scale,
|
||||||
color_offset: self.color_offset,
|
color_offset: self.color_offset,
|
||||||
palette: self.palette,
|
palette: self.palette,
|
||||||
|
shadow_palette: self.shadow_palette,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -594,6 +607,8 @@ impl FractalApp {
|
|||||||
self.color_scale = s.color_scale;
|
self.color_scale = s.color_scale;
|
||||||
self.color_offset = s.color_offset;
|
self.color_offset = s.color_offset;
|
||||||
self.palette = (s.palette as usize).min(PALETTE_NAMES.len() - 1) as u32;
|
self.palette = (s.palette as usize).min(PALETTE_NAMES.len() - 1) as u32;
|
||||||
|
self.shadow_palette =
|
||||||
|
(s.shadow_palette as usize).min(SHADOW_PALETTE_NAMES.len() - 1) as u32;
|
||||||
// The link carries an explicit iteration count; honor it rather than
|
// The link carries an explicit iteration count; honor it rather than
|
||||||
// letting the auto-scaler immediately overwrite it.
|
// letting the auto-scaler immediately overwrite it.
|
||||||
self.auto_iterations = false;
|
self.auto_iterations = false;
|
||||||
@@ -802,14 +817,16 @@ impl FractalApp {
|
|||||||
bailout_sq: BAILOUT_SQ,
|
bailout_sq: BAILOUT_SQ,
|
||||||
is_julia: matches!(self.mode, FractalMode::Julia) as u32,
|
is_julia: matches!(self.mode, FractalMode::Julia) as u32,
|
||||||
palette_id: self.palette,
|
palette_id: self.palette,
|
||||||
|
shadow_palette_id: self.shadow_palette,
|
||||||
aa_level: if self.antialias { 2 } else { 1 },
|
aa_level: if self.antialias { 2 } else { 1 },
|
||||||
kind: self.kind as u32,
|
kind: self.kind as u32,
|
||||||
power: self.power,
|
power: self.power,
|
||||||
dc_offset: self.dc_offset(),
|
dc_offset: self.dc_offset(),
|
||||||
phoenix_p: [self.phoenix_p.0 as f32, self.phoenix_p.1 as f32],
|
phoenix_p: [self.phoenix_p.0 as f32, self.phoenix_p.1 as f32],
|
||||||
lambda_l: [self.lambda_l.0 as f32, self.lambda_l.1 as f32],
|
lambda_l: [self.lambda_l.0 as f32, self.lambda_l.1 as f32],
|
||||||
de_coloring: self.de_coloring as u32,
|
de_coloring: (self.de_coloring | self.shadow) as u32,
|
||||||
_pad: [0],
|
shadow: self.shadow as u32,
|
||||||
|
_pad: [0; _],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -833,11 +850,11 @@ impl FractalApp {
|
|||||||
r_cap: self.buddha_r_cap,
|
r_cap: self.buddha_r_cap,
|
||||||
g_cap: self.buddha_g_cap,
|
g_cap: self.buddha_g_cap,
|
||||||
b_cap: self.buddha_b_cap,
|
b_cap: self.buddha_b_cap,
|
||||||
seed: 0, // set by the callback's own dispatch counter
|
seed: 0, // set by the callback's own dispatch counter
|
||||||
samples_this_dispatch: 0, // set by the callback
|
samples_this_dispatch: 0, // set by the callback
|
||||||
exposure: self.buddha_exposure,
|
exposure: self.buddha_exposure,
|
||||||
width: 0, // set by the callback from size_px
|
width: 0, // set by the callback from size_px
|
||||||
height: 0, // set by the callback from size_px
|
height: 0, // set by the callback from size_px
|
||||||
total_samples: 0.0, // tracked by the renderer across frames
|
total_samples: 0.0, // tracked by the renderer across frames
|
||||||
palette: self.buddha_palette,
|
palette: self.buddha_palette,
|
||||||
_pad: [0; 3],
|
_pad: [0; 3],
|
||||||
@@ -1354,32 +1371,66 @@ impl FractalApp {
|
|||||||
.logarithmic(true),
|
.logarithmic(true),
|
||||||
);
|
);
|
||||||
ui.add(egui::Slider::new(&mut self.color_offset, 0.0..=1.0).text("color offset"));
|
ui.add(egui::Slider::new(&mut self.color_offset, 0.0..=1.0).text("color offset"));
|
||||||
egui::ComboBox::from_label("palette")
|
ui.checkbox(&mut self.shadow, "Shadow");
|
||||||
.selected_text(PALETTE_NAMES[self.palette as usize])
|
if !self.shadow {
|
||||||
.show_ui(ui, |ui| {
|
egui::ComboBox::from_label("palette")
|
||||||
for (i, name) in PALETTE_NAMES.iter().enumerate() {
|
.selected_text(PALETTE_NAMES[self.palette as usize])
|
||||||
ui.selectable_value(&mut self.palette, i as u32, *name);
|
.show_ui(ui, |ui| {
|
||||||
|
for (i, name) in PALETTE_NAMES.iter().enumerate() {
|
||||||
|
ui.selectable_value(&mut self.palette, i as u32, *name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
egui::ComboBox::from_label("palette")
|
||||||
|
.selected_text(SHADOW_PALETTE_NAMES[self.shadow_palette as usize])
|
||||||
|
.show_ui(ui, |ui| {
|
||||||
|
for (i, name) in SHADOW_PALETTE_NAMES.iter().enumerate() {
|
||||||
|
ui.selectable_value(&mut self.shadow_palette, i as u32, *name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if self.shadow && self.shadow_palette as usize == SHADOW_PALETTE_NAMES.len() - 1 {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label("lights:");
|
||||||
|
if ui.button("+").clicked() {
|
||||||
|
self.lights.push(Light::default());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
egui::Grid::new("lights")
|
||||||
|
.striped(true)
|
||||||
|
.num_columns(1)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
self.lights.retain_mut(|light| {
|
||||||
|
let delete = !light.widget(ui);
|
||||||
|
ui.end_row();
|
||||||
|
delete
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ui.separator();
|
||||||
ui.checkbox(&mut self.antialias, "Antialiasing (2×2)")
|
ui.checkbox(&mut self.antialias, "Antialiasing (2×2)")
|
||||||
.on_hover_text("Supersample each pixel for smoother edges (~4× slower).");
|
.on_hover_text("Supersample each pixel for smoother edges (~4× slower).");
|
||||||
ui.checkbox(&mut self.de_coloring, "Distance shading")
|
if !self.shadow {
|
||||||
.on_hover_text(
|
ui.checkbox(&mut self.de_coloring, "Distance shading")
|
||||||
"Shade by distance to the set boundary (from the orbit derivative) \
|
.on_hover_text(
|
||||||
|
"Shade by distance to the set boundary (from the orbit derivative) \
|
||||||
for crisp filaments at deep zoom. Exact for the holomorphic kinds \
|
for crisp filaments at deep zoom. Exact for the holomorphic kinds \
|
||||||
(Mandelbrot/Multibrot/Phoenix), approximate for the abs-based kinds \
|
(Mandelbrot/Multibrot/Phoenix), approximate for the abs-based kinds \
|
||||||
(Burning Ship/Tricorn/Celtic/Perpendicular/Buffalo).",
|
(Burning Ship/Tricorn/Celtic/Perpendicular/Buffalo).",
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
ui.collapsing("Animation", |ui| {
|
ui.collapsing("Animation", |ui| {
|
||||||
ui.checkbox(&mut self.anim.color, "Cycle colours")
|
if !self.shadow {
|
||||||
.on_hover_text("Scroll the palette offset over time.");
|
ui.checkbox(&mut self.anim.color, "Cycle colours")
|
||||||
if self.anim.color {
|
.on_hover_text("Scroll the palette offset over time.");
|
||||||
ui.add(
|
if self.anim.color {
|
||||||
egui::Slider::new(&mut self.anim.color_speed, 0.01..=2.0)
|
ui.add(
|
||||||
.text("cycles/s")
|
egui::Slider::new(&mut self.anim.color_speed, 0.01..=2.0)
|
||||||
.logarithmic(true),
|
.text("cycles/s")
|
||||||
);
|
.logarithmic(true),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.checkbox(&mut self.anim.zoom, "Auto-zoom")
|
ui.checkbox(&mut self.anim.zoom, "Auto-zoom")
|
||||||
@@ -1754,6 +1805,7 @@ impl FractalApp {
|
|||||||
rect,
|
rect,
|
||||||
FractalCallback {
|
FractalCallback {
|
||||||
uniforms,
|
uniforms,
|
||||||
|
lights: self.lights.clone(),
|
||||||
reference: Arc::clone(&self.reference),
|
reference: Arc::clone(&self.reference),
|
||||||
generation: self.generation,
|
generation: self.generation,
|
||||||
size_px,
|
size_px,
|
||||||
|
|||||||
+38
-3
@@ -13,6 +13,8 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use eframe::egui_wgpu::{self, wgpu};
|
use eframe::egui_wgpu::{self, wgpu};
|
||||||
|
|
||||||
|
use crate::lights::{Light, MAX_LIGHT_COUNT};
|
||||||
|
|
||||||
/// Maximum reference-orbit length (points) the storage buffer can hold. Also
|
/// Maximum reference-orbit length (points) the storage buffer can hold. Also
|
||||||
/// bounds the iteration count. 128k points * 8 bytes = 1 MiB.
|
/// bounds the iteration count. 128k points * 8 bytes = 1 MiB.
|
||||||
pub const MAX_REF_POINTS: usize = 1 << 17;
|
pub const MAX_REF_POINTS: usize = 1 << 17;
|
||||||
@@ -46,6 +48,8 @@ fn color_differs(a: &Uniforms, b: &Uniforms) -> bool {
|
|||||||
a.color_offset != b.color_offset
|
a.color_offset != b.color_offset
|
||||||
|| a.color_scale != b.color_scale
|
|| a.color_scale != b.color_scale
|
||||||
|| a.palette_id != b.palette_id
|
|| a.palette_id != b.palette_id
|
||||||
|
|| a.shadow_palette_id != b.shadow_palette_id
|
||||||
|
|| a.shadow != b.shadow
|
||||||
}
|
}
|
||||||
|
|
||||||
/// GPU-side view + coloring parameters. Layout must match `Uniforms` in the
|
/// GPU-side view + coloring parameters. Layout must match `Uniforms` in the
|
||||||
@@ -64,12 +68,14 @@ pub struct Uniforms {
|
|||||||
/// 0 = Mandelbrot, 1 = Julia.
|
/// 0 = Mandelbrot, 1 = Julia.
|
||||||
pub is_julia: u32,
|
pub is_julia: u32,
|
||||||
pub palette_id: u32,
|
pub palette_id: u32,
|
||||||
|
pub shadow_palette_id: u32,
|
||||||
/// Supersampling factor per axis: 1 = off, 2 = 2×2 (4 samples).
|
/// Supersampling factor per axis: 1 = off, 2 = 2×2 (4 samples).
|
||||||
pub aa_level: u32,
|
pub aa_level: u32,
|
||||||
/// Iteration formula (`FractalKind::shader_id`).
|
/// Iteration formula (`FractalKind::shader_id`).
|
||||||
pub kind: u32,
|
pub kind: u32,
|
||||||
/// Exponent for the Multibrot kind.
|
/// Exponent for the Multibrot kind.
|
||||||
pub power: u32,
|
pub power: u32,
|
||||||
|
pub _pad: [u32; 1],
|
||||||
/// Complex offset of the view center from the reference center, so a stale
|
/// Complex offset of the view center from the reference center, so a stale
|
||||||
/// or reused reference (computed at a slightly different center) still maps
|
/// or reused reference (computed at a slightly different center) still maps
|
||||||
/// correctly. Added to every pixel's per-pixel offset.
|
/// correctly. Added to every pixel's per-pixel offset.
|
||||||
@@ -83,8 +89,8 @@ pub struct Uniforms {
|
|||||||
pub lambda_l: [f32; 2],
|
pub lambda_l: [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,
|
||||||
/// Padding to a 16-byte multiple (uniform buffer requirement).
|
// 0 = classic colors, 1 = shadows
|
||||||
pub _pad: [u32; 1],
|
pub shadow: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Offscreen textures for the two-pass render, recreated whenever the widget's
|
/// Offscreen textures for the two-pass render, recreated whenever the widget's
|
||||||
@@ -128,6 +134,7 @@ pub struct FractalRenderer {
|
|||||||
bind_group_layout: wgpu::BindGroupLayout,
|
bind_group_layout: wgpu::BindGroupLayout,
|
||||||
uniform_buffer: wgpu::Buffer,
|
uniform_buffer: wgpu::Buffer,
|
||||||
ref_buffer: wgpu::Buffer,
|
ref_buffer: wgpu::Buffer,
|
||||||
|
lights_buffer: wgpu::Buffer,
|
||||||
bind_group: wgpu::BindGroup,
|
bind_group: wgpu::BindGroup,
|
||||||
target_format: wgpu::TextureFormat,
|
target_format: wgpu::TextureFormat,
|
||||||
/// Generation of the reference orbit currently uploaded to `ref_buffer`.
|
/// Generation of the reference orbit currently uploaded to `ref_buffer`.
|
||||||
@@ -170,6 +177,13 @@ impl FractalRenderer {
|
|||||||
mapped_at_creation: false,
|
mapped_at_creation: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let lights_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
|
label: Some("lights parameters"),
|
||||||
|
size: (MAX_LIGHT_COUNT * std::mem::size_of::<Light>()) as u64,
|
||||||
|
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||||
|
mapped_at_creation: false,
|
||||||
|
});
|
||||||
|
|
||||||
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||||
label: Some("fractal bind group layout"),
|
label: Some("fractal bind group layout"),
|
||||||
entries: &[
|
entries: &[
|
||||||
@@ -301,6 +315,16 @@ impl FractalRenderer {
|
|||||||
},
|
},
|
||||||
count: None,
|
count: None,
|
||||||
},
|
},
|
||||||
|
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,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
let colorize_pipeline_layout =
|
let colorize_pipeline_layout =
|
||||||
@@ -409,6 +433,7 @@ impl FractalRenderer {
|
|||||||
bind_group_layout,
|
bind_group_layout,
|
||||||
uniform_buffer,
|
uniform_buffer,
|
||||||
ref_buffer,
|
ref_buffer,
|
||||||
|
lights_buffer,
|
||||||
bind_group,
|
bind_group,
|
||||||
target_format,
|
target_format,
|
||||||
uploaded_generation: u64::MAX,
|
uploaded_generation: u64::MAX,
|
||||||
@@ -477,6 +502,10 @@ impl FractalRenderer {
|
|||||||
binding: 1,
|
binding: 1,
|
||||||
resource: wgpu::BindingResource::TextureView(&data_view),
|
resource: wgpu::BindingResource::TextureView(&data_view),
|
||||||
},
|
},
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 2,
|
||||||
|
resource: self.lights_buffer.as_entire_binding(),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -781,6 +810,7 @@ pub fn encode_png_with_progress(
|
|||||||
/// colourise pass (see `prepare`).
|
/// colourise pass (see `prepare`).
|
||||||
pub struct FractalCallback {
|
pub struct FractalCallback {
|
||||||
pub uniforms: Uniforms,
|
pub uniforms: Uniforms,
|
||||||
|
pub lights: Vec<Light>,
|
||||||
pub reference: Arc<Vec<[f32; 2]>>,
|
pub reference: Arc<Vec<[f32; 2]>>,
|
||||||
pub generation: u64,
|
pub generation: u64,
|
||||||
/// Widget size in physical pixels — the cache texture resolution.
|
/// Widget size in physical pixels — the cache texture resolution.
|
||||||
@@ -827,7 +857,8 @@ impl egui_wgpu::CallbackTrait for FractalCallback {
|
|||||||
let color_dirty = iter_dirty
|
let color_dirty = iter_dirty
|
||||||
|| renderer.colored.as_ref().is_none_or(|c| {
|
|| renderer.colored.as_ref().is_none_or(|c| {
|
||||||
c.width != width || c.height != height || color_differs(&c.uniforms, &self.uniforms)
|
c.width != width || c.height != height || color_differs(&c.uniforms, &self.uniforms)
|
||||||
});
|
})
|
||||||
|
|| true;
|
||||||
|
|
||||||
if !color_dirty {
|
if !color_dirty {
|
||||||
return Vec::new(); // cache still valid; paint() just blits it
|
return Vec::new(); // cache still valid; paint() just blits it
|
||||||
@@ -839,6 +870,10 @@ impl egui_wgpu::CallbackTrait for FractalCallback {
|
|||||||
0,
|
0,
|
||||||
bytemuck::bytes_of(&self.uniforms),
|
bytemuck::bytes_of(&self.uniforms),
|
||||||
);
|
);
|
||||||
|
let mut bytes = [0; size_of::<Light>() * MAX_LIGHT_COUNT];
|
||||||
|
bytes[..self.lights.len() * size_of::<Light>()]
|
||||||
|
.copy_from_slice(bytemuck::cast_slice(&self.lights));
|
||||||
|
queue.write_buffer(&renderer.lights_buffer, 0, &bytes);
|
||||||
|
|
||||||
if let Some(cache) = &renderer.cache {
|
if let Some(cache) = &renderer.cache {
|
||||||
if iter_dirty {
|
if iter_dirty {
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ pub struct ShareState {
|
|||||||
pub color_offset: f32,
|
pub color_offset: f32,
|
||||||
/// Palette index (`palette_id` in the shader).
|
/// Palette index (`palette_id` in the shader).
|
||||||
pub palette: u32,
|
pub palette: u32,
|
||||||
|
/// Shadow palette index (`shadow_palette_id` in the shader).
|
||||||
|
pub shadow_palette: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ShareState {
|
impl ShareState {
|
||||||
@@ -110,6 +112,7 @@ impl ShareState {
|
|||||||
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),
|
||||||
|
shadow_palette: map.get("spal").and_then(|s| s.parse().ok()).unwrap_or(0),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -134,6 +137,7 @@ mod tests {
|
|||||||
color_scale: 0.02,
|
color_scale: 0.02,
|
||||||
color_offset: 0.25,
|
color_offset: 0.25,
|
||||||
palette: 3,
|
palette: 3,
|
||||||
|
shadow_palette: 1,
|
||||||
};
|
};
|
||||||
let d = ShareState::decode(&s.encode()).unwrap();
|
let d = ShareState::decode(&s.encode()).unwrap();
|
||||||
assert_eq!(d.julia, s.julia);
|
assert_eq!(d.julia, s.julia);
|
||||||
@@ -146,6 +150,7 @@ mod tests {
|
|||||||
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.palette, s.palette);
|
assert_eq!(d.palette, s.palette);
|
||||||
|
assert_eq!(d.shadow_palette, s.shadow_palette);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
use std::f32::consts::PI;
|
||||||
|
|
||||||
|
use bytemuck::{Pod, Zeroable};
|
||||||
|
use egui::{Color32, Ui};
|
||||||
|
|
||||||
|
/// Maximum number of simultaneous lights.
|
||||||
|
pub const MAX_LIGHT_COUNT: usize = 16;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Zeroable, Pod)]
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct Light {
|
||||||
|
pub azimuth: f32,
|
||||||
|
pub altitude: f32,
|
||||||
|
pub color: Color32,
|
||||||
|
pub _pad: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Light {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
azimuth: PI / 4.,
|
||||||
|
altitude: PI / 4.,
|
||||||
|
color: Color32::WHITE,
|
||||||
|
_pad: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Light {
|
||||||
|
pub fn widget(&mut self, ui: &mut Ui) -> bool {
|
||||||
|
let formater = |v, _| format!("{}°", ((v as f32 * 180. / PI) as u32));
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
let del = ui.button("-").clicked();
|
||||||
|
ui.label("color:");
|
||||||
|
ui.color_edit_button_srgba(&mut self.color);
|
||||||
|
ui.label("θ:");
|
||||||
|
ui.add(
|
||||||
|
egui::DragValue::new(&mut self.azimuth)
|
||||||
|
.range(0.0..=PI * 2.)
|
||||||
|
.custom_formatter(formater)
|
||||||
|
.speed(0.02),
|
||||||
|
);
|
||||||
|
ui.label("φ:");
|
||||||
|
ui.add(
|
||||||
|
egui::DragValue::new(&mut self.altitude)
|
||||||
|
.range(0.0..=PI / 2.)
|
||||||
|
.custom_formatter(formater)
|
||||||
|
.speed(0.02),
|
||||||
|
);
|
||||||
|
|
||||||
|
del
|
||||||
|
})
|
||||||
|
.inner
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
// and calls the wasm `main`, which boots eframe onto the page's <canvas>.
|
// and calls the wasm `main`, which boots eframe onto the page's <canvas>.
|
||||||
|
|
||||||
mod app;
|
mod app;
|
||||||
|
mod lights;
|
||||||
mod fractal;
|
mod fractal;
|
||||||
mod view;
|
mod view;
|
||||||
|
|
||||||
|
|||||||
+104
-14
@@ -20,32 +20,43 @@ struct Uniforms {
|
|||||||
bailout_sq: f32,
|
bailout_sq: f32,
|
||||||
is_julia: u32,
|
is_julia: u32,
|
||||||
palette_id: u32,
|
palette_id: u32,
|
||||||
|
shadow_palette_id: u32,
|
||||||
aa_level: u32,
|
aa_level: u32,
|
||||||
kind: u32,
|
kind: u32,
|
||||||
power: u32,
|
power: u32,
|
||||||
dc_offset: vec2<f32>,
|
dc_offset: vec2<f32>,
|
||||||
phoenix_p: vec2<f32>,
|
phoenix_p: vec2<f32>,
|
||||||
|
lambda_l: vec2<f32>,
|
||||||
de_coloring: u32,
|
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>;
|
||||||
|
|
||||||
// Smooth cyclic palettes (Inigo Quilez cosine palettes). Must match the palette
|
// Smooth cyclic palettes (Inigo Quilez cosine palettes). Must match the palette
|
||||||
// in mandelbrot.wgsl.
|
// in mandelbrot.wgsl.
|
||||||
fn palette(id: u32, t: f32) -> vec3<f32> {
|
fn palette(id: u32, t: f32) -> vec3<f32> {
|
||||||
if (id == 4u) {
|
if id == 4u {
|
||||||
return vec3<f32>(t, t, t); // grayscale
|
return vec3<f32>(t, t, t); // grayscale
|
||||||
}
|
}
|
||||||
let a = vec3<f32>(0.5, 0.5, 0.5);
|
let a = vec3<f32>(0.5, 0.5, 0.5);
|
||||||
let b = 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 c = vec3<f32>(1.0, 1.0, 1.0);
|
||||||
var d = vec3<f32>(0.00, 0.10, 0.20); // 0: amber / blue
|
var d = vec3<f32>(0.00, 0.10, 0.20); // 0: amber / blue
|
||||||
if (id == 1u) {
|
if id == 1u {
|
||||||
d = vec3<f32>(0.00, 0.33, 0.67); // rainbow
|
d = vec3<f32>(0.00, 0.33, 0.67); // rainbow
|
||||||
} else if (id == 2u) {
|
} else if id == 2u {
|
||||||
d = vec3<f32>(0.30, 0.20, 0.20); // warm ember
|
d = vec3<f32>(0.30, 0.20, 0.20); // warm ember
|
||||||
} else if (id == 3u) {
|
} else if id == 3u {
|
||||||
c = vec3<f32>(1.0, 1.0, 0.5);
|
c = vec3<f32>(1.0, 1.0, 0.5);
|
||||||
d = vec3<f32>(0.80, 0.90, 0.30); // lime / magenta
|
d = vec3<f32>(0.80, 0.90, 0.30); // lime / magenta
|
||||||
}
|
}
|
||||||
@@ -62,17 +73,96 @@ fn vs_main(@builtin(vertex_index) idx: u32) -> @builtin(position) vec4<f32> {
|
|||||||
return vec4<f32>(verts[idx], 0.0, 1.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> {
|
||||||
let d = textureLoad(data_tex, vec2<i32>(i32(pos.x), i32(pos.y)), 0);
|
if u.shadow != 0u {
|
||||||
let ci = d.r;
|
if textureLoad(data_tex, vec2<i32>(i32(pos.x), i32(pos.y)), 0).b != 0. {
|
||||||
let de = d.g;
|
return vec4<f32>(0.1, 0.1, 0.1, 1.0);
|
||||||
let interior_frac = d.b;
|
} 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 t = fract(ci * u.color_scale + u.color_offset);
|
let normal = normalize(cross(d[1] - d[0], d[2] - d[0]));
|
||||||
var col = palette(u.palette_id, t) * de;
|
|
||||||
// Anti-alias the set boundary: fade toward black by the fraction of the
|
var color: vec3<f32>;
|
||||||
// pixel's sub-samples that landed in the interior.
|
if u.shadow_palette_id == 0u {
|
||||||
col = col * (1.0 - interior_frac);
|
color = compute_light(normal,vec3<f32>(.5, .5, .5)) + vec3<f32>(0.58, 0.85, 1.) * 0.2;
|
||||||
return vec4<f32>(col, 1.0);
|
|
||||||
|
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 {
|
||||||
|
let d = textureLoad(data_tex, vec2<i32>(i32(pos.x), i32(pos.y)), 0);
|
||||||
|
let ci = d.r;
|
||||||
|
let de = d.g;
|
||||||
|
let interior_frac = d.b;
|
||||||
|
|
||||||
|
let t = fract(ci * u.color_scale + u.color_offset);
|
||||||
|
var col = palette(u.palette_id, t) * de;
|
||||||
|
// Anti-alias the set boundary: fade toward black by the fraction of the
|
||||||
|
// pixel's sub-samples that landed in the interior.
|
||||||
|
col = col * (1.0 - interior_frac);
|
||||||
|
return vec4<f32>(col, 1.0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ struct Uniforms {
|
|||||||
bailout_sq: f32,
|
bailout_sq: f32,
|
||||||
is_julia: u32,
|
is_julia: u32,
|
||||||
palette_id: u32,
|
palette_id: u32,
|
||||||
|
shadow_palette_id: u32,
|
||||||
aa_level: u32,
|
aa_level: u32,
|
||||||
// Iteration formula (see the KIND_* constants below).
|
// Iteration formula (see the KIND_* constants below).
|
||||||
kind: u32,
|
kind: u32,
|
||||||
@@ -35,6 +36,8 @@ struct Uniforms {
|
|||||||
lambda_l: vec2<f32>,
|
lambda_l: vec2<f32>,
|
||||||
// 0 = escape-time coloring, 1 = distance-estimation shading.
|
// 0 = escape-time coloring, 1 = distance-estimation shading.
|
||||||
de_coloring: u32,
|
de_coloring: u32,
|
||||||
|
// 0 = classic colors, 1 = shadows
|
||||||
|
shadow: u32,
|
||||||
};
|
};
|
||||||
|
|
||||||
const KIND_MANDELBROT: u32 = 0u;
|
const KIND_MANDELBROT: u32 = 0u;
|
||||||
@@ -158,7 +161,7 @@ fn advance_delta(z: vec2<f32>, e: vec2<f32>) -> vec2<f32> {
|
|||||||
return vec2<f32>(sq.x, -2.0 * (z.x * da + e.x * abs_yf));
|
return vec2<f32>(sq.x, -2.0 * (z.x * da + e.x * abs_yf));
|
||||||
} else if u.kind == KIND_LAMBDA {
|
} else if u.kind == KIND_LAMBDA {
|
||||||
// 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));
|
||||||
}
|
}
|
||||||
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)
|
||||||
@@ -179,7 +182,7 @@ fn fprime(z: vec2<f32>) -> vec2<f32> {
|
|||||||
return f32(p) * zk;
|
return f32(p) * zk;
|
||||||
} 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));
|
||||||
}
|
}
|
||||||
return 2.0 * z;
|
return 2.0 * z;
|
||||||
}
|
}
|
||||||
@@ -326,7 +329,11 @@ fn iterate_sample(offset: vec2<f32>, px: f32) -> Sample {
|
|||||||
let zmag = sqrt(max(z2, 1.0));
|
let zmag = sqrt(max(z2, 1.0));
|
||||||
let dzmag = sqrt(max(dot(dz, dz), 1e-20));
|
let dzmag = sqrt(max(dot(dz, dz), 1e-20));
|
||||||
let d = zmag * log(zmag) / dzmag;
|
let d = zmag * log(zmag) / dzmag;
|
||||||
de = clamp(d / max(px, 1e-30), 0.0, 1.0);
|
var max_de = 1.;
|
||||||
|
if u.shadow != 0u {
|
||||||
|
max_de = 1000.;
|
||||||
|
}
|
||||||
|
de = clamp(d / max(px, 1e-30), 0.0, max_de);
|
||||||
}
|
}
|
||||||
return Sample(ci, de, true);
|
return Sample(ci, de, true);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user