Compare commits

..
2 Commits
Author SHA1 Message Date
surv 8750bb1590 fix: Smooth 2d shadow view of complex multibrot 2026-09-27 13:20:48 +02:00
surv f30921f7a5 fix: Smooth 3d view of complex multibrot 2026-09-27 13:17:35 +02:00
3 changed files with 492 additions and 52 deletions
+358 -50
View File
@@ -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,
+121
View File
@@ -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);
}
+11
View File
@@ -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(