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
10pub 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 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#[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 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}