1use std::sync::Arc;
3
4use glam::{Mat4, UVec2, Vec3, Vec4};
5use image::GenericImageView;
6
7use crate::{
8 stage::{Stage, StageRendering},
9 texture::Texture,
10};
11
12use super::shader::{CubemapDescriptor, CubemapFaceDirection};
13
14pub fn cpu_sample_cubemap(cubemap: &[image::DynamicImage; 6], coord: Vec3) -> Vec4 {
15 let coord = coord.normalize_or(Vec3::X);
16 let (face_index, uv) = CubemapDescriptor::get_face_index_and_uv(coord);
17
18 let image = &cubemap[face_index];
20
21 let (width, height) = image.dimensions();
23 let px = uv.x * (width as f32 - 1.0);
24 let py = uv.y * (height as f32 - 1.0);
25
26 let image::Rgba([r, g, b, a]) = image.get_pixel(px.round() as u32, py.round() as u32);
28
29 Vec4::new(
31 r as f32 / 255.0,
32 g as f32 / 255.0,
33 b as f32 / 255.0,
34 a as f32 / 255.0,
35 )
36}
37
38pub struct SceneCubemap {
42 pipeline: Arc<wgpu::RenderPipeline>,
43 cubemap_texture: wgpu::Texture,
44 depth_texture: crate::texture::Texture,
45 clear_color: wgpu::Color,
46}
47
48impl SceneCubemap {
49 pub fn new(
50 device: &wgpu::Device,
51 size: UVec2,
52 format: wgpu::TextureFormat,
53 clear_color: Vec4,
54 ) -> Self {
55 let label = Some("scene-to-cubemap");
56 let cubemap_texture = device.create_texture(&wgpu::TextureDescriptor {
57 label,
58 size: wgpu::Extent3d {
59 width: size.x,
60 height: size.y,
61 depth_or_array_layers: 6,
62 },
63 mip_level_count: 1,
64 sample_count: 1,
65 dimension: wgpu::TextureDimension::D2,
66 format,
67 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
68 | wgpu::TextureUsages::TEXTURE_BINDING
69 | wgpu::TextureUsages::COPY_DST
70 | wgpu::TextureUsages::COPY_SRC,
71 view_formats: &[],
72 });
73 let depth_texture = Texture::create_depth_texture(device, size.x, size.y, 1, label);
74 let pipeline = Arc::new(Stage::create_primitive_pipeline(device, format, 1));
75 Self {
76 pipeline,
77 cubemap_texture,
78 depth_texture,
79 clear_color: wgpu::Color {
80 r: clear_color.x as f64,
81 g: clear_color.y as f64,
82 b: clear_color.z as f64,
83 a: clear_color.w as f64,
84 },
85 }
86 }
87
88 pub fn run(&self, stage: &Stage) {
89 let previous_camera_id = stage.used_camera_id();
90
91 let camera = stage.geometry.new_camera();
93 stage.use_camera(&camera);
94
95 let fovy = std::f32::consts::FRAC_PI_2;
100 let aspect = self.cubemap_texture.width() as f32 / self.cubemap_texture.height() as f32;
101 let projection = Mat4::perspective_lh(fovy, aspect, 1.0, 25.0);
102 for (i, face) in CubemapFaceDirection::FACES.iter().enumerate() {
105 camera.set_projection_and_view(projection, face.view());
108 let label_s = format!("scene-to-cubemap-{i}");
109 let view = self
110 .cubemap_texture
111 .create_view(&wgpu::TextureViewDescriptor {
112 label: Some(&label_s),
113 base_array_layer: i as u32,
114 array_layer_count: Some(1),
115 dimension: Some(wgpu::TextureViewDimension::D2),
116 ..Default::default()
117 });
118 let color_attachment = wgpu::RenderPassColorAttachment {
119 view: &view,
120 resolve_target: None,
121 ops: wgpu::Operations {
122 load: wgpu::LoadOp::Clear(self.clear_color),
123 store: wgpu::StoreOp::Store,
124 },
125 depth_slice: None,
126 };
127 let depth_stencil_attachment = wgpu::RenderPassDepthStencilAttachment {
128 view: &self.depth_texture.view,
129 depth_ops: Some(wgpu::Operations {
130 load: wgpu::LoadOp::Clear(1.0),
131 store: wgpu::StoreOp::Store,
132 }),
133 stencil_ops: None,
134 };
135 let (_, _) = StageRendering {
136 pipeline: &self.pipeline,
137 stage,
138 color_attachment,
139 depth_stencil_attachment,
140 }
141 .run();
142 }
143
144 stage.use_camera_id(previous_camera_id);
145 }
146}
147
148pub struct EquirectangularImageToCubemapBlitter(pub wgpu::RenderPipeline);
150
151impl EquirectangularImageToCubemapBlitter {
152 pub fn create_bindgroup_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
153 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
154 label: Some("cubemap-making bindgroup"),
155 entries: &[
156 wgpu::BindGroupLayoutEntry {
157 binding: 0,
158 visibility: wgpu::ShaderStages::VERTEX,
159 ty: wgpu::BindingType::Buffer {
160 ty: wgpu::BufferBindingType::Storage { read_only: true },
161 has_dynamic_offset: false,
162 min_binding_size: None,
163 },
164 count: None,
165 },
166 wgpu::BindGroupLayoutEntry {
167 binding: 1,
168 visibility: wgpu::ShaderStages::FRAGMENT,
169 ty: wgpu::BindingType::Texture {
170 sample_type: wgpu::TextureSampleType::Float { filterable: false },
171 view_dimension: wgpu::TextureViewDimension::D2,
172 multisampled: false,
173 },
174 count: None,
175 },
176 wgpu::BindGroupLayoutEntry {
177 binding: 2,
178 visibility: wgpu::ShaderStages::FRAGMENT,
179 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering),
180 count: None,
181 },
182 ],
183 })
184 }
185
186 pub fn create_bindgroup(
187 device: &wgpu::Device,
188 label: Option<&str>,
189 buffer: &wgpu::Buffer,
190 texture: &Texture,
192 ) -> wgpu::BindGroup {
193 device.create_bind_group(&wgpu::BindGroupDescriptor {
194 label,
195 layout: &Self::create_bindgroup_layout(device),
196 entries: &[
197 wgpu::BindGroupEntry {
198 binding: 0,
199 resource: wgpu::BindingResource::Buffer(buffer.as_entire_buffer_binding()),
200 },
201 wgpu::BindGroupEntry {
202 binding: 1,
203 resource: wgpu::BindingResource::TextureView(&texture.view),
204 },
205 wgpu::BindGroupEntry {
206 binding: 2,
207 resource: wgpu::BindingResource::Sampler(&texture.sampler),
208 },
209 ],
210 })
211 }
212
213 pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self {
216 log::trace!("creating cubemap-making render pipeline with format '{format:?}'");
217 let vertex_linkage = crate::linkage::skybox_cubemap_vertex::linkage(device);
218 let fragment_linkage = crate::linkage::skybox_equirectangular_fragment::linkage(device);
219 let bg_layout = Self::create_bindgroup_layout(device);
220 let pp_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
221 label: Some("cubemap-making pipeline layout"),
222 bind_group_layouts: &[&bg_layout],
223 push_constant_ranges: &[],
224 });
225 EquirectangularImageToCubemapBlitter(device.create_render_pipeline(
226 &wgpu::RenderPipelineDescriptor {
227 label: Some("cubemap-making pipeline"),
228 layout: Some(&pp_layout),
229 vertex: wgpu::VertexState {
230 module: &vertex_linkage.module,
231 entry_point: Some(vertex_linkage.entry_point),
232 buffers: &[],
233 compilation_options: Default::default(),
234 },
235 primitive: wgpu::PrimitiveState {
236 topology: wgpu::PrimitiveTopology::TriangleList,
237 strip_index_format: None,
238 front_face: wgpu::FrontFace::Ccw,
239 cull_mode: None,
240 unclipped_depth: false,
241 polygon_mode: wgpu::PolygonMode::Fill,
242 conservative: false,
243 },
244 depth_stencil: None,
245 multisample: wgpu::MultisampleState {
246 mask: !0,
247 alpha_to_coverage_enabled: false,
248 count: 1,
249 },
250 fragment: Some(wgpu::FragmentState {
251 module: &fragment_linkage.module,
252 entry_point: Some(fragment_linkage.entry_point),
253 targets: &[Some(wgpu::ColorTargetState {
254 format,
255 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
256 write_mask: wgpu::ColorWrites::ALL,
257 })],
258 compilation_options: Default::default(),
259 }),
260 multiview: None,
261 cache: None,
262 },
263 ))
264 }
265}
266
267#[cfg(test)]
268mod test {
269 use craballoc::slab::SlabAllocator;
270 use glam::Vec4;
271 use image::GenericImageView;
272
273 use crate::{
274 context::Context,
275 geometry::Vertex,
276 math::{UNIT_INDICES, UNIT_POINTS},
277 test::BlockOnFuture,
278 texture::CopiedTextureBuffer,
279 };
280
281 use super::*;
282
283 #[test]
284 fn hand_rolled_cubemap_sampling() {
285 let width = 256;
286 let height = 256;
287 let ctx = Context::headless(width, height).block();
288 let stage = ctx
289 .new_stage()
290 .with_background_color(Vec4::ZERO)
291 .with_lighting(false)
292 .with_msaa_sample_count(4);
293 let projection = crate::camera::perspective(width as f32, height as f32);
294 let view = Mat4::look_at_rh(Vec3::splat(3.0), Vec3::ZERO, Vec3::Y);
295 let _camera = stage
296 .new_camera()
297 .with_projection_and_view(projection, view);
298 let _rez = stage
301 .new_primitive()
302 .with_vertices(stage.new_vertices(UNIT_POINTS.map(|unit_cube_point| {
303 Vertex::default()
304 .with_position(unit_cube_point * 2.0)
307 .with_color((unit_cube_point + 0.5).extend(1.0))
309 })))
310 .with_indices(stage.new_indices(UNIT_INDICES.map(|u| u as u32)));
311
312 let frame = ctx.get_next_frame().unwrap();
313 stage.render(&frame.view());
314 let img = frame.read_image().block().unwrap();
315 img_diff::assert_img_eq("cubemap/hand_rolled_cubemap_sampling/cube.png", img);
316 frame.present();
317
318 let scene_cubemap = SceneCubemap::new(
319 ctx.get_device(),
320 UVec2::new(width, height),
321 wgpu::TextureFormat::Rgba8Unorm,
322 Vec4::ZERO,
323 );
324 scene_cubemap.run(&stage);
325
326 let slab = SlabAllocator::new(&ctx, "cubemap-sampling-test", wgpu::BufferUsages::empty());
327 let uv = slab.new_value(Vec3::ZERO);
328 let buffer = slab.commit();
329 let label = Some("cubemap-sampling-test");
330 let bind_group_layout =
331 ctx.get_device()
332 .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
333 label,
334 entries: &[
335 wgpu::BindGroupLayoutEntry {
336 binding: 0,
337 visibility: wgpu::ShaderStages::VERTEX,
338 ty: wgpu::BindingType::Buffer {
339 ty: wgpu::BufferBindingType::Storage { read_only: true },
340 has_dynamic_offset: false,
341 min_binding_size: None,
342 },
343 count: None,
344 },
345 wgpu::BindGroupLayoutEntry {
346 binding: 1,
347 visibility: wgpu::ShaderStages::FRAGMENT,
348 ty: wgpu::BindingType::Texture {
349 sample_type: wgpu::TextureSampleType::Float { filterable: true },
350 view_dimension: wgpu::TextureViewDimension::Cube,
351 multisampled: false,
352 },
353 count: None,
354 },
355 wgpu::BindGroupLayoutEntry {
356 binding: 2,
357 visibility: wgpu::ShaderStages::FRAGMENT,
358 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering),
359 count: None,
360 },
361 ],
362 });
363 let cubemap_sampling_pipeline_layout =
364 ctx.get_device()
365 .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
366 label,
367 bind_group_layouts: &[&bind_group_layout],
368 push_constant_ranges: &[],
369 });
370 let vertex = crate::linkage::cubemap_sampling_test_vertex::linkage(ctx.get_device());
371 let fragment = crate::linkage::cubemap_sampling_test_fragment::linkage(ctx.get_device());
372 let cubemap_sampling_pipeline =
373 ctx.get_device()
374 .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
375 label,
376 layout: Some(&cubemap_sampling_pipeline_layout),
377 vertex: wgpu::VertexState {
378 module: &vertex.module,
379 entry_point: Some(vertex.entry_point),
380 compilation_options: wgpu::PipelineCompilationOptions::default(),
381 buffers: &[],
382 },
383 primitive: wgpu::PrimitiveState {
384 topology: wgpu::PrimitiveTopology::TriangleList,
385 strip_index_format: None,
386 front_face: wgpu::FrontFace::Ccw,
387 cull_mode: None,
388 unclipped_depth: false,
389 polygon_mode: wgpu::PolygonMode::Fill,
390 conservative: false,
391 },
392 depth_stencil: None,
393 multisample: wgpu::MultisampleState::default(),
394 fragment: Some(wgpu::FragmentState {
395 module: &fragment.module,
396 entry_point: Some(fragment.entry_point),
397 compilation_options: Default::default(),
398 targets: &[Some(wgpu::ColorTargetState {
399 format: wgpu::TextureFormat::Rgba8Unorm,
400 blend: None,
401 write_mask: wgpu::ColorWrites::all(),
402 })],
403 }),
404 multiview: None,
405 cache: None,
406 });
407
408 let cubemap_view =
409 scene_cubemap
410 .cubemap_texture
411 .create_view(&wgpu::TextureViewDescriptor {
412 label,
413 dimension: Some(wgpu::TextureViewDimension::Cube),
414 ..Default::default()
415 });
416 let cubemap_sampler = ctx.get_device().create_sampler(&wgpu::SamplerDescriptor {
417 label,
418 ..Default::default()
419 });
420 let bind_group = ctx
421 .get_device()
422 .create_bind_group(&wgpu::BindGroupDescriptor {
423 label,
424 layout: &bind_group_layout,
425 entries: &[
426 wgpu::BindGroupEntry {
427 binding: 0,
428 resource: buffer.as_entire_binding(),
429 },
430 wgpu::BindGroupEntry {
431 binding: 1,
432 resource: wgpu::BindingResource::TextureView(&cubemap_view),
433 },
434 wgpu::BindGroupEntry {
435 binding: 2,
436 resource: wgpu::BindingResource::Sampler(&cubemap_sampler),
437 },
438 ],
439 });
440 let render_target = ctx.get_device().create_texture(&wgpu::TextureDescriptor {
441 label,
442 size: wgpu::Extent3d {
443 width: 1,
444 height: 1,
445 depth_or_array_layers: 1,
446 },
447 mip_level_count: 1,
448 sample_count: 1,
449 dimension: wgpu::TextureDimension::D2,
450 format: wgpu::TextureFormat::Rgba8Unorm,
451 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
452 view_formats: &[],
453 });
454 let render_target_view = render_target.create_view(&wgpu::TextureViewDescriptor::default());
455
456 let sample = |dir: Vec3| -> Vec4 {
457 uv.set(dir.normalize_or(Vec3::ZERO));
458 slab.commit();
459
460 let mut encoder = ctx
461 .get_device()
462 .create_command_encoder(&wgpu::CommandEncoderDescriptor { label });
463 {
464 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
465 label,
466 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
467 view: &render_target_view,
468 resolve_target: None,
469 ops: wgpu::Operations {
470 load: wgpu::LoadOp::Clear(wgpu::Color {
471 r: 0.0,
472 g: 0.0,
473 b: 0.0,
474 a: 0.0,
475 }),
476 store: wgpu::StoreOp::Store,
477 },
478 depth_slice: None,
479 })],
480 depth_stencil_attachment: None,
481 timestamp_writes: None,
482 occlusion_query_set: None,
483 });
484 render_pass.set_pipeline(&cubemap_sampling_pipeline);
485 render_pass.set_bind_group(0, &bind_group, &[]);
486 render_pass.draw(0..6, 0..1);
487 }
488 let submission_index = ctx.get_queue().submit(Some(encoder.finish()));
489 ctx.get_device()
490 .poll(wgpu::PollType::WaitForSubmissionIndex(submission_index))
491 .unwrap();
492
493 let img = Texture::read(&ctx, &render_target, 1, 1, 4, 1)
494 .into_image::<u8, image::Rgba<u8>>(ctx.get_device())
495 .block()
496 .unwrap();
497 let image::Rgba([r, g, b, a]) = img.get_pixel(0, 0);
498 Vec4::new(
499 r as f32 / 255.0,
500 g as f32 / 255.0,
501 b as f32 / 255.0,
502 a as f32 / 255.0,
503 )
504 };
505
506 fn index_to_face_string(index: usize) -> &'static str {
507 match index {
508 0 => "+X",
509 1 => "-X",
510 2 => "+Y",
511 3 => "-Y",
512 4 => "+Z",
513 5 => "-Z",
514 _ => "?",
515 }
516 }
517
518 let mut cpu_cubemap = vec![];
519 for i in 0..6 {
520 let img = CopiedTextureBuffer::read_from(
521 &ctx,
522 &scene_cubemap.cubemap_texture,
523 width as usize,
524 height as usize,
525 4,
526 1,
527 0,
528 Some(wgpu::Origin3d { x: 0, y: 0, z: i }),
529 )
530 .into_image::<u8, image::Rgba<u8>>(ctx.get_device())
531 .block()
532 .unwrap();
533
534 img_diff::assert_img_eq(
535 &format!(
536 "cubemap/hand_rolled_cubemap_sampling/face_{}.png",
537 index_to_face_string(i as usize)
538 ),
539 img.clone(),
540 );
541
542 cpu_cubemap.push(img);
543 }
544 let cpu_cubemap = [
545 cpu_cubemap.remove(0),
546 cpu_cubemap.remove(0),
547 cpu_cubemap.remove(0),
548 cpu_cubemap.remove(0),
549 cpu_cubemap.remove(0),
550 cpu_cubemap.remove(0),
551 ];
552
553 {
554 println!("x samples sanity");
556 let x_samples_uv = [
557 UVec2::ZERO,
558 UVec2::new(255, 0),
559 UVec2::new(127, 127),
560 UVec2::new(255, 255),
561 UVec2::new(0, 255),
562 ];
563
564 for uv in x_samples_uv {
565 let image::Rgba([r, g, b, a]) = cpu_cubemap[0].get_pixel(uv.x, uv.y);
566 println!("uv: {uv}");
567 println!("rgba: {r} {g} {b} {a}");
568 }
569 }
570
571 let mut uvs = vec![
572 Vec3::X,
574 Vec3::NEG_X,
575 Vec3::Y,
576 Vec3::NEG_Y,
577 Vec3::Z,
578 Vec3::NEG_Z,
579 ];
580
581 for x in [-1.0, 1.0] {
583 for y in [-1.0, 1.0] {
584 for z in [-1.0, 1.0] {
585 let uv = Vec3::new(x, y, z);
586 uvs.push(uv);
587 }
588 }
589 }
590
591 {
593 let mut prng = crate::math::GpuRng::new(666);
594 let mut rf32 = move || prng.gen_f32(0.0, 1.0);
595 let mut rxvec3 = { || Vec3::new(f32::MAX, rf32(), rf32()).normalize_or(Vec3::X) };
596 uvs.extend((0..20).map(|_| rxvec3()));
598 }
599
600 const THRESHOLD: f32 = 0.005;
601 for uv in uvs.into_iter() {
602 let nuv = uv.normalize_or(Vec3::X);
603 let color = sample(uv);
604 let (face_index, uv2d) =
605 CubemapDescriptor::get_face_index_and_uv(uv.normalize_or(Vec3::X));
606 let px = (uv2d.x * (width as f32 - 1.0)).round() as u32;
607 let py = (uv2d.y * (height as f32 - 1.0)).round() as u32;
608 let puv = UVec2::new(px, py);
609 let cpu_color = cpu_sample_cubemap(&cpu_cubemap, uv);
610 let dir_string = index_to_face_string(face_index);
611 println!(
612 "__uv: {uv},\n_nuv: {nuv},\n_gpu: {color}\n_cpu: {cpu_color}\nfrom: \
613 {dir_string}({face_index}) {uv2d} {puv}\n"
614 );
615 let cmp = pretty_assertions::Comparison::new(&color, &cpu_color);
616 let distance = color.distance(cpu_color);
617 if distance > THRESHOLD {
618 println!("distance: {distance}");
619 println!("{cmp}");
620 panic!("distance {distance} greater than {THRESHOLD}");
621 }
622 }
623 }
624}