Skip to main content

polyscope_render/engine/
postprocessing.rs

1use super::RenderEngine;
2use crate::tone_mapping::ToneMapPass;
3
4impl RenderEngine {
5    /// Creates a screenshot texture for capturing frames.
6    ///
7    /// Returns a texture view (HDR format) that can be used as a render target.
8    /// The pipelines render to HDR format, so we need an HDR texture for rendering,
9    /// then tone map to the final screenshot texture.
10    /// After rendering to this view, call `apply_screenshot_tone_mapping()` then
11    /// `capture_screenshot()` to get the pixel data.
12    pub fn create_screenshot_target(&mut self) -> wgpu::TextureView {
13        // Calculate buffer size with proper alignment
14        let bytes_per_row = Self::aligned_bytes_per_row(self.width);
15        let buffer_size = u64::from(bytes_per_row * self.height);
16
17        // Create HDR texture for rendering (matches pipeline format)
18        let hdr_texture = self.device.create_texture(&wgpu::TextureDescriptor {
19            label: Some("screenshot HDR texture"),
20            size: wgpu::Extent3d {
21                width: self.width,
22                height: self.height,
23                depth_or_array_layers: 1,
24            },
25            mip_level_count: 1,
26            sample_count: 1,
27            dimension: wgpu::TextureDimension::D2,
28            format: wgpu::TextureFormat::Rgba16Float, // HDR format matching pipelines
29            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
30            view_formats: &[],
31        });
32
33        let hdr_view = hdr_texture.create_view(&wgpu::TextureViewDescriptor::default());
34
35        // Create final capture texture (surface format for readback)
36        let texture = self.device.create_texture(&wgpu::TextureDescriptor {
37            label: Some("screenshot texture"),
38            size: wgpu::Extent3d {
39                width: self.width,
40                height: self.height,
41                depth_or_array_layers: 1,
42            },
43            mip_level_count: 1,
44            sample_count: 1,
45            dimension: wgpu::TextureDimension::D2,
46            format: self.surface_config.format,
47            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
48            view_formats: &[],
49        });
50
51        // Create staging buffer for readback
52        let buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
53            label: Some("screenshot buffer"),
54            size: buffer_size,
55            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
56            mapped_at_creation: false,
57        });
58
59        self.screenshot_hdr_texture = Some(hdr_texture);
60        self.screenshot_hdr_view = Some(hdr_view);
61        self.screenshot_texture = Some(texture);
62        self.screenshot_buffer = Some(buffer);
63
64        // Return the HDR view for rendering
65        self.screenshot_hdr_view.as_ref().unwrap().clone()
66    }
67
68    /// Returns the screenshot texture view (for tone mapping output).
69    pub fn screenshot_texture_view(&self) -> Option<wgpu::TextureView> {
70        self.screenshot_texture
71            .as_ref()
72            .map(|t| t.create_view(&wgpu::TextureViewDescriptor::default()))
73    }
74
75    /// Applies tone mapping from the screenshot HDR texture to the final screenshot texture.
76    pub fn apply_screenshot_tone_mapping(&mut self, encoder: &mut wgpu::CommandEncoder) {
77        let Some(hdr_view) = &self.screenshot_hdr_view else {
78            log::error!("Screenshot HDR view not initialized");
79            return;
80        };
81
82        let Some(screenshot_texture) = &self.screenshot_texture else {
83            log::error!("Screenshot texture not initialized");
84            return;
85        };
86
87        let screenshot_view =
88            screenshot_texture.create_view(&wgpu::TextureViewDescriptor::default());
89
90        // Use the existing tone mapping pass
91        // For screenshots, we use the main SSAO output view if available
92        // (Note: SSAO effect depends on the main render resolution, not screenshot resolution)
93        if let Some(tone_map_pass) = &self.tone_map_pass {
94            // Use SSAO output or fall back to HDR view (which is ignored when ssao_enabled=false)
95            let ssao_view = self.ssao_output_view.as_ref().unwrap_or(hdr_view);
96            tone_map_pass.render_to_target(
97                &self.device,
98                encoder,
99                hdr_view,
100                ssao_view,
101                &screenshot_view,
102            );
103        }
104    }
105
106    /// Returns the screenshot depth view for rendering.
107    pub fn screenshot_depth_view(&self) -> &wgpu::TextureView {
108        &self.depth_view
109    }
110
111    /// Calculates bytes per row with proper alignment for wgpu buffer copies.
112    fn aligned_bytes_per_row(width: u32) -> u32 {
113        let bytes_per_pixel = 4u32; // RGBA8
114        let unaligned = width * bytes_per_pixel;
115        let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
116        unaligned.div_ceil(align) * align
117    }
118
119    /// Captures the screenshot after rendering to the screenshot target.
120    ///
121    /// This method copies the screenshot texture to a buffer and reads it back.
122    /// Call this after rendering to the view returned by `create_screenshot_target()`.
123    ///
124    /// Returns the raw RGBA pixel data.
125    pub fn capture_screenshot(&mut self) -> Result<Vec<u8>, crate::screenshot::ScreenshotError> {
126        let texture = self
127            .screenshot_texture
128            .as_ref()
129            .ok_or(crate::screenshot::ScreenshotError::InvalidImageData)?;
130        let buffer = self
131            .screenshot_buffer
132            .as_ref()
133            .ok_or(crate::screenshot::ScreenshotError::InvalidImageData)?;
134
135        let bytes_per_row = Self::aligned_bytes_per_row(self.width);
136
137        // Create encoder and copy texture to buffer
138        let mut encoder = self
139            .device
140            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
141                label: Some("screenshot copy encoder"),
142            });
143
144        encoder.copy_texture_to_buffer(
145            wgpu::TexelCopyTextureInfo {
146                texture,
147                mip_level: 0,
148                origin: wgpu::Origin3d::ZERO,
149                aspect: wgpu::TextureAspect::All,
150            },
151            wgpu::TexelCopyBufferInfo {
152                buffer,
153                layout: wgpu::TexelCopyBufferLayout {
154                    offset: 0,
155                    bytes_per_row: Some(bytes_per_row),
156                    rows_per_image: Some(self.height),
157                },
158            },
159            wgpu::Extent3d {
160                width: self.width,
161                height: self.height,
162                depth_or_array_layers: 1,
163            },
164        );
165
166        self.queue.submit(std::iter::once(encoder.finish()));
167
168        // Map buffer and read data
169        let buffer_slice = buffer.slice(..);
170        let (tx, rx) = std::sync::mpsc::channel();
171        buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
172            tx.send(result).unwrap();
173        });
174        let _ = self.device.poll(wgpu::PollType::wait_indefinitely());
175        rx.recv()
176            .map_err(|_| crate::screenshot::ScreenshotError::BufferMapFailed)?
177            .map_err(|_| crate::screenshot::ScreenshotError::BufferMapFailed)?;
178
179        // Copy data, removing row padding
180        let data = buffer_slice.get_mapped_range();
181        let mut result = Vec::with_capacity((self.width * self.height * 4) as usize);
182        let row_bytes = (self.width * 4) as usize;
183
184        for row in 0..self.height {
185            let start = (row * bytes_per_row) as usize;
186            let end = start + row_bytes;
187            result.extend_from_slice(&data[start..end]);
188        }
189
190        drop(data);
191        buffer.unmap();
192
193        // Clean up screenshot resources
194        self.screenshot_texture = None;
195        self.screenshot_buffer = None;
196        self.screenshot_hdr_texture = None;
197        self.screenshot_hdr_view = None;
198
199        Ok(result)
200    }
201
202    /// Initializes tone mapping resources.
203    pub(crate) fn init_tone_mapping(&mut self) {
204        self.tone_map_pass = Some(ToneMapPass::new(&self.device, self.surface_config.format));
205        self.create_hdr_texture();
206        self.create_normal_texture();
207        self.create_ssao_noise_texture();
208        self.init_ssao_pass();
209    }
210
211    /// Initializes SSAO pass.
212    pub(crate) fn init_ssao_pass(&mut self) {
213        let ssao_pass = crate::ssao_pass::SsaoPass::new(&self.device, self.width, self.height);
214        self.ssao_pass = Some(ssao_pass);
215        self.create_ssao_output_texture();
216    }
217
218    /// Initializes SSAA (supersampling) pass.
219    /// The pipeline uses `Rgba16Float` because it downsamples the HDR texture
220    /// to the HDR intermediate texture (both are `Rgba16Float`).
221    pub(crate) fn init_ssaa_pass(&mut self) {
222        self.ssaa_pass = Some(crate::ssaa_pass::SsaaPass::new(
223            &self.device,
224            wgpu::TextureFormat::Rgba16Float,
225        ));
226    }
227
228    /// Returns the current SSAA factor (1 = off, 2 = 2x, 4 = 4x).
229    #[must_use]
230    pub fn ssaa_factor(&self) -> u32 {
231        self.ssaa_factor
232    }
233
234    /// Sets the SSAA factor and recreates render textures at the new resolution.
235    /// Valid values are 1 (off), 2 (2x supersampling), or 4 (4x supersampling).
236    pub fn set_ssaa_factor(&mut self, factor: u32) {
237        let factor = factor.clamp(1, 4);
238        if factor == self.ssaa_factor {
239            return;
240        }
241
242        // Wait for any in-flight GPU work before destroying textures
243        let _ = self.device.poll(wgpu::PollType::wait_indefinitely());
244
245        self.ssaa_factor = factor;
246
247        // Update SSAA pass uniform
248        if let Some(ref mut ssaa_pass) = self.ssaa_pass {
249            ssaa_pass.set_ssaa_factor(&self.queue, factor);
250        }
251
252        // Recreate all resolution-dependent textures at SSAA resolution
253        self.recreate_ssaa_textures();
254    }
255
256    /// Recreates all resolution-dependent textures at SSAA resolution.
257    pub(crate) fn recreate_ssaa_textures(&mut self) {
258        let ssaa_width = self.width * self.ssaa_factor;
259        let ssaa_height = self.height * self.ssaa_factor;
260
261        // Recreate depth texture at SSAA resolution
262        let (depth_texture, depth_view, depth_only_view) =
263            Self::create_depth_texture(&self.device, ssaa_width, ssaa_height);
264        self.depth_texture = depth_texture;
265        self.depth_view = depth_view;
266        self.depth_only_view = depth_only_view;
267
268        // Recreate HDR texture at SSAA resolution
269        self.create_hdr_texture_with_size(ssaa_width, ssaa_height);
270
271        // Recreate normal G-buffer at SSAA resolution
272        self.create_normal_texture_with_size(ssaa_width, ssaa_height);
273
274        // Recreate SSAO output at SSAA resolution
275        self.create_ssao_output_texture_with_size(ssaa_width, ssaa_height);
276
277        // Resize SSAO pass
278        if let Some(ref mut ssao_pass) = self.ssao_pass {
279            ssao_pass.resize(&self.device, &self.queue, ssaa_width, ssaa_height);
280        }
281
282        // Create intermediate texture for downsampling (at screen resolution)
283        if self.ssaa_factor > 1 {
284            self.create_ssaa_intermediate_texture();
285        } else {
286            self.ssaa_intermediate_texture = None;
287            self.ssaa_intermediate_view = None;
288        }
289    }
290
291    /// Creates the intermediate texture for SSAA downsampling (at screen resolution).
292    pub(crate) fn create_ssaa_intermediate_texture(&mut self) {
293        let texture = self.device.create_texture(&wgpu::TextureDescriptor {
294            label: Some("SSAA Intermediate Texture"),
295            size: wgpu::Extent3d {
296                width: self.width,
297                height: self.height,
298                depth_or_array_layers: 1,
299            },
300            mip_level_count: 1,
301            sample_count: 1,
302            dimension: wgpu::TextureDimension::D2,
303            format: wgpu::TextureFormat::Rgba16Float,
304            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
305            view_formats: &[],
306        });
307
308        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
309        self.ssaa_intermediate_texture = Some(texture);
310        self.ssaa_intermediate_view = Some(view);
311    }
312
313    /// Ensures the depth peel pass is initialized and matches render resolution.
314    pub fn ensure_depth_peel_pass(&mut self) {
315        let (render_w, render_h) = self.render_dimensions();
316
317        if self.mesh_bind_group_layout.is_none() {
318            self.create_mesh_pipeline();
319        }
320
321        if let Some(ref mut pass) = self.depth_peel_pass {
322            pass.resize(&self.device, render_w, render_h);
323        } else {
324            self.depth_peel_pass = Some(crate::depth_peel_pass::DepthPeelPass::new(
325                &self.device,
326                render_w,
327                render_h,
328                self.mesh_bind_group_layout.as_ref().unwrap(),
329                &self.slice_plane_bind_group_layout,
330                &self.matcap_bind_group_layout,
331            ));
332        }
333    }
334
335    /// Returns the depth peel pass, if initialized.
336    pub fn depth_peel_pass(&self) -> Option<&crate::depth_peel_pass::DepthPeelPass> {
337        self.depth_peel_pass.as_ref()
338    }
339
340    /// Returns a mutable reference to the depth peel pass, if initialized.
341    pub fn depth_peel_pass_mut(&mut self) -> Option<&mut crate::depth_peel_pass::DepthPeelPass> {
342        self.depth_peel_pass.as_mut()
343    }
344
345    /// Returns the HDR texture view for rendering the scene.
346    pub fn hdr_view(&self) -> Option<&wgpu::TextureView> {
347        self.hdr_view.as_ref()
348    }
349
350    /// Returns the normal G-buffer view if available.
351    pub fn normal_view(&self) -> Option<&wgpu::TextureView> {
352        self.normal_view.as_ref()
353    }
354
355    /// Returns the SSAO noise texture view if available.
356    pub fn ssao_noise_view(&self) -> Option<&wgpu::TextureView> {
357        self.ssao_noise_view.as_ref()
358    }
359
360    /// Returns the SSAO output texture view if available.
361    pub fn ssao_output_view(&self) -> Option<&wgpu::TextureView> {
362        self.ssao_output_view.as_ref()
363    }
364
365    /// Returns the SSAO pass.
366    pub fn ssao_pass(&self) -> Option<&crate::ssao_pass::SsaoPass> {
367        self.ssao_pass.as_ref()
368    }
369
370    /// Renders the SSAO pass.
371    /// Returns true if SSAO was rendered, false if resources are not available.
372    pub fn render_ssao(
373        &self,
374        encoder: &mut wgpu::CommandEncoder,
375        config: &polyscope_core::SsaoConfig,
376    ) -> bool {
377        // Check if all required resources are available
378        // Use depth_only_view for SSAO (excludes stencil aspect)
379        let (ssao_pass, depth_view, normal_view, noise_view, output_view) = match (
380            &self.ssao_pass,
381            Some(&self.depth_only_view),
382            self.normal_view.as_ref(),
383            self.ssao_noise_view.as_ref(),
384            self.ssao_output_view.as_ref(),
385        ) {
386            (Some(pass), Some(depth), Some(normal), Some(noise), Some(output)) => {
387                (pass, depth, normal, noise, output)
388            }
389            _ => return false,
390        };
391
392        if !config.enabled {
393            return false;
394        }
395
396        // Update SSAO uniforms — use SSAA-scaled dimensions since
397        // SSAO textures are rendered at SSAA resolution
398        let (render_w, render_h) = self.render_dimensions();
399        let proj = self.camera.projection_matrix();
400        let inv_proj = proj.inverse();
401        ssao_pass.update_uniforms(
402            &self.queue,
403            proj,
404            inv_proj,
405            config.radius,
406            config.bias,
407            config.intensity,
408            config.sample_count,
409            render_w as f32,
410            render_h as f32,
411        );
412
413        // Create bind groups
414        let ssao_bind_group =
415            ssao_pass.create_ssao_bind_group(&self.device, depth_view, normal_view, noise_view);
416        // Blur bind group now includes depth view for edge-aware bilateral filtering
417        let blur_bind_group = ssao_pass.create_blur_bind_group(&self.device, depth_view);
418
419        // Render SSAO pass
420        ssao_pass.render_ssao(encoder, &ssao_bind_group);
421
422        // Render blur pass to output texture
423        ssao_pass.render_blur(encoder, output_view, &blur_bind_group);
424
425        true
426    }
427
428    /// Returns the tone map pass.
429    pub fn tone_map_pass(&self) -> Option<&ToneMapPass> {
430        self.tone_map_pass.as_ref()
431    }
432
433    /// Updates tone mapping uniforms.
434    pub fn update_tone_mapping(
435        &self,
436        exposure: f32,
437        white_level: f32,
438        gamma: f32,
439        ssao_enabled: bool,
440    ) {
441        if let Some(tone_map) = &self.tone_map_pass {
442            tone_map.update_uniforms(&self.queue, exposure, white_level, gamma, ssao_enabled);
443        }
444    }
445
446    /// Renders the tone mapping pass from HDR to the output view.
447    /// Uses SSAO texture if available, otherwise uses a default white texture.
448    ///
449    /// When SSAA is enabled (factor > 1):
450    /// 1. Downsamples HDR (SSAA res) → intermediate HDR (screen res)
451    /// 2. Tone maps intermediate HDR → output LDR (SSAO disabled — resolution mismatch)
452    pub fn render_tone_mapping(
453        &self,
454        encoder: &mut wgpu::CommandEncoder,
455        output_view: &wgpu::TextureView,
456    ) {
457        if let (Some(tone_map), Some(hdr_view)) = (&self.tone_map_pass, &self.hdr_view) {
458            // If SSAA is enabled, first downsample HDR, then tone map
459            if self.ssaa_factor > 1 {
460                if let (Some(intermediate_view), Some(ssaa_pass)) =
461                    (&self.ssaa_intermediate_view, &self.ssaa_pass)
462                {
463                    // Step 1: Downsample HDR (SSAA res) -> intermediate HDR (screen res)
464                    ssaa_pass.render_to_target(&self.device, encoder, hdr_view, intermediate_view);
465
466                    // Step 2: Tone map intermediate HDR -> output LDR
467                    // Pass intermediate_view as the SSAO slot — SSAO is disabled via
468                    // ssao_enabled=0 uniform so the texture value is ignored, but the
469                    // bind group requires a valid Float texture of matching format.
470                    let bind_group = tone_map.create_bind_group(
471                        &self.device,
472                        intermediate_view,
473                        intermediate_view,
474                    );
475                    tone_map.render(encoder, output_view, &bind_group);
476                    return;
477                }
478            }
479
480            // No SSAA - tone map directly from HDR to output with SSAO
481            let ssao_view = self.ssao_output_view.as_ref().unwrap_or(hdr_view);
482            let bind_group = tone_map.create_bind_group(&self.device, hdr_view, ssao_view);
483            tone_map.render(encoder, output_view, &bind_group);
484        }
485    }
486}