use bytemuck::{Pod, Zeroable};
use crate::{GpuRenderer, HeadlessSceneRenderer, SceneDdaPerGridCamera};
pub const MAX_POINT_LIGHTS: usize = 32;
pub const MAX_SHADOW_CASTERS: usize = 4;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct GpuLight {
pub position: [f32; 3],
pub radius: f32,
pub color: [f32; 3],
pub intensity: f32,
pub casts_shadow: bool,
pub spot_dir: [f32; 3],
pub cos_inner: f32,
pub cos_outer: f32,
}
#[derive(Clone, Default, PartialEq)]
pub struct SceneLights {
pub enabled: bool,
pub grid_sun_dirs: Vec<[f32; 3]>,
pub sun_color: [f32; 3],
pub sun_intensity: f32,
pub sun_casts_shadow: bool,
pub grid_point_lights: Vec<Vec<GpuLight>>,
pub ambient: [f32; 3],
pub shadow_strength: f32,
pub shadow_bias: f32,
pub shadow_max_dist: f32,
pub shadow_max_steps: u32,
pub world_sun_dir: [f32; 3],
pub world_points: Vec<GpuLight>,
pub style_bands: u32,
pub shadow_tint: [f32; 3],
}
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
pub(crate) struct GpuPointLight {
pub(crate) pos: [f32; 3],
pub(crate) radius: f32,
pub(crate) color: [f32; 3],
pub(crate) intensity: f32,
pub(crate) spot_dir: [f32; 3],
pub(crate) cos_outer: f32,
pub(crate) cos_inner: f32,
pub(crate) casts_shadow: u32,
pub(crate) _pad: [u32; 2],
}
const _: () = assert!(core::mem::size_of::<GpuPointLight>() == 64);
pub(crate) fn upload_grid_point_lights(
device: &wgpu::Device,
lights: &[GpuPointLight],
) -> wgpu::Buffer {
use wgpu::util::DeviceExt;
let one = [GpuPointLight::zeroed()];
let src: &[GpuPointLight] = if lights.is_empty() { &one } else { lights };
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("roxlap-gpu scene_dda.grid_point_lights"),
contents: bytemuck::cast_slice(src),
usage: wgpu::BufferUsages::STORAGE,
})
}
pub(crate) fn inject_grid_sun_dirs(lights: &SceneLights, cam_vec: &mut [SceneDdaPerGridCamera]) {
if lights.grid_sun_dirs.is_empty() {
return;
}
for (g, cam) in cam_vec.iter_mut().enumerate() {
let d = lights.grid_sun_dirs.get(g).copied().unwrap_or([0.0; 3]);
cam.sun_dir = [d[0], d[1], d[2], 0.0];
}
}
pub(crate) fn pack_scene_lights(
lights: &SceneLights,
grid_count: usize,
) -> (Vec<GpuPointLight>, u32, u32) {
let sun_enabled = !lights.grid_sun_dirs.is_empty();
let mut point_count = lights
.grid_point_lights
.first()
.map_or(0, std::vec::Vec::len);
if point_count > MAX_POINT_LIGHTS {
eprintln!(
"roxlap-gpu: {point_count} point lights > MAX_POINT_LIGHTS ({MAX_POINT_LIGHTS}); dropping the excess"
);
point_count = MAX_POINT_LIGHTS;
}
let mut budget = MAX_SHADOW_CASTERS;
if sun_enabled && lights.sun_casts_shadow {
budget = budget.saturating_sub(1);
}
let mut allow_shadow = vec![false; point_count];
let mut demoted = 0usize;
if let Some(rep) = lights.grid_point_lights.first() {
for (i, slot) in allow_shadow.iter_mut().enumerate() {
if rep.get(i).is_some_and(|l| l.casts_shadow) {
if budget > 0 {
*slot = true;
budget -= 1;
} else {
demoted += 1;
}
}
}
}
if demoted > 0 {
eprintln!(
"roxlap-gpu: {demoted} shadow-casting point lights > MAX_SHADOW_CASTERS ({MAX_SHADOW_CASTERS}); demoting the excess to shadowless"
);
}
let mut packed: Vec<GpuPointLight> = Vec::with_capacity(grid_count * point_count);
for g in 0..grid_count {
let row = lights.grid_point_lights.get(g);
for (i, &allow) in allow_shadow.iter().enumerate() {
let p = row.and_then(|r| r.get(i));
packed.push(p.map_or(GpuPointLight::zeroed(), |l| GpuPointLight {
pos: l.position,
radius: l.radius,
color: l.color,
intensity: l.intensity,
spot_dir: l.spot_dir,
cos_outer: l.cos_outer,
cos_inner: l.cos_inner,
casts_shadow: u32::from(l.casts_shadow && allow),
_pad: [0; 2],
}));
}
}
let sun_flags = u32::from(sun_enabled)
| (u32::from(sun_enabled && lights.sun_casts_shadow) << 1)
| (u32::from(lights.enabled) << 2);
(packed, sun_flags, point_count as u32)
}
impl GpuRenderer {
pub fn set_scene_lights(&mut self, lights: SceneLights) {
if self.scene_lights != lights {
self.scene_lights = lights;
self.dirty.mark_lights_changed();
}
}
}
impl HeadlessSceneRenderer {
pub fn set_scene_lights(&mut self, lights: SceneLights) {
self.lights = lights;
}
}