fix: Smooth 2d shadow view of complex multibrot

This commit is contained in:
2026-09-27 13:20:48 +02:00
parent f30921f7a5
commit 8750bb1590
2 changed files with 29 additions and 23 deletions
+24 -18
View File
@@ -167,12 +167,13 @@ 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 3D view of `u` rebuilds its height field as a distance field
/// (see `lipschitz.wgsl`): only Complex Multibrot, whose branch cut makes the
/// DE jump into walls. Every other kind keeps its DE as is.
/// 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 == 2 && (u.kind == cm || (u.morph_w > 0.0 && u.morph_from == cm))
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
@@ -490,7 +491,7 @@ struct CacheTarget {
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
/// 3D Complex Multibrot (see [`wants_envelope`]).
/// shadow / 3D Complex Multibrot (see [`wants_envelope`]).
envelope: Option<(Envelope, wgpu::BindGroup)>,
/// Blit pass input: the colour texture + sampler.
blit_bind_group: wgpu::BindGroup,
@@ -541,7 +542,8 @@ 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 3D Complex Multibrot (see [`wants_envelope`]).
/// 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.
@@ -1055,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(
@@ -1067,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",
@@ -1108,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,
@@ -1124,10 +1128,10 @@ struct RaymarchHandles {
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), the distance-field
/// rebuild (if wanted) and a 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.
@@ -1156,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>,
}
@@ -1358,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 {
@@ -1846,7 +1852,7 @@ impl egui_wgpu::CallbackTrait for FractalCallback {
}
}
// 3D Complex Multibrot: rebuild the DE as a distance field.
// 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)