use std::rc::Rc; use exr::prelude::{ReadChannels, ReadLayers}; use glam::{Vec2, Vec3}; use wgpu::util::DeviceExt; use crate::{ loaders::mesh::{InstanceRaw, Mesh, Vertex}, paths, render::{self, DrawCall, Pipeline}, texture::HeightmapTexture, }; pub struct SnowConfig { pub depth_map_path: String, pub heightmap_path: String, pub terrain_size: Vec2, pub resolution: (u32, u32), } impl SnowConfig { pub fn default() -> Self { Self { depth_map_path: paths::textures::snow_depth(), heightmap_path: paths::textures::terrain_heightmap(), terrain_size: Vec2::new(1000.0, 1000.0), resolution: (1000, 1000), } } } pub struct ClipmapConfig { pub num_levels: u32, pub grid_size: u32, pub base_cell_size: f32, } impl Default for ClipmapConfig { fn default() -> Self { Self { num_levels: 3, grid_size: 192, base_cell_size: 0.25, } } } struct ClipmapLevel { mesh: Rc, instance_buffer: wgpu::Buffer, } pub struct SnowLayer { pub depth_texture: wgpu::Texture, pub depth_texture_view: wgpu::TextureView, pub depth_bind_group: wgpu::BindGroup, pub width: u32, pub height: u32, pub deform_bind_group: wgpu::BindGroup, pub deform_pipeline: wgpu::ComputePipeline, pub deform_params_buffer: wgpu::Buffer, pub terrain_size: Vec2, clipmap_config: ClipmapConfig, levels: Vec, displacement_bind_group: wgpu::BindGroup, heightmap_texture: HeightmapTexture, depth_sampler: wgpu::Sampler, } fn create_instance_buffer() -> wgpu::Buffer { render::with_device(|device| { device.create_buffer(&wgpu::BufferDescriptor { label: Some("Snow Clipmap Instance Buffer"), size: std::mem::size_of::() as u64, usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }) }) } impl SnowLayer { pub fn load(config: &SnowConfig) -> anyhow::Result { println!("\n=== Loading Snow Layer ==="); println!("Depth map path: {}", config.depth_map_path); println!("Heightmap path: {}", config.heightmap_path); println!("Terrain size: {:?}", config.terrain_size); let (depth_data, width, height) = Self::load_depth_map(&config.depth_map_path)?; println!("Using EXR dimensions: {}×{}", width, height); let (depth_texture, depth_texture_view, depth_bind_group) = Self::create_depth_texture(&depth_data, width, height); let (deform_pipeline, deform_bind_group, deform_params_buffer) = Self::create_deform_pipeline(&depth_texture_view); let heightmap_texture = render::with_device(|device| { render::with_queue(|queue| { HeightmapTexture::load(device, queue, &config.heightmap_path) }) })?; let depth_sampler = render::with_device(|device| { device.create_sampler(&wgpu::SamplerDescriptor { label: Some("Snow Depth Sampler"), address_mode_u: wgpu::AddressMode::ClampToEdge, address_mode_v: wgpu::AddressMode::ClampToEdge, mag_filter: wgpu::FilterMode::Nearest, min_filter: wgpu::FilterMode::Nearest, ..Default::default() }) }); let displacement_bind_group = Self::create_displacement_bind_group( &heightmap_texture, &depth_texture_view, &depth_sampler, ); let clipmap_config = ClipmapConfig::default(); let levels = Vec::new(); Ok(Self { depth_texture, depth_texture_view, depth_bind_group, width, height, deform_bind_group, deform_pipeline, deform_params_buffer, terrain_size: config.terrain_size, clipmap_config, levels, displacement_bind_group, heightmap_texture, depth_sampler, }) } fn load_depth_map(path: &str) -> anyhow::Result<(Vec, u32, u32)> { println!("Loading snow depth map from: {}", path); let image = exr::prelude::read() .no_deep_data() .largest_resolution_level() .all_channels() .all_layers() .all_attributes() .from_file(path)?; let layer = &image.layer_data[0]; let width = layer.size.width() as u32; let height = layer.size.height() as u32; println!(" Layer size: {}×{}", width, height); println!( " Available channels: {:?}", layer .channel_data .list .iter() .map(|c| &c.name) .collect::>() ); let channel = layer .channel_data .list .iter() .find(|c| format!("{:?}", c.name).contains("\"R\"")) .or_else(|| layer.channel_data.list.first()) .ok_or_else(|| anyhow::anyhow!("No channels found in EXR"))?; println!(" Using channel: {:?}", channel.name); let depths: Vec = channel.sample_data.values_as_f32().collect(); let min_value = depths.iter().cloned().fold(f32::INFINITY, f32::min); let max_value = depths.iter().cloned().fold(f32::NEG_INFINITY, f32::max); let avg_value = depths.iter().sum::() / depths.len() as f32; let non_zero_count = depths.iter().filter(|&&v| v > 0.0001).count(); println!(" Total values: {}", depths.len()); println!( " Min: {:.6}, Max: {:.6}, Avg: {:.6}", min_value, max_value, avg_value ); println!( " Non-zero values: {} ({:.1}%)", non_zero_count, (non_zero_count as f32 / depths.len() as f32) * 100.0 ); if max_value < 0.0001 { println!( " ⚠️ WARNING: All values are effectively zero! Snow depth map may be invalid." ); } else { println!(" ✓ Snow depth data loaded successfully"); } Ok((depths, width, height)) } fn create_depth_texture( depth_data: &[f32], width: u32, height: u32, ) -> (wgpu::Texture, wgpu::TextureView, wgpu::BindGroup) { render::with_device(|device| { let size = wgpu::Extent3d { width, height, depth_or_array_layers: 1, }; let texture = device.create_texture(&wgpu::TextureDescriptor { label: Some("Snow Depth Texture"), size, mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, format: wgpu::TextureFormat::R32Float, usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::STORAGE_BINDING, view_formats: &[], }); let data_bytes: &[u8] = bytemuck::cast_slice(depth_data); render::with_queue(|queue| { queue.write_texture( wgpu::TexelCopyTextureInfo { texture: &texture, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All, }, data_bytes, wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(width * 4), rows_per_image: Some(height), }, size, ); }); let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default()); let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("Snow Depth Bind Group Layout"), entries: &[wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::FRAGMENT | wgpu::ShaderStages::COMPUTE, ty: wgpu::BindingType::Texture { sample_type: wgpu::TextureSampleType::Float { filterable: false }, view_dimension: wgpu::TextureViewDimension::D2, multisampled: false, }, count: None, }], }); let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("Snow Depth Bind Group"), layout: &bind_group_layout, entries: &[wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&texture_view), }], }); (texture, texture_view, bind_group) }) } fn create_deform_pipeline( depth_texture_view: &wgpu::TextureView, ) -> (wgpu::ComputePipeline, wgpu::BindGroup, wgpu::Buffer) { render::with_device(|device| { let shader_source = std::fs::read_to_string(&paths::shaders::snow_deform()) .expect("Failed to load snow deform shader"); let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("Snow Deform Shader"), source: wgpu::ShaderSource::Wgsl(shader_source.into()), }); let params_buffer = device.create_buffer(&wgpu::BufferDescriptor { label: Some("Snow Deform Params"), size: 32, 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 Deform Bind Group Layout"), entries: &[ wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::COMPUTE, ty: wgpu::BindingType::StorageTexture { access: wgpu::StorageTextureAccess::ReadWrite, format: wgpu::TextureFormat::R32Float, view_dimension: wgpu::TextureViewDimension::D2, }, count: None, }, wgpu::BindGroupLayoutEntry { binding: 1, visibility: wgpu::ShaderStages::COMPUTE, ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: None, }, count: None, }, ], }); let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("Snow Deform Bind Group"), layout: &bind_group_layout, entries: &[ wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(depth_texture_view), }, wgpu::BindGroupEntry { binding: 1, resource: params_buffer.as_entire_binding(), }, ], }); let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("Snow Deform Pipeline Layout"), bind_group_layouts: &[&bind_group_layout], immediate_size: 0, }); let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor { label: Some("Snow Deform Pipeline"), layout: Some(&pipeline_layout), module: &shader, entry_point: Some("deform"), compilation_options: Default::default(), cache: None, }); (pipeline, bind_group, params_buffer) }) } fn create_displacement_bind_group( heightmap: &HeightmapTexture, depth_view: &wgpu::TextureView, depth_sampler: &wgpu::Sampler, ) -> wgpu::BindGroup { render::with_device(|device| { let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("Snow Displacement Bind Group Layout"), entries: &[ wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::VERTEX, ty: wgpu::BindingType::Texture { sample_type: wgpu::TextureSampleType::Float { filterable: false }, view_dimension: wgpu::TextureViewDimension::D2, multisampled: false, }, count: None, }, wgpu::BindGroupLayoutEntry { binding: 1, visibility: wgpu::ShaderStages::VERTEX, ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering), count: None, }, wgpu::BindGroupLayoutEntry { binding: 2, visibility: wgpu::ShaderStages::VERTEX | 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: 3, visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering), count: None, }, ], }); device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("Snow Displacement Bind Group"), layout: &layout, entries: &[ wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&heightmap.view), }, wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&heightmap.sampler), }, wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::TextureView(depth_view), }, wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::Sampler(depth_sampler), }, ], }) }) } fn generate_clipmap_grid(&self, level: u32) -> Mesh { let cell_size = self.clipmap_config.base_cell_size * (1 << level) as f32; let grid_size = self.clipmap_config.grid_size; let half_extent = (grid_size as f32 * cell_size) / 2.0; let mut vertices = Vec::new(); let mut indices = Vec::new(); for z in 0..=grid_size { for x in 0..=grid_size { vertices.push(Vertex { position: [ -half_extent + x as f32 * cell_size, 0.0, -half_extent + z as f32 * cell_size, ], normal: [0.0, 1.0, 0.0], uv: [0.0, 0.0], }); } } let row = grid_size + 1; for z in 0..grid_size { for x in 0..grid_size { let i0 = z * row + x; let i1 = i0 + 1; let i2 = i0 + row; let i3 = i2 + 1; indices.push(i0); indices.push(i2); indices.push(i1); indices.push(i1); indices.push(i2); indices.push(i3); } } let vertex_buffer = render::with_device(|device| { device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some(&format!("Snow Clipmap Level {} Vertex Buffer", level)), contents: bytemuck::cast_slice(&vertices), usage: wgpu::BufferUsages::VERTEX, }) }); let index_buffer = render::with_device(|device| { device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some(&format!("Snow Clipmap Level {} Index Buffer", level)), contents: bytemuck::cast_slice(&indices), usage: wgpu::BufferUsages::INDEX, }) }); let mut aabb_min = Vec3::splat(f32::MAX); let mut aabb_max = Vec3::splat(f32::MIN); for v in &vertices { let p = Vec3::from(v.position); aabb_min = aabb_min.min(p); aabb_max = aabb_max.max(p); } Mesh { vertex_buffer, index_buffer, num_indices: indices.len() as u32, aabb_min, aabb_max, cpu_positions: Vec::new(), cpu_indices: Vec::new(), } } pub fn update(&mut self, center: Vec3) { if self.levels.is_empty() { for level in 0..self.clipmap_config.num_levels { let mesh = self.generate_clipmap_grid(level); let instance_buffer = create_instance_buffer(); self.levels.push(ClipmapLevel { mesh: Rc::new(mesh), instance_buffer, }); } } render::with_queue(|queue| { for (level, clipmap_level) in self.levels.iter().enumerate() { let cell_size = self.clipmap_config.base_cell_size * (1u32 << level) as f32; let snapped_x = (center.x / cell_size).round() * cell_size; let snapped_z = (center.z / cell_size).round() * cell_size; let y_bias = -(level as f32) * 0.005; let model = glam::Mat4::from_translation(glam::Vec3::new(snapped_x, y_bias, snapped_z)); let instance_data = InstanceRaw { model: model.to_cols_array_2d(), dissolve_amount: 0.0, _padding: [0.0; 3], }; queue.write_buffer( &clipmap_level.instance_buffer, 0, bytemuck::cast_slice(&[instance_data]), ); } }); } pub fn get_draw_calls(&self) -> Vec { self.levels .iter() .rev() .map(|level| DrawCall { vertex_buffer: level.mesh.vertex_buffer.clone(), index_buffer: level.mesh.index_buffer.clone(), num_indices: level.mesh.num_indices, model: glam::Mat4::IDENTITY, pipeline: Pipeline::SnowClipmap, instance_buffer: Some(level.instance_buffer.clone()), num_instances: 1, tile_scale: 2.0, enable_dissolve: false, enable_snow_light: true, displacement_bind_group: Some(self.displacement_bind_group.clone()), entity: None, }) .collect() } pub fn deform_at_position(&self, position: Vec3, radius: f32, depth: f32) { render::with_queue(|queue| { let params_data = [ position.x, position.z, radius, depth, self.terrain_size.x, self.terrain_size.y, 0.0, 0.0, ]; let params_bytes: &[u8] = bytemuck::cast_slice(¶ms_data); queue.write_buffer(&self.deform_params_buffer, 0, params_bytes); }); render::with_device(|device| { let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("Snow Deform Encoder"), }); { let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor { label: Some("Snow Deform Pass"), timestamp_writes: None, }); compute_pass.set_pipeline(&self.deform_pipeline); compute_pass.set_bind_group(0, &self.deform_bind_group, &[]); let workgroup_size = 16; let dispatch_x = (self.width + workgroup_size - 1) / workgroup_size; let dispatch_y = (self.height + workgroup_size - 1) / workgroup_size; compute_pass.dispatch_workgroups(dispatch_x, dispatch_y, 1); } render::with_queue(|queue| { queue.submit(Some(encoder.finish())); }); }); } }