Compare commits
2
Commits
b6467fc18c
...
8750bb1590
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8750bb1590 | ||
|
|
f30921f7a5 |
+358
-50
@@ -13,7 +13,9 @@ use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use eframe::egui_wgpu::{self, wgpu};
|
||||
use wgpu::util::DeviceExt as _;
|
||||
|
||||
use super::kind::FractalKind;
|
||||
use super::reference::RefOrbit;
|
||||
use crate::lights::{GpuLight, Light, MAX_LIGHT_COUNT, gpu_lights};
|
||||
|
||||
@@ -159,6 +161,240 @@ fn fullscreen_pipeline(
|
||||
})
|
||||
}
|
||||
|
||||
/// Stride between the per-pass step uniforms in [`Lipschitz::steps`]
|
||||
/// (WebGPU's minimum uniform-buffer offset alignment).
|
||||
const LIPSCHITZ_STRIDE: u32 = 256;
|
||||
/// Step uniforms held: slot k holds step 2^k, enough for any texture size.
|
||||
const LIPSCHITZ_SLOTS: u32 = 32;
|
||||
|
||||
/// Whether the shadow / 3D view of `u` rebuilds its DE height field as a
|
||||
/// distance field (see `lipschitz.wgsl`): only Complex Multibrot, whose
|
||||
/// branch cut makes the DE jump (seams in shadow, walls in 3D). Every other
|
||||
/// kind and classic colouring keep the DE as is.
|
||||
fn wants_envelope(u: &Uniforms) -> bool {
|
||||
let cm = FractalKind::ComplexMultibrot as u32;
|
||||
u.rendering_mode != 0 && (u.kind == cm || (u.morph_w > 0.0 && u.morph_from == cm))
|
||||
}
|
||||
|
||||
/// The distance-field passes (`lipschitz.wgsl`): seed, jump-flood and
|
||||
/// compose pipelines, their shared input layout (data texture, seed texture,
|
||||
/// step uniform at a dynamic offset) and the buffer of every pass's step.
|
||||
#[derive(Clone)]
|
||||
struct Lipschitz {
|
||||
seed: wgpu::RenderPipeline,
|
||||
jump: wgpu::RenderPipeline,
|
||||
compose: wgpu::RenderPipeline,
|
||||
layout: wgpu::BindGroupLayout,
|
||||
steps: wgpu::Buffer,
|
||||
}
|
||||
|
||||
impl Lipschitz {
|
||||
fn new(device: &wgpu::Device) -> Self {
|
||||
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("lipschitz"),
|
||||
source: wgpu::ShaderSource::Wgsl(
|
||||
concat!(
|
||||
include_str!("../shaders/common.wgsl"),
|
||||
include_str!("../shaders/lipschitz.wgsl"),
|
||||
)
|
||||
.into(),
|
||||
),
|
||||
});
|
||||
let texture_entry = |binding| wgpu::BindGroupLayoutEntry {
|
||||
binding,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Texture {
|
||||
sample_type: wgpu::TextureSampleType::Float { filterable: false },
|
||||
view_dimension: wgpu::TextureViewDimension::D2,
|
||||
multisampled: false,
|
||||
},
|
||||
count: None,
|
||||
};
|
||||
let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("lipschitz bind group layout"),
|
||||
entries: &[
|
||||
texture_entry(0),
|
||||
texture_entry(1),
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 2,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Uniform,
|
||||
has_dynamic_offset: true,
|
||||
min_binding_size: wgpu::BufferSize::new(16),
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
],
|
||||
});
|
||||
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("lipschitz pipeline layout"),
|
||||
bind_group_layouts: &[Some(&layout)],
|
||||
immediate_size: 0,
|
||||
});
|
||||
let pipeline = |label, entry| {
|
||||
fullscreen_pipeline(device, label, &module, &pipeline_layout, entry, DATA_FORMAT, &[])
|
||||
};
|
||||
let mut contents = vec![0u8; (LIPSCHITZ_STRIDE * LIPSCHITZ_SLOTS) as usize];
|
||||
for k in 0..LIPSCHITZ_SLOTS {
|
||||
let at = (k * LIPSCHITZ_STRIDE) as usize;
|
||||
contents[at..at + 4].copy_from_slice(&(1i32 << k.min(30)).to_ne_bytes());
|
||||
}
|
||||
let steps = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("lipschitz steps"),
|
||||
contents: &contents,
|
||||
usage: wgpu::BufferUsages::UNIFORM,
|
||||
});
|
||||
Self {
|
||||
seed: pipeline("lipschitz seed pipeline", "fs_seed"),
|
||||
jump: pipeline("lipschitz jump pipeline", "fs_jump"),
|
||||
compose: pipeline("lipschitz compose pipeline", "fs_compose"),
|
||||
layout,
|
||||
steps,
|
||||
}
|
||||
}
|
||||
|
||||
/// A pass input reading the data texture `data` and seed texture `seeds`.
|
||||
fn bind_group(
|
||||
&self,
|
||||
device: &wgpu::Device,
|
||||
data: &wgpu::TextureView,
|
||||
seeds: &wgpu::TextureView,
|
||||
) -> wgpu::BindGroup {
|
||||
device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("lipschitz bind group"),
|
||||
layout: &self.layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: wgpu::BindingResource::TextureView(data),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::TextureView(seeds),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 2,
|
||||
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
|
||||
buffer: &self.steps,
|
||||
offset: 0,
|
||||
size: wgpu::BufferSize::new(16),
|
||||
}),
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Targets for the distance field of one data texture: two ping-pong seed
|
||||
/// textures for jump flooding, and the output (data with the rebuilt DE).
|
||||
struct Envelope {
|
||||
/// `[seeds A, seeds B, output]`, kept so they can be `destroy()`ed with
|
||||
/// the rest of the cache.
|
||||
textures: [wgpu::Texture; 3],
|
||||
views: [wgpu::TextureView; 3],
|
||||
/// Pass inputs: the data texture with seeds A, and with seeds B.
|
||||
inputs: [wgpu::BindGroup; 2],
|
||||
/// Step slot of each jump pass, in order (see [`lipschitz_slots`]).
|
||||
slots: Vec<u32>,
|
||||
}
|
||||
|
||||
impl Envelope {
|
||||
fn new(
|
||||
device: &wgpu::Device,
|
||||
lp: &Lipschitz,
|
||||
data: &wgpu::TextureView,
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> Self {
|
||||
let make = |label| {
|
||||
device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some(label),
|
||||
size: wgpu::Extent3d {
|
||||
width,
|
||||
height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: DATA_FORMAT,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
|
||||
| wgpu::TextureUsages::TEXTURE_BINDING,
|
||||
view_formats: &[],
|
||||
})
|
||||
};
|
||||
let textures = [
|
||||
make("DE distance seeds A"),
|
||||
make("DE distance seeds B"),
|
||||
make("DE distance field"),
|
||||
];
|
||||
let views = textures
|
||||
.each_ref()
|
||||
.map(|t| t.create_view(&wgpu::TextureViewDescriptor::default()));
|
||||
let inputs = [
|
||||
lp.bind_group(device, data, &views[0]),
|
||||
lp.bind_group(device, data, &views[1]),
|
||||
];
|
||||
Self {
|
||||
textures,
|
||||
views,
|
||||
inputs,
|
||||
slots: lipschitz_slots(width, height),
|
||||
}
|
||||
}
|
||||
|
||||
/// The data texture with the rebuilt DE.
|
||||
fn output(&self) -> &wgpu::TextureView {
|
||||
&self.views[2]
|
||||
}
|
||||
|
||||
/// Record seed → jump passes → compose into [`Self::output`].
|
||||
fn record(&self, encoder: &mut wgpu::CommandEncoder, lp: &Lipschitz) {
|
||||
let pass = |encoder: &mut wgpu::CommandEncoder,
|
||||
target: &wgpu::TextureView,
|
||||
pipeline: &wgpu::RenderPipeline,
|
||||
input: &wgpu::BindGroup,
|
||||
slot: u32| {
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("lipschitz pass"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: target,
|
||||
depth_slice: None,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
multiview_mask: None,
|
||||
});
|
||||
pass.set_pipeline(pipeline);
|
||||
pass.set_bind_group(0, input, &[slot * LIPSCHITZ_STRIDE]);
|
||||
pass.draw(0..3, 0..1);
|
||||
};
|
||||
// Seeds into A (the bound seed texture, B, is unread).
|
||||
pass(encoder, &self.views[0], &lp.seed, &self.inputs[1], 0);
|
||||
// Pass i reads seeds i % 2 and writes the other.
|
||||
for (i, &slot) in self.slots.iter().enumerate() {
|
||||
pass(encoder, &self.views[(i + 1) % 2], &lp.jump, &self.inputs[i % 2], slot);
|
||||
}
|
||||
let last = self.slots.len() % 2;
|
||||
pass(encoder, &self.views[2], &lp.compose, &self.inputs[last], 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Jump-flooding step slots (step = 2^slot) for a `width`×`height` texture:
|
||||
/// from about half its larger side down to 1, then one more step-1 pass,
|
||||
/// which fixes most of jump flooding's residual errors.
|
||||
fn lipschitz_slots(width: u32, height: u32) -> Vec<u32> {
|
||||
let top = (width.max(height) / 2).max(1).ilog2();
|
||||
(0..=top).rev().chain(std::iter::once(0)).collect()
|
||||
}
|
||||
|
||||
/// The interactive iteration pipelines for one [`PipelineKey`].
|
||||
struct IteratePipelines {
|
||||
/// 1-spp perturbation iterate → data texture (`fs_data`).
|
||||
@@ -253,6 +489,10 @@ struct CacheTarget {
|
||||
/// Refine pass output + the colourise bind group reading it. Only
|
||||
/// allocated while AA is on: it's a second full-size `Rgba32Float`.
|
||||
aa: Option<(wgpu::TextureView, wgpu::BindGroup)>,
|
||||
/// The distance-field rebuild of the refined (or 1-spp) data + the
|
||||
/// colourise bind group reading it. Allocated on first use, only for
|
||||
/// shadow / 3D Complex Multibrot (see [`wants_envelope`]).
|
||||
envelope: Option<(Envelope, wgpu::BindGroup)>,
|
||||
/// Blit pass input: the colour texture + sampler.
|
||||
blit_bind_group: wgpu::BindGroup,
|
||||
width: u32,
|
||||
@@ -302,6 +542,12 @@ pub struct FractalRenderer {
|
||||
/// Colourise pass: data texture → colour texture (palette mapping).
|
||||
colorize_pipeline: wgpu::RenderPipeline,
|
||||
colorize_bind_group_layout: wgpu::BindGroupLayout,
|
||||
/// Distance-field passes for shadow / 3D Complex Multibrot (see
|
||||
/// [`wants_envelope`]).
|
||||
lipschitz: Lipschitz,
|
||||
/// Whether the cache's envelope matches the current data texture. Not
|
||||
/// implied by iteration: switching shadow → 3D doesn't re-iterate.
|
||||
envelope_valid: bool,
|
||||
|
||||
/// Blit pipeline + resources that copy the colour texture to egui's surface.
|
||||
blit_pipeline: wgpu::RenderPipeline,
|
||||
@@ -633,6 +879,8 @@ impl FractalRenderer {
|
||||
uploaded_lights: None,
|
||||
colorize_pipeline,
|
||||
colorize_bind_group_layout,
|
||||
lipschitz: Lipschitz::new(device),
|
||||
envelope_valid: false,
|
||||
blit_pipeline,
|
||||
blit_bind_group_layout,
|
||||
blit_sampler,
|
||||
@@ -662,7 +910,8 @@ impl FractalRenderer {
|
||||
// Any commands using them were submitted on earlier frames, and
|
||||
// `destroy()` defers the actual free until those finish.
|
||||
if let Some(old) = self.cache.take() {
|
||||
for t in &old.textures {
|
||||
let envelope = old.envelope.iter().flat_map(|e| &e.0.textures);
|
||||
for t in old.textures.iter().chain(envelope) {
|
||||
t.destroy();
|
||||
}
|
||||
}
|
||||
@@ -715,24 +964,13 @@ impl FractalRenderer {
|
||||
let color_view = color_texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
|
||||
let colorize_bind_group_for = |data: &wgpu::TextureView| {
|
||||
device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("colorize bind group"),
|
||||
layout: &self.colorize_bind_group_layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: self.uniform_buffer.as_entire_binding(),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::TextureView(data),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 2,
|
||||
resource: self.lights_buffer.as_entire_binding(),
|
||||
},
|
||||
],
|
||||
})
|
||||
colorize_bind_group(
|
||||
device,
|
||||
&self.colorize_bind_group_layout,
|
||||
&self.uniform_buffer,
|
||||
&self.lights_buffer,
|
||||
data,
|
||||
)
|
||||
};
|
||||
let colorize_bind_group = colorize_bind_group_for(&data_view);
|
||||
let aa_target = data_aa_texture.as_ref().map(|t| {
|
||||
@@ -775,6 +1013,7 @@ impl FractalRenderer {
|
||||
refine_bind_group,
|
||||
colorize_bind_group,
|
||||
aa: aa_target,
|
||||
envelope: None,
|
||||
blit_bind_group,
|
||||
width,
|
||||
height,
|
||||
@@ -818,7 +1057,8 @@ impl FractalRenderer {
|
||||
/// lock on the renderer), its bind-group layout, and the target format.
|
||||
/// In 3D mode (`rendering_mode == 2`) also the interactive iterate →
|
||||
/// refine → colourise chain, since the raymarcher needs a whole data
|
||||
/// texture to march over and `fs_color` has no 3D path.
|
||||
/// texture to march over and `fs_color` has no 3D path. Likewise for
|
||||
/// the distance field ([`wants_envelope`]), which needs the whole image.
|
||||
pub fn export_handles(&self, device: &wgpu::Device, uniforms: &Uniforms) -> ExportHandles {
|
||||
let constants = PipelineKey::from_uniforms(uniforms).constants();
|
||||
let pipeline = fullscreen_pipeline(
|
||||
@@ -830,7 +1070,8 @@ impl FractalRenderer {
|
||||
self.target_format,
|
||||
&constants,
|
||||
);
|
||||
let raymarch = (uniforms.rendering_mode == 2).then(|| RaymarchHandles {
|
||||
let chain = uniforms.rendering_mode == 2 || wants_envelope(uniforms);
|
||||
let raymarch = chain.then(|| RaymarchHandles {
|
||||
iterate: fullscreen_pipeline(
|
||||
device,
|
||||
"fractal export iterate pipeline",
|
||||
@@ -852,6 +1093,7 @@ impl FractalRenderer {
|
||||
colorize: self.colorize_pipeline.clone(),
|
||||
refine_bind_group_layout: self.refine_bind_group_layout.clone(),
|
||||
colorize_bind_group_layout: self.colorize_bind_group_layout.clone(),
|
||||
lipschitz: wants_envelope(uniforms).then(|| self.lipschitz.clone()),
|
||||
});
|
||||
ExportHandles {
|
||||
pipeline,
|
||||
@@ -870,11 +1112,11 @@ pub struct ExportHandles {
|
||||
pipeline: wgpu::RenderPipeline,
|
||||
bind_group_layout: wgpu::BindGroupLayout,
|
||||
format: wgpu::TextureFormat,
|
||||
/// The two-pass chain, for 3D mode only.
|
||||
/// The two-pass chain, for 3D mode and the distance field only.
|
||||
raymarch: Option<RaymarchHandles>,
|
||||
}
|
||||
|
||||
/// The interactive two-pass pipelines, for a 3D export.
|
||||
/// The interactive two-pass pipelines, for a 3D or distance-field export.
|
||||
#[derive(Clone)]
|
||||
struct RaymarchHandles {
|
||||
iterate: wgpu::RenderPipeline,
|
||||
@@ -882,15 +1124,19 @@ struct RaymarchHandles {
|
||||
colorize: wgpu::RenderPipeline,
|
||||
refine_bind_group_layout: wgpu::BindGroupLayout,
|
||||
colorize_bind_group_layout: wgpu::BindGroupLayout,
|
||||
/// The distance-field passes, when [`wants_envelope`].
|
||||
lipschitz: Option<Lipschitz>,
|
||||
}
|
||||
|
||||
/// A 3D export's own data textures and the passes that fill them: the tiles
|
||||
/// iterate into `data_view`, then one refine (if AA) + colourise pass
|
||||
/// raymarches the finished height field into the export target.
|
||||
/// A 3D (or distance-field shadow) export's own data textures and the passes
|
||||
/// that fill them: the tiles iterate into `data_view`, then one refine (if
|
||||
/// AA), the distance-field rebuild (if wanted) and a colourise pass render
|
||||
/// the finished height field into the export target.
|
||||
struct RaymarchExport {
|
||||
iterate: wgpu::RenderPipeline,
|
||||
/// Refine pipeline, output view and input bind group, when AA is on.
|
||||
refine: Option<(wgpu::RenderPipeline, wgpu::TextureView, wgpu::BindGroup)>,
|
||||
envelope: Option<(Lipschitz, Envelope)>,
|
||||
colorize: wgpu::RenderPipeline,
|
||||
colorize_bind_group: wgpu::BindGroup,
|
||||
data_view: wgpu::TextureView,
|
||||
@@ -914,7 +1160,8 @@ pub struct ExportRender {
|
||||
/// Number of horizontal tiles the render is split into.
|
||||
pub tiles: u32,
|
||||
pub swap_rb: bool,
|
||||
/// 3D mode: tiles fill a data texture instead of the target.
|
||||
/// 3D mode or the distance field: tiles fill a data texture instead of
|
||||
/// the target.
|
||||
raymarch: Option<RaymarchExport>,
|
||||
}
|
||||
|
||||
@@ -1066,29 +1313,25 @@ impl ExportRender {
|
||||
refine_bind_group,
|
||||
)
|
||||
});
|
||||
// Colourise reads the refined texture when AA is on.
|
||||
let colorize_input = refine.as_ref().map_or(&data_view, |(_, v, _)| v);
|
||||
let colorize_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("export colorize bind group"),
|
||||
layout: &rm.colorize_bind_group_layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: uniform_buffer.as_entire_binding(),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::TextureView(colorize_input),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 2,
|
||||
resource: lights_buffer.as_entire_binding(),
|
||||
},
|
||||
],
|
||||
// The distance field reads the refined texture when AA is on,
|
||||
// and colourise reads the distance field, else the same texture.
|
||||
let refined = refine.as_ref().map_or(&data_view, |(_, v, _)| v);
|
||||
let envelope = rm.lipschitz.as_ref().map(|lp| {
|
||||
let env = Envelope::new(device, lp, refined, width, height);
|
||||
(lp.clone(), env)
|
||||
});
|
||||
let colorize_input = envelope.as_ref().map_or(refined, |(_, e)| e.output());
|
||||
let colorize_bind_group = colorize_bind_group(
|
||||
device,
|
||||
&rm.colorize_bind_group_layout,
|
||||
&uniform_buffer,
|
||||
&lights_buffer,
|
||||
colorize_input,
|
||||
);
|
||||
RaymarchExport {
|
||||
iterate: rm.iterate.clone(),
|
||||
refine,
|
||||
envelope,
|
||||
colorize: rm.colorize.clone(),
|
||||
colorize_bind_group,
|
||||
data_view,
|
||||
@@ -1120,8 +1363,9 @@ impl ExportRender {
|
||||
|
||||
/// Render one horizontal tile into the export texture and submit it. Tile 0
|
||||
/// clears the whole attachment; later tiles preserve earlier ones. In 3D
|
||||
/// mode the tiles iterate into the data texture instead, and the last one
|
||||
/// also runs the (whole-image) refine + raymarching colourise passes.
|
||||
/// mode (or with the distance field) the tiles iterate into the data
|
||||
/// texture instead, and the last one also runs the whole-image refine,
|
||||
/// distance-field and colourise passes.
|
||||
pub fn render_tile(&self, device: &wgpu::Device, queue: &wgpu::Queue, t: u32) {
|
||||
let (y0, y1) = self.tile_rows(t);
|
||||
if y1 <= y0 {
|
||||
@@ -1176,6 +1420,9 @@ impl ExportRender {
|
||||
&[&self.bind_group, refine_bind_group],
|
||||
);
|
||||
}
|
||||
if let Some((lp, env)) = &rm.envelope {
|
||||
env.record(&mut encoder, lp);
|
||||
}
|
||||
data_pass(
|
||||
&mut encoder,
|
||||
"export colorize pass",
|
||||
@@ -1399,6 +1646,35 @@ pub fn encode_png_with_progress(
|
||||
out
|
||||
}
|
||||
|
||||
/// Colourise pass input: the uniforms, the data texture `data` to colour and
|
||||
/// the lights.
|
||||
fn colorize_bind_group(
|
||||
device: &wgpu::Device,
|
||||
layout: &wgpu::BindGroupLayout,
|
||||
uniforms: &wgpu::Buffer,
|
||||
lights: &wgpu::Buffer,
|
||||
data: &wgpu::TextureView,
|
||||
) -> wgpu::BindGroup {
|
||||
device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("colorize bind group"),
|
||||
layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: uniforms.as_entire_binding(),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::TextureView(data),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 2,
|
||||
resource: lights.as_entire_binding(),
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
/// Record one fullscreen-triangle pass drawing `pipeline` into `target`
|
||||
/// (cleared first), with `bind_groups` bound to groups 0, 1, ...
|
||||
fn data_pass(
|
||||
@@ -1534,6 +1810,25 @@ impl egui_wgpu::CallbackTrait for FractalCallback {
|
||||
if iter_dirty {
|
||||
renderer.ensure_pipelines(device, PipelineKey::from_uniforms(&self.uniforms));
|
||||
}
|
||||
let envelope = wants_envelope(&self.uniforms);
|
||||
if envelope
|
||||
&& let Some(cache) = &renderer.cache
|
||||
&& cache.envelope.is_none()
|
||||
{
|
||||
let src = cache.aa.as_ref().map_or(&cache.data_view, |(v, _)| v);
|
||||
let env = Envelope::new(device, &renderer.lipschitz, src, cache.width, cache.height);
|
||||
let bind_group = colorize_bind_group(
|
||||
device,
|
||||
&renderer.colorize_bind_group_layout,
|
||||
&renderer.uniform_buffer,
|
||||
&renderer.lights_buffer,
|
||||
env.output(),
|
||||
);
|
||||
if let Some(cache) = &mut renderer.cache {
|
||||
cache.envelope = Some((env, bind_group));
|
||||
}
|
||||
}
|
||||
let mut envelope_ran = false;
|
||||
let pipelines = &renderer.pipelines[&PipelineKey::from_uniforms(&self.uniforms)];
|
||||
if let Some(cache) = &renderer.cache {
|
||||
if iter_dirty {
|
||||
@@ -1557,11 +1852,23 @@ impl egui_wgpu::CallbackTrait for FractalCallback {
|
||||
}
|
||||
}
|
||||
|
||||
// Shadow / 3D Complex Multibrot: rebuild the DE as a distance field.
|
||||
let env = cache.envelope.as_ref().filter(|_| envelope);
|
||||
if let Some((env, _)) = env
|
||||
&& (iter_dirty || !renderer.envelope_valid)
|
||||
{
|
||||
env.record(egui_encoder, &renderer.lipschitz);
|
||||
envelope_ran = true;
|
||||
}
|
||||
|
||||
// Colourise pass: data texture → colour texture.
|
||||
let colorize_bind_group = cache
|
||||
let colorize_bind_group = match env {
|
||||
Some((_, bg)) => bg,
|
||||
None => cache
|
||||
.aa
|
||||
.as_ref()
|
||||
.map_or(&cache.colorize_bind_group, |(_, bg)| bg);
|
||||
.map_or(&cache.colorize_bind_group, |(_, bg)| bg),
|
||||
};
|
||||
data_pass(
|
||||
egui_encoder,
|
||||
"fractal colorize pass",
|
||||
@@ -1571,6 +1878,7 @@ impl egui_wgpu::CallbackTrait for FractalCallback {
|
||||
);
|
||||
}
|
||||
|
||||
renderer.envelope_valid = envelope_ran || (renderer.envelope_valid && !iter_dirty);
|
||||
if iter_dirty {
|
||||
renderer.iterated = Some(IterState {
|
||||
uniforms: self.uniforms,
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
// Distance field for the shadow and 3D views of kinds whose map is
|
||||
// discontinuous (Complex Multibrot's principal-branch z^p). The per-pixel
|
||||
// DE, |z|·ln|z|/|dz| = G/|G'|, is only a distance when the potential G is
|
||||
// continuous. Across a branch-cut preimage G jumps (|z| jumps by
|
||||
// e^(2π·Im p)), each side's DE describes the set as continued on its own
|
||||
// branch: seams in shadow mode, walls in the raymarched terrain. A true
|
||||
// distance to the set is continuous, so this rebuilds one from the pixels that are unambiguously
|
||||
// next to the set ("seeds": interior texels, and exterior texels whose DE is
|
||||
// below SEED_DE, i.e. within about a pixel of it):
|
||||
// h(x) = min over seeds y of DE(y) + DIST_SLOPE·|x - y|
|
||||
// computed by jump flooding: `fs_seed` marks seeds, `fs_jump` runs with
|
||||
// `step` halving from about half the texture size down to 1 (plus one more
|
||||
// step-1 pass), each texel keeping the best seed among itself and its 8
|
||||
// neighbours at ±step, and `fs_compose` writes the result, eased by
|
||||
// `ease_distance`, as the data texture's G channel. Seeds are carried as coordinates, so the distance is
|
||||
// Euclidean rather than an 8-direction chamfer approximation.
|
||||
//
|
||||
// The set only counts where it's in view: next to the image edge, set just
|
||||
// outside it is missed and the height there reads a bit high.
|
||||
//
|
||||
// Texel layout of the seed textures: (seed x, seed y, seed DE, 1), or all 0
|
||||
// for "no seed yet". R (palette parameter) and B (interior fraction) of the
|
||||
// data texture pass through untouched.
|
||||
|
||||
struct LipschitzStep {
|
||||
step: i32,
|
||||
_pad0: i32,
|
||||
_pad1: i32,
|
||||
_pad2: i32,
|
||||
};
|
||||
|
||||
@group(0) @binding(0) var data_tex: texture_2d<f32>;
|
||||
@group(0) @binding(1) var seed_tex: texture_2d<f32>;
|
||||
@group(0) @binding(2) var<uniform> ls: LipschitzStep;
|
||||
|
||||
// DE units per texel of a true distance: DE is in units of `pixel_size`,
|
||||
// the texel's (|dx| + |dy|) footprint, i.e. sqrt(2) texels.
|
||||
const DIST_SLOPE: f32 = 0.70710678;
|
||||
// Exterior texels with a DE below this (in DE units) are seeds.
|
||||
const SEED_DE: f32 = 1.0;
|
||||
|
||||
@vertex
|
||||
fn vs_main(@builtin(vertex_index) idx: u32) -> @builtin(position) vec4<f32> {
|
||||
return vec4<f32>(fullscreen_triangle_pos(idx), 0.0, 1.0);
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_seed(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
|
||||
let p = vec2<i32>(pos.xy);
|
||||
let d = textureLoad(data_tex, p, 0);
|
||||
if d.b != 0.0 {
|
||||
return vec4<f32>(vec2<f32>(p), 0.0, 1.0);
|
||||
}
|
||||
if d.g < SEED_DE {
|
||||
return vec4<f32>(vec2<f32>(p), d.g, 1.0);
|
||||
}
|
||||
return vec4<f32>(0.0);
|
||||
}
|
||||
|
||||
// Height at `p` through seed `s` (a seed texel), or +inf for "no seed".
|
||||
fn through(p: vec2<i32>, s: vec4<f32>) -> f32 {
|
||||
if s.a == 0.0 {
|
||||
return 3.0e38;
|
||||
}
|
||||
return s.z + DIST_SLOPE * distance(vec2<f32>(p), s.xy);
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_jump(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
|
||||
let p = vec2<i32>(pos.xy);
|
||||
let hi = vec2<i32>(textureDimensions(seed_tex)) - vec2<i32>(1, 1);
|
||||
let st = ls.step;
|
||||
var best = textureLoad(seed_tex, p, 0);
|
||||
var best_h = through(p, best);
|
||||
for (var dy = -1; dy <= 1; dy++) {
|
||||
for (var dx = -1; dx <= 1; dx++) {
|
||||
if dx == 0 && dy == 0 {
|
||||
continue;
|
||||
}
|
||||
let q = p + st * vec2<i32>(dx, dy);
|
||||
if any(q < vec2<i32>(0, 0)) || any(q > hi) {
|
||||
continue;
|
||||
}
|
||||
let s = textureLoad(seed_tex, q, 0);
|
||||
let h = through(p, s);
|
||||
if h < best_h {
|
||||
best = s;
|
||||
best_h = h;
|
||||
}
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
// Scale of `ease_distance`, in screen heights.
|
||||
const EASE_K: f32 = 0.02;
|
||||
|
||||
// Concave easing of the distance field `h` (DE units, `size_y` texels
|
||||
// tall): K·ln(1 + x/K) in screen heights, the unit `sdf` in colorize.wgsl
|
||||
// reads. Slope 1 at the set, flattening far from it, so the terrain rises
|
||||
// steeply out of the set and then levels off. The slope never exceeds 1,
|
||||
// so the field stays a valid (conservative) distance for the sphere tracer.
|
||||
fn ease_distance(h: f32, size_y: f32) -> f32 {
|
||||
return EASE_K * log(1.0 + h / (size_y * EASE_K)) * size_y;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_compose(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
|
||||
let p = vec2<i32>(pos.xy);
|
||||
let d = textureLoad(data_tex, p, 0);
|
||||
if d.b != 0.0 {
|
||||
return d;
|
||||
}
|
||||
// Not min(h, DE): that keeps the too-low side of every jump, so the
|
||||
// walls come back as shards.
|
||||
let h = through(p, textureLoad(seed_tex, p, 0));
|
||||
if h >= 3.0e38 {
|
||||
return d; // no set anywhere in view: keep the DE
|
||||
}
|
||||
return vec4<f32>(d.r, ease_distance(h, f32(textureDimensions(data_tex).y)), d.b, d.a);
|
||||
}
|
||||
@@ -122,6 +122,17 @@ fn colorize_shader_is_valid() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lipschitz_shader_is_valid() {
|
||||
validate(
|
||||
"lipschitz.wgsl",
|
||||
concat!(
|
||||
include_str!("../src/shaders/common.wgsl"),
|
||||
include_str!("../src/shaders/lipschitz.wgsl"),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blit_shader_is_valid() {
|
||||
validate(
|
||||
|
||||
Reference in New Issue
Block a user