634 lines
22 KiB
Rust
634 lines
22 KiB
Rust
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<Mesh>,
|
||
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<ClipmapLevel>,
|
||
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::<InstanceRaw>() as u64,
|
||
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
|
||
mapped_at_creation: false,
|
||
})
|
||
})
|
||
}
|
||
|
||
impl SnowLayer
|
||
{
|
||
pub fn load(config: &SnowConfig) -> anyhow::Result<Self>
|
||
{
|
||
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<f32>, 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::<Vec<_>>()
|
||
);
|
||
|
||
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<f32> = 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::<f32>() / 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<DrawCall>
|
||
{
|
||
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()));
|
||
});
|
||
});
|
||
}
|
||
}
|