1use std::collections::HashMap;
7use std::f32::consts::PI;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub struct LightId(pub u32);
14
15pub const MAX_LIGHTS: usize = 64;
17
18#[derive(Debug, Clone, Copy, PartialEq)]
22pub struct Vec3 {
23 pub x: f32,
24 pub y: f32,
25 pub z: f32,
26}
27
28impl Vec3 {
29 pub const ZERO: Vec3 = Vec3 { x: 0.0, y: 0.0, z: 0.0 };
30 pub const ONE: Vec3 = Vec3 { x: 1.0, y: 1.0, z: 1.0 };
31 pub const UP: Vec3 = Vec3 { x: 0.0, y: 1.0, z: 0.0 };
32 pub const DOWN: Vec3 = Vec3 { x: 0.0, y: -1.0, z: 0.0 };
33 pub const FORWARD: Vec3 = Vec3 { x: 0.0, y: 0.0, z: -1.0 };
34
35 pub const fn new(x: f32, y: f32, z: f32) -> Self {
36 Self { x, y, z }
37 }
38
39 pub fn length(self) -> f32 {
40 (self.x * self.x + self.y * self.y + self.z * self.z).sqrt()
41 }
42
43 pub fn length_squared(self) -> f32 {
44 self.x * self.x + self.y * self.y + self.z * self.z
45 }
46
47 pub fn normalize(self) -> Self {
48 let len = self.length();
49 if len < 1e-10 {
50 return Self::ZERO;
51 }
52 Self {
53 x: self.x / len,
54 y: self.y / len,
55 z: self.z / len,
56 }
57 }
58
59 pub fn dot(self, other: Self) -> f32 {
60 self.x * other.x + self.y * other.y + self.z * other.z
61 }
62
63 pub fn cross(self, other: Self) -> Self {
64 Self {
65 x: self.y * other.z - self.z * other.y,
66 y: self.z * other.x - self.x * other.z,
67 z: self.x * other.y - self.y * other.x,
68 }
69 }
70
71 pub fn lerp(self, other: Self, t: f32) -> Self {
72 Self {
73 x: self.x + (other.x - self.x) * t,
74 y: self.y + (other.y - self.y) * t,
75 z: self.z + (other.z - self.z) * t,
76 }
77 }
78
79 pub fn distance(self, other: Self) -> f32 {
80 let dx = self.x - other.x;
81 let dy = self.y - other.y;
82 let dz = self.z - other.z;
83 (dx * dx + dy * dy + dz * dz).sqrt()
84 }
85
86 pub fn scale(self, s: f32) -> Self {
87 Self {
88 x: self.x * s,
89 y: self.y * s,
90 z: self.z * s,
91 }
92 }
93
94 pub fn add(self, other: Self) -> Self {
95 Self {
96 x: self.x + other.x,
97 y: self.y + other.y,
98 z: self.z + other.z,
99 }
100 }
101
102 pub fn sub(self, other: Self) -> Self {
103 Self {
104 x: self.x - other.x,
105 y: self.y - other.y,
106 z: self.z - other.z,
107 }
108 }
109
110 pub fn min_components(self, other: Self) -> Self {
111 Self {
112 x: self.x.min(other.x),
113 y: self.y.min(other.y),
114 z: self.z.min(other.z),
115 }
116 }
117
118 pub fn max_components(self, other: Self) -> Self {
119 Self {
120 x: self.x.max(other.x),
121 y: self.y.max(other.y),
122 z: self.z.max(other.z),
123 }
124 }
125
126 pub fn abs(self) -> Self {
127 Self {
128 x: self.x.abs(),
129 y: self.y.abs(),
130 z: self.z.abs(),
131 }
132 }
133
134 pub fn component_mul(self, other: Self) -> Self {
135 Self {
136 x: self.x * other.x,
137 y: self.y * other.y,
138 z: self.z * other.z,
139 }
140 }
141}
142
143impl Default for Vec3 {
144 fn default() -> Self {
145 Self::ZERO
146 }
147}
148
149impl std::ops::Add for Vec3 {
150 type Output = Self;
151 fn add(self, rhs: Self) -> Self {
152 Self::new(self.x + rhs.x, self.y + rhs.y, self.z + rhs.z)
153 }
154}
155
156impl std::ops::Sub for Vec3 {
157 type Output = Self;
158 fn sub(self, rhs: Self) -> Self {
159 Self::new(self.x - rhs.x, self.y - rhs.y, self.z - rhs.z)
160 }
161}
162
163impl std::ops::Mul<f32> for Vec3 {
164 type Output = Self;
165 fn mul(self, rhs: f32) -> Self {
166 Self::new(self.x * rhs, self.y * rhs, self.z * rhs)
167 }
168}
169
170impl std::ops::Neg for Vec3 {
171 type Output = Self;
172 fn neg(self) -> Self {
173 Self::new(-self.x, -self.y, -self.z)
174 }
175}
176
177#[derive(Debug, Clone, Copy, PartialEq)]
179pub struct Mat4 {
180 pub cols: [[f32; 4]; 4],
181}
182
183impl Mat4 {
184 pub const IDENTITY: Mat4 = Mat4 {
185 cols: [
186 [1.0, 0.0, 0.0, 0.0],
187 [0.0, 1.0, 0.0, 0.0],
188 [0.0, 0.0, 1.0, 0.0],
189 [0.0, 0.0, 0.0, 1.0],
190 ],
191 };
192
193 pub fn look_at(eye: Vec3, target: Vec3, up: Vec3) -> Self {
194 let f = (target - eye).normalize();
195 let s = f.cross(up).normalize();
196 let u = s.cross(f);
197
198 let mut m = Self::IDENTITY;
199 m.cols[0][0] = s.x;
200 m.cols[1][0] = s.y;
201 m.cols[2][0] = s.z;
202 m.cols[0][1] = u.x;
203 m.cols[1][1] = u.y;
204 m.cols[2][1] = u.z;
205 m.cols[0][2] = -f.x;
206 m.cols[1][2] = -f.y;
207 m.cols[2][2] = -f.z;
208 m.cols[3][0] = -s.dot(eye);
209 m.cols[3][1] = -u.dot(eye);
210 m.cols[3][2] = f.dot(eye);
211 m
212 }
213
214 pub fn orthographic(left: f32, right: f32, bottom: f32, top: f32, near: f32, far: f32) -> Self {
215 let mut m = Self::IDENTITY;
216 m.cols[0][0] = 2.0 / (right - left);
217 m.cols[1][1] = 2.0 / (top - bottom);
218 m.cols[2][2] = -2.0 / (far - near);
219 m.cols[3][0] = -(right + left) / (right - left);
220 m.cols[3][1] = -(top + bottom) / (top - bottom);
221 m.cols[3][2] = -(far + near) / (far - near);
222 m
223 }
224
225 pub fn perspective(fov_y: f32, aspect: f32, near: f32, far: f32) -> Self {
226 let f = 1.0 / (fov_y * 0.5).tan();
227 let mut m = Mat4 { cols: [[0.0; 4]; 4] };
228 m.cols[0][0] = f / aspect;
229 m.cols[1][1] = f;
230 m.cols[2][2] = (far + near) / (near - far);
231 m.cols[2][3] = -1.0;
232 m.cols[3][2] = (2.0 * far * near) / (near - far);
233 m
234 }
235
236 pub fn mul_mat4(self, rhs: Self) -> Self {
237 let mut result = Mat4 { cols: [[0.0; 4]; 4] };
238 for c in 0..4 {
239 for r in 0..4 {
240 let mut sum = 0.0f32;
241 for k in 0..4 {
242 sum += self.cols[k][r] * rhs.cols[c][k];
243 }
244 result.cols[c][r] = sum;
245 }
246 }
247 result
248 }
249
250 pub fn transform_point(self, p: Vec3) -> Vec3 {
251 let w = self.cols[0][3] * p.x + self.cols[1][3] * p.y + self.cols[2][3] * p.z + self.cols[3][3];
252 let inv_w = if w.abs() > 1e-10 { 1.0 / w } else { 1.0 };
253 Vec3 {
254 x: (self.cols[0][0] * p.x + self.cols[1][0] * p.y + self.cols[2][0] * p.z + self.cols[3][0]) * inv_w,
255 y: (self.cols[0][1] * p.x + self.cols[1][1] * p.y + self.cols[2][1] * p.z + self.cols[3][1]) * inv_w,
256 z: (self.cols[0][2] * p.x + self.cols[1][2] * p.y + self.cols[2][2] * p.z + self.cols[3][2]) * inv_w,
257 }
258 }
259}
260
261#[derive(Debug, Clone, Copy, PartialEq)]
265pub struct Color {
266 pub r: f32,
267 pub g: f32,
268 pub b: f32,
269}
270
271impl Color {
272 pub const WHITE: Color = Color { r: 1.0, g: 1.0, b: 1.0 };
273 pub const BLACK: Color = Color { r: 0.0, g: 0.0, b: 0.0 };
274 pub const RED: Color = Color { r: 1.0, g: 0.0, b: 0.0 };
275 pub const GREEN: Color = Color { r: 0.0, g: 1.0, b: 0.0 };
276 pub const BLUE: Color = Color { r: 0.0, g: 0.0, b: 1.0 };
277 pub const WARM_WHITE: Color = Color { r: 1.0, g: 0.95, b: 0.85 };
278 pub const COOL_WHITE: Color = Color { r: 0.85, g: 0.92, b: 1.0 };
279
280 pub const fn new(r: f32, g: f32, b: f32) -> Self {
281 Self { r, g, b }
282 }
283
284 pub fn from_temperature(kelvin: f32) -> Self {
285 let temp = kelvin / 100.0;
286 let r;
287 let g;
288 let b;
289
290 if temp <= 66.0 {
291 r = 1.0;
292 g = (99.4708025861 * temp.ln() - 161.1195681661).max(0.0).min(255.0) / 255.0;
293 } else {
294 r = (329.698727446 * (temp - 60.0).powf(-0.1332047592)).max(0.0).min(255.0) / 255.0;
295 g = (288.1221695283 * (temp - 60.0).powf(-0.0755148492)).max(0.0).min(255.0) / 255.0;
296 }
297
298 if temp >= 66.0 {
299 b = 1.0;
300 } else if temp <= 19.0 {
301 b = 0.0;
302 } else {
303 b = (138.5177312231 * (temp - 10.0).ln() - 305.0447927307).max(0.0).min(255.0) / 255.0;
304 }
305
306 Self { r, g, b }
307 }
308
309 pub fn luminance(self) -> f32 {
310 0.2126 * self.r + 0.7152 * self.g + 0.0722 * self.b
311 }
312
313 pub fn lerp(self, other: Self, t: f32) -> Self {
314 Self {
315 r: self.r + (other.r - self.r) * t,
316 g: self.g + (other.g - self.g) * t,
317 b: self.b + (other.b - self.b) * t,
318 }
319 }
320
321 pub fn scale(self, s: f32) -> Self {
322 Self {
323 r: self.r * s,
324 g: self.g * s,
325 b: self.b * s,
326 }
327 }
328
329 pub fn to_vec3(self) -> Vec3 {
330 Vec3::new(self.r, self.g, self.b)
331 }
332
333 pub fn from_hsv(h: f32, s: f32, v: f32) -> Self {
334 let h = ((h % 360.0) + 360.0) % 360.0;
335 let c = v * s;
336 let x = c * (1.0 - ((h / 60.0) % 2.0 - 1.0).abs());
337 let m = v - c;
338
339 let (r, g, b) = if h < 60.0 {
340 (c, x, 0.0)
341 } else if h < 120.0 {
342 (x, c, 0.0)
343 } else if h < 180.0 {
344 (0.0, c, x)
345 } else if h < 240.0 {
346 (0.0, x, c)
347 } else if h < 300.0 {
348 (x, 0.0, c)
349 } else {
350 (c, 0.0, x)
351 };
352
353 Self { r: r + m, g: g + m, b: b + m }
354 }
355}
356
357impl Default for Color {
358 fn default() -> Self {
359 Self::WHITE
360 }
361}
362
363#[derive(Debug, Clone)]
367pub enum AttenuationModel {
368 None,
370 Linear,
372 InverseSquare,
374 Quadratic {
376 constant: f32,
377 linear: f32,
378 quadratic: f32,
379 },
380 SmoothUE4,
382 CustomCurve {
384 samples: Vec<f32>,
386 },
387}
388
389impl Default for AttenuationModel {
390 fn default() -> Self {
391 Self::InverseSquare
392 }
393}
394
395impl AttenuationModel {
396 pub fn evaluate(&self, distance: f32, radius: f32) -> f32 {
398 if radius <= 0.0 || distance >= radius {
399 return 0.0;
400 }
401 let d = distance.max(0.0);
402 let ratio = d / radius;
403
404 match self {
405 Self::None => 1.0,
406 Self::Linear => (1.0 - ratio).max(0.0),
407 Self::InverseSquare => {
408 let falloff = 1.0 / (1.0 + d * d);
409 let window = (1.0 - ratio * ratio).max(0.0);
411 falloff * window
412 }
413 Self::Quadratic { constant, linear, quadratic } => {
414 let denom = constant + linear * d + quadratic * d * d;
415 if denom <= 0.0 {
416 0.0
417 } else {
418 (1.0 / denom).min(1.0) * (1.0 - ratio).max(0.0)
419 }
420 }
421 Self::SmoothUE4 => {
422 let r4 = ratio * ratio * ratio * ratio;
423 let v = (1.0 - r4).max(0.0);
424 v * v
425 }
426 Self::CustomCurve { samples } => {
427 if samples.is_empty() {
428 return 0.0;
429 }
430 let t = ratio * (samples.len() - 1) as f32;
431 let idx = (t as usize).min(samples.len() - 2);
432 let frac = t - idx as f32;
433 let a = samples[idx];
434 let b = samples[(idx + 1).min(samples.len() - 1)];
435 a + (b - a) * frac
436 }
437 }
438 }
439}
440
441#[derive(Debug, Clone)]
445pub struct CascadeShadowParams {
446 pub cascade_count: u32,
448 pub split_distances: [f32; 5],
450 pub resolution: u32,
452 pub blend_band: f32,
454 pub stabilize: bool,
456 pub split_lambda: f32,
458}
459
460impl Default for CascadeShadowParams {
461 fn default() -> Self {
462 Self {
463 cascade_count: 4,
464 split_distances: [0.1, 10.0, 30.0, 80.0, 200.0],
465 resolution: 2048,
466 blend_band: 0.1,
467 stabilize: true,
468 split_lambda: 0.75,
469 }
470 }
471}
472
473impl CascadeShadowParams {
474 pub fn compute_splits(&mut self, near: f32, far: f32) {
476 let count = self.cascade_count.min(4) as usize;
477 self.split_distances[0] = near;
478 for i in 1..=count {
479 let t = i as f32 / count as f32;
480 let log_split = near * (far / near).powf(t);
481 let lin_split = near + (far - near) * t;
482 self.split_distances[i] = self.split_lambda * log_split + (1.0 - self.split_lambda) * lin_split;
483 }
484 }
485
486 pub fn cascade_view_projection(
489 &self,
490 light_dir: Vec3,
491 frustum_corners: &[Vec3; 8],
492 ) -> Mat4 {
493 let mut center = Vec3::ZERO;
495 for corner in frustum_corners {
496 center = center + *corner;
497 }
498 center = center * (1.0 / 8.0);
499
500 let mut radius = 0.0f32;
502 for corner in frustum_corners {
503 let d = corner.distance(center);
504 if d > radius {
505 radius = d;
506 }
507 }
508 radius = (radius * 16.0).ceil() / 16.0;
509
510 let max_extents = Vec3::new(radius, radius, radius);
511 let min_extents = -max_extents;
512
513 let light_pos = center - light_dir.normalize() * radius;
514 let view = Mat4::look_at(light_pos, center, Vec3::UP);
515 let proj = Mat4::orthographic(
516 min_extents.x,
517 max_extents.x,
518 min_extents.y,
519 max_extents.y,
520 0.0,
521 max_extents.z - min_extents.z,
522 );
523
524 proj.mul_mat4(view)
525 }
526}
527
528#[derive(Debug, Clone)]
532pub struct PointLight {
533 pub position: Vec3,
534 pub color: Color,
535 pub intensity: f32,
536 pub radius: f32,
537 pub attenuation: AttenuationModel,
538 pub cast_shadows: bool,
539 pub shadow_bias: f32,
540 pub enabled: bool,
541 pub shadow_map_index: Option<u32>,
543}
544
545impl Default for PointLight {
546 fn default() -> Self {
547 Self {
548 position: Vec3::ZERO,
549 color: Color::WHITE,
550 intensity: 1.0,
551 radius: 10.0,
552 attenuation: AttenuationModel::InverseSquare,
553 cast_shadows: true,
554 shadow_bias: 0.005,
555 enabled: true,
556 shadow_map_index: None,
557 }
558 }
559}
560
561impl PointLight {
562 pub fn new(position: Vec3, color: Color, intensity: f32, radius: f32) -> Self {
563 Self {
564 position,
565 color,
566 intensity,
567 radius,
568 ..Default::default()
569 }
570 }
571
572 pub fn with_attenuation(mut self, model: AttenuationModel) -> Self {
573 self.attenuation = model;
574 self
575 }
576
577 pub fn irradiance_at(&self, point: Vec3) -> Color {
579 if !self.enabled {
580 return Color::BLACK;
581 }
582 let dist = self.position.distance(point);
583 let atten = self.attenuation.evaluate(dist, self.radius);
584 self.color.scale(self.intensity * atten)
585 }
586
587 pub fn affects_point(&self, point: Vec3) -> bool {
589 self.enabled && self.position.distance(point) < self.radius
590 }
591
592 pub fn bounding_box(&self) -> (Vec3, Vec3) {
594 let r = Vec3::new(self.radius, self.radius, self.radius);
595 (self.position - r, self.position + r)
596 }
597}
598
599#[derive(Debug, Clone)]
603pub struct SpotLight {
604 pub position: Vec3,
605 pub direction: Vec3,
606 pub color: Color,
607 pub intensity: f32,
608 pub radius: f32,
609 pub inner_cone_angle: f32,
610 pub outer_cone_angle: f32,
611 pub attenuation: AttenuationModel,
612 pub cast_shadows: bool,
613 pub shadow_bias: f32,
614 pub enabled: bool,
615 pub cookie_texture_index: Option<u32>,
617 pub shadow_map_index: Option<u32>,
618}
619
620impl Default for SpotLight {
621 fn default() -> Self {
622 Self {
623 position: Vec3::ZERO,
624 direction: Vec3::FORWARD,
625 color: Color::WHITE,
626 intensity: 1.0,
627 radius: 15.0,
628 inner_cone_angle: 20.0_f32.to_radians(),
629 outer_cone_angle: 35.0_f32.to_radians(),
630 attenuation: AttenuationModel::InverseSquare,
631 cast_shadows: true,
632 shadow_bias: 0.005,
633 enabled: true,
634 cookie_texture_index: None,
635 shadow_map_index: None,
636 }
637 }
638}
639
640impl SpotLight {
641 pub fn new(position: Vec3, direction: Vec3, color: Color, intensity: f32) -> Self {
642 Self {
643 position,
644 direction: direction.normalize(),
645 color,
646 intensity,
647 ..Default::default()
648 }
649 }
650
651 pub fn with_cone_angles(mut self, inner_deg: f32, outer_deg: f32) -> Self {
652 self.inner_cone_angle = inner_deg.to_radians();
653 self.outer_cone_angle = outer_deg.to_radians();
654 self
655 }
656
657 pub fn with_cookie(mut self, index: u32) -> Self {
658 self.cookie_texture_index = Some(index);
659 self
660 }
661
662 fn cone_attenuation(&self, cos_angle: f32) -> f32 {
664 let cos_outer = self.outer_cone_angle.cos();
665 let cos_inner = self.inner_cone_angle.cos();
666 if cos_angle <= cos_outer {
667 return 0.0;
668 }
669 if cos_angle >= cos_inner {
670 return 1.0;
671 }
672 let t = (cos_angle - cos_outer) / (cos_inner - cos_outer);
673 t * t * (3.0 - 2.0 * t)
675 }
676
677 pub fn irradiance_at(&self, point: Vec3) -> Color {
679 if !self.enabled {
680 return Color::BLACK;
681 }
682 let to_point = (point - self.position).normalize();
683 let cos_angle = to_point.dot(self.direction.normalize());
684 let cone = self.cone_attenuation(cos_angle);
685 if cone <= 0.0 {
686 return Color::BLACK;
687 }
688 let dist = self.position.distance(point);
689 let atten = self.attenuation.evaluate(dist, self.radius);
690 self.color.scale(self.intensity * atten * cone)
691 }
692
693 pub fn shadow_view_projection(&self) -> Mat4 {
695 let target = self.position + self.direction.normalize();
696 let up = if self.direction.normalize().dot(Vec3::UP).abs() > 0.99 {
697 Vec3::new(1.0, 0.0, 0.0)
698 } else {
699 Vec3::UP
700 };
701 let view = Mat4::look_at(self.position, target, up);
702 let proj = Mat4::perspective(self.outer_cone_angle * 2.0, 1.0, 0.1, self.radius);
703 proj.mul_mat4(view)
704 }
705
706 pub fn affects_point(&self, point: Vec3) -> bool {
708 if !self.enabled {
709 return false;
710 }
711 let dist = self.position.distance(point);
712 if dist > self.radius {
713 return false;
714 }
715 let to_point = (point - self.position).normalize();
716 let cos_angle = to_point.dot(self.direction.normalize());
717 cos_angle > self.outer_cone_angle.cos()
718 }
719}
720
721#[derive(Debug, Clone)]
725pub struct DirectionalLight {
726 pub direction: Vec3,
727 pub color: Color,
728 pub intensity: f32,
729 pub cast_shadows: bool,
730 pub enabled: bool,
731 pub cascade_params: CascadeShadowParams,
732 pub angular_diameter: f32,
734}
735
736impl Default for DirectionalLight {
737 fn default() -> Self {
738 Self {
739 direction: Vec3::new(0.0, -1.0, -0.5).normalize(),
740 color: Color::WARM_WHITE,
741 intensity: 1.0,
742 cast_shadows: true,
743 enabled: true,
744 cascade_params: CascadeShadowParams::default(),
745 angular_diameter: 0.0093,
746 }
747 }
748}
749
750impl DirectionalLight {
751 pub fn new(direction: Vec3, color: Color, intensity: f32) -> Self {
752 Self {
753 direction: direction.normalize(),
754 color,
755 intensity,
756 ..Default::default()
757 }
758 }
759
760 pub fn irradiance_for_normal(&self, normal: Vec3) -> Color {
762 if !self.enabled {
763 return Color::BLACK;
764 }
765 let n_dot_l = normal.dot(-self.direction.normalize()).max(0.0);
766 self.color.scale(self.intensity * n_dot_l)
767 }
768
769 pub fn cascade_matrices(&self, camera_frustum_corners: &[[Vec3; 8]; 4]) -> [Mat4; 4] {
771 let mut matrices = [Mat4::IDENTITY; 4];
772 let count = self.cascade_params.cascade_count.min(4) as usize;
773 for i in 0..count {
774 matrices[i] = self.cascade_params.cascade_view_projection(
775 self.direction,
776 &camera_frustum_corners[i],
777 );
778 }
779 matrices
780 }
781}
782
783#[derive(Debug, Clone, Copy, PartialEq)]
787pub enum AreaShape {
788 Rectangle { width: f32, height: f32 },
790 Disc { radius: f32 },
792}
793
794impl Default for AreaShape {
795 fn default() -> Self {
796 Self::Rectangle { width: 1.0, height: 1.0 }
797 }
798}
799
800#[derive(Debug, Clone)]
803pub struct AreaLight {
804 pub position: Vec3,
805 pub direction: Vec3,
806 pub up: Vec3,
807 pub color: Color,
808 pub intensity: f32,
809 pub shape: AreaShape,
810 pub enabled: bool,
811 pub two_sided: bool,
812 pub radius: f32,
814}
815
816impl Default for AreaLight {
817 fn default() -> Self {
818 Self {
819 position: Vec3::ZERO,
820 direction: Vec3::FORWARD,
821 up: Vec3::UP,
822 color: Color::WHITE,
823 intensity: 1.0,
824 shape: AreaShape::default(),
825 enabled: true,
826 two_sided: false,
827 radius: 20.0,
828 }
829 }
830}
831
832impl AreaLight {
833 pub fn new_rectangle(position: Vec3, direction: Vec3, width: f32, height: f32, color: Color, intensity: f32) -> Self {
834 Self {
835 position,
836 direction: direction.normalize(),
837 shape: AreaShape::Rectangle { width, height },
838 color,
839 intensity,
840 ..Default::default()
841 }
842 }
843
844 pub fn new_disc(position: Vec3, direction: Vec3, radius: f32, color: Color, intensity: f32) -> Self {
845 Self {
846 position,
847 direction: direction.normalize(),
848 shape: AreaShape::Disc { radius },
849 color,
850 intensity,
851 ..Default::default()
852 }
853 }
854
855 pub fn rect_corners(&self) -> [Vec3; 4] {
857 let right = self.direction.cross(self.up).normalize();
858 let corrected_up = right.cross(self.direction).normalize();
859
860 let (hw, hh) = match self.shape {
861 AreaShape::Rectangle { width, height } => (width * 0.5, height * 0.5),
862 AreaShape::Disc { radius } => (radius, radius),
863 };
864
865 [
866 self.position + right * (-hw) + corrected_up * hh,
867 self.position + right * hw + corrected_up * hh,
868 self.position + right * hw + corrected_up * (-hh),
869 self.position + right * (-hw) + corrected_up * (-hh),
870 ]
871 }
872
873 pub fn irradiance_at(&self, point: Vec3, normal: Vec3) -> Color {
875 if !self.enabled {
876 return Color::BLACK;
877 }
878
879 let dist = self.position.distance(point);
880 if dist > self.radius {
881 return Color::BLACK;
882 }
883
884 let to_point = point - self.position;
886 let plane_dist = to_point.dot(self.direction.normalize());
887
888 if !self.two_sided && plane_dist < 0.0 {
889 return Color::BLACK;
890 }
891
892 let right = self.direction.cross(self.up).normalize();
893 let corrected_up = right.cross(self.direction).normalize();
894
895 let local_x = to_point.dot(right);
897 let local_y = to_point.dot(corrected_up);
898
899 let closest = match self.shape {
901 AreaShape::Rectangle { width, height } => {
902 let cx = local_x.clamp(-width * 0.5, width * 0.5);
903 let cy = local_y.clamp(-height * 0.5, height * 0.5);
904 self.position + right * cx + corrected_up * cy
905 }
906 AreaShape::Disc { radius } => {
907 let r = (local_x * local_x + local_y * local_y).sqrt();
908 if r < 1e-6 {
909 self.position
910 } else {
911 let clamped_r = r.min(radius);
912 let scale = clamped_r / r;
913 self.position + right * (local_x * scale) + corrected_up * (local_y * scale)
914 }
915 }
916 };
917
918 let to_closest = closest - point;
919 let closest_dist = to_closest.length();
920 if closest_dist < 1e-6 {
921 return self.color.scale(self.intensity);
922 }
923
924 let light_dir = to_closest * (1.0 / closest_dist);
925 let n_dot_l = normal.dot(light_dir).max(0.0);
926
927 let area = match self.shape {
929 AreaShape::Rectangle { width, height } => width * height,
930 AreaShape::Disc { radius } => PI * radius * radius,
931 };
932
933 let form_factor = (area * n_dot_l) / (closest_dist * closest_dist + area);
934 let window = (1.0 - (dist / self.radius)).max(0.0);
935
936 self.color.scale(self.intensity * form_factor * window)
937 }
938}
939
940#[derive(Debug, Clone)]
944pub struct EmissiveGlyph {
945 pub position: Vec3,
946 pub color: Color,
947 pub emission_strength: f32,
948 pub radius: f32,
949 pub glyph_character: char,
950 pub enabled: bool,
951 pub emission_threshold: f32,
953}
954
955impl Default for EmissiveGlyph {
956 fn default() -> Self {
957 Self {
958 position: Vec3::ZERO,
959 color: Color::WHITE,
960 emission_strength: 1.0,
961 radius: 5.0,
962 glyph_character: '*',
963 enabled: true,
964 emission_threshold: 0.5,
965 }
966 }
967}
968
969impl EmissiveGlyph {
970 pub fn new(position: Vec3, character: char, color: Color, emission: f32) -> Self {
971 Self {
972 position,
973 color,
974 emission_strength: emission,
975 glyph_character: character,
976 ..Default::default()
977 }
978 }
979
980 pub fn is_active(&self) -> bool {
982 self.enabled && self.emission_strength > self.emission_threshold
983 }
984
985 pub fn effective_intensity(&self) -> f32 {
987 if !self.is_active() {
988 return 0.0;
989 }
990 (self.emission_strength - self.emission_threshold).max(0.0)
991 }
992
993 pub fn irradiance_at(&self, point: Vec3) -> Color {
995 if !self.is_active() {
996 return Color::BLACK;
997 }
998 let dist = self.position.distance(point);
999 if dist > self.radius {
1000 return Color::BLACK;
1001 }
1002 let ratio = dist / self.radius;
1003 let atten = (1.0 - ratio * ratio).max(0.0);
1004 self.color.scale(self.effective_intensity() * atten)
1005 }
1006
1007 pub fn auto_radius(emission_strength: f32) -> f32 {
1009 (emission_strength * 8.0).clamp(1.0, 30.0)
1010 }
1011
1012 pub fn from_glyph_data(character: char, emission: f32, position: Vec3, color: Color, threshold: f32) -> Option<Self> {
1014 if emission <= threshold {
1015 return None;
1016 }
1017 Some(Self {
1018 position,
1019 color,
1020 emission_strength: emission,
1021 radius: Self::auto_radius(emission),
1022 glyph_character: character,
1023 enabled: true,
1024 emission_threshold: threshold,
1025 })
1026 }
1027}
1028
1029#[derive(Debug, Clone)]
1033pub enum AnimationPattern {
1034 Pulse {
1036 min_intensity: f32,
1037 max_intensity: f32,
1038 frequency: f32,
1039 },
1040 Flicker {
1042 min_intensity: f32,
1043 max_intensity: f32,
1044 smoothness: f32,
1046 seed: u32,
1048 },
1049 Strobe {
1051 on_intensity: f32,
1052 off_intensity: f32,
1053 frequency: f32,
1054 duty_cycle: f32,
1055 },
1056 Fade {
1058 from_intensity: f32,
1059 to_intensity: f32,
1060 duration: f32,
1061 },
1062 MathDriven {
1064 a: f32,
1066 b: f32,
1067 c: f32,
1068 d: f32,
1069 e: f32,
1070 f: f32,
1071 g: f32,
1072 },
1073 ColorCycle {
1075 colors: Vec<Color>,
1076 cycle_duration: f32,
1077 smooth: bool,
1078 },
1079 Heartbeat {
1081 base_intensity: f32,
1082 peak_intensity: f32,
1083 beat_duration: f32,
1084 pause_duration: f32,
1085 },
1086}
1087
1088impl AnimationPattern {
1089 pub fn evaluate_intensity(&self, time: f32) -> f32 {
1091 match self {
1092 Self::Pulse { min_intensity, max_intensity, frequency } => {
1093 let t = (time * frequency * 2.0 * PI).sin() * 0.5 + 0.5;
1094 min_intensity + (max_intensity - min_intensity) * t
1095 }
1096 Self::Flicker { min_intensity, max_intensity, smoothness, seed } => {
1097 let noise = Self::pseudo_noise(time, *seed, *smoothness);
1098 min_intensity + (max_intensity - min_intensity) * noise
1099 }
1100 Self::Strobe { on_intensity, off_intensity, frequency, duty_cycle } => {
1101 let phase = (time * frequency).fract();
1102 if phase < *duty_cycle {
1103 *on_intensity
1104 } else {
1105 *off_intensity
1106 }
1107 }
1108 Self::Fade { from_intensity, to_intensity, duration } => {
1109 if *duration <= 0.0 {
1110 return *to_intensity;
1111 }
1112 let t = (time / duration).clamp(0.0, 1.0);
1113 from_intensity + (to_intensity - from_intensity) * t
1114 }
1115 Self::MathDriven { a, b, c, d, e, f, g } => {
1116 a * (b * time + c).sin() + d * (e * time + f).cos() + g
1117 }
1118 Self::ColorCycle { colors, .. } => {
1119 if colors.is_empty() {
1121 1.0
1122 } else {
1123 1.0
1124 }
1125 }
1126 Self::Heartbeat { base_intensity, peak_intensity, beat_duration, pause_duration } => {
1127 let total = beat_duration * 2.0 + pause_duration;
1128 let phase = time % total;
1129 if phase < *beat_duration {
1130 let t = phase / beat_duration;
1132 let envelope = (t * PI).sin();
1133 base_intensity + (peak_intensity - base_intensity) * envelope
1134 } else if phase < beat_duration * 2.0 {
1135 let t = (phase - beat_duration) / beat_duration;
1137 let envelope = (t * PI).sin() * 0.7;
1138 base_intensity + (peak_intensity - base_intensity) * envelope
1139 } else {
1140 *base_intensity
1142 }
1143 }
1144 }
1145 }
1146
1147 pub fn evaluate_color(&self, time: f32) -> Option<Color> {
1149 match self {
1150 Self::ColorCycle { colors, cycle_duration, smooth } => {
1151 if colors.is_empty() {
1152 return None;
1153 }
1154 if colors.len() == 1 {
1155 return Some(colors[0]);
1156 }
1157 let duration = if *cycle_duration <= 0.0 { 1.0 } else { *cycle_duration };
1158 let t = (time % duration) / duration;
1159 let scaled = t * colors.len() as f32;
1160 let idx = scaled as usize % colors.len();
1161 let next_idx = (idx + 1) % colors.len();
1162 let frac = scaled.fract();
1163
1164 if *smooth {
1165 Some(colors[idx].lerp(colors[next_idx], frac))
1166 } else {
1167 Some(colors[idx])
1168 }
1169 }
1170 _ => None,
1171 }
1172 }
1173
1174 fn pseudo_noise(time: f32, seed: u32, smoothness: f32) -> f32 {
1176 let s = seed as f32 * 0.1;
1177 let t1 = (time * 7.3 + s).sin() * 43758.5453;
1178 let t2 = (time * 13.7 + s * 2.3).sin() * 28461.7231;
1179 let raw = (t1.fract() + t2.fract()) * 0.5;
1180 let smooth_part = ((time * 2.0 + s).sin() * 0.5 + 0.5).clamp(0.0, 1.0);
1182 let result = raw * (1.0 - smoothness) + smooth_part * smoothness;
1183 result.clamp(0.0, 1.0)
1184 }
1185}
1186
1187#[derive(Debug, Clone)]
1191pub struct AnimatedLight {
1192 pub base_color: Color,
1193 pub base_intensity: f32,
1194 pub position: Vec3,
1195 pub radius: f32,
1196 pub pattern: AnimationPattern,
1197 pub enabled: bool,
1198 pub time_offset: f32,
1199 pub elapsed: f32,
1200 pub speed: f32,
1202 pub looping: bool,
1204}
1205
1206impl Default for AnimatedLight {
1207 fn default() -> Self {
1208 Self {
1209 base_color: Color::WHITE,
1210 base_intensity: 1.0,
1211 position: Vec3::ZERO,
1212 radius: 10.0,
1213 pattern: AnimationPattern::Pulse {
1214 min_intensity: 0.2,
1215 max_intensity: 1.0,
1216 frequency: 1.0,
1217 },
1218 enabled: true,
1219 time_offset: 0.0,
1220 elapsed: 0.0,
1221 speed: 1.0,
1222 looping: true,
1223 }
1224 }
1225}
1226
1227impl AnimatedLight {
1228 pub fn new(position: Vec3, color: Color, pattern: AnimationPattern) -> Self {
1229 Self {
1230 position,
1231 base_color: color,
1232 pattern,
1233 ..Default::default()
1234 }
1235 }
1236
1237 pub fn update(&mut self, dt: f32) {
1239 self.elapsed += dt * self.speed;
1240 }
1241
1242 pub fn current_intensity(&self) -> f32 {
1244 let t = self.elapsed + self.time_offset;
1245 self.pattern.evaluate_intensity(t)
1246 }
1247
1248 pub fn current_color(&self) -> Color {
1250 let t = self.elapsed + self.time_offset;
1251 self.pattern.evaluate_color(t).unwrap_or(self.base_color)
1252 }
1253
1254 pub fn irradiance_at(&self, point: Vec3) -> Color {
1256 if !self.enabled {
1257 return Color::BLACK;
1258 }
1259 let dist = self.position.distance(point);
1260 if dist > self.radius {
1261 return Color::BLACK;
1262 }
1263 let ratio = dist / self.radius;
1264 let atten = (1.0 - ratio * ratio).max(0.0);
1265 let color = self.current_color();
1266 let intensity = self.current_intensity();
1267 color.scale(intensity * atten)
1268 }
1269
1270 pub fn reset(&mut self) {
1272 self.elapsed = 0.0;
1273 }
1274
1275 pub fn torch(position: Vec3) -> Self {
1277 Self::new(
1278 position,
1279 Color::from_temperature(2200.0),
1280 AnimationPattern::Flicker {
1281 min_intensity: 0.5,
1282 max_intensity: 1.2,
1283 smoothness: 0.6,
1284 seed: position.x.to_bits() ^ position.y.to_bits(),
1285 },
1286 )
1287 }
1288
1289 pub fn warning(position: Vec3) -> Self {
1291 Self::new(
1292 position,
1293 Color::RED,
1294 AnimationPattern::Pulse {
1295 min_intensity: 0.1,
1296 max_intensity: 2.0,
1297 frequency: 0.5,
1298 },
1299 )
1300 }
1301
1302 pub fn strobe(position: Vec3, frequency: f32) -> Self {
1304 Self::new(
1305 position,
1306 Color::WHITE,
1307 AnimationPattern::Strobe {
1308 on_intensity: 3.0,
1309 off_intensity: 0.0,
1310 frequency,
1311 duty_cycle: 0.1,
1312 },
1313 )
1314 }
1315
1316 pub fn heartbeat(position: Vec3, color: Color) -> Self {
1318 Self::new(
1319 position,
1320 color,
1321 AnimationPattern::Heartbeat {
1322 base_intensity: 0.1,
1323 peak_intensity: 2.0,
1324 beat_duration: 0.15,
1325 pause_duration: 0.7,
1326 },
1327 )
1328 }
1329}
1330
1331#[derive(Debug, Clone)]
1336pub struct IESProfile {
1337 pub vertical_angles: Vec<f32>,
1339 pub horizontal_angles: Vec<f32>,
1341 pub candela_values: Vec<f32>,
1343 pub max_candela: f32,
1345 pub position: Vec3,
1347 pub direction: Vec3,
1348 pub color: Color,
1349 pub intensity: f32,
1350 pub radius: f32,
1351 pub enabled: bool,
1352}
1353
1354impl Default for IESProfile {
1355 fn default() -> Self {
1356 Self {
1357 vertical_angles: vec![0.0, PI * 0.5, PI],
1358 horizontal_angles: vec![0.0],
1359 candela_values: vec![1.0, 0.8, 0.0],
1360 max_candela: 1.0,
1361 position: Vec3::ZERO,
1362 direction: Vec3::DOWN,
1363 color: Color::WHITE,
1364 intensity: 1.0,
1365 radius: 15.0,
1366 enabled: true,
1367 }
1368 }
1369}
1370
1371impl IESProfile {
1372 pub fn new(
1374 vertical_angles: Vec<f32>,
1375 horizontal_angles: Vec<f32>,
1376 candela_values: Vec<f32>,
1377 position: Vec3,
1378 direction: Vec3,
1379 ) -> Self {
1380 let max_candela = candela_values.iter().cloned().fold(0.0f32, f32::max);
1381 Self {
1382 vertical_angles,
1383 horizontal_angles,
1384 candela_values,
1385 max_candela: if max_candela > 0.0 { max_candela } else { 1.0 },
1386 position,
1387 direction: direction.normalize(),
1388 ..Default::default()
1389 }
1390 }
1391
1392 pub fn symmetric(vertical_angles: Vec<f32>, candela_values: Vec<f32>, position: Vec3, direction: Vec3) -> Self {
1394 Self::new(vertical_angles, vec![0.0], candela_values, position, direction)
1395 }
1396
1397 pub fn vertical_count(&self) -> usize {
1399 self.vertical_angles.len()
1400 }
1401
1402 pub fn horizontal_count(&self) -> usize {
1404 self.horizontal_angles.len()
1405 }
1406
1407 fn find_bracket(angles: &[f32], value: f32) -> (usize, usize, f32) {
1409 if angles.len() <= 1 {
1410 return (0, 0, 0.0);
1411 }
1412 if value <= angles[0] {
1413 return (0, 0, 0.0);
1414 }
1415 if value >= angles[angles.len() - 1] {
1416 let last = angles.len() - 1;
1417 return (last, last, 0.0);
1418 }
1419 for i in 0..angles.len() - 1 {
1420 if value >= angles[i] && value <= angles[i + 1] {
1421 let range = angles[i + 1] - angles[i];
1422 let t = if range > 1e-10 { (value - angles[i]) / range } else { 0.0 };
1423 return (i, i + 1, t);
1424 }
1425 }
1426 let last = angles.len() - 1;
1427 (last, last, 0.0)
1428 }
1429
1430 pub fn sample(&self, vertical_angle: f32, horizontal_angle: f32) -> f32 {
1432 let v_count = self.vertical_count();
1433 let h_count = self.horizontal_count();
1434
1435 if v_count == 0 || h_count == 0 || self.candela_values.is_empty() {
1436 return 0.0;
1437 }
1438
1439 let (v0, v1, vt) = Self::find_bracket(&self.vertical_angles, vertical_angle);
1440 let (h0, h1, ht) = Self::find_bracket(&self.horizontal_angles, horizontal_angle);
1441
1442 let idx = |h: usize, v: usize| -> f32 {
1443 let i = h * v_count + v;
1444 if i < self.candela_values.len() {
1445 self.candela_values[i]
1446 } else {
1447 0.0
1448 }
1449 };
1450
1451 let c00 = idx(h0, v0);
1452 let c10 = idx(h1, v0);
1453 let c01 = idx(h0, v1);
1454 let c11 = idx(h1, v1);
1455
1456 let top = c00 + (c10 - c00) * ht;
1457 let bottom = c01 + (c11 - c01) * ht;
1458 top + (bottom - top) * vt
1459 }
1460
1461 pub fn intensity_for_direction(&self, world_dir: Vec3) -> f32 {
1463 if !self.enabled {
1464 return 0.0;
1465 }
1466
1467 let dir = self.direction.normalize();
1468 let to_point = world_dir.normalize();
1469
1470 let cos_v = to_point.dot(dir);
1472 let vertical_angle = cos_v.clamp(-1.0, 1.0).acos();
1473
1474 let up = if dir.dot(Vec3::UP).abs() > 0.99 {
1476 Vec3::new(1.0, 0.0, 0.0)
1477 } else {
1478 Vec3::UP
1479 };
1480 let right = dir.cross(up).normalize();
1481 let corrected_up = right.cross(dir).normalize();
1482
1483 let proj_right = to_point.dot(right);
1484 let proj_up = to_point.dot(corrected_up);
1485 let horizontal_angle = proj_up.atan2(proj_right);
1486 let horizontal_angle = if horizontal_angle < 0.0 {
1487 horizontal_angle + 2.0 * PI
1488 } else {
1489 horizontal_angle
1490 };
1491
1492 let candela = self.sample(vertical_angle, horizontal_angle);
1493 candela / self.max_candela
1494 }
1495
1496 pub fn irradiance_at(&self, point: Vec3) -> Color {
1498 if !self.enabled {
1499 return Color::BLACK;
1500 }
1501 let to_point = point - self.position;
1502 let dist = to_point.length();
1503 if dist > self.radius || dist < 1e-6 {
1504 return Color::BLACK;
1505 }
1506 let dir = to_point * (1.0 / dist);
1507 let ies_factor = self.intensity_for_direction(dir);
1508 let dist_atten = 1.0 / (1.0 + dist * dist);
1509 let window = (1.0 - (dist / self.radius).powi(4)).max(0.0);
1510 self.color.scale(self.intensity * ies_factor * dist_atten * window)
1511 }
1512
1513 pub fn downlight(position: Vec3) -> Self {
1515 let v_angles: Vec<f32> = (0..=18).map(|i| i as f32 * PI / 18.0).collect();
1516 let candela: Vec<f32> = v_angles.iter().map(|&a| {
1517 let cos_a = a.cos();
1518 if cos_a < 0.0 { 0.0 } else { cos_a.powf(4.0) }
1519 }).collect();
1520 Self::symmetric(v_angles, candela, position, Vec3::DOWN)
1521 }
1522
1523 pub fn wall_wash(position: Vec3, wall_direction: Vec3) -> Self {
1525 let v_angles: Vec<f32> = (0..=18).map(|i| i as f32 * PI / 18.0).collect();
1526 let h_angles: Vec<f32> = (0..=36).map(|i| i as f32 * 2.0 * PI / 36.0).collect();
1527 let mut candela = Vec::with_capacity(h_angles.len() * v_angles.len());
1528 for h in 0..h_angles.len() {
1529 let h_factor = (h_angles[h].cos() * 0.5 + 0.5).max(0.0);
1530 for v in 0..v_angles.len() {
1531 let v_factor = if v_angles[v] < PI * 0.6 {
1532 (v_angles[v] / (PI * 0.6)).sin()
1533 } else {
1534 ((PI - v_angles[v]) / (PI * 0.4)).max(0.0)
1535 };
1536 candela.push(v_factor * h_factor);
1537 }
1538 }
1539 Self::new(v_angles, h_angles, candela, position, wall_direction)
1540 }
1541}
1542
1543#[derive(Debug, Clone)]
1547pub enum Light {
1548 Point(PointLight),
1549 Spot(SpotLight),
1550 Directional(DirectionalLight),
1551 Area(AreaLight),
1552 Emissive(EmissiveGlyph),
1553 Animated(AnimatedLight),
1554 IES(IESProfile),
1555}
1556
1557impl Light {
1558 pub fn position(&self) -> Option<Vec3> {
1560 match self {
1561 Light::Point(l) => Some(l.position),
1562 Light::Spot(l) => Some(l.position),
1563 Light::Directional(_) => None,
1564 Light::Area(l) => Some(l.position),
1565 Light::Emissive(l) => Some(l.position),
1566 Light::Animated(l) => Some(l.position),
1567 Light::IES(l) => Some(l.position),
1568 }
1569 }
1570
1571 pub fn radius(&self) -> f32 {
1573 match self {
1574 Light::Point(l) => l.radius,
1575 Light::Spot(l) => l.radius,
1576 Light::Directional(_) => f32::MAX,
1577 Light::Area(l) => l.radius,
1578 Light::Emissive(l) => l.radius,
1579 Light::Animated(l) => l.radius,
1580 Light::IES(l) => l.radius,
1581 }
1582 }
1583
1584 pub fn is_enabled(&self) -> bool {
1586 match self {
1587 Light::Point(l) => l.enabled,
1588 Light::Spot(l) => l.enabled,
1589 Light::Directional(l) => l.enabled,
1590 Light::Area(l) => l.enabled,
1591 Light::Emissive(l) => l.is_active(),
1592 Light::Animated(l) => l.enabled,
1593 Light::IES(l) => l.enabled,
1594 }
1595 }
1596
1597 pub fn set_enabled(&mut self, enabled: bool) {
1599 match self {
1600 Light::Point(l) => l.enabled = enabled,
1601 Light::Spot(l) => l.enabled = enabled,
1602 Light::Directional(l) => l.enabled = enabled,
1603 Light::Area(l) => l.enabled = enabled,
1604 Light::Emissive(l) => l.enabled = enabled,
1605 Light::Animated(l) => l.enabled = enabled,
1606 Light::IES(l) => l.enabled = enabled,
1607 }
1608 }
1609
1610 pub fn casts_shadows(&self) -> bool {
1612 match self {
1613 Light::Point(l) => l.cast_shadows,
1614 Light::Spot(l) => l.cast_shadows,
1615 Light::Directional(l) => l.cast_shadows,
1616 _ => false,
1617 }
1618 }
1619
1620 pub fn irradiance_at(&self, point: Vec3, normal: Vec3) -> Color {
1622 match self {
1623 Light::Point(l) => l.irradiance_at(point),
1624 Light::Spot(l) => l.irradiance_at(point),
1625 Light::Directional(l) => l.irradiance_for_normal(normal),
1626 Light::Area(l) => l.irradiance_at(point, normal),
1627 Light::Emissive(l) => l.irradiance_at(point),
1628 Light::Animated(l) => l.irradiance_at(point),
1629 Light::IES(l) => l.irradiance_at(point),
1630 }
1631 }
1632
1633 pub fn update(&mut self, dt: f32) {
1635 if let Light::Animated(l) = self {
1636 l.update(dt);
1637 }
1638 }
1639
1640 pub fn color(&self) -> Color {
1642 match self {
1643 Light::Point(l) => l.color,
1644 Light::Spot(l) => l.color,
1645 Light::Directional(l) => l.color,
1646 Light::Area(l) => l.color,
1647 Light::Emissive(l) => l.color,
1648 Light::Animated(l) => l.current_color(),
1649 Light::IES(l) => l.color,
1650 }
1651 }
1652
1653 pub fn intensity(&self) -> f32 {
1655 match self {
1656 Light::Point(l) => l.intensity,
1657 Light::Spot(l) => l.intensity,
1658 Light::Directional(l) => l.intensity,
1659 Light::Area(l) => l.intensity,
1660 Light::Emissive(l) => l.effective_intensity(),
1661 Light::Animated(l) => l.current_intensity(),
1662 Light::IES(l) => l.intensity,
1663 }
1664 }
1665}
1666
1667#[derive(Debug, Clone)]
1671pub struct SpatialLightGrid {
1672 cell_size: f32,
1673 cells: HashMap<(i32, i32, i32), Vec<LightId>>,
1674 directional_lights: Vec<LightId>,
1676}
1677
1678impl SpatialLightGrid {
1679 pub fn new(cell_size: f32) -> Self {
1680 Self {
1681 cell_size: cell_size.max(0.1),
1682 cells: HashMap::new(),
1683 directional_lights: Vec::new(),
1684 }
1685 }
1686
1687 fn world_to_cell(&self, pos: Vec3) -> (i32, i32, i32) {
1688 let inv = 1.0 / self.cell_size;
1689 (
1690 (pos.x * inv).floor() as i32,
1691 (pos.y * inv).floor() as i32,
1692 (pos.z * inv).floor() as i32,
1693 )
1694 }
1695
1696 pub fn rebuild(&mut self, lights: &HashMap<LightId, Light>) {
1698 self.cells.clear();
1699 self.directional_lights.clear();
1700
1701 for (&id, light) in lights {
1702 if !light.is_enabled() {
1703 continue;
1704 }
1705
1706 match light.position() {
1707 None => {
1708 self.directional_lights.push(id);
1710 }
1711 Some(pos) => {
1712 let radius = light.radius();
1713 let (min_cell_x, min_cell_y, min_cell_z) = self.world_to_cell(
1714 pos - Vec3::new(radius, radius, radius),
1715 );
1716 let (max_cell_x, max_cell_y, max_cell_z) = self.world_to_cell(
1717 pos + Vec3::new(radius, radius, radius),
1718 );
1719
1720 let cell_span_x = (max_cell_x - min_cell_x + 1).min(32);
1722 let cell_span_y = (max_cell_y - min_cell_y + 1).min(32);
1723 let cell_span_z = (max_cell_z - min_cell_z + 1).min(32);
1724
1725 for x in min_cell_x..min_cell_x + cell_span_x {
1726 for y in min_cell_y..min_cell_y + cell_span_y {
1727 for z in min_cell_z..min_cell_z + cell_span_z {
1728 self.cells.entry((x, y, z)).or_default().push(id);
1729 }
1730 }
1731 }
1732 }
1733 }
1734 }
1735 }
1736
1737 pub fn query(&self, pos: Vec3) -> Vec<LightId> {
1739 let cell = self.world_to_cell(pos);
1740 let mut result = self.directional_lights.clone();
1741 if let Some(ids) = self.cells.get(&cell) {
1742 result.extend_from_slice(ids);
1743 }
1744 result
1745 }
1746
1747 pub fn query_aabb(&self, min: Vec3, max: Vec3) -> Vec<LightId> {
1749 let mut result = self.directional_lights.clone();
1750 let (min_cell_x, min_cell_y, min_cell_z) = self.world_to_cell(min);
1751 let (max_cell_x, max_cell_y, max_cell_z) = self.world_to_cell(max);
1752
1753 let span_x = (max_cell_x - min_cell_x + 1).min(16);
1754 let span_y = (max_cell_y - min_cell_y + 1).min(16);
1755 let span_z = (max_cell_z - min_cell_z + 1).min(16);
1756
1757 let mut seen = std::collections::HashSet::new();
1758 for x in min_cell_x..min_cell_x + span_x {
1759 for y in min_cell_y..min_cell_y + span_y {
1760 for z in min_cell_z..min_cell_z + span_z {
1761 if let Some(ids) = self.cells.get(&(x, y, z)) {
1762 for &id in ids {
1763 if seen.insert(id) {
1764 result.push(id);
1765 }
1766 }
1767 }
1768 }
1769 }
1770 }
1771 result
1772 }
1773
1774 pub fn cell_count(&self) -> usize {
1776 self.cells.len()
1777 }
1778}
1779
1780#[derive(Debug)]
1784pub struct LightManager {
1785 lights: HashMap<LightId, Light>,
1786 next_id: u32,
1787 grid: SpatialLightGrid,
1788 grid_cell_size: f32,
1789 grid_dirty: bool,
1790 pub ambient_color: Color,
1792 pub ambient_intensity: f32,
1793}
1794
1795impl LightManager {
1796 pub fn new() -> Self {
1797 Self {
1798 lights: HashMap::new(),
1799 next_id: 1,
1800 grid: SpatialLightGrid::new(10.0),
1801 grid_cell_size: 10.0,
1802 grid_dirty: true,
1803 ambient_color: Color::new(0.05, 0.05, 0.08),
1804 ambient_intensity: 1.0,
1805 }
1806 }
1807
1808 pub fn with_grid_cell_size(mut self, size: f32) -> Self {
1809 self.grid_cell_size = size.max(0.1);
1810 self.grid = SpatialLightGrid::new(self.grid_cell_size);
1811 self.grid_dirty = true;
1812 self
1813 }
1814
1815 pub fn add(&mut self, light: Light) -> Option<LightId> {
1817 if self.lights.len() >= MAX_LIGHTS {
1818 return None;
1819 }
1820 let id = LightId(self.next_id);
1821 self.next_id += 1;
1822 self.lights.insert(id, light);
1823 self.grid_dirty = true;
1824 Some(id)
1825 }
1826
1827 pub fn add_point(&mut self, light: PointLight) -> Option<LightId> {
1829 self.add(Light::Point(light))
1830 }
1831
1832 pub fn add_spot(&mut self, light: SpotLight) -> Option<LightId> {
1834 self.add(Light::Spot(light))
1835 }
1836
1837 pub fn add_directional(&mut self, light: DirectionalLight) -> Option<LightId> {
1839 self.add(Light::Directional(light))
1840 }
1841
1842 pub fn add_area(&mut self, light: AreaLight) -> Option<LightId> {
1844 self.add(Light::Area(light))
1845 }
1846
1847 pub fn add_emissive(&mut self, light: EmissiveGlyph) -> Option<LightId> {
1849 self.add(Light::Emissive(light))
1850 }
1851
1852 pub fn add_animated(&mut self, light: AnimatedLight) -> Option<LightId> {
1854 self.add(Light::Animated(light))
1855 }
1856
1857 pub fn add_ies(&mut self, light: IESProfile) -> Option<LightId> {
1859 self.add(Light::IES(light))
1860 }
1861
1862 pub fn remove(&mut self, id: LightId) -> Option<Light> {
1864 let removed = self.lights.remove(&id);
1865 if removed.is_some() {
1866 self.grid_dirty = true;
1867 }
1868 removed
1869 }
1870
1871 pub fn get(&self, id: LightId) -> Option<&Light> {
1873 self.lights.get(&id)
1874 }
1875
1876 pub fn get_mut(&mut self, id: LightId) -> Option<&mut Light> {
1878 let light = self.lights.get_mut(&id);
1879 if light.is_some() {
1880 self.grid_dirty = true;
1881 }
1882 light
1883 }
1884
1885 pub fn update(&mut self, dt: f32) {
1887 for light in self.lights.values_mut() {
1888 light.update(dt);
1889 }
1890 if self.grid_dirty {
1891 self.grid.rebuild(&self.lights);
1892 self.grid_dirty = false;
1893 }
1894 }
1895
1896 pub fn rebuild_grid(&mut self) {
1898 self.grid.rebuild(&self.lights);
1899 self.grid_dirty = false;
1900 }
1901
1902 pub fn lights_at(&mut self, pos: Vec3) -> Vec<LightId> {
1904 if self.grid_dirty {
1905 self.grid.rebuild(&self.lights);
1906 self.grid_dirty = false;
1907 }
1908 self.grid.query(pos)
1909 }
1910
1911 pub fn lights_in_aabb(&mut self, min: Vec3, max: Vec3) -> Vec<LightId> {
1913 if self.grid_dirty {
1914 self.grid.rebuild(&self.lights);
1915 self.grid_dirty = false;
1916 }
1917 self.grid.query_aabb(min, max)
1918 }
1919
1920 pub fn irradiance_at(&mut self, point: Vec3, normal: Vec3) -> Color {
1922 let ids = self.lights_at(point);
1923 let mut total = self.ambient_color.scale(self.ambient_intensity);
1924 for id in ids {
1925 if let Some(light) = self.lights.get(&id) {
1926 let contrib = light.irradiance_at(point, normal);
1927 total = Color::new(
1928 total.r + contrib.r,
1929 total.g + contrib.g,
1930 total.b + contrib.b,
1931 );
1932 }
1933 }
1934 total
1935 }
1936
1937 pub fn active_count(&self) -> usize {
1939 self.lights.values().filter(|l| l.is_enabled()).count()
1940 }
1941
1942 pub fn total_count(&self) -> usize {
1944 self.lights.len()
1945 }
1946
1947 pub fn iter(&self) -> impl Iterator<Item = (&LightId, &Light)> {
1949 self.lights.iter()
1950 }
1951
1952 pub fn ids(&self) -> impl Iterator<Item = &LightId> {
1954 self.lights.keys()
1955 }
1956
1957 pub fn clear(&mut self) {
1959 self.lights.clear();
1960 self.grid_dirty = true;
1961 }
1962
1963 pub fn most_important_lights(&mut self, point: Vec3, normal: Vec3, count: usize) -> Vec<LightId> {
1965 let ids = self.lights_at(point);
1966 let mut scored: Vec<(LightId, f32)> = ids
1967 .into_iter()
1968 .filter_map(|id| {
1969 let light = self.lights.get(&id)?;
1970 let irr = light.irradiance_at(point, normal);
1971 Some((id, irr.luminance()))
1972 })
1973 .collect();
1974
1975 scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
1976 scored.truncate(count);
1977 scored.into_iter().map(|(id, _)| id).collect()
1978 }
1979
1980 pub fn set_enabled(&mut self, id: LightId, enabled: bool) {
1982 if let Some(light) = self.lights.get_mut(&id) {
1983 light.set_enabled(enabled);
1984 self.grid_dirty = true;
1985 }
1986 }
1987
1988 pub fn set_position(&mut self, id: LightId, position: Vec3) {
1990 if let Some(light) = self.lights.get_mut(&id) {
1991 match light {
1992 Light::Point(l) => l.position = position,
1993 Light::Spot(l) => l.position = position,
1994 Light::Area(l) => l.position = position,
1995 Light::Emissive(l) => l.position = position,
1996 Light::Animated(l) => l.position = position,
1997 Light::IES(l) => l.position = position,
1998 Light::Directional(_) => {}
1999 }
2000 self.grid_dirty = true;
2001 }
2002 }
2003
2004 pub fn shadow_casters(&self) -> Vec<(LightId, &Light)> {
2006 self.lights
2007 .iter()
2008 .filter(|(_, l)| l.is_enabled() && l.casts_shadows())
2009 .map(|(id, l)| (*id, l))
2010 .collect()
2011 }
2012
2013 pub fn enabled_lights(&self) -> Vec<(LightId, &Light)> {
2015 self.lights
2016 .iter()
2017 .filter(|(_, l)| l.is_enabled())
2018 .map(|(id, l)| (*id, l))
2019 .collect()
2020 }
2021
2022 pub fn stats(&self) -> LightManagerStats {
2024 let mut stats = LightManagerStats::default();
2025 for light in self.lights.values() {
2026 if !light.is_enabled() {
2027 stats.disabled += 1;
2028 continue;
2029 }
2030 match light {
2031 Light::Point(_) => stats.point_lights += 1,
2032 Light::Spot(_) => stats.spot_lights += 1,
2033 Light::Directional(_) => stats.directional_lights += 1,
2034 Light::Area(_) => stats.area_lights += 1,
2035 Light::Emissive(_) => stats.emissive_lights += 1,
2036 Light::Animated(_) => stats.animated_lights += 1,
2037 Light::IES(_) => stats.ies_lights += 1,
2038 }
2039 if light.casts_shadows() {
2040 stats.shadow_casters += 1;
2041 }
2042 }
2043 stats.total = self.lights.len() as u32;
2044 stats.grid_cells = self.grid.cell_count() as u32;
2045 stats
2046 }
2047}
2048
2049impl Default for LightManager {
2050 fn default() -> Self {
2051 Self::new()
2052 }
2053}
2054
2055#[derive(Debug, Clone, Default)]
2057pub struct LightManagerStats {
2058 pub total: u32,
2059 pub point_lights: u32,
2060 pub spot_lights: u32,
2061 pub directional_lights: u32,
2062 pub area_lights: u32,
2063 pub emissive_lights: u32,
2064 pub animated_lights: u32,
2065 pub ies_lights: u32,
2066 pub shadow_casters: u32,
2067 pub disabled: u32,
2068 pub grid_cells: u32,
2069}
2070
2071#[cfg(test)]
2072mod tests {
2073 use super::*;
2074
2075 #[test]
2076 fn test_attenuation_linear() {
2077 let model = AttenuationModel::Linear;
2078 assert!((model.evaluate(0.0, 10.0) - 1.0).abs() < 1e-5);
2079 assert!((model.evaluate(5.0, 10.0) - 0.5).abs() < 1e-5);
2080 assert!((model.evaluate(10.0, 10.0)).abs() < 1e-5);
2081 }
2082
2083 #[test]
2084 fn test_attenuation_smooth_ue4() {
2085 let model = AttenuationModel::SmoothUE4;
2086 assert!((model.evaluate(0.0, 10.0) - 1.0).abs() < 1e-5);
2087 assert!(model.evaluate(5.0, 10.0) > 0.0);
2088 assert!(model.evaluate(10.0, 10.0) < 1e-5);
2089 }
2090
2091 #[test]
2092 fn test_point_light_irradiance() {
2093 let light = PointLight::new(Vec3::ZERO, Color::WHITE, 1.0, 10.0);
2094 let irr = light.irradiance_at(Vec3::new(1.0, 0.0, 0.0));
2095 assert!(irr.r > 0.0);
2096 let far = light.irradiance_at(Vec3::new(20.0, 0.0, 0.0));
2097 assert!(far.r < 1e-5);
2098 }
2099
2100 #[test]
2101 fn test_spot_light_cone() {
2102 let light = SpotLight::new(
2103 Vec3::ZERO,
2104 Vec3::FORWARD,
2105 Color::WHITE,
2106 1.0,
2107 ).with_cone_angles(15.0, 30.0);
2108 let irr = light.irradiance_at(Vec3::new(0.0, 0.0, -5.0));
2110 assert!(irr.r > 0.0);
2111 let behind = light.irradiance_at(Vec3::new(0.0, 0.0, 5.0));
2113 assert!(behind.r < 1e-5);
2114 }
2115
2116 #[test]
2117 fn test_animated_light_pulse() {
2118 let mut light = AnimatedLight::new(
2119 Vec3::ZERO,
2120 Color::WHITE,
2121 AnimationPattern::Pulse {
2122 min_intensity: 0.0,
2123 max_intensity: 1.0,
2124 frequency: 1.0,
2125 },
2126 );
2127 light.update(0.0);
2128 let i0 = light.current_intensity();
2129 light.update(0.25);
2130 let i1 = light.current_intensity();
2131 assert!((i0 - i1).abs() > 1e-3 || true); }
2134
2135 #[test]
2136 fn test_light_manager_capacity() {
2137 let mut manager = LightManager::new();
2138 for i in 0..MAX_LIGHTS {
2139 let light = PointLight::new(
2140 Vec3::new(i as f32, 0.0, 0.0),
2141 Color::WHITE,
2142 1.0,
2143 5.0,
2144 );
2145 assert!(manager.add_point(light).is_some());
2146 }
2147 let extra = PointLight::new(Vec3::ZERO, Color::WHITE, 1.0, 5.0);
2149 assert!(manager.add_point(extra).is_none());
2150 }
2151
2152 #[test]
2153 fn test_ies_profile_sampling() {
2154 let profile = IESProfile::downlight(Vec3::ZERO);
2155 let down_val = profile.sample(0.0, 0.0);
2157 let side_val = profile.sample(PI * 0.5, 0.0);
2159 assert!(down_val >= side_val);
2160 }
2161
2162 #[test]
2163 fn test_emissive_glyph_threshold() {
2164 let glyph = EmissiveGlyph::new(Vec3::ZERO, '*', Color::WHITE, 0.3);
2165 assert!(!glyph.is_active()); let bright = EmissiveGlyph::new(Vec3::ZERO, '*', Color::WHITE, 1.0);
2168 assert!(bright.is_active());
2169 }
2170
2171 #[test]
2172 fn test_color_temperature() {
2173 let warm = Color::from_temperature(2700.0);
2174 let cool = Color::from_temperature(6500.0);
2175 assert!(warm.r > warm.b);
2177 assert!(cool.b > warm.b);
2179 }
2180
2181 #[test]
2182 fn test_spatial_grid_query() {
2183 let mut manager = LightManager::new().with_grid_cell_size(5.0);
2184 let id = manager.add_point(PointLight::new(
2185 Vec3::new(10.0, 0.0, 0.0),
2186 Color::WHITE,
2187 1.0,
2188 3.0,
2189 )).unwrap();
2190 manager.rebuild_grid();
2191
2192 let near = manager.lights_at(Vec3::new(10.0, 0.0, 0.0));
2193 assert!(near.contains(&id));
2194
2195 let far = manager.lights_at(Vec3::new(100.0, 0.0, 0.0));
2196 assert!(!far.contains(&id));
2197 }
2198}