1use 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
15pub 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
86pub(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#[derive(Debug, Clone)]
160pub struct Skybox {
161 is_empty: Arc<AtomicBool>,
162 environment_cubemap: Texture,
164}
165
166impl Skybox {
167 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 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 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 pub fn environment_cubemap_texture(&self) -> &texture::Texture {
248 &self.environment_cubemap
249 }
250
251 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 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 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 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}