Skip to main content

wisp/filter/
drop_shadow.rs

1//! `DropShadowFilter` — alpha extract → blur → composite under source.
2//!
3//! Four logical passes implemented inside a single `Filter::render_pass`
4//! invocation (we manage our own scratch render targets — `scratch_a` and
5//! `scratch_b`):
6//!   1. Extract the source alpha, offset by `offset` and tinted with `color`,
7//!      into the first scratch.
8//!   2. Horizontal Gaussian blur on the first scratch into the second.
9//!   3. Vertical Gaussian blur back into the first scratch (now the blurred
10//!      shadow).
11//!   4. Composite source over the blurred shadow into `output`.
12
13use bytemuck::{Pod, Zeroable};
14use wgpu::util::DeviceExt;
15
16use crate::application::Application;
17use crate::color::Color;
18use crate::filter::{Filter, FilterContext};
19use crate::texture::render_texture::RenderTexture;
20
21/// Drop-shadow post-process.
22#[derive(Debug, Clone, Copy)]
23pub struct DropShadowFilter {
24    /// Texel offset of the shadow relative to the source.
25    pub offset: glam::Vec2,
26    /// Gaussian blur radius applied to the shadow.
27    pub blur: f32,
28    /// Shadow color (RGB tints the shadow; alpha multiplies the source alpha).
29    pub color: Color,
30}
31
32impl Default for DropShadowFilter {
33    fn default() -> Self {
34        Self {
35            offset: glam::Vec2::new(4.0, 4.0),
36            blur: 6.0,
37            color: Color::rgba(0.0, 0.0, 0.0, 0.5),
38        }
39    }
40}
41
42#[repr(C)]
43#[derive(Clone, Copy, Pod, Zeroable)]
44struct ExtractUniforms {
45    offset: [f32; 2],
46    _pad: [f32; 2],
47    color: [f32; 4],
48}
49
50impl Filter for DropShadowFilter {
51    fn passes(&self) -> u32 {
52        1
53    }
54
55    fn render_pass(
56        &self,
57        ctx: &mut FilterContext<'_>,
58        input: &RenderTexture,
59        output: &RenderTexture,
60        _pass: u32,
61    ) {
62        let app = ctx.app;
63        let format = output.format();
64        let scratch_a = RenderTexture::with_format(app, input.width(), input.height(), format);
65        let scratch_b = RenderTexture::with_format(app, input.width(), input.height(), format);
66
67        run_extract(app, ctx.encoder, input, &scratch_a, self.offset, self.color);
68        super::blur::run_blur_pass(
69            app,
70            ctx.encoder,
71            &scratch_a,
72            &scratch_b,
73            [1.0, 0.0],
74            self.blur,
75        );
76        super::blur::run_blur_pass(
77            app,
78            ctx.encoder,
79            &scratch_b,
80            &scratch_a,
81            [0.0, 1.0],
82            self.blur,
83        );
84        run_composite(app, ctx.encoder, &scratch_a, input, output);
85    }
86}
87
88fn run_extract(
89    app: &Application,
90    encoder: &mut wgpu::CommandEncoder,
91    input: &RenderTexture,
92    output: &RenderTexture,
93    offset: glam::Vec2,
94    color: Color,
95) {
96    let device = app.device();
97    let cache = extract_pipeline(app, output.format());
98
99    let uniforms = ExtractUniforms {
100        offset: [offset.x, offset.y],
101        _pad: [0.0, 0.0],
102        color: [color.r, color.g, color.b, color.a],
103    };
104    let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
105        label: Some("wisp::ds_extract uniforms"),
106        contents: bytemuck::bytes_of(&uniforms),
107        usage: wgpu::BufferUsages::UNIFORM,
108    });
109
110    let uniform_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
111        label: Some("wisp::ds_extract uniform bg"),
112        layout: &cache.uniform_layout,
113        entries: &[wgpu::BindGroupEntry {
114            binding: 0,
115            resource: uniform_buffer.as_entire_binding(),
116        }],
117    });
118    let texture_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
119        label: Some("wisp::ds_extract texture bg"),
120        layout: &cache.texture_layout,
121        entries: &[
122            wgpu::BindGroupEntry {
123                binding: 0,
124                resource: wgpu::BindingResource::TextureView(input.view()),
125            },
126            wgpu::BindGroupEntry {
127                binding: 1,
128                resource: wgpu::BindingResource::Sampler(input.sampler()),
129            },
130        ],
131    });
132
133    let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
134        label: Some("wisp::ds_extract pass"),
135        color_attachments: &[Some(wgpu::RenderPassColorAttachment {
136            view: output.view(),
137            resolve_target: None,
138            ops: wgpu::Operations {
139                load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
140                store: wgpu::StoreOp::Store,
141            },
142        })],
143        depth_stencil_attachment: None,
144        timestamp_writes: None,
145        occlusion_query_set: None,
146    });
147    pass.set_pipeline(&cache.pipeline);
148    pass.set_bind_group(0, &uniform_bg, &[]);
149    pass.set_bind_group(1, &texture_bg, &[]);
150    pass.draw(0..3, 0..1);
151}
152
153fn run_composite(
154    app: &Application,
155    encoder: &mut wgpu::CommandEncoder,
156    shadow: &RenderTexture,
157    source: &RenderTexture,
158    output: &RenderTexture,
159) {
160    let device = app.device();
161    let cache = composite_pipeline(app, output.format());
162
163    let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
164        label: Some("wisp::ds_composite bg"),
165        layout: &cache.bind_layout,
166        entries: &[
167            wgpu::BindGroupEntry {
168                binding: 0,
169                resource: wgpu::BindingResource::TextureView(shadow.view()),
170            },
171            wgpu::BindGroupEntry {
172                binding: 1,
173                resource: wgpu::BindingResource::TextureView(source.view()),
174            },
175            wgpu::BindGroupEntry {
176                binding: 2,
177                resource: wgpu::BindingResource::Sampler(source.sampler()),
178            },
179        ],
180    });
181
182    let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
183        label: Some("wisp::ds_composite pass"),
184        color_attachments: &[Some(wgpu::RenderPassColorAttachment {
185            view: output.view(),
186            resolve_target: None,
187            ops: wgpu::Operations {
188                load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
189                store: wgpu::StoreOp::Store,
190            },
191        })],
192        depth_stencil_attachment: None,
193        timestamp_writes: None,
194        occlusion_query_set: None,
195    });
196    pass.set_pipeline(&cache.pipeline);
197    pass.set_bind_group(0, &bg, &[]);
198    pass.draw(0..3, 0..1);
199}
200
201struct ExtractCache {
202    pipeline: wgpu::RenderPipeline,
203    uniform_layout: wgpu::BindGroupLayout,
204    texture_layout: wgpu::BindGroupLayout,
205}
206
207fn extract_pipeline(app: &Application, format: wgpu::TextureFormat) -> ExtractCache {
208    let device = app.device();
209    let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
210        label: Some("wisp::ds_extract"),
211        source: wgpu::ShaderSource::Wgsl(
212            include_str!("../../shaders/filter_drop_shadow_extract.wgsl").into(),
213        ),
214    });
215
216    let uniform_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
217        label: Some("wisp::ds_extract uniform layout"),
218        entries: &[wgpu::BindGroupLayoutEntry {
219            binding: 0,
220            visibility: wgpu::ShaderStages::FRAGMENT,
221            ty: wgpu::BindingType::Buffer {
222                ty: wgpu::BufferBindingType::Uniform,
223                has_dynamic_offset: false,
224                min_binding_size: None,
225            },
226            count: None,
227        }],
228    });
229    let texture_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
230        label: Some("wisp::ds_extract texture layout"),
231        entries: &[
232            wgpu::BindGroupLayoutEntry {
233                binding: 0,
234                visibility: wgpu::ShaderStages::FRAGMENT,
235                ty: wgpu::BindingType::Texture {
236                    sample_type: wgpu::TextureSampleType::Float { filterable: true },
237                    view_dimension: wgpu::TextureViewDimension::D2,
238                    multisampled: false,
239                },
240                count: None,
241            },
242            wgpu::BindGroupLayoutEntry {
243                binding: 1,
244                visibility: wgpu::ShaderStages::FRAGMENT,
245                ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
246                count: None,
247            },
248        ],
249    });
250
251    let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
252        label: Some("wisp::ds_extract pipeline layout"),
253        bind_group_layouts: &[&uniform_layout, &texture_layout],
254        push_constant_ranges: &[],
255    });
256    let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
257        label: Some("wisp::ds_extract pipeline"),
258        layout: Some(&layout),
259        vertex: wgpu::VertexState {
260            module: &shader,
261            entry_point: Some("main_vs"),
262            buffers: &[],
263            compilation_options: wgpu::PipelineCompilationOptions::default(),
264        },
265        fragment: Some(wgpu::FragmentState {
266            module: &shader,
267            entry_point: Some("main_fs"),
268            targets: &[Some(wgpu::ColorTargetState {
269                format,
270                blend: None,
271                write_mask: wgpu::ColorWrites::ALL,
272            })],
273            compilation_options: wgpu::PipelineCompilationOptions::default(),
274        }),
275        primitive: wgpu::PrimitiveState::default(),
276        depth_stencil: None,
277        multisample: wgpu::MultisampleState::default(),
278        multiview: None,
279        cache: None,
280    });
281
282    ExtractCache {
283        pipeline,
284        uniform_layout,
285        texture_layout,
286    }
287}
288
289struct CompositeCache {
290    pipeline: wgpu::RenderPipeline,
291    bind_layout: wgpu::BindGroupLayout,
292}
293
294fn composite_pipeline(app: &Application, format: wgpu::TextureFormat) -> CompositeCache {
295    let device = app.device();
296    let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
297        label: Some("wisp::ds_composite"),
298        source: wgpu::ShaderSource::Wgsl(
299            include_str!("../../shaders/filter_drop_shadow_composite.wgsl").into(),
300        ),
301    });
302
303    let bind_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
304        label: Some("wisp::ds_composite layout"),
305        entries: &[
306            wgpu::BindGroupLayoutEntry {
307                binding: 0,
308                visibility: wgpu::ShaderStages::FRAGMENT,
309                ty: wgpu::BindingType::Texture {
310                    sample_type: wgpu::TextureSampleType::Float { filterable: true },
311                    view_dimension: wgpu::TextureViewDimension::D2,
312                    multisampled: false,
313                },
314                count: None,
315            },
316            wgpu::BindGroupLayoutEntry {
317                binding: 1,
318                visibility: wgpu::ShaderStages::FRAGMENT,
319                ty: wgpu::BindingType::Texture {
320                    sample_type: wgpu::TextureSampleType::Float { filterable: true },
321                    view_dimension: wgpu::TextureViewDimension::D2,
322                    multisampled: false,
323                },
324                count: None,
325            },
326            wgpu::BindGroupLayoutEntry {
327                binding: 2,
328                visibility: wgpu::ShaderStages::FRAGMENT,
329                ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
330                count: None,
331            },
332        ],
333    });
334
335    let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
336        label: Some("wisp::ds_composite pipeline layout"),
337        bind_group_layouts: &[&bind_layout],
338        push_constant_ranges: &[],
339    });
340    let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
341        label: Some("wisp::ds_composite pipeline"),
342        layout: Some(&layout),
343        vertex: wgpu::VertexState {
344            module: &shader,
345            entry_point: Some("main_vs"),
346            buffers: &[],
347            compilation_options: wgpu::PipelineCompilationOptions::default(),
348        },
349        fragment: Some(wgpu::FragmentState {
350            module: &shader,
351            entry_point: Some("main_fs"),
352            targets: &[Some(wgpu::ColorTargetState {
353                format,
354                blend: None,
355                write_mask: wgpu::ColorWrites::ALL,
356            })],
357            compilation_options: wgpu::PipelineCompilationOptions::default(),
358        }),
359        primitive: wgpu::PrimitiveState::default(),
360        depth_stencil: None,
361        multisample: wgpu::MultisampleState::default(),
362        multiview: None,
363        cache: None,
364    });
365
366    CompositeCache {
367        pipeline,
368        bind_layout,
369    }
370}