feat: add shadow coloring

This commit is contained in:
2026-09-19 20:50:17 +02:00
parent 7c347a5abf
commit 908c13bfb0
8 changed files with 290 additions and 44 deletions
+38 -3
View File
@@ -13,6 +13,8 @@ use std::sync::Arc;
use eframe::egui_wgpu::{self, wgpu};
use crate::lights::{Light, MAX_LIGHT_COUNT};
/// Maximum reference-orbit length (points) the storage buffer can hold. Also
/// bounds the iteration count. 128k points * 8 bytes = 1 MiB.
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_scale != b.color_scale
|| 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
@@ -64,12 +68,14 @@ pub struct Uniforms {
/// 0 = Mandelbrot, 1 = Julia.
pub is_julia: u32,
pub palette_id: u32,
pub shadow_palette_id: u32,
/// Supersampling factor per axis: 1 = off, 2 = 2×2 (4 samples).
pub aa_level: u32,
/// Iteration formula (`FractalKind::shader_id`).
pub kind: u32,
/// Exponent for the Multibrot kind.
pub power: u32,
pub _pad: [u32; 1],
/// Complex offset of the view center from the reference center, so a stale
/// or reused reference (computed at a slightly different center) still maps
/// correctly. Added to every pixel's per-pixel offset.
@@ -83,8 +89,8 @@ pub struct Uniforms {
pub lambda_l: [f32; 2],
/// 0 = escape-time coloring, 1 = distance-estimation shading.
pub de_coloring: u32,
/// Padding to a 16-byte multiple (uniform buffer requirement).
pub _pad: [u32; 1],
// 0 = classic colors, 1 = shadows
pub shadow: u32,
}
/// Offscreen textures for the two-pass render, recreated whenever the widget's
@@ -128,6 +134,7 @@ pub struct FractalRenderer {
bind_group_layout: wgpu::BindGroupLayout,
uniform_buffer: wgpu::Buffer,
ref_buffer: wgpu::Buffer,
lights_buffer: wgpu::Buffer,
bind_group: wgpu::BindGroup,
target_format: wgpu::TextureFormat,
/// Generation of the reference orbit currently uploaded to `ref_buffer`.
@@ -170,6 +177,13 @@ impl FractalRenderer {
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 {
label: Some("fractal bind group layout"),
entries: &[
@@ -301,6 +315,16 @@ impl FractalRenderer {
},
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 =
@@ -409,6 +433,7 @@ impl FractalRenderer {
bind_group_layout,
uniform_buffer,
ref_buffer,
lights_buffer,
bind_group,
target_format,
uploaded_generation: u64::MAX,
@@ -477,6 +502,10 @@ impl FractalRenderer {
binding: 1,
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`).
pub struct FractalCallback {
pub uniforms: Uniforms,
pub lights: Vec<Light>,
pub reference: Arc<Vec<[f32; 2]>>,
pub generation: u64,
/// Widget size in physical pixels — the cache texture resolution.
@@ -827,7 +857,8 @@ impl egui_wgpu::CallbackTrait for FractalCallback {
let color_dirty = iter_dirty
|| renderer.colored.as_ref().is_none_or(|c| {
c.width != width || c.height != height || color_differs(&c.uniforms, &self.uniforms)
});
})
|| true;
if !color_dirty {
return Vec::new(); // cache still valid; paint() just blits it
@@ -839,6 +870,10 @@ impl egui_wgpu::CallbackTrait for FractalCallback {
0,
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 iter_dirty {
+5
View File
@@ -29,6 +29,8 @@ pub struct ShareState {
pub color_offset: f32,
/// Palette index (`palette_id` in the shader).
pub palette: u32,
/// Shadow palette index (`shadow_palette_id` in the shader).
pub shadow_palette: u32,
}
impl ShareState {
@@ -110,6 +112,7 @@ impl ShareState {
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),
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_offset: 0.25,
palette: 3,
shadow_palette: 1,
};
let d = ShareState::decode(&s.encode()).unwrap();
assert_eq!(d.julia, s.julia);
@@ -146,6 +150,7 @@ mod tests {
assert_eq!(d.julia_c, s.julia_c);
assert_eq!(d.phoenix_p, s.phoenix_p);
assert_eq!(d.palette, s.palette);
assert_eq!(d.shadow_palette, s.shadow_palette);
}
#[test]