Skip to main content

ferrum_wgpu/renderer/
hdr.rs

1use crate::renderer::{self, Texture, texture};
2use anyhow::Error;
3use image::{DynamicImage, ImageDecoder, codecs::hdr::HdrDecoder, codecs::openexr::OpenExrDecoder};
4use wgpu::{
5    BindGroup, BindGroupLayout, CommandEncoder, ComputePass, ComputePipeline, Operations,
6    PipelineLayout, RenderPass, RenderPipeline, ShaderModule, ShaderModuleDescriptor,
7    TextureFormat,
8};
9
10/// Skybox: HDR pipeline (tonemapping), environment cubemap and the render
11/// pipeline that paints the sky where no geometry was drawn.
12pub struct EnviroimentDesc {
13    pub bytes: Vec<u8>,
14    pub format: SkyFormat,
15}
16
17impl EnviroimentDesc {
18    pub fn new(bytes: Vec<u8>, format: SkyFormat) -> Self {
19        Self { bytes, format }
20    }
21}
22
23pub struct SkyRig {
24    pub texture: texture::CubeTexture,
25    pub bind_group: BindGroup,
26    pub pipeline: RenderPipeline,
27}
28
29impl SkyRig {
30    pub fn new(
31        device: &wgpu::Device,
32        queue: &wgpu::Queue,
33        _config: &wgpu::SurfaceConfiguration,
34        camera_layout: &BindGroupLayout,
35        format: wgpu::TextureFormat,
36        desc: EnviroimentDesc,
37    ) -> anyhow::Result<Self, Error> {
38        let hdr_loader: HdrLoader = HdrLoader::new(device);
39
40        let sky_texture: texture::CubeTexture = hdr_loader.load_equirectangular_bytes(
41            device,
42            queue,
43            &desc.bytes,
44            &desc.format,
45            None,
46            Some("sky_texture"),
47        )?;
48
49        let environment_layout: BindGroupLayout =
50            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
51                label: Some("environment_layout"),
52                entries: &[
53                    wgpu::BindGroupLayoutEntry {
54                        binding: 0,
55                        visibility: wgpu::ShaderStages::FRAGMENT,
56                        ty: wgpu::BindingType::Texture {
57                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
58                            view_dimension: wgpu::TextureViewDimension::Cube,
59                            multisampled: false,
60                        },
61                        count: None,
62                    },
63                    wgpu::BindGroupLayoutEntry {
64                        binding: 1,
65                        visibility: wgpu::ShaderStages::FRAGMENT,
66                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
67                        count: None,
68                    },
69                ],
70            });
71
72        let bind_group: BindGroup = device.create_bind_group(&wgpu::BindGroupDescriptor {
73            label: Some("environment_bind_group"),
74            layout: &environment_layout,
75            entries: &[
76                wgpu::BindGroupEntry {
77                    binding: 0,
78                    resource: wgpu::BindingResource::TextureView(sky_texture.view()),
79                },
80                wgpu::BindGroupEntry {
81                    binding: 1,
82                    resource: wgpu::BindingResource::Sampler(sky_texture.sampler()),
83                },
84            ],
85        });
86
87        let layout: PipelineLayout =
88            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
89                label: Some("Sky Pipeline Layout"),
90                bind_group_layouts: &[Some(camera_layout), Some(&environment_layout)],
91                immediate_size: 0,
92            });
93
94        // LessEqual with the fullscreen triangle at z=1.0: the sky only wins on
95        // pixels the geometry left untouched.
96        let pipeline: RenderPipeline = renderer::create_render_pipeline(
97            device,
98            &layout,
99            format,
100            Some(Texture::DEPTH_FORMAT),
101            &[],
102            wgpu::PrimitiveTopology::TriangleList,
103            wgpu::include_wgsl!("../shaders/sky.wgsl"),
104            wgpu::CompareFunction::LessEqual,
105        );
106
107        Ok(Self {
108            texture: sky_texture,
109            bind_group,
110            pipeline,
111        })
112    }
113}
114
115/// Equirectangular sky file format.
116#[derive(Debug, Clone, Copy)]
117pub enum SkyFormat {
118    Hdr,
119    Exr,
120}
121
122pub struct HdrPipeline {
123    pipeline: wgpu::RenderPipeline,
124    bind_group: wgpu::BindGroup,
125    texture: renderer::Texture,
126    width: u32,
127    heigth: u32,
128    format: wgpu::TextureFormat,
129    layout: wgpu::BindGroupLayout,
130}
131
132impl HdrPipeline {
133    pub fn new(device: &wgpu::Device, config: &wgpu::SurfaceConfiguration) -> Self {
134        let width: u32 = config.width;
135        let heigth: u32 = config.height;
136
137        let format: TextureFormat = wgpu::TextureFormat::Rgba16Float;
138
139        let texture: texture::Texture = texture::Texture::create_2d_texture(
140            device,
141            width,
142            heigth,
143            format,
144            wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT,
145            wgpu::FilterMode::Nearest,
146            Some("Hdr::texture"),
147        );
148
149        let layout: BindGroupLayout =
150            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
151                label: Some("Hdr::layout"),
152                entries: &[
153                    wgpu::BindGroupLayoutEntry {
154                        binding: 0,
155                        visibility: wgpu::ShaderStages::FRAGMENT,
156                        ty: wgpu::BindingType::Texture {
157                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
158                            view_dimension: wgpu::TextureViewDimension::D2,
159                            multisampled: false,
160                        },
161                        count: None,
162                    },
163                    wgpu::BindGroupLayoutEntry {
164                        binding: 1,
165                        visibility: wgpu::ShaderStages::FRAGMENT,
166                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
167                        count: None,
168                    },
169                ],
170            });
171
172        let bind_group: BindGroup = device.create_bind_group(&wgpu::BindGroupDescriptor {
173            label: Some("Hdr::bind_group"),
174            layout: &layout,
175            entries: &[
176                wgpu::BindGroupEntry {
177                    binding: 0,
178                    resource: wgpu::BindingResource::TextureView(&texture.view),
179                },
180                wgpu::BindGroupEntry {
181                    binding: 1,
182                    resource: wgpu::BindingResource::Sampler(&texture.sampler),
183                },
184            ],
185        });
186
187        let shader: ShaderModuleDescriptor = wgpu::include_wgsl!("../shaders/hdr.wgsl");
188        let pipeline_layout: PipelineLayout =
189            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
190                label: Some("Hdr::pipeline_layout"),
191                bind_group_layouts: &[Some(&layout)],
192                immediate_size: 0,
193            });
194
195        let pipeline: RenderPipeline = renderer::create_render_pipeline(
196            device,
197            &pipeline_layout,
198            config.format.add_srgb_suffix(),
199            None,
200            &[],
201            wgpu::PrimitiveTopology::TriangleList,
202            shader,
203            wgpu::CompareFunction::LessEqual,
204        );
205
206        Self {
207            pipeline,
208            bind_group,
209            layout,
210            texture,
211            width,
212            heigth,
213            format,
214        }
215    }
216
217    pub fn resize(&mut self, device: &wgpu::Device, width: u32, height: u32) {
218        self.texture = texture::Texture::create_2d_texture(
219            device,
220            width,
221            height,
222            wgpu::TextureFormat::Rgba16Float,
223            wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT,
224            wgpu::FilterMode::Nearest,
225            Some("Hdr::texture"),
226        );
227
228        self.bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
229            label: Some("Hrd::bind_group"),
230            layout: &self.layout,
231            entries: &[
232                wgpu::BindGroupEntry {
233                    binding: 0,
234                    resource: wgpu::BindingResource::TextureView(&self.texture.view),
235                },
236                wgpu::BindGroupEntry {
237                    binding: 1,
238                    resource: wgpu::BindingResource::Sampler(&self.texture.sampler),
239                },
240            ],
241        });
242
243        self.width = width;
244        self.heigth = height;
245    }
246
247    pub fn view(&self) -> &wgpu::TextureView {
248        &self.texture.view
249    }
250
251    pub fn format(&self) -> wgpu::TextureFormat {
252        self.format
253    }
254
255    pub fn process(&self, encoder: &mut wgpu::CommandEncoder, ouput: &wgpu::TextureView) {
256        let mut pass: RenderPass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
257            label: Some("Hdr::process"),
258            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
259                view: ouput,
260                depth_slice: None,
261                resolve_target: None,
262                ops: Operations {
263                    load: wgpu::LoadOp::Load,
264                    store: wgpu::StoreOp::Store,
265                },
266            })],
267            depth_stencil_attachment: None,
268            timestamp_writes: None,
269            occlusion_query_set: None,
270            multiview_mask: None,
271        });
272
273        pass.set_pipeline(&self.pipeline);
274        pass.set_bind_group(0, &self.bind_group, &[]);
275        pass.draw(0..3, 0..1);
276    }
277}
278
279pub struct HdrLoader {
280    source_format: wgpu::TextureFormat,
281    cube_format: wgpu::TextureFormat,
282    equirect_layout: wgpu::BindGroupLayout,
283    equirect_to_cubemap: wgpu::ComputePipeline,
284}
285
286impl HdrLoader {
287    pub fn new(device: &wgpu::Device) -> Self {
288        let module: ShaderModule =
289            device.create_shader_module(wgpu::include_wgsl!("../shaders/equirectangular.wgsl"));
290        // Source equirectangular: 32-bit float because that's what .hdr / .exr give us.
291        // Cubemap destination: 16-bit float, filterable on every device without features.
292        let source_format: TextureFormat = wgpu::TextureFormat::Rgba32Float;
293        let cube_format: TextureFormat = wgpu::TextureFormat::Rgba16Float;
294        let equirect_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
295            label: Some("HdrLoader::equirect_layout"),
296            entries: &[
297                wgpu::BindGroupLayoutEntry {
298                    binding: 0,
299                    visibility: wgpu::ShaderStages::COMPUTE,
300                    ty: wgpu::BindingType::Texture {
301                        sample_type: wgpu::TextureSampleType::Float { filterable: false },
302                        view_dimension: wgpu::TextureViewDimension::D2,
303                        multisampled: false,
304                    },
305                    count: None,
306                },
307                wgpu::BindGroupLayoutEntry {
308                    binding: 1,
309                    visibility: wgpu::ShaderStages::COMPUTE,
310                    ty: wgpu::BindingType::StorageTexture {
311                        access: wgpu::StorageTextureAccess::WriteOnly,
312                        format: cube_format,
313                        view_dimension: wgpu::TextureViewDimension::D2Array,
314                    },
315                    count: None,
316                },
317            ],
318        });
319
320        let pipeline_layout: PipelineLayout =
321            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
322                label: Some("Cubemap pipeline_layout"),
323                bind_group_layouts: &[Some(&equirect_layout)],
324                immediate_size: 0,
325            });
326
327        let equirect_to_cubemap: ComputePipeline =
328            device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
329                label: Some("equirect_to_cubemap"),
330                layout: Some(&pipeline_layout),
331                module: &module,
332                entry_point: Some("compute_equirect_to_cubemap"),
333                compilation_options: Default::default(),
334                cache: None,
335            });
336
337        Self {
338            equirect_to_cubemap,
339            source_format,
340            cube_format,
341            equirect_layout,
342        }
343    }
344
345    pub fn load_equirectangular_bytes(
346        &self,
347        device: &wgpu::Device,
348        queue: &wgpu::Queue,
349        data: &[u8],
350        format: &SkyFormat,
351        dst_size: Option<u32>,
352        label: Option<&str>,
353    ) -> anyhow::Result<texture::CubeTexture> {
354        let (pixels, width, height): (Vec<[f32; 4]>, u32, u32) = match format {
355            SkyFormat::Hdr => Self::decode_radiance_hdr(data)?,
356            SkyFormat::Exr => Self::decode_openexr(data)?,
357        };
358
359        let dst_size: u32 = dst_size.unwrap_or_else(|| Self::cube_face_size_for_source(width));
360
361        let src: Texture = texture::Texture::create_2d_texture(
362            device,
363            width,
364            height,
365            self.source_format,
366            wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
367            wgpu::FilterMode::Linear,
368            None,
369        );
370
371        queue.write_texture(
372            wgpu::TexelCopyTextureInfo {
373                texture: &src.texture,
374                mip_level: 0,
375                origin: wgpu::Origin3d::ZERO,
376                aspect: wgpu::TextureAspect::All,
377            },
378            bytemuck::cast_slice(&pixels),
379            wgpu::TexelCopyBufferLayout {
380                offset: 0,
381                bytes_per_row: Some(
382                    src.texture.size().width * std::mem::size_of::<[f32; 4]>() as u32,
383                ),
384                rows_per_image: Some(src.texture.size().height),
385            },
386            src.texture.size(),
387        );
388
389        let dst: texture::CubeTexture = texture::CubeTexture::create_2d(
390            device,
391            dst_size,
392            dst_size,
393            self.cube_format,
394            1,
395            wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::TEXTURE_BINDING,
396            wgpu::FilterMode::Linear,
397            label,
398        );
399
400        let dst_view: wgpu::TextureView = dst.texture().create_view(&wgpu::TextureViewDescriptor {
401            label,
402            dimension: Some(wgpu::TextureViewDimension::D2Array),
403            ..Default::default()
404        });
405
406        let bind_group: BindGroup = device.create_bind_group(&wgpu::BindGroupDescriptor {
407            label,
408            layout: &self.equirect_layout,
409            entries: &[
410                wgpu::BindGroupEntry {
411                    binding: 0,
412                    resource: wgpu::BindingResource::TextureView(&src.view),
413                },
414                wgpu::BindGroupEntry {
415                    binding: 1,
416                    resource: wgpu::BindingResource::TextureView(&dst_view),
417                },
418            ],
419        });
420
421        let mut encoder: CommandEncoder = device.create_command_encoder(&Default::default());
422        let mut pass: ComputePass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
423            label,
424            timestamp_writes: None,
425        });
426
427        let num_workgroups: u32 = dst_size.div_ceil(16);
428        pass.set_pipeline(&self.equirect_to_cubemap);
429        pass.set_bind_group(0, &bind_group, &[]);
430        pass.dispatch_workgroups(num_workgroups, num_workgroups, 6);
431
432        drop(pass);
433
434        queue.submit([encoder.finish()]);
435
436        Ok(dst)
437    }
438
439    fn decode_radiance_hdr(data: &[u8]) -> anyhow::Result<(Vec<[f32; 4]>, u32, u32)> {
440        let hdr_decoder: HdrDecoder<std::io::Cursor<&[u8]>> =
441            HdrDecoder::new(std::io::Cursor::new(data))?;
442        let meta = hdr_decoder.metadata();
443        let (width, height) = (meta.width, meta.height);
444
445        #[cfg(not(target_arch = "wasm32"))]
446        let pixels: Vec<[f32; 4]> = {
447            let mut pixels: Vec<[f32; 4]> = vec![[0.0; 4]; width as usize * height as usize];
448            hdr_decoder.read_image_transform(
449                |pix| {
450                    let rgb = pix.to_hdr();
451                    [rgb.0[0], rgb.0[1], rgb.0[2], 1.0f32]
452                },
453                &mut pixels[..],
454            )?;
455            pixels
456        };
457        #[cfg(target_arch = "wasm32")]
458        let pixels: Vec<[f32; 4]> = hdr_decoder
459            .read_image_native()?
460            .into_iter()
461            .map(|pix| {
462                let rgb = pix.to_hdr();
463                [rgb.0[0], rgb.0[1], rgb.0[2], 1.0f32]
464            })
465            .collect();
466
467        Ok((pixels, width, height))
468    }
469
470    fn decode_openexr(data: &[u8]) -> anyhow::Result<(Vec<[f32; 4]>, u32, u32)> {
471        let decoder = OpenExrDecoder::new(std::io::Cursor::new(data))?;
472        let (width, height) = decoder.dimensions();
473        let dynamic = DynamicImage::from_decoder(decoder)?;
474        let rgba = dynamic.into_rgba32f();
475        let pixels: Vec<[f32; 4]> = rgba
476            .as_raw()
477            .chunks_exact(4)
478            .map(|c| [c[0], c[1], c[2], c[3]])
479            .collect();
480        Ok((pixels, width, height))
481    }
482
483    fn cube_face_size_for_source(source_width: u32) -> u32 {
484        let target: u32 = source_width.max(64) / 6;
485        1u32 << (31 - target.leading_zeros())
486    }
487}