use crate::core::World;
use crate::math::{Vec3, Vec4};
use crate::renderer::components::{DirectionalLight, LightRole, PointLight, SpotLight};
use crate::renderer::gpu_types::LightData;
use crate::renderer::MAX_LIGHTS;
use gizmo_physics_core::components::{GlobalTransform, Transform};
pub struct SceneLights {
pub lights: [LightData; MAX_LIGHTS],
pub num_lights: u32,
pub sun_dir: Vec3,
pub sun_col: Vec4,
pub has_sun: bool,
pub shadow_point_index: i32,
}
pub fn collect_scene_lights(world: &World) -> SceneLights {
let globals = world.borrow::<GlobalTransform>();
let locals = world.borrow::<Transform>();
let world_tf = |e| {
globals
.get(e)
.map(|g| {
let (_, rot, pos) = g.matrix.to_scale_rotation_translation();
(pos, rot)
})
.or_else(|| locals.get(e).map(|t| (t.position, t.rotation)))
};
let mut lights = [LightData::default(); MAX_LIGHTS];
let mut num_lights = 0usize;
let mut shadow_point_index: i32 = -1;
if let Some(q) = world.query::<&PointLight>() {
for (e, light) in q.iter() {
if num_lights >= MAX_LIGHTS {
break;
}
let Some((pos, _)) = world_tf(e) else { continue };
if shadow_point_index < 0 {
shadow_point_index = num_lights as i32;
}
lights[num_lights] = LightData {
position: [pos.x, pos.y, pos.z, light.intensity],
color: [light.color.x, light.color.y, light.color.z, light.radius],
direction: [0.0, -1.0, 0.0, 0.0],
params: [0.0, 0.0, 0.0, 0.0], };
num_lights += 1;
}
}
if let Some(q) = world.query::<&SpotLight>() {
for (e, light) in q.iter() {
if num_lights >= MAX_LIGHTS {
break;
}
let Some((pos, rot)) = world_tf(e) else { continue };
let dir = rot.mul_vec3(Vec3::new(0.0, 0.0, -1.0)).normalize();
lights[num_lights] = LightData {
position: [pos.x, pos.y, pos.z, light.intensity],
color: [light.color.x, light.color.y, light.color.z, light.radius],
direction: [dir.x, dir.y, dir.z, light.inner_angle.cos()],
params: [light.outer_angle.cos(), 1.0, 0.0, 0.0], };
num_lights += 1;
}
}
let mut sun_dir = Vec3::new(0.0, -1.0, 0.0);
let mut sun_col = Vec4::new(0.0, 0.0, 0.0, 0.0); let mut has_sun = false;
if let Some(q) = world.query::<&DirectionalLight>() {
for (e, light) in q.iter() {
if light.role == LightRole::Sun {
if let Some((_, rot)) = world_tf(e) {
sun_dir = rot.mul_vec3(Vec3::new(0.0, 0.0, -1.0)).normalize();
sun_col = Vec4::new(light.color.x, light.color.y, light.color.z, light.intensity);
has_sun = true;
}
break; }
}
}
SceneLights {
lights,
num_lights: num_lights as u32,
sun_dir,
sun_col,
has_sun,
shadow_point_index,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ShadowCaster {
SunOnly,
SunOrFirstLight,
}
pub struct SceneSetup {
pub frame: crate::renderer::SceneFrame,
pub cascade_view_projs: [gizmo_math::Mat4; 4],
pub lights: SceneLights,
}
pub struct SceneSetupInputs {
pub camera: crate::renderer::CameraFrame,
pub aspect: f32,
pub cam_fov: f32,
pub shadow_caster: ShadowCaster,
pub environment: crate::renderer::EnvironmentFrame,
pub point_shadows_enabled: bool,
pub elapsed_time: f32,
}
pub fn collect_scene_setup(world: &World, inputs: &SceneSetupInputs) -> SceneSetup {
use gizmo_math::Mat4;
let lights = collect_scene_lights(world);
let shadow_dir = match inputs.shadow_caster {
ShadowCaster::SunOnly => Some(lights.sun_dir),
ShadowCaster::SunOrFirstLight => {
if lights.has_sun {
Some(lights.sun_dir)
} else if lights.num_lights > 0 {
let p = lights.lights[0].position;
Some((Vec3::ZERO - Vec3::new(p[0], p[1], p[2])).normalize())
} else {
None
}
}
};
let cascades = crate::renderer::compute_directional_cascades(
inputs.camera.position,
inputs.camera.forward,
inputs.aspect,
inputs.cam_fov,
inputs.camera.near,
inputs.camera.far,
shadow_dir.unwrap_or(Vec3::new(0.0, -1.0, 0.0)),
);
let cascade_view_projs =
if shadow_dir.is_some() { cascades.view_projs } else { [Mat4::IDENTITY; 4] };
SceneSetup {
frame: crate::renderer::SceneFrame {
camera: inputs.camera,
sun: crate::renderer::SunFrame {
direction: lights.sun_dir,
color: [
lights.sun_col.x,
lights.sun_col.y,
lights.sun_col.z,
lights.sun_col.w,
],
present: lights.has_sun,
},
lights: lights.lights,
num_lights: lights.num_lights,
shadows: crate::renderer::ShadowFrame {
cascade_view_projs,
cascade_splits: cascades.splits,
point_caster: u32::try_from(lights.shadow_point_index).ok(),
point_shadows_enabled: inputs.point_shadows_enabled,
},
environment: inputs.environment,
elapsed_time: inputs.elapsed_time,
},
cascade_view_projs,
lights,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::World;
use crate::renderer::components::{PointLight, SpotLight};
use gizmo_physics_core::components::GlobalTransform;
#[test]
fn spotlight_cutoffs_are_stored_as_cosines() {
let mut world = World::new();
let e = world.spawn();
world.add_component(e, GlobalTransform::default());
world.add_component(e, SpotLight::new(Vec3::ONE, 10.0, 30.0, 0.4, 0.6));
let l = collect_scene_lights(&world);
assert_eq!(l.num_lights, 1);
let spot = l.lights[0];
assert_eq!(spot.params[1], 1.0, "params.y == 1 marks a spot light");
assert!(
(spot.direction[3] - 0.4_f32.cos()).abs() < 1e-5,
"inner cutoff must be cos(inner_angle), got {}",
spot.direction[3]
);
assert!(
(spot.params[0] - 0.6_f32.cos()).abs() < 1e-5,
"outer cutoff must be cos(outer_angle), got {}",
spot.params[0]
);
assert!(spot.direction[3] > spot.params[0]);
}
#[test]
fn point_before_spot_and_transform_fallback() {
let mut world = World::new();
let p = world.spawn();
world.add_component(p, GlobalTransform::default());
world.add_component(p, PointLight::new(Vec3::ONE, 5.0, 12.0));
let s = world.spawn();
world.add_component(s, Transform::new(Vec3::new(1.0, 2.0, 3.0)));
world.add_component(s, SpotLight::new(Vec3::ONE, 7.0, 20.0, 0.3, 0.5));
let l = collect_scene_lights(&world);
assert_eq!(l.num_lights, 2);
assert_eq!(l.lights[0].params[1], 0.0, "point light packed first");
assert_eq!(l.lights[1].params[1], 1.0, "spot light packed second");
assert_eq!(l.lights[1].position, [1.0, 2.0, 3.0, 7.0]);
assert_eq!(l.shadow_point_index, 0, "first point light is the shadow caster");
}
#[test]
fn no_point_light_has_no_shadow_caster() {
let mut world = World::new();
let s = world.spawn();
world.add_component(s, GlobalTransform::default());
world.add_component(s, SpotLight::new(Vec3::ONE, 7.0, 20.0, 0.3, 0.5));
let l = collect_scene_lights(&world);
assert_eq!(l.num_lights, 1);
assert_eq!(l.shadow_point_index, -1, "no point light → no point-shadow caster");
}
}