Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8750bb1590 | ||
|
|
f30921f7a5 |
+360
-52
@@ -13,7 +13,9 @@ use std::collections::HashMap;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use eframe::egui_wgpu::{self, wgpu};
|
use eframe::egui_wgpu::{self, wgpu};
|
||||||
|
use wgpu::util::DeviceExt as _;
|
||||||
|
|
||||||
|
use super::kind::FractalKind;
|
||||||
use super::reference::RefOrbit;
|
use super::reference::RefOrbit;
|
||||||
use crate::lights::{GpuLight, Light, MAX_LIGHT_COUNT, gpu_lights};
|
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`].
|
/// The interactive iteration pipelines for one [`PipelineKey`].
|
||||||
struct IteratePipelines {
|
struct IteratePipelines {
|
||||||
/// 1-spp perturbation iterate → data texture (`fs_data`).
|
/// 1-spp perturbation iterate → data texture (`fs_data`).
|
||||||
@@ -253,6 +489,10 @@ struct CacheTarget {
|
|||||||
/// Refine pass output + the colourise bind group reading it. Only
|
/// Refine pass output + the colourise bind group reading it. Only
|
||||||
/// allocated while AA is on: it's a second full-size `Rgba32Float`.
|
/// allocated while AA is on: it's a second full-size `Rgba32Float`.
|
||||||
aa: Option<(wgpu::TextureView, wgpu::BindGroup)>,
|
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 pass input: the colour texture + sampler.
|
||||||
blit_bind_group: wgpu::BindGroup,
|
blit_bind_group: wgpu::BindGroup,
|
||||||
width: u32,
|
width: u32,
|
||||||
@@ -302,6 +542,12 @@ pub struct FractalRenderer {
|
|||||||
/// Colourise pass: data texture → colour texture (palette mapping).
|
/// Colourise pass: data texture → colour texture (palette mapping).
|
||||||
colorize_pipeline: wgpu::RenderPipeline,
|
colorize_pipeline: wgpu::RenderPipeline,
|
||||||
colorize_bind_group_layout: wgpu::BindGroupLayout,
|
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 + resources that copy the colour texture to egui's surface.
|
||||||
blit_pipeline: wgpu::RenderPipeline,
|
blit_pipeline: wgpu::RenderPipeline,
|
||||||
@@ -633,6 +879,8 @@ impl FractalRenderer {
|
|||||||
uploaded_lights: None,
|
uploaded_lights: None,
|
||||||
colorize_pipeline,
|
colorize_pipeline,
|
||||||
colorize_bind_group_layout,
|
colorize_bind_group_layout,
|
||||||
|
lipschitz: Lipschitz::new(device),
|
||||||
|
envelope_valid: false,
|
||||||
blit_pipeline,
|
blit_pipeline,
|
||||||
blit_bind_group_layout,
|
blit_bind_group_layout,
|
||||||
blit_sampler,
|
blit_sampler,
|
||||||
@@ -662,7 +910,8 @@ impl FractalRenderer {
|
|||||||
// Any commands using them were submitted on earlier frames, and
|
// Any commands using them were submitted on earlier frames, and
|
||||||
// `destroy()` defers the actual free until those finish.
|
// `destroy()` defers the actual free until those finish.
|
||||||
if let Some(old) = self.cache.take() {
|
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();
|
t.destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -715,24 +964,13 @@ impl FractalRenderer {
|
|||||||
let color_view = color_texture.create_view(&wgpu::TextureViewDescriptor::default());
|
let color_view = color_texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||||
|
|
||||||
let colorize_bind_group_for = |data: &wgpu::TextureView| {
|
let colorize_bind_group_for = |data: &wgpu::TextureView| {
|
||||||
device.create_bind_group(&wgpu::BindGroupDescriptor {
|
colorize_bind_group(
|
||||||
label: Some("colorize bind group"),
|
device,
|
||||||
layout: &self.colorize_bind_group_layout,
|
&self.colorize_bind_group_layout,
|
||||||
entries: &[
|
&self.uniform_buffer,
|
||||||
wgpu::BindGroupEntry {
|
&self.lights_buffer,
|
||||||
binding: 0,
|
data,
|
||||||
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(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
};
|
};
|
||||||
let colorize_bind_group = colorize_bind_group_for(&data_view);
|
let colorize_bind_group = colorize_bind_group_for(&data_view);
|
||||||
let aa_target = data_aa_texture.as_ref().map(|t| {
|
let aa_target = data_aa_texture.as_ref().map(|t| {
|
||||||
@@ -775,6 +1013,7 @@ impl FractalRenderer {
|
|||||||
refine_bind_group,
|
refine_bind_group,
|
||||||
colorize_bind_group,
|
colorize_bind_group,
|
||||||
aa: aa_target,
|
aa: aa_target,
|
||||||
|
envelope: None,
|
||||||
blit_bind_group,
|
blit_bind_group,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
@@ -818,7 +1057,8 @@ impl FractalRenderer {
|
|||||||
/// lock on the renderer), its bind-group layout, and the target format.
|
/// lock on the renderer), its bind-group layout, and the target format.
|
||||||
/// In 3D mode (`rendering_mode == 2`) also the interactive iterate →
|
/// In 3D mode (`rendering_mode == 2`) also the interactive iterate →
|
||||||
/// refine → colourise chain, since the raymarcher needs a whole data
|
/// 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 {
|
pub fn export_handles(&self, device: &wgpu::Device, uniforms: &Uniforms) -> ExportHandles {
|
||||||
let constants = PipelineKey::from_uniforms(uniforms).constants();
|
let constants = PipelineKey::from_uniforms(uniforms).constants();
|
||||||
let pipeline = fullscreen_pipeline(
|
let pipeline = fullscreen_pipeline(
|
||||||
@@ -830,7 +1070,8 @@ impl FractalRenderer {
|
|||||||
self.target_format,
|
self.target_format,
|
||||||
&constants,
|
&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(
|
iterate: fullscreen_pipeline(
|
||||||
device,
|
device,
|
||||||
"fractal export iterate pipeline",
|
"fractal export iterate pipeline",
|
||||||
@@ -852,6 +1093,7 @@ impl FractalRenderer {
|
|||||||
colorize: self.colorize_pipeline.clone(),
|
colorize: self.colorize_pipeline.clone(),
|
||||||
refine_bind_group_layout: self.refine_bind_group_layout.clone(),
|
refine_bind_group_layout: self.refine_bind_group_layout.clone(),
|
||||||
colorize_bind_group_layout: self.colorize_bind_group_layout.clone(),
|
colorize_bind_group_layout: self.colorize_bind_group_layout.clone(),
|
||||||
|
lipschitz: wants_envelope(uniforms).then(|| self.lipschitz.clone()),
|
||||||
});
|
});
|
||||||
ExportHandles {
|
ExportHandles {
|
||||||
pipeline,
|
pipeline,
|
||||||
@@ -870,11 +1112,11 @@ pub struct ExportHandles {
|
|||||||
pipeline: wgpu::RenderPipeline,
|
pipeline: wgpu::RenderPipeline,
|
||||||
bind_group_layout: wgpu::BindGroupLayout,
|
bind_group_layout: wgpu::BindGroupLayout,
|
||||||
format: wgpu::TextureFormat,
|
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>,
|
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)]
|
#[derive(Clone)]
|
||||||
struct RaymarchHandles {
|
struct RaymarchHandles {
|
||||||
iterate: wgpu::RenderPipeline,
|
iterate: wgpu::RenderPipeline,
|
||||||
@@ -882,15 +1124,19 @@ struct RaymarchHandles {
|
|||||||
colorize: wgpu::RenderPipeline,
|
colorize: wgpu::RenderPipeline,
|
||||||
refine_bind_group_layout: wgpu::BindGroupLayout,
|
refine_bind_group_layout: wgpu::BindGroupLayout,
|
||||||
colorize_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
|
/// A 3D (or distance-field shadow) export's own data textures and the passes
|
||||||
/// iterate into `data_view`, then one refine (if AA) + colourise pass
|
/// that fill them: the tiles iterate into `data_view`, then one refine (if
|
||||||
/// raymarches the finished height field into the export target.
|
/// AA), the distance-field rebuild (if wanted) and a colourise pass render
|
||||||
|
/// the finished height field into the export target.
|
||||||
struct RaymarchExport {
|
struct RaymarchExport {
|
||||||
iterate: wgpu::RenderPipeline,
|
iterate: wgpu::RenderPipeline,
|
||||||
/// Refine pipeline, output view and input bind group, when AA is on.
|
/// Refine pipeline, output view and input bind group, when AA is on.
|
||||||
refine: Option<(wgpu::RenderPipeline, wgpu::TextureView, wgpu::BindGroup)>,
|
refine: Option<(wgpu::RenderPipeline, wgpu::TextureView, wgpu::BindGroup)>,
|
||||||
|
envelope: Option<(Lipschitz, Envelope)>,
|
||||||
colorize: wgpu::RenderPipeline,
|
colorize: wgpu::RenderPipeline,
|
||||||
colorize_bind_group: wgpu::BindGroup,
|
colorize_bind_group: wgpu::BindGroup,
|
||||||
data_view: wgpu::TextureView,
|
data_view: wgpu::TextureView,
|
||||||
@@ -914,7 +1160,8 @@ pub struct ExportRender {
|
|||||||
/// Number of horizontal tiles the render is split into.
|
/// Number of horizontal tiles the render is split into.
|
||||||
pub tiles: u32,
|
pub tiles: u32,
|
||||||
pub swap_rb: bool,
|
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>,
|
raymarch: Option<RaymarchExport>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1066,29 +1313,25 @@ impl ExportRender {
|
|||||||
refine_bind_group,
|
refine_bind_group,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
// Colourise reads the refined texture when AA is on.
|
// The distance field reads the refined texture when AA is on,
|
||||||
let colorize_input = refine.as_ref().map_or(&data_view, |(_, v, _)| v);
|
// and colourise reads the distance field, else the same texture.
|
||||||
let colorize_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
let refined = refine.as_ref().map_or(&data_view, |(_, v, _)| v);
|
||||||
label: Some("export colorize bind group"),
|
let envelope = rm.lipschitz.as_ref().map(|lp| {
|
||||||
layout: &rm.colorize_bind_group_layout,
|
let env = Envelope::new(device, lp, refined, width, height);
|
||||||
entries: &[
|
(lp.clone(), env)
|
||||||
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(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
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 {
|
RaymarchExport {
|
||||||
iterate: rm.iterate.clone(),
|
iterate: rm.iterate.clone(),
|
||||||
refine,
|
refine,
|
||||||
|
envelope,
|
||||||
colorize: rm.colorize.clone(),
|
colorize: rm.colorize.clone(),
|
||||||
colorize_bind_group,
|
colorize_bind_group,
|
||||||
data_view,
|
data_view,
|
||||||
@@ -1120,8 +1363,9 @@ impl ExportRender {
|
|||||||
|
|
||||||
/// Render one horizontal tile into the export texture and submit it. Tile 0
|
/// Render one horizontal tile into the export texture and submit it. Tile 0
|
||||||
/// clears the whole attachment; later tiles preserve earlier ones. In 3D
|
/// clears the whole attachment; later tiles preserve earlier ones. In 3D
|
||||||
/// mode the tiles iterate into the data texture instead, and the last one
|
/// mode (or with the distance field) the tiles iterate into the data
|
||||||
/// also runs the (whole-image) refine + raymarching colourise passes.
|
/// 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) {
|
pub fn render_tile(&self, device: &wgpu::Device, queue: &wgpu::Queue, t: u32) {
|
||||||
let (y0, y1) = self.tile_rows(t);
|
let (y0, y1) = self.tile_rows(t);
|
||||||
if y1 <= y0 {
|
if y1 <= y0 {
|
||||||
@@ -1176,6 +1420,9 @@ impl ExportRender {
|
|||||||
&[&self.bind_group, refine_bind_group],
|
&[&self.bind_group, refine_bind_group],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if let Some((lp, env)) = &rm.envelope {
|
||||||
|
env.record(&mut encoder, lp);
|
||||||
|
}
|
||||||
data_pass(
|
data_pass(
|
||||||
&mut encoder,
|
&mut encoder,
|
||||||
"export colorize pass",
|
"export colorize pass",
|
||||||
@@ -1399,6 +1646,35 @@ pub fn encode_png_with_progress(
|
|||||||
out
|
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`
|
/// Record one fullscreen-triangle pass drawing `pipeline` into `target`
|
||||||
/// (cleared first), with `bind_groups` bound to groups 0, 1, ...
|
/// (cleared first), with `bind_groups` bound to groups 0, 1, ...
|
||||||
fn data_pass(
|
fn data_pass(
|
||||||
@@ -1534,6 +1810,25 @@ impl egui_wgpu::CallbackTrait for FractalCallback {
|
|||||||
if iter_dirty {
|
if iter_dirty {
|
||||||
renderer.ensure_pipelines(device, PipelineKey::from_uniforms(&self.uniforms));
|
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)];
|
let pipelines = &renderer.pipelines[&PipelineKey::from_uniforms(&self.uniforms)];
|
||||||
if let Some(cache) = &renderer.cache {
|
if let Some(cache) = &renderer.cache {
|
||||||
if iter_dirty {
|
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.
|
// Colourise pass: data texture → colour texture.
|
||||||
let colorize_bind_group = cache
|
let colorize_bind_group = match env {
|
||||||
.aa
|
Some((_, bg)) => bg,
|
||||||
.as_ref()
|
None => cache
|
||||||
.map_or(&cache.colorize_bind_group, |(_, bg)| bg);
|
.aa
|
||||||
|
.as_ref()
|
||||||
|
.map_or(&cache.colorize_bind_group, |(_, bg)| bg),
|
||||||
|
};
|
||||||
data_pass(
|
data_pass(
|
||||||
egui_encoder,
|
egui_encoder,
|
||||||
"fractal colorize pass",
|
"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 {
|
if iter_dirty {
|
||||||
renderer.iterated = Some(IterState {
|
renderer.iterated = Some(IterState {
|
||||||
uniforms: self.uniforms,
|
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]
|
#[test]
|
||||||
fn blit_shader_is_valid() {
|
fn blit_shader_is_valid() {
|
||||||
validate(
|
validate(
|
||||||
|
|||||||
Reference in New Issue
Block a user