Skip to main content

polyscope_render/
tone_mapping.rs

1//! Tone mapping post-processing pass.
2
3use std::num::NonZeroU64;
4use wgpu::util::DeviceExt;
5
6/// GPU representation of tone mapping uniforms.
7#[repr(C)]
8#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
9pub struct ToneMapUniforms {
10    pub exposure: f32,
11    pub white_level: f32,
12    pub gamma: f32,
13    pub ssao_enabled: u32, // 0 = disabled, 1 = enabled
14    /// Padding to 32 bytes (workaround for wgpu late binding size validation).
15    #[allow(clippy::pub_underscore_fields)]
16    pub _padding: [f32; 4],
17}
18
19impl Default for ToneMapUniforms {
20    fn default() -> Self {
21        Self {
22            exposure: 1.0,
23            white_level: 1.0,
24            gamma: 2.2,
25            ssao_enabled: 0,
26            _padding: [0.0; 4],
27        }
28    }
29}
30
31/// Tone mapping render resources.
32pub struct ToneMapPass {
33    pipeline: wgpu::RenderPipeline,
34    bind_group_layout: wgpu::BindGroupLayout,
35    uniform_buffer: wgpu::Buffer,
36    sampler: wgpu::Sampler,
37}
38
39impl ToneMapPass {
40    /// Creates a new tone mapping pass.
41    #[must_use]
42    pub fn new(device: &wgpu::Device, output_format: wgpu::TextureFormat) -> Self {
43        // Create bind group layout
44        let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
45            label: Some("Tone Map Bind Group Layout"),
46            entries: &[
47                // Input texture
48                wgpu::BindGroupLayoutEntry {
49                    binding: 0,
50                    visibility: wgpu::ShaderStages::FRAGMENT,
51                    ty: wgpu::BindingType::Texture {
52                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
53                        view_dimension: wgpu::TextureViewDimension::D2,
54                        multisampled: false,
55                    },
56                    count: None,
57                },
58                // Sampler
59                wgpu::BindGroupLayoutEntry {
60                    binding: 1,
61                    visibility: wgpu::ShaderStages::FRAGMENT,
62                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
63                    count: None,
64                },
65                // Uniforms
66                wgpu::BindGroupLayoutEntry {
67                    binding: 2,
68                    visibility: wgpu::ShaderStages::FRAGMENT,
69                    ty: wgpu::BindingType::Buffer {
70                        ty: wgpu::BufferBindingType::Uniform,
71                        has_dynamic_offset: false,
72                        min_binding_size: NonZeroU64::new(32),
73                    },
74                    count: None,
75                },
76                // SSAO texture
77                wgpu::BindGroupLayoutEntry {
78                    binding: 3,
79                    visibility: wgpu::ShaderStages::FRAGMENT,
80                    ty: wgpu::BindingType::Texture {
81                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
82                        view_dimension: wgpu::TextureViewDimension::D2,
83                        multisampled: false,
84                    },
85                    count: None,
86                },
87            ],
88        });
89
90        // Create shader
91        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
92            label: Some("Tone Map Shader"),
93            source: wgpu::ShaderSource::Wgsl(include_str!("shaders/tone_map.wgsl").into()),
94        });
95
96        // Create pipeline layout
97        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
98            label: Some("Tone Map Pipeline Layout"),
99            bind_group_layouts: &[&bind_group_layout],
100            push_constant_ranges: &[],
101        });
102
103        // Create render pipeline
104        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
105            label: Some("Tone Map Pipeline"),
106            layout: Some(&pipeline_layout),
107            vertex: wgpu::VertexState {
108                module: &shader,
109                entry_point: Some("vs_main"),
110                buffers: &[],
111                compilation_options: wgpu::PipelineCompilationOptions::default(),
112            },
113            fragment: Some(wgpu::FragmentState {
114                module: &shader,
115                entry_point: Some("fs_main"),
116                targets: &[Some(wgpu::ColorTargetState {
117                    format: output_format,
118                    blend: None,
119                    write_mask: wgpu::ColorWrites::ALL,
120                })],
121                compilation_options: wgpu::PipelineCompilationOptions::default(),
122            }),
123            primitive: wgpu::PrimitiveState {
124                topology: wgpu::PrimitiveTopology::TriangleList,
125                ..Default::default()
126            },
127            depth_stencil: None,
128            multisample: wgpu::MultisampleState::default(),
129            multiview: None,
130            cache: None,
131        });
132
133        // Create uniform buffer
134        let uniforms = ToneMapUniforms::default();
135        let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
136            label: Some("Tone Map Uniform Buffer"),
137            contents: bytemuck::cast_slice(&[uniforms]),
138            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
139        });
140
141        // Create sampler
142        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
143            label: Some("Tone Map Sampler"),
144            mag_filter: wgpu::FilterMode::Linear,
145            min_filter: wgpu::FilterMode::Linear,
146            ..Default::default()
147        });
148
149        Self {
150            pipeline,
151            bind_group_layout,
152            uniform_buffer,
153            sampler,
154        }
155    }
156
157    /// Updates the tone mapping uniforms.
158    pub fn update_uniforms(
159        &self,
160        queue: &wgpu::Queue,
161        exposure: f32,
162        white_level: f32,
163        gamma: f32,
164        ssao_enabled: bool,
165    ) {
166        let uniforms = ToneMapUniforms {
167            exposure,
168            white_level,
169            gamma,
170            ssao_enabled: u32::from(ssao_enabled),
171            _padding: [0.0; 4],
172        };
173        queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniforms]));
174    }
175
176    /// Creates a bind group for rendering.
177    #[must_use]
178    pub fn create_bind_group(
179        &self,
180        device: &wgpu::Device,
181        input_view: &wgpu::TextureView,
182        ssao_view: &wgpu::TextureView,
183    ) -> wgpu::BindGroup {
184        device.create_bind_group(&wgpu::BindGroupDescriptor {
185            label: Some("Tone Map Bind Group"),
186            layout: &self.bind_group_layout,
187            entries: &[
188                wgpu::BindGroupEntry {
189                    binding: 0,
190                    resource: wgpu::BindingResource::TextureView(input_view),
191                },
192                wgpu::BindGroupEntry {
193                    binding: 1,
194                    resource: wgpu::BindingResource::Sampler(&self.sampler),
195                },
196                wgpu::BindGroupEntry {
197                    binding: 2,
198                    resource: self.uniform_buffer.as_entire_binding(),
199                },
200                wgpu::BindGroupEntry {
201                    binding: 3,
202                    resource: wgpu::BindingResource::TextureView(ssao_view),
203                },
204            ],
205        })
206    }
207
208    /// Renders the tone mapping pass.
209    pub fn render(
210        &self,
211        encoder: &mut wgpu::CommandEncoder,
212        output_view: &wgpu::TextureView,
213        bind_group: &wgpu::BindGroup,
214    ) {
215        let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
216            label: Some("Tone Map Pass"),
217            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
218                view: output_view,
219                resolve_target: None,
220                ops: wgpu::Operations {
221                    load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
222                    store: wgpu::StoreOp::Store,
223                },
224                depth_slice: None,
225            })],
226            depth_stencil_attachment: None,
227            ..Default::default()
228        });
229
230        render_pass.set_pipeline(&self.pipeline);
231        render_pass.set_bind_group(0, bind_group, &[]);
232        render_pass.draw(0..3, 0..1); // Fullscreen triangle
233    }
234
235    /// Renders tone mapping from input HDR texture to output texture.
236    /// Convenience method that creates a bind group and renders in one call.
237    pub fn render_to_target(
238        &self,
239        device: &wgpu::Device,
240        encoder: &mut wgpu::CommandEncoder,
241        input_view: &wgpu::TextureView,
242        ssao_view: &wgpu::TextureView,
243        output_view: &wgpu::TextureView,
244    ) {
245        let bind_group = self.create_bind_group(device, input_view, ssao_view);
246        self.render(encoder, output_view, &bind_group);
247    }
248}