renderling/skybox/
cpu.rs

1//! CPU-side code for skybox rendering.
2use core::sync::atomic::AtomicBool;
3use std::sync::Arc;
4
5use craballoc::{prelude::SlabAllocator, runtime::WgpuRuntime};
6use glam::{Mat4, UVec2, Vec3};
7
8use crate::{
9    atlas::AtlasImage,
10    camera::Camera,
11    cubemap::EquirectangularImageToCubemapBlitter,
12    texture::{self, Texture},
13};
14
15/// Render pipeline used to draw a skybox.
16pub struct SkyboxRenderPipeline {
17    pub pipeline: wgpu::RenderPipeline,
18    msaa_sample_count: u32,
19}
20
21impl SkyboxRenderPipeline {
22    pub fn msaa_sample_count(&self) -> u32 {
23        self.msaa_sample_count
24    }
25}
26
27fn skybox_bindgroup_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
28    device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
29        label: Some("skybox bindgroup"),
30        entries: &[
31            wgpu::BindGroupLayoutEntry {
32                binding: 0,
33                visibility: wgpu::ShaderStages::VERTEX,
34                ty: wgpu::BindingType::Buffer {
35                    ty: wgpu::BufferBindingType::Storage { read_only: true },
36                    has_dynamic_offset: false,
37                    min_binding_size: None,
38                },
39                count: None,
40            },
41            wgpu::BindGroupLayoutEntry {
42                binding: 1,
43                visibility: wgpu::ShaderStages::FRAGMENT,
44                ty: wgpu::BindingType::Texture {
45                    sample_type: wgpu::TextureSampleType::Float { filterable: true },
46                    view_dimension: wgpu::TextureViewDimension::Cube,
47                    multisampled: false,
48                },
49                count: None,
50            },
51            wgpu::BindGroupLayoutEntry {
52                binding: 2,
53                visibility: wgpu::ShaderStages::FRAGMENT,
54                ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
55                count: None,
56            },
57        ],
58    })
59}
60
61pub(crate) fn create_skybox_bindgroup(
62    device: &wgpu::Device,
63    slab_buffer: &wgpu::Buffer,
64    texture: &Texture,
65) -> wgpu::BindGroup {
66    device.create_bind_group(&wgpu::BindGroupDescriptor {
67        label: Some("skybox"),
68        layout: &skybox_bindgroup_layout(device),
69        entries: &[
70            wgpu::BindGroupEntry {
71                binding: 0,
72                resource: slab_buffer.as_entire_binding(),
73            },
74            wgpu::BindGroupEntry {
75                binding: 1,
76                resource: wgpu::BindingResource::TextureView(&texture.view),
77            },
78            wgpu::BindGroupEntry {
79                binding: 2,
80                resource: wgpu::BindingResource::Sampler(&texture.sampler),
81            },
82        ],
83    })
84}
85
86/// Create the skybox rendering pipeline.
87pub(crate) fn create_skybox_render_pipeline(
88    device: &wgpu::Device,
89    format: wgpu::TextureFormat,
90    multisample_count: Option<u32>,
91) -> SkyboxRenderPipeline {
92    log::trace!("creating skybox render pipeline with format '{format:?}'");
93    let vertex_linkage = crate::linkage::skybox_vertex::linkage(device);
94    let fragment_linkage = crate::linkage::skybox_cubemap_fragment::linkage(device);
95    let bg_layout = skybox_bindgroup_layout(device);
96    let pp_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
97        label: Some("skybox pipeline layout"),
98        bind_group_layouts: &[&bg_layout],
99        push_constant_ranges: &[],
100    });
101    let msaa_sample_count = multisample_count.unwrap_or(1);
102    SkyboxRenderPipeline {
103        pipeline: device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
104            label: Some("skybox render pipeline"),
105            layout: Some(&pp_layout),
106            vertex: wgpu::VertexState {
107                module: &vertex_linkage.module,
108                entry_point: Some(vertex_linkage.entry_point),
109                buffers: &[],
110                compilation_options: Default::default(),
111            },
112            primitive: wgpu::PrimitiveState {
113                topology: wgpu::PrimitiveTopology::TriangleList,
114                strip_index_format: None,
115                front_face: wgpu::FrontFace::Ccw,
116                cull_mode: None,
117                unclipped_depth: false,
118                polygon_mode: wgpu::PolygonMode::Fill,
119                conservative: false,
120            },
121            depth_stencil: Some(wgpu::DepthStencilState {
122                format: wgpu::TextureFormat::Depth32Float,
123                depth_write_enabled: true,
124                depth_compare: wgpu::CompareFunction::LessEqual,
125                stencil: wgpu::StencilState::default(),
126                bias: wgpu::DepthBiasState::default(),
127            }),
128            multisample: wgpu::MultisampleState {
129                mask: !0,
130                alpha_to_coverage_enabled: false,
131                count: msaa_sample_count,
132            },
133            fragment: Some(wgpu::FragmentState {
134                module: &fragment_linkage.module,
135                entry_point: Some(fragment_linkage.entry_point),
136                targets: &[Some(wgpu::ColorTargetState {
137                    format,
138                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
139                    write_mask: wgpu::ColorWrites::ALL,
140                })],
141                compilation_options: Default::default(),
142            }),
143            multiview: None,
144            cache: None,
145        }),
146        msaa_sample_count,
147    }
148}
149
150/// An HDR skybox.
151///
152/// Skyboxes provide an environment cubemap around all your scenery
153/// that acts as a background.
154///
155/// A [`Skybox`] can also be used to create [`Ibl`], which illuminates
156/// your scene using the environment map as a light source.
157///
158/// All clones of a skybox point to the same underlying data.
159#[derive(Debug, Clone)]
160pub struct Skybox {
161    is_empty: Arc<AtomicBool>,
162    // Cubemap texture of the environment cubemap
163    environment_cubemap: Texture,
164}
165
166impl Skybox {
167    /// Create an empty, transparent skybox.
168    pub fn empty(runtime: impl AsRef<WgpuRuntime>) -> Self {
169        let runtime = runtime.as_ref();
170        log::trace!("creating empty skybox");
171        let hdr_img = AtlasImage {
172            pixels: vec![0u8; 4 * 4],
173            size: UVec2::splat(1),
174            format: crate::atlas::AtlasImageFormat::R32G32B32A32FLOAT,
175            apply_linear_transfer: false,
176        };
177        let s = Self::new(runtime, hdr_img);
178        s.is_empty.store(true, std::sync::atomic::Ordering::Relaxed);
179        s
180    }
181
182    /// Create a new `Skybox`.
183    pub fn new(runtime: impl AsRef<WgpuRuntime>, hdr_img: AtlasImage) -> Self {
184        let runtime = runtime.as_ref();
185        log::trace!("creating skybox");
186
187        let slab = SlabAllocator::new(runtime, "skybox-slab", wgpu::BufferUsages::VERTEX);
188        let proj = Mat4::perspective_rh(std::f32::consts::FRAC_PI_2, 1.0, 0.1, 10.0);
189        let camera = Camera::new(&slab).with_projection(proj);
190        let buffer = slab.commit();
191        let mut buffer_upkeep = || {
192            let possibly_new_buffer = slab.commit();
193            debug_assert!(!possibly_new_buffer.is_new_this_commit());
194        };
195
196        let equirectangular_texture = Skybox::hdr_texture_from_atlas_image(runtime, hdr_img);
197        let views = [
198            Mat4::look_at_rh(
199                Vec3::new(0.0, 0.0, 0.0),
200                Vec3::new(1.0, 0.0, 0.0),
201                Vec3::new(0.0, -1.0, 0.0),
202            ),
203            Mat4::look_at_rh(
204                Vec3::new(0.0, 0.0, 0.0),
205                Vec3::new(-1.0, 0.0, 0.0),
206                Vec3::new(0.0, -1.0, 0.0),
207            ),
208            Mat4::look_at_rh(
209                Vec3::new(0.0, 0.0, 0.0),
210                Vec3::new(0.0, -1.0, 0.0),
211                Vec3::new(0.0, 0.0, -1.0),
212            ),
213            Mat4::look_at_rh(
214                Vec3::new(0.0, 0.0, 0.0),
215                Vec3::new(0.0, 1.0, 0.0),
216                Vec3::new(0.0, 0.0, 1.0),
217            ),
218            Mat4::look_at_rh(
219                Vec3::new(0.0, 0.0, 0.0),
220                Vec3::new(0.0, 0.0, 1.0),
221                Vec3::new(0.0, -1.0, 0.0),
222            ),
223            Mat4::look_at_rh(
224                Vec3::new(0.0, 0.0, 0.0),
225                Vec3::new(0.0, 0.0, -1.0),
226                Vec3::new(0.0, -1.0, 0.0),
227            ),
228        ];
229
230        // Create environment map.
231        let environment_cubemap = Skybox::create_environment_map_from_hdr(
232            runtime,
233            &buffer,
234            &mut buffer_upkeep,
235            &equirectangular_texture,
236            &camera,
237            views,
238        );
239
240        Skybox {
241            is_empty: Arc::new(false.into()),
242            environment_cubemap,
243        }
244    }
245
246    /// Return a reference to the environment cubemap texture.
247    pub fn environment_cubemap_texture(&self) -> &texture::Texture {
248        &self.environment_cubemap
249    }
250
251    /// Convert an HDR [`AtlasImage`] into a texture.
252    pub fn hdr_texture_from_atlas_image(
253        runtime: impl AsRef<WgpuRuntime>,
254        img: AtlasImage,
255    ) -> Texture {
256        let runtime = runtime.as_ref();
257        Texture::new_with(
258            runtime,
259            Some("create hdr texture"),
260            None,
261            Some(runtime.device.create_sampler(&wgpu::SamplerDescriptor {
262                mag_filter: wgpu::FilterMode::Nearest,
263                min_filter: wgpu::FilterMode::Nearest,
264                mipmap_filter: wgpu::FilterMode::Nearest,
265                ..Default::default()
266            })),
267            wgpu::TextureFormat::Rgba32Float,
268            4,
269            4,
270            img.size.x,
271            img.size.y,
272            1,
273            &img.pixels,
274        )
275    }
276
277    /// Create an HDR equirectangular texture from bytes.
278    pub fn create_hdr_texture(runtime: impl AsRef<WgpuRuntime>, hdr_data: &[u8]) -> Texture {
279        let runtime = runtime.as_ref();
280        let img = AtlasImage::from_hdr_bytes(hdr_data).unwrap();
281        Self::hdr_texture_from_atlas_image(runtime, img)
282    }
283
284    fn create_environment_map_from_hdr(
285        runtime: impl AsRef<WgpuRuntime>,
286        buffer: &wgpu::Buffer,
287        buffer_upkeep: impl FnMut(),
288        hdr_texture: &Texture,
289        camera: &Camera,
290        views: [Mat4; 6],
291    ) -> Texture {
292        let runtime = runtime.as_ref();
293        let device = &runtime.device;
294        let queue = &runtime.queue;
295        // Create the cubemap-making pipeline.
296        let pipeline =
297            EquirectangularImageToCubemapBlitter::new(device, wgpu::TextureFormat::Rgba16Float);
298
299        let resources = (
300            device,
301            queue,
302            Some("hdr environment map"),
303            wgpu::BufferUsages::VERTEX,
304        );
305        let bindgroup = EquirectangularImageToCubemapBlitter::create_bindgroup(
306            device,
307            resources.2,
308            buffer,
309            hdr_texture,
310        );
311
312        texture::Texture::render_cubemap(
313            runtime,
314            "skybox-environment",
315            &pipeline.0,
316            buffer_upkeep,
317            camera,
318            &bindgroup,
319            views,
320            512,
321            Some(9),
322        )
323    }
324
325    /// Returns whether this skybox is empty.
326    pub fn is_empty(&self) -> bool {
327        self.is_empty.load(std::sync::atomic::Ordering::Relaxed)
328    }
329}
330
331#[cfg(test)]
332mod test {
333    use glam::Vec3;
334
335    use crate::{context::Context, test::BlockOnFuture};
336
337    #[test]
338    fn hdr_skybox_scene() {
339        let ctx = Context::headless(600, 400).block();
340        let proj = crate::camera::perspective(600.0, 400.0);
341        let view = crate::camera::look_at(Vec3::new(0.0, 0.0, 2.0), Vec3::ZERO, Vec3::Y);
342        let stage = ctx.new_stage();
343        let _camera = stage.new_camera().with_projection_and_view(proj, view);
344        let skybox = stage
345            .new_skybox_from_path("../../img/hdr/resting_place.hdr")
346            .unwrap();
347        stage.use_skybox(&skybox);
348        let frame = ctx.get_next_frame().unwrap();
349        stage.render(&frame.view());
350        let img = frame.read_linear_image().block().unwrap();
351        img_diff::assert_img_eq("skybox/hdr.png", img);
352    }
353}