use bytemuck::{Pod, Zeroable}; use glam::{Vec2, Vec3}; use wgpu::util::DeviceExt; #[repr(C)] #[derive(Clone, Copy, Pod, Zeroable)] struct AccumulationUniforms { terrain_min_xz: [f32; 2], terrain_max_xz: [f32; 2], decay_rate: f32, delta_time: f32, spotlight_count: u32, _padding: u32, light_view_projections: [[[f32; 4]; 4]; crate::render::MAX_SPOTLIGHTS], shadow_bias: f32, terrain_height_scale: f32, _padding3: f32, _padding4: f32, spotlights: [crate::render::SpotlightRaw; crate::render::MAX_SPOTLIGHTS], } pub struct SnowLightAccumulation { texture_ping: wgpu::Texture, texture_pong: wgpu::Texture, view_ping: wgpu::TextureView, view_pong: wgpu::TextureView, bind_group_layout: wgpu::BindGroupLayout, bind_group_ping: Option, bind_group_pong: Option, uniform_buffer: wgpu::Buffer, pipeline: wgpu::RenderPipeline, quad_vb: wgpu::Buffer, quad_ib: wgpu::Buffer, quad_num_indices: u32, current: bool, needs_clear: bool, terrain_min: Vec2, terrain_max: Vec2, pub decay_rate: f32, } impl SnowLightAccumulation { pub fn new(device: &wgpu::Device, terrain_min: Vec2, terrain_max: Vec2, resolution: u32) -> Self { let size = wgpu::Extent3d { width: resolution, height: resolution, depth_or_array_layers: 1, }; let texture_desc = wgpu::TextureDescriptor { label: Some("Snow Light Accumulation"), size, mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, format: wgpu::TextureFormat::R16Float, usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, view_formats: &[], }; let texture_ping = device.create_texture(&texture_desc); let texture_pong = device.create_texture(&texture_desc); let view_ping = texture_ping.create_view(&wgpu::TextureViewDescriptor::default()); let view_pong = texture_pong.create_view(&wgpu::TextureViewDescriptor::default()); let sampler = device.create_sampler(&wgpu::SamplerDescriptor { label: Some("Snow Light Sampler"), address_mode_u: wgpu::AddressMode::ClampToEdge, address_mode_v: wgpu::AddressMode::ClampToEdge, mag_filter: wgpu::FilterMode::Linear, min_filter: wgpu::FilterMode::Linear, ..Default::default() }); let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor { label: Some("Snow Light Accumulation Uniforms"), size: std::mem::size_of::() as wgpu::BufferAddress, usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }); let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("Snow Light Accumulation Bind Group Layout"), entries: &[ wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Texture { sample_type: wgpu::TextureSampleType::Float { filterable: true }, view_dimension: wgpu::TextureViewDimension::D2, multisampled: false, }, count: None, }, wgpu::BindGroupLayoutEntry { binding: 1, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), count: None, }, wgpu::BindGroupLayoutEntry { binding: 2, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: None, }, count: None, }, wgpu::BindGroupLayoutEntry { binding: 3, 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: 4, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering), count: None, }, wgpu::BindGroupLayoutEntry { binding: 5, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Texture { sample_type: wgpu::TextureSampleType::Depth, view_dimension: wgpu::TextureViewDimension::D2Array, multisampled: false, }, count: None, }, wgpu::BindGroupLayoutEntry { binding: 6, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison), count: None, }, wgpu::BindGroupLayoutEntry { binding: 7, 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: 8, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering), count: None, }, ], }); let compiler = wesl::Wesl::new("src/shaders"); let shader_source = compiler .compile(&"package::snow_light_accumulation".parse().unwrap()) .inspect_err(|e| eprintln!("WESL error: {e}")) .unwrap() .to_string(); let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("Snow Light Accumulation Shader"), source: wgpu::ShaderSource::Wgsl(shader_source.into()), }); let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("Snow Light Accumulation Pipeline Layout"), bind_group_layouts: &[&bind_group_layout], push_constant_ranges: &[], }); let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: Some("Snow Light Accumulation Pipeline"), layout: Some(&pipeline_layout), vertex: wgpu::VertexState { module: &shader, entry_point: Some("vs_main"), buffers: &[], compilation_options: Default::default(), }, fragment: Some(wgpu::FragmentState { module: &shader, entry_point: Some("fs_main"), targets: &[Some(wgpu::ColorTargetState { format: wgpu::TextureFormat::R16Float, blend: Some(wgpu::BlendState::REPLACE), write_mask: wgpu::ColorWrites::ALL, })], compilation_options: Default::default(), }), primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, strip_index_format: None, front_face: wgpu::FrontFace::Ccw, cull_mode: None, polygon_mode: wgpu::PolygonMode::Fill, unclipped_depth: false, conservative: false, }, depth_stencil: None, multisample: wgpu::MultisampleState { count: 1, mask: !0, alpha_to_coverage_enabled: false, }, multiview: None, cache: None, }); let vertices: &[f32] = &[ -1.0, -1.0, 0.0, 1.0, 3.0, -1.0, 2.0, 1.0, -1.0, 3.0, 0.0, -1.0, ]; let quad_vb = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("Snow Light Quad VB"), contents: bytemuck::cast_slice(vertices), usage: wgpu::BufferUsages::VERTEX, }); let indices: &[u16] = &[0, 1, 2]; let quad_ib = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("Snow Light Quad IB"), contents: bytemuck::cast_slice(indices), usage: wgpu::BufferUsages::INDEX, }); Self { texture_ping, texture_pong, view_ping, view_pong, bind_group_layout, bind_group_ping: None, bind_group_pong: None, uniform_buffer, pipeline, quad_vb, quad_ib, quad_num_indices: 3, current: false, needs_clear: true, terrain_min, terrain_max, decay_rate: 0.015, } } pub fn clear(&self, encoder: &mut wgpu::CommandEncoder) { encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("Clear Snow Light Ping"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { view: &self.view_ping, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), store: wgpu::StoreOp::Store, }, depth_slice: None, })], depth_stencil_attachment: None, timestamp_writes: None, occlusion_query_set: None, }); encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("Clear Snow Light Pong"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { view: &self.view_pong, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), store: wgpu::StoreOp::Store, }, depth_slice: None, })], depth_stencil_attachment: None, timestamp_writes: None, occlusion_query_set: None, }); } pub fn set_heightmap( &mut self, device: &wgpu::Device, heightmap_view: &wgpu::TextureView, heightmap_sampler: &wgpu::Sampler, shadow_map_view: &wgpu::TextureView, shadow_sampler: &wgpu::Sampler, snow_depth_view: &wgpu::TextureView, snow_depth_sampler: &wgpu::Sampler, ) { let sampler = device.create_sampler(&wgpu::SamplerDescriptor { label: Some("Snow Light Sampler"), address_mode_u: wgpu::AddressMode::ClampToEdge, address_mode_v: wgpu::AddressMode::ClampToEdge, mag_filter: wgpu::FilterMode::Linear, min_filter: wgpu::FilterMode::Linear, ..Default::default() }); self.bind_group_ping = Some(device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("Snow Light Accumulation Bind Group Ping"), layout: &self.bind_group_layout, entries: &[ wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&self.view_pong), }, wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&sampler), }, wgpu::BindGroupEntry { binding: 2, resource: self.uniform_buffer.as_entire_binding(), }, wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::TextureView(heightmap_view), }, wgpu::BindGroupEntry { binding: 4, resource: wgpu::BindingResource::Sampler(heightmap_sampler), }, wgpu::BindGroupEntry { binding: 5, resource: wgpu::BindingResource::TextureView(shadow_map_view), }, wgpu::BindGroupEntry { binding: 6, resource: wgpu::BindingResource::Sampler(shadow_sampler), }, wgpu::BindGroupEntry { binding: 7, resource: wgpu::BindingResource::TextureView(snow_depth_view), }, wgpu::BindGroupEntry { binding: 8, resource: wgpu::BindingResource::Sampler(snow_depth_sampler), }, ], })); self.bind_group_pong = Some(device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("Snow Light Accumulation Bind Group Pong"), layout: &self.bind_group_layout, entries: &[ wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&self.view_ping), }, wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&sampler), }, wgpu::BindGroupEntry { binding: 2, resource: self.uniform_buffer.as_entire_binding(), }, wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::TextureView(heightmap_view), }, wgpu::BindGroupEntry { binding: 4, resource: wgpu::BindingResource::Sampler(heightmap_sampler), }, wgpu::BindGroupEntry { binding: 5, resource: wgpu::BindingResource::TextureView(shadow_map_view), }, wgpu::BindGroupEntry { binding: 6, resource: wgpu::BindingResource::Sampler(shadow_sampler), }, wgpu::BindGroupEntry { binding: 7, resource: wgpu::BindingResource::TextureView(snow_depth_view), }, wgpu::BindGroupEntry { binding: 8, resource: wgpu::BindingResource::Sampler(snow_depth_sampler), }, ], })); } pub fn render( &mut self, encoder: &mut wgpu::CommandEncoder, queue: &wgpu::Queue, spotlights: &[crate::render::Spotlight], delta_time: f32, light_view_projections: &[glam::Mat4], shadow_bias: f32, terrain_height_scale: f32, ) { if self.needs_clear { self.clear(encoder); self.needs_clear = false; } let mut spotlight_array = [crate::render::SpotlightRaw::default(); crate::render::MAX_SPOTLIGHTS]; for (i, spotlight) in spotlights .iter() .take(crate::render::MAX_SPOTLIGHTS) .enumerate() { spotlight_array[i] = spotlight.to_raw(); } let uniforms = AccumulationUniforms { terrain_min_xz: self.terrain_min.to_array(), terrain_max_xz: self.terrain_max.to_array(), decay_rate: self.decay_rate, delta_time, spotlight_count: spotlights.len().min(crate::render::MAX_SPOTLIGHTS) as u32, _padding: 0, light_view_projections: { let mut arr = [[[0.0f32; 4]; 4]; crate::render::MAX_SPOTLIGHTS]; for (i, mat) in light_view_projections .iter() .take(crate::render::MAX_SPOTLIGHTS) .enumerate() { arr[i] = mat.to_cols_array_2d(); } arr }, shadow_bias, terrain_height_scale, _padding3: 0.0, _padding4: 0.0, spotlights: spotlight_array, }; queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniforms])); let write_view = if self.current { &self.view_ping } else { &self.view_pong }; let bind_group = if self.current { self.bind_group_ping.as_ref() } else { self.bind_group_pong.as_ref() }; let Some(bind_group) = bind_group else { return; }; { let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("Snow Light Accumulation Pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { view: write_view, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store, }, depth_slice: None, })], depth_stencil_attachment: None, timestamp_writes: None, occlusion_query_set: None, }); render_pass.set_pipeline(&self.pipeline); render_pass.set_bind_group(0, bind_group, &[]); render_pass.set_vertex_buffer(0, self.quad_vb.slice(..)); render_pass.set_index_buffer(self.quad_ib.slice(..), wgpu::IndexFormat::Uint16); render_pass.draw_indexed(0..self.quad_num_indices, 0, 0..1); } self.current = !self.current; } pub fn read_view(&self) -> &wgpu::TextureView { if self.current { &self.view_pong } else { &self.view_ping } } pub fn create_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout { device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("Snow Persistent Light Bind Group Layout"), entries: &[ wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Texture { sample_type: wgpu::TextureSampleType::Float { filterable: true }, view_dimension: wgpu::TextureViewDimension::D2, multisampled: false, }, count: None, }, wgpu::BindGroupLayoutEntry { binding: 1, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), count: None, }, ], }) } pub fn create_read_bind_group( &self, device: &wgpu::Device, layout: &wgpu::BindGroupLayout, ) -> wgpu::BindGroup { let sampler = device.create_sampler(&wgpu::SamplerDescriptor { label: Some("Snow Persistent Light Sampler"), address_mode_u: wgpu::AddressMode::ClampToEdge, address_mode_v: wgpu::AddressMode::ClampToEdge, mag_filter: wgpu::FilterMode::Linear, min_filter: wgpu::FilterMode::Linear, ..Default::default() }); device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("Snow Persistent Light Bind Group"), layout, entries: &[ wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(self.read_view()), }, wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&sampler), }, ], }) } }