diff --git a/blender/shadow_mapping_points.blend b/blender/shadow_mapping_points.blend index 2fbb72a9..3cc96a8a 100644 Binary files a/blender/shadow_mapping_points.blend and b/blender/shadow_mapping_points.blend differ diff --git a/crates/renderling/shaders/cubemap-cubemap_sampling_test_fragment.spv b/crates/renderling/shaders/cubemap-cubemap_sampling_test_fragment.spv new file mode 100644 index 00000000..6ece1052 Binary files /dev/null and b/crates/renderling/shaders/cubemap-cubemap_sampling_test_fragment.spv differ diff --git a/crates/renderling/shaders/cubemap-cubemap_sampling_test_fragment.wgsl b/crates/renderling/shaders/cubemap-cubemap_sampling_test_fragment.wgsl new file mode 100644 index 00000000..709fdc59 --- /dev/null +++ b/crates/renderling/shaders/cubemap-cubemap_sampling_test_fragment.wgsl @@ -0,0 +1,21 @@ +@group(0) @binding(2) +var global: sampler; +var global_1: vec4; +var global_2: vec3; +@group(0) @binding(1) +var global_3: texture_cube; + +fn function() { + let _e4 = global_2; + let _e5 = textureSample(global_3, global, _e4); + global_1 = _e5; + return; +} + +@fragment +fn cubemapcubemap_sampling_test_fragment(@location(0) param: vec3) -> @location(0) vec4 { + global_2 = param; + function(); + let _e3 = global_1; + return _e3; +} diff --git a/crates/renderling/shaders/cubemap-cubemap_sampling_test_vertex.spv b/crates/renderling/shaders/cubemap-cubemap_sampling_test_vertex.spv new file mode 100644 index 00000000..07a2fe23 Binary files /dev/null and b/crates/renderling/shaders/cubemap-cubemap_sampling_test_vertex.spv differ diff --git a/crates/renderling/shaders/cubemap-cubemap_sampling_test_vertex.wgsl b/crates/renderling/shaders/cubemap-cubemap_sampling_test_vertex.wgsl new file mode 100644 index 00000000..f091122c --- /dev/null +++ b/crates/renderling/shaders/cubemap-cubemap_sampling_test_vertex.wgsl @@ -0,0 +1,47 @@ +struct type_11 { + member: vec3, +} + +struct VertexOutput { + @location(0) member: vec3, + @builtin(position) member_1: vec4, +} + +var global: u32; +@group(0) @binding(0) +var global_1: type_11; +var global_2: vec4 = vec4(0f, 0f, 0f, 1f); +var global_3: vec3; + +fn function() { + var local: array, 6>; + + switch bitcast(0u) { + default: { + let _e16 = global; + let _e18 = (_e16 % 6u); + local = array, 6>(vec4(-1f, -1f, 0.5f, 1f), vec4(1f, -1f, 0.5f, 1f), vec4(1f, 1f, 0.5f, 1f), vec4(1f, 1f, 0.5f, 1f), vec4(-1f, 1f, 0.5f, 1f), vec4(-1f, -1f, 0.5f, 1f)); + if (_e18 < 6u) { + } else { + break; + } + let _e21 = local[_e18]; + global_2 = _e21; + let _e22 = global_1.member; + global_3 = _e22; + break; + } + } + return; +} + +@vertex +fn cubemapcubemap_sampling_test_vertex(@builtin(vertex_index) param: u32) -> VertexOutput { + global = param; + function(); + let _e5 = global_2.y; + global_2.y = -(_e5); + let _e7 = global_3; + let _e8 = global_2; + return VertexOutput(_e7, _e8); +} diff --git a/crates/renderling/shaders/ibl-diffuse_irradiance-di_convolution_fragment.spv b/crates/renderling/shaders/ibl-diffuse_irradiance-di_convolution_fragment.spv index 6d3d9ac0..0843942c 100644 Binary files a/crates/renderling/shaders/ibl-diffuse_irradiance-di_convolution_fragment.spv and b/crates/renderling/shaders/ibl-diffuse_irradiance-di_convolution_fragment.spv differ diff --git a/crates/renderling/shaders/ibl-diffuse_irradiance-di_convolution_fragment.wgsl b/crates/renderling/shaders/ibl-diffuse_irradiance-di_convolution_fragment.wgsl index 52ca6527..c4886f26 100644 --- a/crates/renderling/shaders/ibl-diffuse_irradiance-di_convolution_fragment.wgsl +++ b/crates/renderling/shaders/ibl-diffuse_irradiance-di_convolution_fragment.wgsl @@ -9,19 +9,19 @@ fn function() { var phi_273_: vec3; var phi_308_: vec3; var phi_343_: vec3; - var phi_138_: vec3; - var phi_141_: f32; - var phi_143_: f32; - var phi_153_: vec3; - var phi_156_: f32; - var phi_158_: f32; + var phi_146_: vec3; + var phi_149_: f32; + var phi_151_: f32; + var phi_161_: vec3; + var phi_164_: f32; + var phi_166_: f32; var phi_378_: vec3; - var phi_154_: vec3; - var phi_157_: f32; - var phi_159_: f32; - var phi_139_: vec3; - var phi_142_: f32; - var phi_144_: f32; + var phi_162_: vec3; + var phi_165_: f32; + var phi_167_: f32; + var phi_147_: vec3; + var phi_150_: f32; + var phi_152_: f32; var local: f32; var local_1: vec3; var local_2: vec3; @@ -56,26 +56,26 @@ fn function() { phi_343_ = (vec3(_e45, _e49, _e52) * (1f / _e57)); } let _e62 = phi_343_; - phi_138_ = vec3(0f, 0f, 0f); - phi_141_ = 0f; - phi_143_ = 0f; + phi_146_ = vec3(0f, 0f, 0f); + phi_149_ = 0f; + phi_151_ = 0f; loop { - let _e64 = phi_138_; - let _e66 = phi_141_; - let _e68 = phi_143_; + let _e64 = phi_146_; + let _e66 = phi_149_; + let _e68 = phi_151_; local = _e68; local_1 = _e64; local_2 = _e64; local_3 = _e64; let _e69 = (_e66 < 6.2831855f); if _e69 { - phi_153_ = _e64; - phi_156_ = 0f; - phi_158_ = _e68; + phi_161_ = _e64; + phi_164_ = 0f; + phi_166_ = _e68; loop { - let _e71 = phi_153_; - let _e73 = phi_156_; - let _e75 = phi_158_; + let _e71 = phi_161_; + let _e73 = phi_164_; + let _e75 = phi_166_; local_4 = _e71; local_5 = _e75; let _e76 = (_e73 < 1.5707964f); @@ -95,43 +95,43 @@ fn function() { } let _e104 = phi_378_; let _e105 = textureSample(global_3, global_2, _e104); - phi_154_ = vec3(fma((_e105.x * _e82), _e77, _e71.x), fma((_e105.y * _e82), _e77, _e71.y), fma((_e105.z * _e82), _e77, _e71.z)); - phi_157_ = (_e73 + 0.025f); - phi_159_ = (_e75 + 1f); + phi_162_ = vec3(fma((_e105.x * _e82), _e77, _e71.x), fma((_e105.y * _e82), _e77, _e71.y), fma((_e105.z * _e82), _e77, _e71.z)); + phi_165_ = (_e73 + 0.025f); + phi_167_ = (_e75 + 1f); } else { - phi_154_ = vec3(); - phi_157_ = f32(); - phi_159_ = f32(); + phi_162_ = vec3(); + phi_165_ = f32(); + phi_167_ = f32(); } - let _e122 = phi_154_; - let _e124 = phi_157_; - let _e126 = phi_159_; + let _e122 = phi_162_; + let _e124 = phi_165_; + let _e126 = phi_167_; continue; continuing { - phi_153_ = _e122; - phi_156_ = _e124; - phi_158_ = _e126; + phi_161_ = _e122; + phi_164_ = _e124; + phi_166_ = _e126; break if !(_e76); } } let _e167 = local_4; - phi_139_ = _e167; - phi_142_ = (_e66 + 0.025f); + phi_147_ = _e167; + phi_150_ = (_e66 + 0.025f); let _e171 = local_5; - phi_144_ = _e171; + phi_152_ = _e171; } else { - phi_139_ = vec3(); - phi_142_ = f32(); - phi_144_ = f32(); + phi_147_ = vec3(); + phi_150_ = f32(); + phi_152_ = f32(); } - let _e130 = phi_139_; - let _e132 = phi_142_; - let _e134 = phi_144_; + let _e130 = phi_147_; + let _e132 = phi_150_; + let _e134 = phi_152_; continue; continuing { - phi_138_ = _e130; - phi_141_ = _e132; - phi_143_ = _e134; + phi_146_ = _e130; + phi_149_ = _e132; + phi_151_ = _e134; break if !(_e69); } } diff --git a/crates/renderling/shaders/manifest.json b/crates/renderling/shaders/manifest.json index 95b9fcc1..e9fea022 100644 --- a/crates/renderling/shaders/manifest.json +++ b/crates/renderling/shaders/manifest.json @@ -59,6 +59,16 @@ "entry_point": "convolution::prefilter_environment_cubemap_vertex", "wgsl_entry_point": "convolutionprefilter_environment_cubemap_vertex" }, + { + "source_path": "shaders/cubemap-cubemap_sampling_test_fragment.spv", + "entry_point": "cubemap::cubemap_sampling_test_fragment", + "wgsl_entry_point": "cubemapcubemap_sampling_test_fragment" + }, + { + "source_path": "shaders/cubemap-cubemap_sampling_test_vertex.spv", + "entry_point": "cubemap::cubemap_sampling_test_vertex", + "wgsl_entry_point": "cubemapcubemap_sampling_test_vertex" + }, { "source_path": "shaders/cull-compute_copy_depth_to_pyramid.spv", "entry_point": "cull::compute_copy_depth_to_pyramid", diff --git a/crates/renderling/src/cubemap.rs b/crates/renderling/src/cubemap.rs index aef2a8fd..f1076ddc 100644 --- a/crates/renderling/src/cubemap.rs +++ b/crates/renderling/src/cubemap.rs @@ -1,121 +1,125 @@ -//! Render pipelines and layouts for creating cubemaps. +//! Cubemap utilities. +//! +//! Shaders, render pipelines and layouts for creating and sampling cubemaps. +use crabslab::{Array, Id, Slab}; +use glam::{Vec2, Vec3, Vec3Swizzles, Vec4}; +use spirv_std::{num_traits::Zero, spirv}; -use crate::texture::Texture; +#[cfg(cpu)] +mod cpu; +#[cfg(cpu)] +pub use cpu::*; -pub fn cubemap_making_bindgroup_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout { - device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("cubemap-making bindgroup"), - entries: &[ - wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Storage { read_only: true }, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 1, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - sample_type: wgpu::TextureSampleType::Float { filterable: false }, - view_dimension: wgpu::TextureViewDimension::D2, - multisampled: false, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 2, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering), - count: None, - }, - ], - }) +use crate::{ + atlas::{AtlasDescriptor, AtlasTexture}, + math::{IsSampler, Sample2dArray}, +}; + +/// Vertex shader for testing cubemap sampling. +#[spirv(vertex)] +pub fn cubemap_sampling_test_vertex( + #[spirv(vertex_index)] vertex_index: u32, + #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] uv: &Vec3, + out_uv: &mut Vec3, + #[spirv(position)] out_clip_coords: &mut Vec4, +) { + let vertex_index = vertex_index as usize % 6; + *out_clip_coords = crate::math::CLIP_SPACE_COORD_QUAD_CCW[vertex_index]; + *out_uv = *uv; +} + +/// Vertex shader for testing cubemap sampling. +#[spirv(fragment)] +pub fn cubemap_sampling_test_fragment( + #[spirv(descriptor_set = 0, binding = 1)] cubemap: &spirv_std::image::Cubemap, + #[spirv(descriptor_set = 0, binding = 2)] sampler: &spirv_std::Sampler, + in_uv: Vec3, + frag_color: &mut Vec4, +) { + *frag_color = cubemap.sample(*sampler, in_uv); } -pub fn cubemap_making_bindgroup( - device: &wgpu::Device, - label: Option<&str>, - buffer: &wgpu::Buffer, - // The texture to sample the environment from - texture: &Texture, -) -> wgpu::BindGroup { - device.create_bind_group(&wgpu::BindGroupDescriptor { - label, - layout: &cubemap_making_bindgroup_layout(device), - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Buffer(buffer.as_entire_buffer_binding()), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::TextureView(&texture.view), - }, - wgpu::BindGroupEntry { - binding: 2, - resource: wgpu::BindingResource::Sampler(&texture.sampler), - }, - ], - }) +pub struct CubemapDescriptor { + atlas_descriptor_id: Id, + faces: Array, } -pub struct CubemapMakingRenderPipeline(pub wgpu::RenderPipeline); +impl CubemapDescriptor { + /// Return the face index and UV coordinates that can be used to sample + /// a cubemap from the given directional coordinate. + /// + /// `coord` must be normalized, and must **not be zero**. + pub fn get_face_index_and_uv(coord: Vec3) -> (usize, Vec2) { + // Unpack coordinate components + let x = coord.x; + let y = coord.y; + let z = coord.z; + + // Determine the major direction + let abs_x = x.abs(); + let abs_y = y.abs(); + let abs_z = z.abs(); + + // Initialize face index and UV coordinates + let (face_index, u, v); + + if abs_x >= abs_y && abs_x >= abs_z { + // X-major direction + if x > 0.0 { + face_index = 0; // +X + u = -z / x; + v = -y / x; + } else { + face_index = 1; // -X + u = -z / x; + v = y / x; + } + } else if abs_y >= abs_x && abs_y >= abs_z { + // Y-major direction + if y > 0.0 { + face_index = 2; // +Y + u = x / y; + v = z / y; + } else { + face_index = 3; // -Y + u = x / y; + v = -z / y; + } + } else { + // Z-major direction + if z > 0.0 { + face_index = 4; // +Z + u = x / z; + v = -y / z; + } else { + face_index = 5; // -Z + u = -x / z; + v = -y / z; + } + } + + // Convert from range [-1, 1] to [0, 1] + let u = (u + 1.0) / 2.0; + let v = (v + 1.0) / 2.0; + + (face_index, Vec2::new(u, v)) + } -impl CubemapMakingRenderPipeline { - /// Create the rendering pipeline that creates cubemaps from equirectangular - /// images. - pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self { - log::trace!("creating cubemap-making render pipeline with format '{format:?}'"); - let vertex_linkage = crate::linkage::skybox_cubemap_vertex::linkage(device); - let fragment_linkage = crate::linkage::skybox_equirectangular_fragment::linkage(device); - let bg_layout = cubemap_making_bindgroup_layout(device); - let pp_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("cubemap-making pipeline layout"), - bind_group_layouts: &[&bg_layout], - push_constant_ranges: &[], - }); - CubemapMakingRenderPipeline(device.create_render_pipeline( - &wgpu::RenderPipelineDescriptor { - label: Some("cubemap-making pipeline"), - layout: Some(&pp_layout), - vertex: wgpu::VertexState { - module: &vertex_linkage.module, - entry_point: Some(vertex_linkage.entry_point), - buffers: &[], - compilation_options: Default::default(), - }, - primitive: wgpu::PrimitiveState { - topology: wgpu::PrimitiveTopology::TriangleList, - strip_index_format: None, - front_face: wgpu::FrontFace::Ccw, - cull_mode: None, - unclipped_depth: false, - polygon_mode: wgpu::PolygonMode::Fill, - conservative: false, - }, - depth_stencil: None, - multisample: wgpu::MultisampleState { - mask: !0, - alpha_to_coverage_enabled: false, - count: 1, - }, - fragment: Some(wgpu::FragmentState { - module: &fragment_linkage.module, - entry_point: Some(fragment_linkage.entry_point), - targets: &[Some(wgpu::ColorTargetState { - format, - blend: Some(wgpu::BlendState::ALPHA_BLENDING), - write_mask: wgpu::ColorWrites::ALL, - })], - compilation_options: Default::default(), - }), - multiview: None, - cache: None, - }, - )) + /// Sample the cubemap with a directional coordinate. + pub fn sample(&self, coord: Vec3, slab: &[u32], atlas: &A, sampler: &S) -> Vec4 + where + A: Sample2dArray, + S: IsSampler, + { + let coord = if coord.length().is_zero() { + Vec3::X + } else { + coord.normalize() + }; + let (face_index, uv) = Self::get_face_index_and_uv(coord); + let atlas_image = slab.read_unchecked(self.faces.at(face_index)); + let atlas_desc = slab.read_unchecked(self.atlas_descriptor_id); + let uv = atlas_image.uv(uv, atlas_desc.size.xy()); + atlas.sample_by_lod(*sampler, uv, 0.0) } } diff --git a/crates/renderling/src/cubemap/cpu.rs b/crates/renderling/src/cubemap/cpu.rs new file mode 100644 index 00000000..12870d11 --- /dev/null +++ b/crates/renderling/src/cubemap/cpu.rs @@ -0,0 +1,737 @@ +//! CPU side of the cubemap module. +use std::sync::Arc; + +use glam::{Mat4, UVec2, Vec3, Vec4}; +use image::GenericImageView; + +use crate::{ + camera::Camera, + stage::{Stage, StageRendering}, + texture::Texture, +}; + +use super::CubemapDescriptor; + +pub fn cpu_sample_cubemap(cubemap: &[image::DynamicImage; 6], coord: Vec3) -> Vec4 { + let coord = coord.normalize_or(Vec3::X); + let (face_index, uv) = CubemapDescriptor::get_face_index_and_uv(coord); + + // Get the selected image + let image = &cubemap[face_index]; + + // Convert 2D UV to pixel coordinates + let (width, height) = image.dimensions(); + let px = uv.x * (width as f32 - 1.0); + let py = uv.y * (height as f32 - 1.0); + + // Sample using the nearest neighbor for simplicity + let image::Rgba([r, g, b, a]) = image.get_pixel(px.round() as u32, py.round() as u32); + + // Convert the sampled pixel to Vec4 + Vec4::new( + r as f32 / 255.0, + g as f32 / 255.0, + b as f32 / 255.0, + a as f32 / 255.0, + ) +} + +/// Represents one side of a cubemap. +/// +/// Assumes the camera is at the origin. +pub struct CubemapFaceDirection { + /// Where is the camera + pub dir: Vec3, + /// Which direction is up + pub up: Vec3, + /// Which direct is right + pub right: Vec3, +} + +impl CubemapFaceDirection { + pub const X: Self = Self { + dir: Vec3::X, + up: Vec3::NEG_Y, + right: Vec3::NEG_Z, + }; + pub const NEG_X: Self = Self { + dir: Vec3::NEG_X, + up: Vec3::NEG_Y, + right: Vec3::Z, + }; + pub const NEG_Y: Self = Self { + dir: Vec3::NEG_Y, + up: Vec3::NEG_Z, + right: Vec3::X, + }; + pub const Y: Self = Self { + dir: Vec3::Y, + up: Vec3::Z, + right: Vec3::X, + }; + pub const Z: Self = Self { + dir: Vec3::Z, + up: Vec3::NEG_Y, + right: Vec3::X, + }; + pub const NEG_Z: Self = Self { + dir: Vec3::NEG_Z, + up: Vec3::NEG_Y, + right: Vec3::NEG_X, + }; + pub const FACES: [Self; 6] = [ + CubemapFaceDirection::X, + CubemapFaceDirection::NEG_X, + CubemapFaceDirection::NEG_Y, + CubemapFaceDirection::Y, + CubemapFaceDirection::Z, + CubemapFaceDirection::NEG_Z, + ]; + + pub fn view(&self) -> Mat4 { + Mat4::look_at_rh(Vec3::ZERO, self.dir, self.up) + } + + pub fn to_corners_tr_tl_br_bl(self) -> [Vec3; 4] { + let tr = self.dir + self.up + self.right; + let tl = self.dir + self.up - self.right; + let br = self.dir - self.up + self.right; + let bl = self.dir - self.up - self.right; + [tr, tl, br, bl] + } + + pub fn to_tri_list(self) -> [Vec3; 6] { + let [tr, tl, br, bl] = self.to_corners_tr_tl_br_bl(); + [tr, tl, bl, tr, bl, br] + } +} + +/// A cubemap that acts as a render target for an entire scene. +/// +/// Use this to create and update a skybox with scene geometry. +pub struct SceneCubemap { + pipeline: Arc, + cubemap_texture: wgpu::Texture, + depth_texture: crate::texture::Texture, + clear_color: wgpu::Color, +} + +impl SceneCubemap { + pub fn new( + device: &wgpu::Device, + size: UVec2, + format: wgpu::TextureFormat, + clear_color: Vec4, + ) -> Self { + let label = Some("scene-to-cubemap"); + let cubemap_texture = device.create_texture(&wgpu::TextureDescriptor { + label, + size: wgpu::Extent3d { + width: size.x, + height: size.y, + depth_or_array_layers: 6, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT + | wgpu::TextureUsages::TEXTURE_BINDING + | wgpu::TextureUsages::COPY_DST + | wgpu::TextureUsages::COPY_SRC, + view_formats: &[], + }); + let depth_texture = Texture::create_depth_texture(device, size.x, size.y, 1, label); + let pipeline = Arc::new(Stage::create_stage_render_pipeline(device, format, 1)); + Self { + pipeline, + cubemap_texture, + depth_texture, + clear_color: wgpu::Color { + r: clear_color.x as f64, + g: clear_color.y as f64, + b: clear_color.z as f64, + a: clear_color.w as f64, + }, + } + } + + pub fn run(&self, stage: &Stage) { + // create a camera for our cube + let camera = stage.new_value(Camera::default()); + + let mut prev_camera_ids = vec![]; + for rlet in stage.renderlets_iter() { + if let Some(rlet) = rlet.upgrade() { + let mut guard = rlet.lock(); + prev_camera_ids.push(guard.camera_id); + // Overwrite the renderlet's camera + guard.camera_id = camera.id(); + } + } + + // By setting this to 90 degrees (PI/2 radians) we make sure the viewing field + // is exactly large enough to fill a single face of the cubemap such that all + // faces align correctly to each other at the edges. + let fovy = std::f32::consts::FRAC_PI_2; + let aspect = self.cubemap_texture.width() as f32 / self.cubemap_texture.height() as f32; + let projection = Mat4::perspective_rh(fovy, aspect, 1.0, 25.0); + // Render each face by rendering the scene from each camera angle into the cubemap + for (i, face) in CubemapFaceDirection::FACES.iter().enumerate() { + // Update the camera angle, no need to sync as calling `Stage::render` does this + // implicitly + camera.modify(|c| c.set_projection_and_view(projection, face.view())); + let label_s = format!("scene-to-cubemap-{i}"); + let view = self + .cubemap_texture + .create_view(&wgpu::TextureViewDescriptor { + label: Some(&label_s), + base_array_layer: i as u32, + array_layer_count: Some(1), + dimension: Some(wgpu::TextureViewDimension::D2), + ..Default::default() + }); + let color_attachment = wgpu::RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(self.clear_color), + store: wgpu::StoreOp::Store, + }, + }; + let depth_stencil_attachment = wgpu::RenderPassDepthStencilAttachment { + view: &self.depth_texture.view, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Clear(1.0), + store: wgpu::StoreOp::Store, + }), + stencil_ops: None, + }; + let (_, _) = StageRendering { + pipeline: &self.pipeline, + stage, + color_attachment, + depth_stencil_attachment, + } + .run(); + } + } +} + +/// A render pipeline for blitting an equirectangular image as a cubemap. +pub struct EquirectangularImageToCubemapBlitter(pub wgpu::RenderPipeline); + +impl EquirectangularImageToCubemapBlitter { + pub fn create_bindgroup_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout { + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("cubemap-making bindgroup"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Storage { read_only: true }, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: false }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering), + count: None, + }, + ], + }) + } + + pub fn create_bindgroup( + device: &wgpu::Device, + label: Option<&str>, + buffer: &wgpu::Buffer, + // The texture to sample the environment from + texture: &Texture, + ) -> wgpu::BindGroup { + device.create_bind_group(&wgpu::BindGroupDescriptor { + label, + layout: &Self::create_bindgroup_layout(device), + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer(buffer.as_entire_buffer_binding()), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::TextureView(&texture.view), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: wgpu::BindingResource::Sampler(&texture.sampler), + }, + ], + }) + } + + /// Create the rendering pipeline that creates cubemaps from equirectangular + /// images. + pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self { + log::trace!("creating cubemap-making render pipeline with format '{format:?}'"); + let vertex_linkage = crate::linkage::skybox_cubemap_vertex::linkage(device); + let fragment_linkage = crate::linkage::skybox_equirectangular_fragment::linkage(device); + let bg_layout = Self::create_bindgroup_layout(device); + let pp_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("cubemap-making pipeline layout"), + bind_group_layouts: &[&bg_layout], + push_constant_ranges: &[], + }); + EquirectangularImageToCubemapBlitter(device.create_render_pipeline( + &wgpu::RenderPipelineDescriptor { + label: Some("cubemap-making pipeline"), + layout: Some(&pp_layout), + vertex: wgpu::VertexState { + module: &vertex_linkage.module, + entry_point: Some(vertex_linkage.entry_point), + buffers: &[], + compilation_options: Default::default(), + }, + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: None, + unclipped_depth: false, + polygon_mode: wgpu::PolygonMode::Fill, + conservative: false, + }, + depth_stencil: None, + multisample: wgpu::MultisampleState { + mask: !0, + alpha_to_coverage_enabled: false, + count: 1, + }, + fragment: Some(wgpu::FragmentState { + module: &fragment_linkage.module, + entry_point: Some(fragment_linkage.entry_point), + targets: &[Some(wgpu::ColorTargetState { + format, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + multiview: None, + cache: None, + }, + )) + } +} + +#[cfg(test)] +mod test { + use core::fmt::Debug; + + use assert_approx_eq::assert_approx_eq; + use craballoc::slab::SlabAllocator; + use glam::{Quat, Vec2, Vec4}; + use image::GenericImageView; + + use crate::{ + atlas::{Atlas, AtlasImage}, + math::{UNIT_INDICES, UNIT_POINTS}, + stage::{Renderlet, Vertex}, + }; + + use super::*; + + #[test] + fn cubemap_face_views() { + // This tests that the cubemap faces `views` make a valid cubemap. + + // We got this array from the original skybox code, which we know makes + // valid cubemaps. + let skybox_views = [ + Mat4::look_at_rh( + Vec3::new(0.0, 0.0, 0.0), + Vec3::new(1.0, 0.0, 0.0), + Vec3::new(0.0, -1.0, 0.0), + ), + Mat4::look_at_rh( + Vec3::new(0.0, 0.0, 0.0), + Vec3::new(-1.0, 0.0, 0.0), + Vec3::new(0.0, -1.0, 0.0), + ), + Mat4::look_at_rh( + Vec3::new(0.0, 0.0, 0.0), + Vec3::new(0.0, -1.0, 0.0), + Vec3::new(0.0, 0.0, -1.0), + ), + Mat4::look_at_rh( + Vec3::new(0.0, 0.0, 0.0), + Vec3::new(0.0, 1.0, 0.0), + Vec3::new(0.0, 0.0, 1.0), + ), + Mat4::look_at_rh( + Vec3::new(0.0, 0.0, 0.0), + Vec3::new(0.0, 0.0, 1.0), + Vec3::new(0.0, -1.0, 0.0), + ), + Mat4::look_at_rh( + Vec3::new(0.0, 0.0, 0.0), + Vec3::new(0.0, 0.0, -1.0), + Vec3::new(0.0, -1.0, 0.0), + ), + ]; + let views = CubemapFaceDirection::FACES.map(|f| f.view()); + assert_eq!(skybox_views, views); + } + + #[test] + fn hand_rolled_cubemap_sampling() { + let width = 256; + let height = 256; + let ctx = crate::Context::headless(width, height); + let stage = ctx + .new_stage() + .with_background_color(Vec4::ZERO) + .with_lighting(false) + .with_msaa_sample_count(4); + let camera = + stage.new_value( + Camera::default_perspective(width as f32, height as f32) + .with_view(Mat4::look_at_rh(Vec3::splat(3.0), Vec3::ZERO, Vec3::Y)), + ); + // geometry is the "clip cube" where colors are normalized 3d space coords + let vertices = stage.new_array(UNIT_POINTS.map(|unit_cube_point| { + Vertex::default() + // multiply by 2.0 because the unit cube's AABB bounds are at 0.5, and we want 1.0 + .with_position(unit_cube_point * 2.0) + // "normalize" (really "shift") the space coord from [-0.5, 0.5] to [0.0, 1.0] + // ...but flip y + .with_color((unit_cube_point * Vec3::new(1.0, -1.0, 1.0) + 0.5).extend(1.0)) + })); + let indices = stage.new_array(UNIT_INDICES.map(|u| u as u32)); + let renderlet = stage.new_value(Renderlet { + vertices_array: vertices.array(), + indices_array: indices.array(), + camera_id: camera.id(), + ..Default::default() + }); + stage.add_renderlet(&renderlet); + + let scene_cubemap = SceneCubemap::new( + ctx.get_device(), + UVec2::new(width, height), + wgpu::TextureFormat::Rgba8Unorm, + Vec4::ZERO, + ); + + scene_cubemap.run(&stage); + + let frame = ctx.get_next_frame().unwrap(); + crate::test::capture_gpu_frame( + &ctx, + "cubemap/hand_rolled_cubemap_sampling/frame.gputrace", + || stage.render(&frame.view()), + ); + let img = frame.read_image().unwrap(); + img_diff::save("cubemap/hand_rolled_cubemap_sampling/cube.png", img); + frame.present(); + + let slab = SlabAllocator::new(&ctx, wgpu::BufferUsages::empty()); + let uv = slab.new_value(Vec3::ZERO); + let buffer = slab.commit(); + let label = Some("cubemap-sampling-test"); + let bind_group_layout = + ctx.get_device() + .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label, + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Storage { read_only: true }, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + view_dimension: wgpu::TextureViewDimension::Cube, + multisampled: false, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering), + count: None, + }, + ], + }); + let cubemap_sampling_pipeline_layout = + ctx.get_device() + .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label, + bind_group_layouts: &[&bind_group_layout], + push_constant_ranges: &[], + }); + let vertex = crate::linkage::cubemap_sampling_test_vertex::linkage(ctx.get_device()); + let fragment = crate::linkage::cubemap_sampling_test_fragment::linkage(ctx.get_device()); + let cubemap_sampling_pipeline = + ctx.get_device() + .create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label, + layout: Some(&cubemap_sampling_pipeline_layout), + vertex: wgpu::VertexState { + module: &vertex.module, + entry_point: Some(vertex.entry_point), + compilation_options: wgpu::PipelineCompilationOptions::default(), + buffers: &[], + }, + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: None, + unclipped_depth: false, + polygon_mode: wgpu::PolygonMode::Fill, + conservative: false, + }, + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + fragment: Some(wgpu::FragmentState { + module: &fragment.module, + entry_point: Some(fragment.entry_point), + compilation_options: Default::default(), + targets: &[Some(wgpu::ColorTargetState { + format: wgpu::TextureFormat::Rgba8Unorm, + blend: None, + write_mask: wgpu::ColorWrites::all(), + })], + }), + multiview: None, + cache: None, + }); + + let cubemap_view = + scene_cubemap + .cubemap_texture + .create_view(&wgpu::TextureViewDescriptor { + label, + dimension: Some(wgpu::TextureViewDimension::Cube), + ..Default::default() + }); + let cubemap_sampler = ctx.get_device().create_sampler(&wgpu::SamplerDescriptor { + label, + ..Default::default() + }); + let bind_group = ctx + .get_device() + .create_bind_group(&wgpu::BindGroupDescriptor { + label, + layout: &bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: buffer.as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::TextureView(&cubemap_view), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: wgpu::BindingResource::Sampler(&cubemap_sampler), + }, + ], + }); + let render_target = ctx.get_device().create_texture(&wgpu::TextureDescriptor { + label, + size: wgpu::Extent3d { + width: 1, + height: 1, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8Unorm, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC, + view_formats: &[], + }); + let render_target_view = render_target.create_view(&wgpu::TextureViewDescriptor::default()); + + let sample = |dir: Vec3| -> Vec4 { + uv.set(dir.normalize_or(Vec3::ZERO)); + slab.commit(); + + let mut encoder = ctx + .get_device() + .create_command_encoder(&wgpu::CommandEncoderDescriptor { label }); + { + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label, + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &render_target_view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color { + r: 0.0, + g: 0.0, + b: 0.0, + a: 0.0, + }), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + render_pass.set_pipeline(&cubemap_sampling_pipeline); + render_pass.set_bind_group(0, &bind_group, &[]); + render_pass.draw(0..6, 0..1); + } + let submission_index = ctx.get_queue().submit(Some(encoder.finish())); + ctx.get_device() + .poll(wgpu::Maintain::wait_for(submission_index)); + + let img = Texture::read(&ctx, &render_target, 1, 1, 4, 1) + .into_image::>(ctx.get_device()) + .unwrap(); + let image::Rgba([r, g, b, a]) = img.get_pixel(0, 0); + Vec4::new( + r as f32 / 255.0, + g as f32 / 255.0, + b as f32 / 255.0, + a as f32 / 255.0, + ) + }; + + fn index_to_face_string(index: usize) -> &'static str { + match index { + 0 => "+X", + 1 => "-X", + 2 => "+Y", + 3 => "-Y", + 4 => "+Z", + 5 => "-Z", + _ => "?", + } + } + + let mut cpu_cubemap = vec![]; + for i in 0..6 { + let img = Texture::read_from( + &ctx, + &scene_cubemap.cubemap_texture, + width as usize, + height as usize, + 4, + 1, + 0, + Some(wgpu::Origin3d { x: 0, y: 0, z: i }), + ) + .into_image::>(ctx.get_device()) + .unwrap(); + + img_diff::save( + &format!( + "cubemap/hand_rolled_cubemap_sampling/face_{}.png", + index_to_face_string(i as usize) + ), + img.clone(), + ); + + cpu_cubemap.push(img); + } + let cpu_cubemap = [ + cpu_cubemap.remove(0), + cpu_cubemap.remove(0), + cpu_cubemap.remove(0), + cpu_cubemap.remove(0), + cpu_cubemap.remove(0), + cpu_cubemap.remove(0), + ]; + + { + // assert a few sanity checks on the cpu cubemap + println!("x samples sanity"); + let x_samples_uv = [ + UVec2::ZERO, + UVec2::new(255, 0), + UVec2::new(127, 127), + UVec2::new(255, 255), + UVec2::new(0, 255), + ]; + + for uv in x_samples_uv { + let image::Rgba([r, g, b, a]) = cpu_cubemap[0].get_pixel(uv.x, uv.y); + println!("uv: {uv}"); + println!("rgba: {r} {g} {b} {a}"); + } + } + + let mut uvs = vec![ + // start with cardinal directions + Vec3::X, + Vec3::NEG_X, + Vec3::Y, + Vec3::NEG_Y, + Vec3::Z, + Vec3::NEG_Z, + ]; + + // add corners to the uvs to sample + for x in [-1.0, 1.0] { + for y in [-1.0, 1.0] { + for z in [-1.0, 1.0] { + let uv = Vec3::new(x, y, z); + uvs.push(uv); + } + } + } + + // add zero + uvs.push(Vec3::ZERO); + + const THRESHOLD: f32 = 0.005; + for uv in uvs.into_iter() { + let color = sample(uv); + let (face_index, uv2d) = + CubemapDescriptor::get_face_index_and_uv(uv.normalize_or(Vec3::X)); + let cpu_color = cpu_sample_cubemap(&cpu_cubemap, uv); + let dir_string = index_to_face_string(face_index); + println!( + "__uv: {uv},\n\ + _gpu: {color}\n\ + _cpu: {cpu_color}\n\ + from: {dir_string}({face_index}) {uv2d}\n" + ); + let cmp = pretty_assertions::Comparison::new(&color, &cpu_color); + let distance = color.distance(cpu_color); + if distance > THRESHOLD { + println!("distance: {distance}"); + println!("{cmp}"); + panic!("distance {distance} greater than {THRESHOLD}"); + } + } + } +} diff --git a/crates/renderling/src/draw/cpu.rs b/crates/renderling/src/draw/cpu.rs index 4b56ef53..a8ec0653 100644 --- a/crates/renderling/src/draw/cpu.rs +++ b/crates/renderling/src/draw/cpu.rs @@ -34,7 +34,12 @@ impl InternalRenderlet { } fn copy_inner(&self) -> Option { - self.inner.upgrade().map(|hy| hy.get()) + let hy = self.get_hybrid()?; + Some(hy.get()) + } + + fn get_hybrid(&self) -> Option> { + self.inner.upgrade() } } @@ -267,12 +272,8 @@ impl DrawCalls { } /// Iterator over all staged [`Renderlet`]s. - pub fn renderlets_iter(&self) -> impl Iterator { - self.internal_renderlets - .iter() - .filter_map(|ir| ir.copy_inner()) - .collect::>() - .into_iter() + pub fn renderlets_iter(&self) -> impl Iterator> + '_ { + self.internal_renderlets.iter().map(|ir| ir.inner.clone()) } /// Perform upkeep on queued draw calls and synchronize internal buffers. diff --git a/crates/renderling/src/lib.rs b/crates/renderling/src/lib.rs index 6031e831..e5956709 100644 --- a/crates/renderling/src/lib.rs +++ b/crates/renderling/src/lib.rs @@ -149,7 +149,6 @@ pub mod color; #[cfg(not(target_arch = "spirv"))] mod context; pub mod convolution; -#[cfg(not(target_arch = "spirv"))] pub mod cubemap; pub mod cull; pub mod debug; diff --git a/crates/renderling/src/light.rs b/crates/renderling/src/light.rs index afab0c35..c559ccf0 100644 --- a/crates/renderling/src/light.rs +++ b/crates/renderling/src/light.rs @@ -358,7 +358,7 @@ impl PointLightDescriptor { z_near: f32, z_far: f32, ) -> (Mat4, [Mat4; 6]) { - let p = Mat4::perspective_rh(std::f32::consts::FRAC_PI_2, 1.0, z_near, z_far); + let p = Mat4::perspective_rh(core::f32::consts::FRAC_PI_2, 1.0, z_near, z_far); let eye = parent_light_transform.transform_point3(self.position); ( p, diff --git a/crates/renderling/src/linkage.rs b/crates/renderling/src/linkage.rs index 124a3497..3bcbaff6 100644 --- a/crates/renderling/src/linkage.rs +++ b/crates/renderling/src/linkage.rs @@ -18,6 +18,8 @@ pub mod compute_copy_depth_to_pyramid; pub mod compute_copy_depth_to_pyramid_multisampled; pub mod compute_culling; pub mod compute_downsample_depth_pyramid; +pub mod cubemap_sampling_test_fragment; +pub mod cubemap_sampling_test_vertex; pub mod debug_overlay_fragment; pub mod debug_overlay_vertex; pub mod di_convolution_fragment; diff --git a/crates/renderling/src/linkage/cubemap_sampling_test_fragment.rs b/crates/renderling/src/linkage/cubemap_sampling_test_fragment.rs new file mode 100644 index 00000000..8783ec44 --- /dev/null +++ b/crates/renderling/src/linkage/cubemap_sampling_test_fragment.rs @@ -0,0 +1,40 @@ +#![allow(dead_code)] +//! Automatically generated by Renderling's `build.rs`. +use crate::linkage::ShaderLinkage; +#[cfg(not(target_arch = "wasm32"))] +mod target { + pub const ENTRY_POINT: &str = "cubemap::cubemap_sampling_test_fragment"; + pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { + wgpu::include_spirv!("../../shaders/cubemap-cubemap_sampling_test_fragment.spv") + } + pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { + log::debug!( + "creating native linkage for {}", + "cubemap_sampling_test_fragment" + ); + super::ShaderLinkage { + entry_point: ENTRY_POINT, + module: device.create_shader_module(descriptor()).into(), + } + } +} +#[cfg(target_arch = "wasm32")] +mod target { + pub const ENTRY_POINT: &str = "cubemapcubemap_sampling_test_fragment"; + pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { + wgpu::include_wgsl!("../../shaders/cubemap-cubemap_sampling_test_fragment.wgsl") + } + pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { + log::debug!( + "creating web linkage for {}", + "cubemap_sampling_test_fragment" + ); + super::ShaderLinkage { + entry_point: ENTRY_POINT, + module: device.create_shader_module(descriptor()).into(), + } + } +} +pub fn linkage(device: &wgpu::Device) -> ShaderLinkage { + target::linkage(device) +} diff --git a/crates/renderling/src/linkage/cubemap_sampling_test_vertex.rs b/crates/renderling/src/linkage/cubemap_sampling_test_vertex.rs new file mode 100644 index 00000000..40c8ba5f --- /dev/null +++ b/crates/renderling/src/linkage/cubemap_sampling_test_vertex.rs @@ -0,0 +1,40 @@ +#![allow(dead_code)] +//! Automatically generated by Renderling's `build.rs`. +use crate::linkage::ShaderLinkage; +#[cfg(not(target_arch = "wasm32"))] +mod target { + pub const ENTRY_POINT: &str = "cubemap::cubemap_sampling_test_vertex"; + pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { + wgpu::include_spirv!("../../shaders/cubemap-cubemap_sampling_test_vertex.spv") + } + pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { + log::debug!( + "creating native linkage for {}", + "cubemap_sampling_test_vertex" + ); + super::ShaderLinkage { + entry_point: ENTRY_POINT, + module: device.create_shader_module(descriptor()).into(), + } + } +} +#[cfg(target_arch = "wasm32")] +mod target { + pub const ENTRY_POINT: &str = "cubemapcubemap_sampling_test_vertex"; + pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { + wgpu::include_wgsl!("../../shaders/cubemap-cubemap_sampling_test_vertex.wgsl") + } + pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { + log::debug!( + "creating web linkage for {}", + "cubemap_sampling_test_vertex" + ); + super::ShaderLinkage { + entry_point: ENTRY_POINT, + module: device.create_shader_module(descriptor()).into(), + } + } +} +pub fn linkage(device: &wgpu::Device) -> ShaderLinkage { + target::linkage(device) +} diff --git a/crates/renderling/src/math.rs b/crates/renderling/src/math.rs index c7d57225..7dd5c501 100644 --- a/crates/renderling/src/math.rs +++ b/crates/renderling/src/math.rs @@ -81,7 +81,6 @@ impl SampleCube for Cubemap { #[cfg(not(target_arch = "spirv"))] mod cpu { - use image::GenericImageView; use super::*; @@ -208,48 +207,7 @@ mod cpu { direction: glam::Vec3, _lod: f32, ) -> glam::Vec4 { - // Take the absolute value of the direction vector components - let abs_direction = direction.abs(); - let (max_dim, u, v): (usize, f32, f32); - - // Determine which face of the cubemap the direction vector is pointing towards - // by finding the largest component of the vector. - // The u and v texture coordinates within that face are calculated by dividing - // the other two components of the direction vector by the largest component. - if abs_direction.x >= abs_direction.y && abs_direction.x >= abs_direction.z { - max_dim = if direction.x >= 0.0 { 0 } else { 1 }; - u = -direction.z / abs_direction.x; - v = -direction.y / abs_direction.x; - } else if abs_direction.y >= abs_direction.x && abs_direction.y >= abs_direction.z { - max_dim = if direction.y >= 0.0 { 2 } else { 3 }; - u = direction.x / abs_direction.y; - v = -direction.z / abs_direction.y; - } else { - max_dim = if direction.z >= 0.0 { 4 } else { 5 }; - u = direction.x / abs_direction.z; - v = direction.y / abs_direction.z; - } - - // Get the dimensions of the cubemap image - let (width, height) = self.images[max_dim].dimensions(); - // Convert the u and v coordinates from [-1, 1] to [0, width/height] - let tex_u = ((u + 1.0) * 0.5 * (width as f32 - 1.0)).round() as u32; - if tex_u >= self.images[max_dim].width() { - return glam::Vec4::ZERO; - } - let tex_v = ((1.0 - v) * 0.5 * (height as f32 - 1.0)).round() as u32; - if tex_v >= self.images[max_dim].height() { - return glam::Vec4::ZERO; - } - - // Sample and return the color from the appropriate image in the cubemap - let pixel = self.images[max_dim].get_pixel(tex_u, tex_v); - glam::Vec4::new( - pixel[0] as f32 / 255.0, - pixel[1] as f32 / 255.0, - pixel[2] as f32 / 255.0, - pixel[3] as f32 / 255.0, - ) + crate::cubemap::cpu_sample_cubemap(&self.images, direction) } } diff --git a/crates/renderling/src/skybox/cpu.rs b/crates/renderling/src/skybox/cpu.rs index 098acb7c..16243db9 100644 --- a/crates/renderling/src/skybox/cpu.rs +++ b/crates/renderling/src/skybox/cpu.rs @@ -8,7 +8,7 @@ use glam::{Mat4, UVec2, Vec3}; use crate::{ atlas::AtlasImage, camera::Camera, convolution::VertexPrefilterEnvironmentCubemapIds, - texture::Texture, + cubemap::EquirectangularImageToCubemapBlitter, texture::Texture, }; /// Render pipeline used to draw a skybox. @@ -330,10 +330,8 @@ impl Skybox { let device = &runtime.device; let queue = &runtime.queue; // Create the cubemap-making pipeline. - let pipeline = crate::cubemap::CubemapMakingRenderPipeline::new( - device, - wgpu::TextureFormat::Rgba16Float, - ); + let pipeline = + EquirectangularImageToCubemapBlitter::new(device, wgpu::TextureFormat::Rgba16Float); let resources = ( device, @@ -341,8 +339,12 @@ impl Skybox { Some("hdr environment map"), wgpu::BufferUsages::VERTEX, ); - let bindgroup = - crate::cubemap::cubemap_making_bindgroup(device, resources.2, buffer, hdr_texture); + let bindgroup = EquirectangularImageToCubemapBlitter::create_bindgroup( + device, + resources.2, + buffer, + hdr_texture, + ); Self::render_cubemap( runtime, diff --git a/crates/renderling/src/stage/cpu.rs b/crates/renderling/src/stage/cpu.rs index 62033c3d..ce4a61f4 100644 --- a/crates/renderling/src/stage/cpu.rs +++ b/crates/renderling/src/stage/cpu.rs @@ -66,70 +66,101 @@ fn create_msaa_textureview( .create_view(&wgpu::TextureViewDescriptor::default()) } -fn create_stage_render_pipeline( - device: &wgpu::Device, - multisample_count: u32, -) -> wgpu::RenderPipeline { - log::trace!("creating stage render pipeline"); - let label = Some("stage render"); - let vertex_linkage = crate::linkage::renderlet_vertex::linkage(device); - let fragment_linkage = crate::linkage::renderlet_fragment::linkage(device); - let stage_slab_buffers_layout = crate::linkage::slab_bindgroup_layout(device); - let atlas_and_skybox_layout = crate::linkage::atlas_and_skybox_bindgroup_layout(device); - let light_bindgroup_layout = crate::light::Lighting::create_bindgroup_layout(device); - let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label, - bind_group_layouts: &[ - &stage_slab_buffers_layout, - &atlas_and_skybox_layout, - &light_bindgroup_layout, - ], - push_constant_ranges: &[], - }); - - device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label, - layout: Some(&layout), - vertex: wgpu::VertexState { - module: &vertex_linkage.module, - entry_point: Some(vertex_linkage.entry_point), - buffers: &[], - compilation_options: Default::default(), - }, - primitive: wgpu::PrimitiveState { - topology: wgpu::PrimitiveTopology::TriangleList, - strip_index_format: None, - front_face: wgpu::FrontFace::Ccw, - cull_mode: None, - unclipped_depth: false, - polygon_mode: wgpu::PolygonMode::Fill, - conservative: false, - }, - depth_stencil: Some(wgpu::DepthStencilState { - format: wgpu::TextureFormat::Depth32Float, - depth_write_enabled: true, - depth_compare: wgpu::CompareFunction::Less, - stencil: wgpu::StencilState::default(), - bias: wgpu::DepthBiasState::default(), - }), - multisample: wgpu::MultisampleState { - mask: !0, - alpha_to_coverage_enabled: false, - count: multisample_count, - }, - fragment: Some(wgpu::FragmentState { - module: &fragment_linkage.module, - entry_point: Some(fragment_linkage.entry_point), - targets: &[Some(wgpu::ColorTargetState { - format: wgpu::TextureFormat::Rgba16Float, - blend: Some(wgpu::BlendState::ALPHA_BLENDING), - write_mask: wgpu::ColorWrites::ALL, - })], - compilation_options: Default::default(), - }), - multiview: None, - cache: None, - }) +/// Performs a rendering of an entire scene, given the resources at hand. +pub struct StageRendering<'a> { + // TODO: include the rest of the needed paramaters from `stage`, and then remove `stage` + pub stage: &'a Stage, + pub pipeline: &'a wgpu::RenderPipeline, + pub color_attachment: wgpu::RenderPassColorAttachment<'a>, + pub depth_stencil_attachment: wgpu::RenderPassDepthStencilAttachment<'a>, +} + +impl StageRendering<'_> { + /// Run the stage rendering. + /// + /// Returns the queue submission index and the indirect draw buffer, if available. + pub fn run(self) -> (wgpu::SubmissionIndex, Option>) { + self.stage.tick_internal(); + self.stage.lighting.upkeep(); + + let mut draw_calls = self.stage.draw_calls.write().unwrap(); + let depth_texture = self.stage.depth_texture.read().unwrap(); + // UNWRAP: safe because we know the depth texture format will always match + let maybe_indirect_buffer = draw_calls.pre_draw(&depth_texture).unwrap(); + { + log::info!("rendering"); + let label = Some("stage render"); + + log::info!("getting slab buffers bindgroup"); + let slab_buffers_bindgroup = { + log::info!("getting write lock"); + let mut stage_slab_buffer = self.stage.stage_slab_buffer.write().unwrap(); + log::info!("got write lock"); + let should_invalidate_buffers_bindgroup = stage_slab_buffer.update_if_invalid(); + self.stage + .buffers_bindgroup + .get(should_invalidate_buffers_bindgroup, || { + log::info!("renewing invalid stage slab buffers bindgroup"); + crate::linkage::slab_bindgroup( + self.stage.device(), + &stage_slab_buffer, + // UNWRAP: POP + &self + .stage + .stage_pipeline + .read() + .unwrap() + .get_bind_group_layout(0), + ) + }) + }; + + log::info!("getting stage slab buffer"); + let stage_slab_buffer = self.stage.stage_slab_buffer.read().unwrap(); + let textures_bindgroup = self.stage.get_textures_bindgroup(); + log::info!("got stage slab buffer and shadow map depth texture"); + + let light_bindgroup = self.stage.lighting.get_bindgroup(); + let has_skybox = self.stage.has_skybox.load(Ordering::Relaxed); + let may_skybox_pipeline_and_bindgroup = if has_skybox { + Some( + self.stage + .get_skybox_pipeline_and_bindgroup(&stage_slab_buffer), + ) + } else { + None + }; + + let mut encoder = self + .stage + .device() + .create_command_encoder(&wgpu::CommandEncoderDescriptor { label }); + { + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label, + color_attachments: &[Some(self.color_attachment)], + depth_stencil_attachment: Some(self.depth_stencil_attachment), + ..Default::default() + }); + + render_pass.set_pipeline(self.pipeline); + render_pass.set_bind_group(0, Some(slab_buffers_bindgroup.as_ref()), &[]); + render_pass.set_bind_group(1, Some(textures_bindgroup.as_ref()), &[]); + render_pass.set_bind_group(2, Some(&light_bindgroup), &[]); + draw_calls.draw(&mut render_pass); + + if let Some((pipeline, bindgroup)) = may_skybox_pipeline_and_bindgroup.as_ref() { + // UNWRAP: if we can't acquire the lock we want to panic. + let skybox = self.stage.skybox.read().unwrap(); + render_pass.set_pipeline(&pipeline.pipeline); + render_pass.set_bind_group(0, Some(bindgroup.as_ref()), &[]); + render_pass.draw(0..36, skybox.camera.inner()..skybox.camera.inner() + 1); + } + } + let sindex = self.stage.queue().submit(std::iter::once(encoder.finish())); + (sindex, maybe_indirect_buffer) + } + } } /// Represents an entire scene worth of rendering data. @@ -184,6 +215,73 @@ impl Deref for Stage { } impl Stage { + pub fn create_stage_render_pipeline( + device: &wgpu::Device, + fragment_color_format: wgpu::TextureFormat, + multisample_count: u32, + ) -> wgpu::RenderPipeline { + log::trace!("creating stage render pipeline"); + let label = Some("stage render"); + let vertex_linkage = crate::linkage::renderlet_vertex::linkage(device); + let fragment_linkage = crate::linkage::renderlet_fragment::linkage(device); + let stage_slab_buffers_layout = crate::linkage::slab_bindgroup_layout(device); + let atlas_and_skybox_layout = crate::linkage::atlas_and_skybox_bindgroup_layout(device); + let light_bindgroup_layout = crate::light::Lighting::create_bindgroup_layout(device); + let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label, + bind_group_layouts: &[ + &stage_slab_buffers_layout, + &atlas_and_skybox_layout, + &light_bindgroup_layout, + ], + push_constant_ranges: &[], + }); + + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label, + layout: Some(&layout), + vertex: wgpu::VertexState { + module: &vertex_linkage.module, + entry_point: Some(vertex_linkage.entry_point), + buffers: &[], + compilation_options: Default::default(), + }, + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: None, + unclipped_depth: false, + polygon_mode: wgpu::PolygonMode::Fill, + conservative: false, + }, + depth_stencil: Some(wgpu::DepthStencilState { + format: wgpu::TextureFormat::Depth32Float, + depth_write_enabled: true, + depth_compare: wgpu::CompareFunction::Less, + stencil: wgpu::StencilState::default(), + bias: wgpu::DepthBiasState::default(), + }), + multisample: wgpu::MultisampleState { + mask: !0, + alpha_to_coverage_enabled: false, + count: multisample_count, + }, + fragment: Some(wgpu::FragmentState { + module: &fragment_linkage.module, + entry_point: Some(fragment_linkage.entry_point), + targets: &[Some(wgpu::ColorTargetState { + format: fragment_color_format, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + multiview: None, + cache: None, + }) + } + /// Create a new stage. pub fn new(ctx: &crate::Context) -> Self { let runtime = ctx.runtime(); @@ -217,7 +315,11 @@ impl Stage { ctx.get_render_target().format().add_srgb_suffix(), &bloom.get_mix_texture(), ); - let stage_pipeline = create_stage_render_pipeline(device, multisample_count); + let stage_pipeline = Self::create_stage_render_pipeline( + device, + wgpu::TextureFormat::Rgba16Float, + multisample_count, + ); let stage_slab_buffer = mngr.commit(); let lighting = Lighting::new(&mngr); @@ -291,8 +393,11 @@ impl Stage { log::debug!("setting multisample count to {multisample_count}"); // UNWRAP: POP - *self.stage_pipeline.write().unwrap() = - create_stage_render_pipeline(self.device(), multisample_count); + *self.stage_pipeline.write().unwrap() = Self::create_stage_render_pipeline( + self.device(), + wgpu::TextureFormat::Rgba16Float, + multisample_count, + ); let size = self.get_size(); // UNWRAP: POP *self.depth_texture.write().unwrap() = Texture::create_depth_texture( @@ -698,10 +803,16 @@ impl Stage { } /// Iterator over all staged [`Renderlet`]s. - pub fn renderlets_iter(&self) -> impl Iterator { + /// + /// This iterator returns `Renderlets` wrapped in `WeakHybrid`, because they + /// are stored by weak references internally. + /// + /// You should have references of your own, but this is here as a convenience + /// method, and is used internally. + pub fn renderlets_iter(&self) -> impl Iterator> { // UNWRAP: panic on purpose let guard = self.draw_calls.read().unwrap(); - guard.renderlets_iter() + guard.renderlets_iter().collect::>().into_iter() } /// Returns a clone of the current depth texture. @@ -751,6 +862,109 @@ impl Stage { } pub fn render(&self, view: &wgpu::TextureView) { + // UNWRAP: POP + let background_color = *self.background_color.read().unwrap(); + // UNWRAP: POP + let msaa_target = self.msaa_render_target.read().unwrap(); + let clear_colors = self.clear_color_attachments.load(Ordering::Relaxed); + let hdr_texture = self.hdr_texture.read().unwrap(); + + let mk_ops = |store| wgpu::Operations { + load: if clear_colors { + wgpu::LoadOp::Clear(background_color) + } else { + wgpu::LoadOp::Load + }, + store, + }; + let render_pass_color_attachment = if let Some(msaa_view) = msaa_target.as_ref() { + wgpu::RenderPassColorAttachment { + ops: mk_ops(wgpu::StoreOp::Discard), + view: msaa_view, + resolve_target: Some(&hdr_texture.view), + } + } else { + wgpu::RenderPassColorAttachment { + ops: mk_ops(wgpu::StoreOp::Store), + view: &hdr_texture.view, + resolve_target: None, + } + }; + + let depth_texture = self.depth_texture.read().unwrap(); + let clear_depth = self.clear_depth_attachments.load(Ordering::Relaxed); + let render_pass_depth_attachment = wgpu::RenderPassDepthStencilAttachment { + view: &depth_texture.view, + depth_ops: Some(wgpu::Operations { + load: if clear_depth { + wgpu::LoadOp::Clear(1.0) + } else { + wgpu::LoadOp::Load + }, + store: wgpu::StoreOp::Store, + }), + stencil_ops: None, + }; + let pipeline_guard = self.stage_pipeline.read().unwrap(); + let (_submission_index, maybe_indirect_buffer) = StageRendering { + pipeline: &pipeline_guard, + stage: self, + color_attachment: render_pass_color_attachment, + depth_stencil_attachment: render_pass_depth_attachment, + } + .run(); + + // then render bloom + if self.has_bloom.load(Ordering::Relaxed) { + self.bloom.bloom(self.device(), self.queue()); + } else { + // copy the input hdr texture to the bloom mix texture + let mut encoder = + self.device() + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("no bloom copy"), + }); + let bloom_mix_texture = self.bloom.get_mix_texture(); + encoder.copy_texture_to_texture( + wgpu::TexelCopyTextureInfo { + texture: &self.hdr_texture.read().unwrap().texture, + mip_level: 0, + origin: wgpu::Origin3d { x: 0, y: 0, z: 0 }, + aspect: wgpu::TextureAspect::All, + }, + wgpu::TexelCopyTextureInfo { + texture: &bloom_mix_texture.texture, + mip_level: 0, + origin: wgpu::Origin3d { x: 0, y: 0, z: 0 }, + aspect: wgpu::TextureAspect::All, + }, + wgpu::Extent3d { + width: bloom_mix_texture.width(), + height: bloom_mix_texture.height(), + depth_or_array_layers: 1, + }, + ); + self.queue().submit(std::iter::once(encoder.finish())); + } + + // then render tonemapping + self.tonemapping.render(self.device(), self.queue(), view); + + // then render the debug overlay + if self.has_debug_overlay.load(Ordering::Relaxed) { + if let Some(indirect_draw_buffer) = maybe_indirect_buffer { + self.debug_overlay.render( + self.device(), + self.queue(), + view, + &self.stage_slab_buffer.read().unwrap(), + &indirect_draw_buffer, + ); + } + } + } + + pub fn render_old(&self, view: &wgpu::TextureView) { self.tick_internal(); self.lighting.upkeep();