2025-05-06 17:12:22 +02:00
|
|
|
use bytemuck::{Pod, Zeroable};
|
2025-05-07 20:12:10 +02:00
|
|
|
use cgmath::{EuclideanSpace, Matrix4, Point3, Transform, Vector3, Zero};
|
2025-05-07 19:45:35 +02:00
|
|
|
use wgpu::util::DeviceExt;
|
2025-05-06 17:12:22 +02:00
|
|
|
|
|
|
|
|
#[repr(u32)]
|
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
|
|
|
pub enum LightType {
|
|
|
|
|
Directional = 0,
|
|
|
|
|
Point = 1,
|
|
|
|
|
Spot = 2
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-07 19:45:35 +02:00
|
|
|
pub const CLUSTER_COUNT_X: usize = 16;
|
|
|
|
|
pub const CLUSTER_COUNT_Y: usize = 9;
|
|
|
|
|
pub const CLUSTER_COUNT_Z: usize = 24;
|
|
|
|
|
pub const MAX_LIGHTS_PER_CLUSTER: usize = 32;
|
|
|
|
|
pub const TOTAL_CLUSTERS: usize = CLUSTER_COUNT_X * CLUSTER_COUNT_Y * CLUSTER_COUNT_Z;
|
|
|
|
|
|
2025-05-06 17:12:22 +02:00
|
|
|
#[repr(C)]
|
|
|
|
|
#[derive(Clone, Copy, Pod, Zeroable, Debug)]
|
|
|
|
|
pub struct GpuLight {
|
|
|
|
|
pub position: [f32; 3],
|
|
|
|
|
pub light_type: u32,
|
|
|
|
|
pub color: [f32; 3],
|
|
|
|
|
pub intensity: f32,
|
|
|
|
|
pub direction: [f32; 3],
|
|
|
|
|
pub range: f32,
|
|
|
|
|
pub inner_cutoff: f32,
|
|
|
|
|
pub outer_cutoff: f32,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub struct Light {
|
|
|
|
|
pub light_type: LightType,
|
|
|
|
|
pub position: Vector3<f32>,
|
|
|
|
|
pub direction: Vector3<f32>,
|
|
|
|
|
pub color: Vector3<f32>,
|
|
|
|
|
pub intensity: f32,
|
|
|
|
|
pub range: f32,
|
|
|
|
|
pub inner_cutoff: f32,
|
|
|
|
|
pub outer_cutoff: f32,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Light {
|
|
|
|
|
pub fn to_gpu(&self) -> GpuLight {
|
|
|
|
|
GpuLight {
|
|
|
|
|
position: self.position.into(),
|
|
|
|
|
light_type: self.light_type as u32,
|
|
|
|
|
color: self.color.into(),
|
|
|
|
|
intensity: self.intensity,
|
|
|
|
|
direction: self.direction.into(),
|
|
|
|
|
range: self.range,
|
|
|
|
|
inner_cutoff: self.inner_cutoff,
|
|
|
|
|
outer_cutoff: self.outer_cutoff,
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-05-07 20:12:10 +02:00
|
|
|
|
|
|
|
|
pub fn new_point(position: Vector3<f32>, color: Vector3<f32>, intensity: f32, min_attenuation: f32) -> Self {
|
|
|
|
|
let range = (intensity / min_attenuation).sqrt().max(1.0);
|
|
|
|
|
|
|
|
|
|
Self {
|
|
|
|
|
light_type: LightType::Point,
|
|
|
|
|
position,
|
|
|
|
|
direction: Vector3::zero(),
|
|
|
|
|
color,
|
|
|
|
|
intensity,
|
|
|
|
|
range,
|
|
|
|
|
inner_cutoff: 0.0,
|
|
|
|
|
outer_cutoff: 0.0,
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-05-06 17:12:22 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub struct LightManager {
|
|
|
|
|
pub lights: Vec<Light>,
|
|
|
|
|
pub buffer: wgpu::Buffer,
|
|
|
|
|
pub bind_group: wgpu::BindGroup,
|
2025-05-07 14:41:37 +02:00
|
|
|
pub count_buffer: wgpu::Buffer,
|
2025-05-07 19:45:35 +02:00
|
|
|
pub layout: wgpu::BindGroupLayout,
|
|
|
|
|
pub cluster_buffers: Option<ClusterBuffers>,
|
2025-05-07 14:41:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[repr(C)]
|
|
|
|
|
#[derive(Clone, Copy, Pod, Zeroable, Debug)]
|
|
|
|
|
pub struct LightCount {
|
|
|
|
|
pub count: u32,
|
2025-05-06 17:12:22 +02:00
|
|
|
}
|
|
|
|
|
|
2025-05-07 19:45:35 +02:00
|
|
|
pub struct ClusterBuffers {
|
|
|
|
|
pub light_indices: wgpu::Buffer,
|
|
|
|
|
pub offsets: wgpu::Buffer,
|
|
|
|
|
pub bind_group: wgpu::BindGroup,
|
|
|
|
|
pub layout: wgpu::BindGroupLayout,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
pub struct ClusterAssignment {
|
|
|
|
|
pub cluster_light_indices: Vec<u32>,
|
|
|
|
|
pub cluster_offsets: Vec<(u32, u32)>,
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-06 17:12:22 +02:00
|
|
|
impl LightManager {
|
|
|
|
|
pub fn new(device: &wgpu::Device, max_lights: usize) -> Self {
|
|
|
|
|
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
|
|
|
|
label: Some("Light Buffer"),
|
|
|
|
|
size: (max_lights * size_of::<GpuLight>()) as wgpu::BufferAddress,
|
2025-05-07 14:41:37 +02:00
|
|
|
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
|
2025-05-06 17:12:22 +02:00
|
|
|
mapped_at_creation: false,
|
|
|
|
|
});
|
|
|
|
|
|
2025-05-07 19:45:35 +02:00
|
|
|
let count_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
|
|
|
|
label: Some("Light Count Buffer"),
|
|
|
|
|
size: size_of::<LightCount>() as wgpu::BufferAddress,
|
|
|
|
|
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
|
|
|
|
mapped_at_creation: false,
|
|
|
|
|
});
|
|
|
|
|
|
2025-05-06 17:12:22 +02:00
|
|
|
let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
|
|
|
|
label: Some("Light Bind Group Layout"),
|
2025-05-07 14:41:37 +02:00
|
|
|
entries: &[
|
|
|
|
|
wgpu::BindGroupLayoutEntry {
|
|
|
|
|
binding: 0,
|
|
|
|
|
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
|
|
|
|
|
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::Buffer {
|
|
|
|
|
ty: wgpu::BufferBindingType::Uniform,
|
|
|
|
|
has_dynamic_offset: false,
|
|
|
|
|
min_binding_size: None,
|
|
|
|
|
},
|
|
|
|
|
count: None,
|
2025-05-06 17:12:22 +02:00
|
|
|
},
|
2025-05-07 14:41:37 +02:00
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
|
2025-05-06 17:12:22 +02:00
|
|
|
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
|
|
|
label: Some("Light Bind Group"),
|
|
|
|
|
layout: &layout,
|
2025-05-07 14:41:37 +02:00
|
|
|
entries: &[
|
|
|
|
|
wgpu::BindGroupEntry {
|
|
|
|
|
binding: 0,
|
|
|
|
|
resource: buffer.as_entire_binding(),
|
|
|
|
|
},
|
|
|
|
|
wgpu::BindGroupEntry {
|
|
|
|
|
binding: 1,
|
|
|
|
|
resource: count_buffer.as_entire_binding(),
|
|
|
|
|
},
|
|
|
|
|
],
|
2025-05-06 17:12:22 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Self {
|
|
|
|
|
lights: Vec::new(),
|
|
|
|
|
buffer,
|
2025-05-07 19:45:35 +02:00
|
|
|
count_buffer,
|
2025-05-06 17:12:22 +02:00
|
|
|
bind_group,
|
|
|
|
|
layout,
|
2025-05-07 19:45:35 +02:00
|
|
|
cluster_buffers: None,
|
2025-05-06 17:12:22 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn add_light(&mut self, light: Light) {
|
|
|
|
|
self.lights.push(light);
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-07 19:45:35 +02:00
|
|
|
pub fn clear(&mut self) {
|
|
|
|
|
self.lights.clear();
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-06 17:12:22 +02:00
|
|
|
pub fn update_gpu(&self, queue: &wgpu::Queue) {
|
2025-05-07 19:45:35 +02:00
|
|
|
let gpu_lights: Vec<GpuLight> = self.lights.iter().map(|l| l.to_gpu()).collect();
|
|
|
|
|
queue.write_buffer(&self.buffer, 0, bytemuck::cast_slice(&gpu_lights));
|
2025-05-07 14:41:37 +02:00
|
|
|
|
|
|
|
|
let count = LightCount {
|
|
|
|
|
count: self.lights.len() as u32,
|
|
|
|
|
};
|
|
|
|
|
queue.write_buffer(&self.count_buffer, 0, bytemuck::bytes_of(&count));
|
2025-05-06 17:12:22 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn bind_group(&self) -> &wgpu::BindGroup {
|
|
|
|
|
&self.bind_group
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn layout(&self) -> &wgpu::BindGroupLayout {
|
|
|
|
|
&self.layout
|
|
|
|
|
}
|
2025-05-07 19:45:35 +02:00
|
|
|
|
|
|
|
|
pub fn create_cluster_buffers(
|
|
|
|
|
&self,
|
|
|
|
|
device: &wgpu::Device,
|
|
|
|
|
assignment: &ClusterAssignment,
|
|
|
|
|
) -> ClusterBuffers {
|
|
|
|
|
let cluster_light_indices = if assignment.cluster_light_indices.is_empty() {
|
|
|
|
|
vec![0u32]
|
|
|
|
|
} else {
|
|
|
|
|
assignment.cluster_light_indices.clone()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let offset_pairs: Vec<[u32; 2]> = if assignment.cluster_offsets.is_empty() {
|
|
|
|
|
vec![[0, 0]]
|
|
|
|
|
} else {
|
|
|
|
|
assignment.cluster_offsets
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|&(o, c)| [o, c])
|
|
|
|
|
.collect()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let light_index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
|
|
|
|
label: Some("Cluster Light Indices"),
|
|
|
|
|
contents: bytemuck::cast_slice(&cluster_light_indices),
|
|
|
|
|
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let offset_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
|
|
|
|
label: Some("Cluster Offsets"),
|
|
|
|
|
contents: bytemuck::cast_slice(&offset_pairs),
|
|
|
|
|
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
|
|
|
|
label: Some("Cluster Bind Group Layout"),
|
|
|
|
|
entries: &[
|
|
|
|
|
wgpu::BindGroupLayoutEntry {
|
|
|
|
|
binding: 0,
|
|
|
|
|
visibility: wgpu::ShaderStages::FRAGMENT,
|
|
|
|
|
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::Buffer {
|
|
|
|
|
ty: wgpu::BufferBindingType::Storage { read_only: true },
|
|
|
|
|
has_dynamic_offset: false,
|
|
|
|
|
min_binding_size: None,
|
|
|
|
|
},
|
|
|
|
|
count: None,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
|
|
|
label: Some("Cluster Bind Group"),
|
|
|
|
|
layout: &layout,
|
|
|
|
|
entries: &[
|
|
|
|
|
wgpu::BindGroupEntry {
|
|
|
|
|
binding: 0,
|
|
|
|
|
resource: light_index_buffer.as_entire_binding(),
|
|
|
|
|
},
|
|
|
|
|
wgpu::BindGroupEntry {
|
|
|
|
|
binding: 1,
|
|
|
|
|
resource: offset_buffer.as_entire_binding(),
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
ClusterBuffers {
|
|
|
|
|
light_indices: light_index_buffer,
|
|
|
|
|
offsets: offset_buffer,
|
|
|
|
|
layout,
|
|
|
|
|
bind_group,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn update_cluster_buffers(
|
|
|
|
|
&mut self,
|
|
|
|
|
device: &wgpu::Device,
|
|
|
|
|
queue: &wgpu::Queue,
|
|
|
|
|
assignment: &ClusterAssignment,
|
|
|
|
|
) {
|
2025-05-07 20:12:10 +02:00
|
|
|
let new_index_bytes = assignment.cluster_light_indices.len() * std::mem::size_of::<u32>();
|
|
|
|
|
let new_offset_bytes = assignment.cluster_offsets.len() * std::mem::size_of::<[u32; 2]>();
|
|
|
|
|
|
|
|
|
|
let needs_resize = self.cluster_buffers.as_ref().map(|buffers| {
|
|
|
|
|
let index_size_ok = new_index_bytes <= buffers.light_indices.size() as usize;
|
|
|
|
|
let offset_size_ok = new_offset_bytes <= buffers.offsets.size() as usize;
|
|
|
|
|
!(index_size_ok && offset_size_ok)
|
|
|
|
|
}).unwrap_or(true);
|
|
|
|
|
|
|
|
|
|
if needs_resize {
|
|
|
|
|
let buffers = self.create_cluster_buffers(device, assignment);
|
|
|
|
|
self.cluster_buffers = Some(buffers);
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-07 19:45:35 +02:00
|
|
|
if let Some(buffers) = &self.cluster_buffers {
|
|
|
|
|
queue.write_buffer(&buffers.light_indices, 0, bytemuck::cast_slice(&assignment.cluster_light_indices));
|
|
|
|
|
|
|
|
|
|
let offset_pairs: Vec<[u32; 2]> = assignment
|
|
|
|
|
.cluster_offsets
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|&(o, c)| [o, c])
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
queue.write_buffer(&buffers.offsets, 0, bytemuck::cast_slice(&offset_pairs));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn compute_cluster_assignments(
|
|
|
|
|
&self,
|
|
|
|
|
view_matrix: Matrix4<f32>,
|
|
|
|
|
_projection_matrix: Matrix4<f32>,
|
|
|
|
|
_screen_width: f32,
|
|
|
|
|
_screen_height: f32,
|
|
|
|
|
) -> ClusterAssignment {
|
|
|
|
|
let mut cluster_light_lists = vec![Vec::new(); TOTAL_CLUSTERS];
|
|
|
|
|
|
|
|
|
|
let log_near = 0.1f32.log2();
|
|
|
|
|
let log_far = 1000.0f32.log2();
|
|
|
|
|
let log_range = log_far - log_near;
|
|
|
|
|
|
|
|
|
|
for (i, light) in self.lights.iter().enumerate() {
|
|
|
|
|
let pos_view = view_matrix.transform_point(Point3::from_vec(light.position));
|
|
|
|
|
let radius = light.range;
|
|
|
|
|
|
|
|
|
|
let z_bounds = [
|
|
|
|
|
(-pos_view.z - radius).max(0.1).log2(),
|
|
|
|
|
(-pos_view.z + radius).max(0.1).log2(),
|
|
|
|
|
];
|
|
|
|
|
let z_start = ((z_bounds[0].min(z_bounds[1]) - log_near) / log_range * CLUSTER_COUNT_Z as f32).floor() as usize;
|
|
|
|
|
let z_end = ((z_bounds[0].max(z_bounds[1]) - log_near) / log_range * CLUSTER_COUNT_Z as f32).ceil() as usize;
|
|
|
|
|
|
|
|
|
|
for z in z_start.min(CLUSTER_COUNT_Z)..z_end.min(CLUSTER_COUNT_Z) {
|
|
|
|
|
for y in 0..CLUSTER_COUNT_Y {
|
|
|
|
|
for x in 0..CLUSTER_COUNT_X {
|
|
|
|
|
let cluster = x + y * CLUSTER_COUNT_X + z * CLUSTER_COUNT_X * CLUSTER_COUNT_Y;
|
|
|
|
|
|
|
|
|
|
if cluster_light_lists[cluster].len() < MAX_LIGHTS_PER_CLUSTER {
|
|
|
|
|
cluster_light_lists[cluster].push(i as u32);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut cluster_light_indices = Vec::new();
|
|
|
|
|
let mut cluster_offsets = Vec::with_capacity(TOTAL_CLUSTERS);
|
|
|
|
|
|
|
|
|
|
for lights in cluster_light_lists {
|
|
|
|
|
let offset = cluster_light_indices.len() as u32;
|
|
|
|
|
let count = lights.len() as u32;
|
|
|
|
|
cluster_light_indices.extend(lights);
|
|
|
|
|
cluster_offsets.push((offset, count));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ClusterAssignment {
|
|
|
|
|
cluster_light_indices,
|
|
|
|
|
cluster_offsets,
|
|
|
|
|
}
|
2025-05-06 17:12:22 +02:00
|
|
|
}
|
2025-05-07 19:45:35 +02:00
|
|
|
}
|