proof_engine/tiled_lighting/
mod.rs1use glam::{Vec3, Vec4, Mat4};
12
13#[derive(Debug, Clone, Copy)]
15pub struct TiledLight {
16 pub position: Vec3,
17 pub radius: f32,
18 pub color: Vec3,
19 pub intensity: f32,
20 pub light_type: TiledLightType,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq)]
24pub enum TiledLightType {
25 Point,
26 Spot { direction: Vec3, angle: f32 },
27}
28
29#[derive(Debug, Clone)]
31pub struct Tile {
32 pub light_indices: Vec<u16>,
33}
34
35#[derive(Debug, Clone)]
37pub struct TiledConfig {
38 pub tile_size: u32,
39 pub max_lights_per_tile: u32,
40 pub screen_width: u32,
41 pub screen_height: u32,
42}
43
44impl Default for TiledConfig {
45 fn default() -> Self {
46 Self { tile_size: 16, max_lights_per_tile: 64, screen_width: 1920, screen_height: 1080 }
47 }
48}
49
50pub struct TiledLighting {
52 pub config: TiledConfig,
53 pub lights: Vec<TiledLight>,
54 pub tiles: Vec<Tile>,
55 pub tiles_x: u32,
56 pub tiles_y: u32,
57}
58
59impl TiledLighting {
60 pub fn new(config: TiledConfig) -> Self {
61 let tiles_x = (config.screen_width + config.tile_size - 1) / config.tile_size;
62 let tiles_y = (config.screen_height + config.tile_size - 1) / config.tile_size;
63 let tile_count = (tiles_x * tiles_y) as usize;
64 Self {
65 tiles: vec![Tile { light_indices: Vec::new() }; tile_count],
66 tiles_x, tiles_y, lights: Vec::new(), config,
67 }
68 }
69
70 pub fn set_lights(&mut self, lights: Vec<TiledLight>) {
72 self.lights = lights;
73 }
74
75 pub fn cull(&mut self, view: &Mat4, proj: &Mat4) {
77 for tile in &mut self.tiles { tile.light_indices.clear(); }
78 let vp = *proj * *view;
79
80 for (li, light) in self.lights.iter().enumerate() {
81 let center_clip = vp * Vec4::new(light.position.x, light.position.y, light.position.z, 1.0);
83 if center_clip.w <= 0.0 { continue; } let ndc_x = center_clip.x / center_clip.w;
85 let ndc_y = center_clip.y / center_clip.w;
86
87 let screen_radius = light.radius / center_clip.w * self.config.screen_width as f32 * 0.5;
89 let pixel_x = (ndc_x * 0.5 + 0.5) * self.config.screen_width as f32;
90 let pixel_y = (ndc_y * 0.5 + 0.5) * self.config.screen_height as f32;
91
92 let min_tx = ((pixel_x - screen_radius) / self.config.tile_size as f32).floor().max(0.0) as u32;
94 let max_tx = ((pixel_x + screen_radius) / self.config.tile_size as f32).ceil().min(self.tiles_x as f32) as u32;
95 let min_ty = ((pixel_y - screen_radius) / self.config.tile_size as f32).floor().max(0.0) as u32;
96 let max_ty = ((pixel_y + screen_radius) / self.config.tile_size as f32).ceil().min(self.tiles_y as f32) as u32;
97
98 for ty in min_ty..max_ty {
99 for tx in min_tx..max_tx {
100 let idx = (ty * self.tiles_x + tx) as usize;
101 if idx < self.tiles.len() && self.tiles[idx].light_indices.len() < self.config.max_lights_per_tile as usize {
102 self.tiles[idx].light_indices.push(li as u16);
103 }
104 }
105 }
106 }
107 }
108
109 pub fn lights_for_tile(&self, tx: u32, ty: u32) -> &[u16] {
111 let idx = (ty * self.tiles_x + tx) as usize;
112 if idx < self.tiles.len() { &self.tiles[idx].light_indices } else { &[] }
113 }
114
115 pub fn lights_at_pixel(&self, px: u32, py: u32) -> &[u16] {
117 self.lights_for_tile(px / self.config.tile_size, py / self.config.tile_size)
118 }
119
120 pub fn total_assignments(&self) -> usize {
122 self.tiles.iter().map(|t| t.light_indices.len()).sum()
123 }
124
125 pub fn avg_lights_per_tile(&self) -> f32 {
127 self.total_assignments() as f32 / self.tiles.len().max(1) as f32
128 }
129
130 pub fn glsl_source() -> &'static str {
132 r#"
133// Tiled lighting: evaluate all lights in a tile for a fragment
134vec3 evaluate_tiled_lights(vec3 world_pos, vec3 normal, vec3 albedo,
135 float roughness, float metallic,
136 sampler2D light_grid, sampler1D light_data,
137 int tile_x, int tile_y) {
138 vec3 result = vec3(0.0);
139 // Read light count and indices from tile grid texture
140 int count = int(texelFetch(light_grid, ivec2(tile_x, tile_y), 0).r);
141 for (int i = 0; i < count && i < 64; i++) {
142 int light_idx = int(texelFetch(light_grid, ivec2(tile_x * 64 + i + 1, tile_y), 0).r);
143 // Fetch light params from 1D texture
144 vec4 pos_radius = texelFetch(light_data, light_idx * 2, 0);
145 vec4 color_intensity = texelFetch(light_data, light_idx * 2 + 1, 0);
146 vec3 light_pos = pos_radius.xyz;
147 float radius = pos_radius.w;
148 vec3 light_color = color_intensity.rgb;
149 float intensity = color_intensity.a;
150
151 vec3 L = light_pos - world_pos;
152 float dist = length(L);
153 if (dist > radius) continue;
154 L /= dist;
155 float attenuation = intensity * max(1.0 - dist / radius, 0.0);
156 attenuation *= attenuation; // quadratic falloff
157 float NdotL = max(dot(normal, L), 0.0);
158 result += albedo * light_color * NdotL * attenuation;
159 }
160 return result;
161}
162 "#
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169
170 #[test]
171 fn test_tile_assignment() {
172 let mut tl = TiledLighting::new(TiledConfig { screen_width: 320, screen_height: 240, ..Default::default() });
173 tl.set_lights(vec![TiledLight {
174 position: Vec3::new(0.0, 0.0, -5.0), radius: 10.0,
175 color: Vec3::ONE, intensity: 1.0, light_type: TiledLightType::Point,
176 }]);
177 let view = Mat4::look_at_rh(Vec3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, -1.0), Vec3::Y);
178 let proj = Mat4::perspective_rh_gl(60.0f32.to_radians(), 320.0/240.0, 0.1, 100.0);
179 tl.cull(&view, &proj);
180 assert!(tl.total_assignments() > 0, "light should be assigned to some tiles");
181 }
182}