1use glam::{Vec2, Vec3, Vec4, Mat4};
19use std::collections::HashMap;
20use crate::math::MathFunction;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
25pub struct LightId(pub u32);
26
27impl LightId {
28 pub fn invalid() -> Self { Self(u32::MAX) }
29 pub fn is_valid(self) -> bool { self.0 != u32::MAX }
30}
31
32#[derive(Debug, Clone)]
36pub enum Attenuation {
37 Constant,
39 Linear,
41 InverseSquare,
43 WindowedInverseSquare { range: f32 },
45 Math(MathFunction),
47 Polynomial { constant: f32, linear: f32, quadratic: f32 },
49}
50
51impl Attenuation {
52 pub fn evaluate(&self, distance: f32, max_range: f32) -> f32 {
54 let d = distance.max(1e-4);
55 match self {
56 Self::Constant => 1.0,
57 Self::Linear => (1.0 - (d / max_range.max(1e-4))).max(0.0),
58 Self::InverseSquare => 1.0 / (d * d),
59 Self::WindowedInverseSquare { range } => {
60 let r = range.max(1e-4);
61 let atten = 1.0 / (d * d);
62 let window = (1.0 - (d / r).powi(4)).max(0.0).powi(2);
63 atten * window
64 }
65 Self::Math(f) => {
66 let t = (d / max_range.max(1e-4)).clamp(0.0, 1.0);
67 f.evaluate(t, t).max(0.0)
68 }
69 Self::Polynomial { constant, linear, quadratic } => {
70 1.0 / (constant + linear * d + quadratic * d * d).max(1e-4)
71 }
72 }
73 }
74}
75
76#[derive(Debug, Clone)]
79pub struct PointLight {
80 pub id: LightId,
81 pub position: Vec3,
82 pub color: Vec3,
83 pub intensity: f32,
84 pub range: f32,
85 pub attenuation: Attenuation,
86 pub cast_shadow: bool,
87 pub enabled: bool,
88 pub tag: Option<String>,
90}
91
92impl PointLight {
93 pub fn new(position: Vec3, color: Vec3, intensity: f32, range: f32) -> Self {
94 Self {
95 id: LightId::invalid(),
96 position,
97 color,
98 intensity,
99 range,
100 attenuation: Attenuation::WindowedInverseSquare { range },
101 cast_shadow: false,
102 enabled: true,
103 tag: None,
104 }
105 }
106
107 pub fn with_shadow(mut self) -> Self { self.cast_shadow = true; self }
108 pub fn with_attenuation(mut self, a: Attenuation) -> Self { self.attenuation = a; self }
109 pub fn with_tag(mut self, t: impl Into<String>) -> Self { self.tag = Some(t.into()); self }
110
111 pub fn intensity_at(&self, p: Vec3) -> f32 {
112 let dist = (p - self.position).length();
113 if dist >= self.range { return 0.0; }
114 self.intensity * self.attenuation.evaluate(dist, self.range)
115 }
116
117 pub fn contribution(&self, p: Vec3, n: Vec3) -> Vec3 {
119 let to_light = self.position - p;
120 let dist = to_light.length();
121 if dist >= self.range || !self.enabled { return Vec3::ZERO; }
122 let dir = to_light / dist.max(1e-7);
123 let ndl = n.dot(dir).max(0.0);
124 let att = self.attenuation.evaluate(dist, self.range);
125 self.color * self.intensity * att * ndl
126 }
127}
128
129#[derive(Debug, Clone)]
132pub struct SpotLight {
133 pub id: LightId,
134 pub position: Vec3,
135 pub direction: Vec3,
136 pub color: Vec3,
137 pub intensity: f32,
138 pub range: f32,
139 pub inner_angle: f32,
141 pub outer_angle: f32,
143 pub attenuation: Attenuation,
144 pub cast_shadow: bool,
145 pub enabled: bool,
146 pub tag: Option<String>,
147}
148
149impl SpotLight {
150 pub fn new(position: Vec3, direction: Vec3, color: Vec3, intensity: f32, range: f32) -> Self {
151 Self {
152 id: LightId::invalid(),
153 position,
154 direction: direction.normalize_or_zero(),
155 color,
156 intensity,
157 range,
158 inner_angle: 0.35,
159 outer_angle: 0.65,
160 attenuation: Attenuation::WindowedInverseSquare { range },
161 cast_shadow: false,
162 enabled: true,
163 tag: None,
164 }
165 }
166
167 pub fn with_cone(mut self, inner: f32, outer: f32) -> Self {
168 self.inner_angle = inner;
169 self.outer_angle = outer;
170 self
171 }
172
173 pub fn cone_attenuation(&self, to_light_dir: Vec3) -> f32 {
174 let cos_theta = to_light_dir.dot(-self.direction).max(0.0);
175 let cos_inner = self.inner_angle.cos();
176 let cos_outer = self.outer_angle.cos();
177 ((cos_theta - cos_outer) / (cos_inner - cos_outer + 1e-7)).clamp(0.0, 1.0).powi(2)
178 }
179
180 pub fn contribution(&self, p: Vec3, n: Vec3) -> Vec3 {
181 if !self.enabled { return Vec3::ZERO; }
182 let to_light = self.position - p;
183 let dist = to_light.length();
184 if dist >= self.range { return Vec3::ZERO; }
185 let dir = to_light / dist.max(1e-7);
186 let ndl = n.dot(dir).max(0.0);
187 let dist_atten = self.attenuation.evaluate(dist, self.range);
188 let cone_atten = self.cone_attenuation(dir);
189 self.color * self.intensity * dist_atten * cone_atten * ndl
190 }
191}
192
193#[derive(Debug, Clone)]
196pub struct DirectionalLight {
197 pub id: LightId,
198 pub direction: Vec3,
199 pub color: Vec3,
200 pub intensity: f32,
201 pub cast_shadow: bool,
202 pub shadow_map: Option<ShadowMapConfig>,
203 pub enabled: bool,
204 pub angular_size: f32,
206}
207
208impl DirectionalLight {
209 pub fn sun(direction: Vec3, color: Vec3, intensity: f32) -> Self {
210 Self {
211 id: LightId::invalid(),
212 direction: direction.normalize_or_zero(),
213 color,
214 intensity,
215 cast_shadow: false,
216 shadow_map: None,
217 enabled: true,
218 angular_size: 0.5,
219 }
220 }
221
222 pub fn with_shadow(mut self, cfg: ShadowMapConfig) -> Self {
223 self.cast_shadow = true;
224 self.shadow_map = Some(cfg);
225 self
226 }
227
228 pub fn contribution(&self, n: Vec3) -> Vec3 {
229 if !self.enabled { return Vec3::ZERO; }
230 let ndl = n.dot(-self.direction).max(0.0);
231 self.color * self.intensity * ndl
232 }
233
234 pub fn shadow_view_proj(&self, scene_center: Vec3, scene_radius: f32) -> Mat4 {
236 let eye = scene_center - self.direction * scene_radius * 2.0;
237 let up = if self.direction.dot(Vec3::Y).abs() < 0.99 { Vec3::Y } else { Vec3::Z };
238 let view = Mat4::look_at_rh(eye, scene_center, up);
239 let proj = Mat4::orthographic_rh(
240 -scene_radius, scene_radius,
241 -scene_radius, scene_radius,
242 0.1, scene_radius * 4.0,
243 );
244 proj * view
245 }
246}
247
248#[derive(Debug, Clone)]
251pub struct AmbientLight {
252 pub sky_color: Vec3,
254 pub ground_color: Vec3,
256 pub intensity: f32,
257}
258
259impl AmbientLight {
260 pub fn uniform(color: Vec3, intensity: f32) -> Self {
261 Self { sky_color: color, ground_color: color, intensity }
262 }
263
264 pub fn hemisphere(sky: Vec3, ground: Vec3, intensity: f32) -> Self {
265 Self { sky_color: sky, ground_color: ground, intensity }
266 }
267
268 pub fn evaluate(&self, normal: Vec3) -> Vec3 {
270 let t = (normal.dot(Vec3::Y) * 0.5 + 0.5).clamp(0.0, 1.0);
271 (self.sky_color * t + self.ground_color * (1.0 - t)) * self.intensity
272 }
273}
274
275impl Default for AmbientLight {
276 fn default() -> Self {
277 Self::uniform(Vec3::new(0.1, 0.1, 0.15), 0.5)
278 }
279}
280
281#[derive(Debug, Clone)]
284pub struct ShadowMapConfig {
285 pub resolution: u32,
286 pub bias: f32,
287 pub normal_bias: f32,
288 pub pcf_samples: u32,
290 pub pcf_radius: f32,
292 pub cascade_count: u32,
294 pub cascade_splits: Vec<f32>,
295}
296
297impl Default for ShadowMapConfig {
298 fn default() -> Self {
299 Self {
300 resolution: 2048,
301 bias: 0.005,
302 normal_bias: 0.01,
303 pcf_samples: 16,
304 pcf_radius: 1.5,
305 cascade_count: 3,
306 cascade_splits: vec![0.05, 0.15, 0.4, 1.0],
307 }
308 }
309}
310
311impl ShadowMapConfig {
312 pub fn high_quality() -> Self {
313 Self { resolution: 4096, pcf_samples: 32, pcf_radius: 2.0, ..Default::default() }
314 }
315
316 pub fn performance() -> Self {
317 Self { resolution: 1024, pcf_samples: 4, pcf_radius: 1.0, cascade_count: 1, cascade_splits: vec![1.0], ..Default::default() }
318 }
319}
320
321#[derive(Debug, Clone)]
327pub struct LightProbe {
328 pub id: LightId,
329 pub position: Vec3,
330 pub radius: f32,
331 pub sh_coeffs: [Vec3; 9],
333 pub weight: f32,
334 pub enabled: bool,
335}
336
337impl LightProbe {
338 pub fn new(position: Vec3, radius: f32) -> Self {
339 Self {
340 id: LightId::invalid(),
341 position,
342 radius,
343 sh_coeffs: [Vec3::ZERO; 9],
344 weight: 1.0,
345 enabled: true,
346 }
347 }
348
349 pub fn evaluate_sh(&self, normal: Vec3) -> Vec3 {
352 let c0 = 0.282_095_f32;
354 let c1 = 0.488_603_f32;
356 let sh0 = self.sh_coeffs[0] * c0;
357 let sh1 = self.sh_coeffs[1] * c1 * normal.y;
358 let sh2 = self.sh_coeffs[2] * c1 * normal.z;
359 let sh3 = self.sh_coeffs[3] * c1 * normal.x;
360 (sh0 + sh1 + sh2 + sh3).max(Vec3::ZERO) * self.weight
361 }
362
363 pub fn from_uniform_color(position: Vec3, radius: f32, color: Vec3) -> Self {
365 let mut probe = Self::new(position, radius);
366 probe.sh_coeffs[0] = color * (1.0 / 0.282_095_f32);
368 probe
369 }
370
371 pub fn from_hemisphere(position: Vec3, radius: f32, sky: Vec3, ground: Vec3) -> Self {
373 let mut probe = Self::new(position, radius);
374 probe.sh_coeffs[0] = (sky + ground) * 0.5 * (1.0 / 0.282_095_f32);
375 probe.sh_coeffs[2] = (sky - ground) * (1.0 / 0.488_603_f32);
376 probe
377 }
378}
379
380#[derive(Debug, Clone)]
384pub struct SsaoConfig {
385 pub enabled: bool,
386 pub sample_count: u32,
387 pub radius: f32,
388 pub bias: f32,
389 pub intensity: f32,
391 pub blur_passes: u32,
393 pub blur_radius: f32,
394 pub resolution_scale: f32,
396}
397
398impl Default for SsaoConfig {
399 fn default() -> Self {
400 Self {
401 enabled: true,
402 sample_count: 32,
403 radius: 0.5,
404 bias: 0.025,
405 intensity: 1.0,
406 blur_passes: 2,
407 blur_radius: 2.0,
408 resolution_scale: 0.5,
409 }
410 }
411}
412
413impl SsaoConfig {
414 pub fn high_quality() -> Self {
415 Self { sample_count: 64, blur_passes: 4, resolution_scale: 1.0, ..Default::default() }
416 }
417 pub fn performance() -> Self {
418 Self { sample_count: 8, blur_passes: 1, resolution_scale: 0.25, ..Default::default() }
419 }
420 pub fn disabled() -> Self { Self { enabled: false, ..Default::default() } }
421}
422
423#[derive(Debug, Clone)]
427pub struct VolumetricConfig {
428 pub enabled: bool,
429 pub sample_count: u32,
430 pub density: f32,
431 pub scattering: f32,
432 pub absorption: f32,
433 pub sun_intensity: f32,
435 pub fog_color: Vec3,
437 pub fog_density: f32,
438 pub fog_height: f32,
440 pub fog_falloff: f32,
441 pub resolution_scale: f32,
442}
443
444impl Default for VolumetricConfig {
445 fn default() -> Self {
446 Self {
447 enabled: false,
448 sample_count: 64,
449 density: 0.05,
450 scattering: 0.5,
451 absorption: 0.02,
452 sun_intensity: 1.0,
453 fog_color: Vec3::new(0.8, 0.85, 1.0),
454 fog_density: 0.002,
455 fog_height: 50.0,
456 fog_falloff: 0.1,
457 resolution_scale: 0.5,
458 }
459 }
460}
461
462#[derive(Debug, Clone)]
466pub struct LightTile {
467 pub point_light_indices: Vec<u32>,
469 pub spot_light_indices: Vec<u32>,
470 pub depth_min: f32,
472 pub depth_max: f32,
473}
474
475impl LightTile {
476 pub fn new() -> Self {
477 Self {
478 point_light_indices: Vec::new(),
479 spot_light_indices: Vec::new(),
480 depth_min: 0.0,
481 depth_max: 1.0,
482 }
483 }
484
485 pub fn total_lights(&self) -> usize {
486 self.point_light_indices.len() + self.spot_light_indices.len()
487 }
488}
489
490pub struct LightCuller {
495 pub tile_size_x: u32,
496 pub tile_size_y: u32,
497 pub screen_width: u32,
498 pub screen_height: u32,
499 pub tiles: Vec<LightTile>,
500 pub max_lights_per_tile: usize,
501}
502
503impl LightCuller {
504 pub fn new(screen_w: u32, screen_h: u32, tile_size: u32) -> Self {
505 let tx = (screen_w + tile_size - 1) / tile_size;
506 let ty = (screen_h + tile_size - 1) / tile_size;
507 let n = (tx * ty) as usize;
508 Self {
509 tile_size_x: tile_size,
510 tile_size_y: tile_size,
511 screen_width: screen_w,
512 screen_height: screen_h,
513 tiles: (0..n).map(|_| LightTile::new()).collect(),
514 max_lights_per_tile: 256,
515 }
516 }
517
518 pub fn tile_count_x(&self) -> u32 { (self.screen_width + self.tile_size_x - 1) / self.tile_size_x }
519 pub fn tile_count_y(&self) -> u32 { (self.screen_height + self.tile_size_y - 1) / self.tile_size_y }
520
521 pub fn tile_index(&self, tx: u32, ty: u32) -> usize {
522 (ty * self.tile_count_x() + tx) as usize
523 }
524
525 pub fn cull_point_lights(
527 &mut self,
528 lights: &[PointLight],
529 view_proj: Mat4,
530 ) {
531 for tile in &mut self.tiles { tile.point_light_indices.clear(); }
532
533 for (i, light) in lights.iter().enumerate() {
534 if !light.enabled { continue; }
535
536 let clip = view_proj * light.position.extend(1.0);
538 if clip.w.abs() < 1e-6 { continue; }
539 let ndc = clip.truncate() / clip.w;
540
541 let screen_radius = {
543 let edge = view_proj * (light.position + Vec3::X * light.range).extend(1.0);
544 let edge_ndc = if edge.w.abs() > 1e-6 { (edge.truncate() / edge.w) } else { continue };
545 ((edge_ndc - ndc).length()).abs() * 0.5
546 };
547
548 let sx = ((ndc.x * 0.5 + 0.5) * self.screen_width as f32) as i32;
550 let sy = ((ndc.y * 0.5 + 0.5) * self.screen_height as f32) as i32;
551 let sr = (screen_radius * self.screen_width as f32) as i32 + 1;
552
553 let tx_size = self.tile_size_x as i32;
554 let ty_size = self.tile_size_y as i32;
555 let tcx = self.tile_count_x() as i32;
556 let tcy = self.tile_count_y() as i32;
557
558 let tx_min = ((sx - sr) / tx_size).max(0);
559 let tx_max = ((sx + sr) / tx_size + 1).min(tcx);
560 let ty_min = ((sy - sr) / ty_size).max(0);
561 let ty_max = ((sy + sr) / ty_size + 1).min(tcy);
562
563 for ty in ty_min..ty_max {
564 for tx in tx_min..tx_max {
565 let idx = self.tile_index(tx as u32, ty as u32);
566 if idx < self.tiles.len() {
567 let tile = &mut self.tiles[idx];
568 if tile.point_light_indices.len() < self.max_lights_per_tile {
569 tile.point_light_indices.push(i as u32);
570 }
571 }
572 }
573 }
574 }
575 }
576
577 pub fn resize(&mut self, screen_w: u32, screen_h: u32) {
578 self.screen_width = screen_w;
579 self.screen_height = screen_h;
580 let tx = (screen_w + self.tile_size_x - 1) / self.tile_size_x;
581 let ty = (screen_h + self.tile_size_y - 1) / self.tile_size_y;
582 let n = (tx * ty) as usize;
583 self.tiles = (0..n).map(|_| LightTile::new()).collect();
584 }
585}
586
587#[derive(Debug, Clone, Default)]
591pub struct EmissiveAccumulator {
592 pub sources: Vec<EmissiveSource>,
593 pub threshold: f32,
595 pub max_sources: usize,
596}
597
598#[derive(Debug, Clone)]
599pub struct EmissiveSource {
600 pub position: Vec3,
601 pub color: Vec3,
602 pub emission: f32,
603}
604
605impl EmissiveAccumulator {
606 pub fn new() -> Self {
607 Self { sources: Vec::new(), threshold: 0.5, max_sources: 64 }
608 }
609
610 pub fn push(&mut self, position: Vec3, color: Vec3, emission: f32) {
611 if emission < self.threshold { return; }
612 if self.sources.len() >= self.max_sources { return; }
613 self.sources.push(EmissiveSource { position, color, emission });
614 }
615
616 pub fn clear(&mut self) { self.sources.clear(); }
617
618 pub fn to_point_lights(&self, intensity_scale: f32) -> Vec<PointLight> {
620 self.sources.iter().map(|s| {
621 PointLight::new(
622 s.position,
623 s.color,
624 s.emission * intensity_scale,
625 s.emission * 3.0,
626 )
627 }).collect()
628 }
629}
630
631pub struct LightManager {
635 pub point_lights: Vec<PointLight>,
636 pub spot_lights: Vec<SpotLight>,
637 pub directional: Option<DirectionalLight>,
638 pub ambient: AmbientLight,
639 pub probes: Vec<LightProbe>,
640 pub ssao: SsaoConfig,
641 pub volumetric: VolumetricConfig,
642 pub emissive: EmissiveAccumulator,
643 pub culler: Option<LightCuller>,
644 next_id: u32,
645 emissive_lights: Vec<PointLight>,
647}
648
649impl LightManager {
650 pub fn new() -> Self {
651 Self {
652 point_lights: Vec::new(),
653 spot_lights: Vec::new(),
654 directional: None,
655 ambient: AmbientLight::default(),
656 probes: Vec::new(),
657 ssao: SsaoConfig::default(),
658 volumetric: VolumetricConfig::default(),
659 emissive: EmissiveAccumulator::new(),
660 culler: None,
661 next_id: 1,
662 emissive_lights: Vec::new(),
663 }
664 }
665
666 fn next_id(&mut self) -> LightId {
667 let id = LightId(self.next_id);
668 self.next_id += 1;
669 id
670 }
671
672 pub fn add_point_light(&mut self, mut light: PointLight) -> LightId {
673 let id = self.next_id();
674 light.id = id;
675 self.point_lights.push(light);
676 id
677 }
678
679 pub fn add_spot_light(&mut self, mut light: SpotLight) -> LightId {
680 let id = self.next_id();
681 light.id = id;
682 self.spot_lights.push(light);
683 id
684 }
685
686 pub fn set_directional(&mut self, mut light: DirectionalLight) -> LightId {
687 let id = self.next_id();
688 light.id = id;
689 self.directional = Some(light);
690 id
691 }
692
693 pub fn add_probe(&mut self, mut probe: LightProbe) -> LightId {
694 let id = self.next_id();
695 probe.id = id;
696 self.probes.push(probe);
697 id
698 }
699
700 pub fn remove(&mut self, id: LightId) {
701 self.point_lights.retain(|l| l.id != id);
702 self.spot_lights.retain(|l| l.id != id);
703 self.probes.retain(|p| p.id != id);
704 if self.directional.as_ref().map(|d| d.id) == Some(id) {
705 self.directional = None;
706 }
707 }
708
709 pub fn get_point_light_mut(&mut self, id: LightId) -> Option<&mut PointLight> {
710 self.point_lights.iter_mut().find(|l| l.id == id)
711 }
712
713 pub fn get_spot_light_mut(&mut self, id: LightId) -> Option<&mut SpotLight> {
714 self.spot_lights.iter_mut().find(|l| l.id == id)
715 }
716
717 pub fn init_culler(&mut self, screen_w: u32, screen_h: u32) {
719 self.culler = Some(LightCuller::new(screen_w, screen_h, 16));
720 }
721
722 pub fn flush_emissive(&mut self, intensity_scale: f32) {
724 self.emissive_lights = self.emissive.to_point_lights(intensity_scale);
725 self.emissive.clear();
726 }
727
728 pub fn cull(&mut self, view_proj: Mat4) {
730 if let Some(ref mut culler) = self.culler {
731 let all_points: Vec<PointLight> = self.point_lights.iter()
732 .chain(self.emissive_lights.iter())
733 .cloned()
734 .collect();
735 culler.cull_point_lights(&all_points, view_proj);
736 }
737 }
738
739 pub fn light_count(&self) -> usize {
741 self.point_lights.len()
742 + self.spot_lights.len()
743 + if self.directional.is_some() { 1 } else { 0 }
744 }
745
746 pub fn evaluate_cpu(&self, p: Vec3, n: Vec3) -> Vec3 {
749 let mut color = self.ambient.evaluate(n);
750
751 if let Some(ref dir) = self.directional {
752 color += dir.contribution(n);
753 }
754 for light in &self.point_lights {
755 color += light.contribution(p, n);
756 }
757 for light in &self.spot_lights {
758 color += light.contribution(p, n);
759 }
760 let mut total_probe_weight = 0.0_f32;
762 let mut probe_color = Vec3::ZERO;
763 for probe in &self.probes {
764 if !probe.enabled { continue; }
765 let dist = (probe.position - p).length();
766 if dist > probe.radius { continue; }
767 let w = (1.0 - dist / probe.radius).clamp(0.0, 1.0) * probe.weight;
768 probe_color += probe.evaluate_sh(n) * w;
769 total_probe_weight += w;
770 }
771 if total_probe_weight > 1e-4 {
772 color += probe_color / total_probe_weight;
773 }
774 color
775 }
776
777 pub fn remove_by_tag(&mut self, tag: &str) {
779 self.point_lights.retain(|l| l.tag.as_deref() != Some(tag));
780 self.spot_lights.retain(|l| l.tag.as_deref() != Some(tag));
781 }
782
783 pub fn set_enabled_by_tag(&mut self, tag: &str, enabled: bool) {
785 for l in &mut self.point_lights {
786 if l.tag.as_deref() == Some(tag) { l.enabled = enabled; }
787 }
788 for l in &mut self.spot_lights {
789 if l.tag.as_deref() == Some(tag) { l.enabled = enabled; }
790 }
791 }
792
793 pub fn scale_intensity(&mut self, factor: f32) {
795 for l in &mut self.point_lights { l.intensity *= factor; }
796 for l in &mut self.spot_lights { l.intensity *= factor; }
797 if let Some(ref mut d) = self.directional { d.intensity *= factor; }
798 }
799}
800
801impl Default for LightManager {
802 fn default() -> Self { Self::new() }
803}
804
805impl LightManager {
808 pub fn preset_daylight() -> Self {
810 let mut mgr = Self::new();
811 mgr.set_directional(DirectionalLight::sun(
812 Vec3::new(-0.3, -0.9, -0.3),
813 Vec3::new(1.0, 0.95, 0.85),
814 3.0,
815 ));
816 mgr.ambient = AmbientLight::hemisphere(
817 Vec3::new(0.5, 0.65, 0.9),
818 Vec3::new(0.2, 0.2, 0.15),
819 0.4,
820 );
821 mgr
822 }
823
824 pub fn preset_dungeon() -> Self {
826 let mut mgr = Self::new();
827 mgr.ambient = AmbientLight::uniform(Vec3::new(0.03, 0.03, 0.05), 0.1);
828 mgr
829 }
830
831 pub fn preset_void() -> Self {
833 let mut mgr = Self::new();
834 mgr.ambient = AmbientLight::uniform(Vec3::ZERO, 0.0);
835 mgr
836 }
837
838 pub fn preset_combat_arena(center: Vec3) -> Self {
840 let mut mgr = Self::preset_dungeon();
841 mgr.add_point_light(
842 PointLight::new(center + Vec3::new(0.0, 8.0, 0.0), Vec3::new(1.0, 0.2, 0.1), 4.0, 20.0)
843 .with_tag("arena"),
844 );
845 mgr.add_point_light(
846 PointLight::new(center + Vec3::new(5.0, 3.0, 0.0), Vec3::new(0.3, 0.3, 1.0), 2.0, 12.0)
847 .with_tag("arena"),
848 );
849 mgr.add_point_light(
850 PointLight::new(center + Vec3::new(-5.0, 3.0, 0.0), Vec3::new(0.3, 0.3, 1.0), 2.0, 12.0)
851 .with_tag("arena"),
852 );
853 mgr
854 }
855
856 pub fn preset_interior(center: Vec3) -> Self {
858 let mut mgr = Self::new();
859 mgr.ambient = AmbientLight::hemisphere(
860 Vec3::new(0.9, 0.85, 0.7),
861 Vec3::new(0.3, 0.25, 0.2),
862 0.3,
863 );
864 mgr.add_point_light(
865 PointLight::new(center + Vec3::new(0.0, 3.0, 0.0), Vec3::new(1.0, 0.9, 0.7), 5.0, 10.0)
866 .with_shadow()
867 .with_tag("ceiling"),
868 );
869 mgr
870 }
871
872 pub fn preset_moonlight() -> Self {
874 let mut mgr = Self::new();
875 mgr.set_directional(DirectionalLight::sun(
876 Vec3::new(-0.2, -0.8, -0.5),
877 Vec3::new(0.6, 0.65, 0.9),
878 0.8,
879 ));
880 mgr.ambient = AmbientLight::hemisphere(
881 Vec3::new(0.05, 0.06, 0.15),
882 Vec3::new(0.02, 0.02, 0.04),
883 0.2,
884 );
885 mgr
886 }
887
888 pub fn preset_neon(center: Vec3) -> Self {
890 let mut mgr = Self::new();
891 mgr.ambient = AmbientLight::uniform(Vec3::new(0.02, 0.01, 0.04), 0.15);
892 let neons = [
893 (Vec3::new(1.0, 0.1, 0.8), Vec3::new(-4.0, 2.0, 0.0)),
894 (Vec3::new(0.1, 0.9, 1.0), Vec3::new(4.0, 2.0, 0.0)),
895 (Vec3::new(1.0, 0.8, 0.0), Vec3::new(0.0, 2.0, 4.0)),
896 (Vec3::new(0.2, 1.0, 0.3), Vec3::new(0.0, 2.0, -4.0)),
897 ];
898 for (color, offset) in neons {
899 mgr.add_point_light(
900 PointLight::new(center + offset, color, 3.0, 8.0).with_tag("neon"),
901 );
902 }
903 mgr
904 }
905
906 pub fn preset_cavern() -> Self {
908 let mut mgr = Self::new();
909 mgr.ambient = AmbientLight::uniform(Vec3::new(0.0, 0.05, 0.15), 0.2);
910 mgr
911 }
912}
913
914#[derive(Debug, Clone)]
918pub struct PbrMaterial {
919 pub albedo: Vec3,
921 pub alpha: f32,
923 pub metallic: f32,
925 pub roughness: f32,
927 pub ao: f32,
929 pub emissive: Vec3,
931 pub ior: f32,
933 pub anisotropy: f32,
935 pub anisotropy_dir: Vec3,
937 pub clearcoat: f32,
939 pub clearcoat_rough: f32,
941 pub sss_color: Vec3,
943 pub sss_radius: f32,
945}
946
947impl PbrMaterial {
948 pub fn dielectric(albedo: Vec3, roughness: f32) -> Self {
949 Self {
950 albedo,
951 alpha: 1.0,
952 metallic: 0.0,
953 roughness: roughness.clamp(0.04, 1.0),
954 ao: 1.0,
955 emissive: Vec3::ZERO,
956 ior: 1.5,
957 anisotropy: 0.0,
958 anisotropy_dir: Vec3::X,
959 clearcoat: 0.0,
960 clearcoat_rough: 0.0,
961 sss_color: Vec3::ZERO,
962 sss_radius: 0.0,
963 }
964 }
965
966 pub fn metal(albedo: Vec3, roughness: f32) -> Self {
967 Self { metallic: 1.0, ..Self::dielectric(albedo, roughness) }
968 }
969
970 pub fn emissive_mat(albedo: Vec3, emissive: Vec3) -> Self {
971 Self { emissive, ..Self::dielectric(albedo, 0.5) }
972 }
973
974 pub fn glass(ior: f32, roughness: f32) -> Self {
975 Self {
976 albedo: Vec3::ONE,
977 alpha: 0.02,
978 ior,
979 roughness,
980 metallic: 0.0,
981 ao: 1.0,
982 emissive: Vec3::ZERO,
983 anisotropy: 0.0,
984 anisotropy_dir: Vec3::X,
985 clearcoat: 0.0,
986 clearcoat_rough: 0.0,
987 sss_color: Vec3::ZERO,
988 sss_radius: 0.0,
989 }
990 }
991
992 pub fn f0(&self) -> Vec3 {
994 let f0_dielectric = Vec3::splat(((self.ior - 1.0) / (self.ior + 1.0)).powi(2));
995 f0_dielectric.lerp(self.albedo, self.metallic)
996 }
997}
998
999impl Default for PbrMaterial {
1000 fn default() -> Self {
1001 Self::dielectric(Vec3::new(0.8, 0.8, 0.8), 0.5)
1002 }
1003}
1004
1005pub struct PbrLighting;
1011
1012impl PbrLighting {
1013 #[inline]
1015 pub fn fresnel_schlick(cos_theta: f32, f0: Vec3) -> Vec3 {
1016 f0 + (Vec3::ONE - f0) * (1.0 - cos_theta).max(0.0).powi(5)
1017 }
1018
1019 #[inline]
1021 pub fn geometry_schlick_ggx(n_dot_v: f32, roughness: f32) -> f32 {
1022 let r = roughness + 1.0;
1023 let k = (r * r) / 8.0;
1024 n_dot_v / (n_dot_v * (1.0 - k) + k)
1025 }
1026
1027 #[inline]
1029 pub fn geometry_smith(n_dot_v: f32, n_dot_l: f32, roughness: f32) -> f32 {
1030 Self::geometry_schlick_ggx(n_dot_v, roughness)
1031 * Self::geometry_schlick_ggx(n_dot_l, roughness)
1032 }
1033
1034 #[inline]
1036 pub fn ndf_ggx(n_dot_h: f32, roughness: f32) -> f32 {
1037 let a = roughness * roughness;
1038 let a2 = a * a;
1039 let n_dot_h2 = n_dot_h * n_dot_h;
1040 let denom = n_dot_h2 * (a2 - 1.0) + 1.0;
1041 a2 / (std::f32::consts::PI * denom * denom + 1e-7)
1042 }
1043
1044 pub fn brdf(
1046 normal: Vec3,
1047 view_dir: Vec3,
1048 light_dir: Vec3,
1049 mat: &PbrMaterial,
1050 ) -> Vec3 {
1051 let n_dot_l = normal.dot(light_dir).max(0.0);
1052 let n_dot_v = normal.dot(view_dir).max(1e-7);
1053 if n_dot_l < 1e-7 { return Vec3::ZERO; }
1054
1055 let h = (view_dir + light_dir).normalize_or_zero();
1056 let n_dot_h = normal.dot(h).clamp(0.0, 1.0);
1057 let h_dot_v = h.dot(view_dir).clamp(0.0, 1.0);
1058
1059 let f0 = mat.f0();
1060 let f = Self::fresnel_schlick(h_dot_v, f0);
1061 let d = Self::ndf_ggx(n_dot_h, mat.roughness.max(0.04));
1062 let g = Self::geometry_smith(n_dot_v, n_dot_l, mat.roughness.max(0.04));
1063
1064 let specular = (d * g * f) / (4.0 * n_dot_v * n_dot_l + 1e-7);
1065
1066 let k_s = f;
1068 let k_d = (Vec3::ONE - k_s) * (1.0 - mat.metallic);
1069 let diffuse = k_d * mat.albedo / std::f32::consts::PI;
1070
1071 (diffuse + specular) * n_dot_l
1072 }
1073
1074 pub fn shade(
1076 position: Vec3,
1077 normal: Vec3,
1078 view_pos: Vec3,
1079 mat: &PbrMaterial,
1080 manager: &LightManager,
1081 ) -> Vec3 {
1082 let view_dir = (view_pos - position).normalize_or_zero();
1083 let mut lo = Vec3::ZERO;
1084
1085 if let Some(ref dir_light) = manager.directional {
1087 if dir_light.enabled {
1088 let light_dir = (-dir_light.direction).normalize_or_zero();
1089 let radiance = dir_light.color * dir_light.intensity;
1090 lo += Self::brdf(normal, view_dir, light_dir, mat) * radiance;
1091 }
1092 }
1093
1094 for light in &manager.point_lights {
1096 if !light.enabled { continue; }
1097 let to_light = light.position - position;
1098 let dist = to_light.length();
1099 if dist >= light.range { continue; }
1100 let light_dir = to_light / dist.max(1e-7);
1101 let att = light.attenuation.evaluate(dist, light.range);
1102 let radiance = light.color * light.intensity * att;
1103 lo += Self::brdf(normal, view_dir, light_dir, mat) * radiance;
1104 }
1105
1106 for light in &manager.spot_lights {
1108 if !light.enabled { continue; }
1109 let to_light = light.position - position;
1110 let dist = to_light.length();
1111 if dist >= light.range { continue; }
1112 let light_dir = to_light / dist.max(1e-7);
1113 let dist_atten = light.attenuation.evaluate(dist, light.range);
1114 let cone_atten = light.cone_attenuation(light_dir);
1115 let radiance = light.color * light.intensity * dist_atten * cone_atten;
1116 lo += Self::brdf(normal, view_dir, light_dir, mat) * radiance;
1117 }
1118
1119 let ambient = {
1121 let mut best_probe_w = 0.0_f32;
1122 let mut best_probe_col = Vec3::ZERO;
1123 for probe in &manager.probes {
1124 if !probe.enabled { continue; }
1125 let dist = (probe.position - position).length();
1126 if dist > probe.radius { continue; }
1127 let w = (1.0 - dist / probe.radius).clamp(0.0, 1.0) * probe.weight;
1128 best_probe_col += probe.evaluate_sh(normal) * w;
1129 best_probe_w += w;
1130 }
1131 if best_probe_w > 1e-4 {
1132 best_probe_col / best_probe_w * mat.albedo * mat.ao
1133 } else {
1134 manager.ambient.evaluate(normal) * mat.albedo * mat.ao
1135 }
1136 };
1137
1138 lo + ambient + mat.emissive
1139 }
1140
1141 pub fn shade_sss(
1143 position: Vec3,
1144 normal: Vec3,
1145 view_pos: Vec3,
1146 mat: &PbrMaterial,
1147 manager: &LightManager,
1148 ) -> Vec3 {
1149 if mat.sss_radius < 1e-4 { return Vec3::ZERO; }
1150 let mut sss = Vec3::ZERO;
1151 let _view_dir = (view_pos - position).normalize_or_zero();
1152 for light in &manager.point_lights {
1154 if !light.enabled { continue; }
1155 let to_light = light.position - position;
1156 let dist = to_light.length();
1157 if dist >= light.range { continue; }
1158 let light_dir = to_light / dist.max(1e-7);
1159 let att = light.attenuation.evaluate(dist, light.range);
1160 let wrap = (normal.dot(light_dir) + mat.sss_radius) / (1.0 + mat.sss_radius);
1162 let wrap = wrap.max(0.0);
1163 sss += mat.sss_color * light.color * light.intensity * att * wrap;
1164 }
1165 sss
1166 }
1167}
1168
1169#[derive(Debug, Clone)]
1173pub struct RectLight {
1174 pub id: LightId,
1175 pub position: Vec3,
1176 pub right: Vec3,
1178 pub up: Vec3,
1180 pub color: Vec3,
1181 pub intensity: f32,
1182 pub two_sided: bool,
1183 pub enabled: bool,
1184 pub tag: Option<String>,
1185}
1186
1187impl RectLight {
1188 pub fn new(position: Vec3, right: Vec3, up: Vec3, color: Vec3, intensity: f32) -> Self {
1189 Self {
1190 id: LightId::invalid(),
1191 position,
1192 right,
1193 up,
1194 color,
1195 intensity,
1196 two_sided: false,
1197 enabled: true,
1198 tag: None,
1199 }
1200 }
1201
1202 pub fn width(&self) -> f32 { self.right.length() * 2.0 }
1203 pub fn height(&self) -> f32 { self.up.length() * 2.0 }
1204 pub fn area(&self) -> f32 { self.width() * self.height() }
1205 pub fn normal(&self) -> Vec3 { self.right.normalize_or_zero().cross(self.up.normalize_or_zero()).normalize_or_zero() }
1206
1207 pub fn nearest_point(&self, p: Vec3) -> Vec3 {
1209 let local = p - self.position;
1210 let r_hat = self.right.normalize_or_zero();
1211 let u_hat = self.up.normalize_or_zero();
1212 let r_half = self.right.length();
1213 let u_half = self.up.length();
1214 let r_proj = local.dot(r_hat).clamp(-r_half, r_half);
1215 let u_proj = local.dot(u_hat).clamp(-u_half, u_half);
1216 self.position + r_hat * r_proj + u_hat * u_proj
1217 }
1218
1219 pub fn irradiance_at(&self, p: Vec3, n: Vec3) -> Vec3 {
1221 if !self.enabled { return Vec3::ZERO; }
1222 let nearest = self.nearest_point(p);
1223 let to_light = nearest - p;
1224 let dist = to_light.length().max(1e-4);
1225 let light_dir = to_light / dist;
1226 let n_dot_l = n.dot(light_dir).max(0.0);
1227 let front_ok = if self.two_sided {
1228 true
1229 } else {
1230 self.normal().dot(-light_dir) >= 0.0
1231 };
1232 if !front_ok { return Vec3::ZERO; }
1233 let solid_angle = (self.area() / (dist * dist)).min(1.0);
1235 self.color * self.intensity * n_dot_l * solid_angle
1236 }
1237}
1238
1239#[derive(Debug, Clone)]
1241pub struct DiskLight {
1242 pub id: LightId,
1243 pub position: Vec3,
1244 pub normal: Vec3,
1245 pub radius: f32,
1246 pub color: Vec3,
1247 pub intensity: f32,
1248 pub two_sided: bool,
1249 pub enabled: bool,
1250 pub tag: Option<String>,
1251}
1252
1253impl DiskLight {
1254 pub fn new(position: Vec3, normal: Vec3, radius: f32, color: Vec3, intensity: f32) -> Self {
1255 Self {
1256 id: LightId::invalid(),
1257 position,
1258 normal: normal.normalize_or_zero(),
1259 radius,
1260 color,
1261 intensity,
1262 two_sided: false,
1263 enabled: true,
1264 tag: None,
1265 }
1266 }
1267
1268 pub fn area(&self) -> f32 { std::f32::consts::PI * self.radius * self.radius }
1269
1270 pub fn irradiance_at(&self, p: Vec3, n: Vec3) -> Vec3 {
1271 if !self.enabled { return Vec3::ZERO; }
1272 let to_light = self.position - p;
1273 let dist = to_light.length().max(1e-4);
1274 let light_dir = to_light / dist;
1275 let n_dot_l = n.dot(light_dir).max(0.0);
1276 let solid_angle = (self.area() / (dist * dist)).min(1.0);
1277 self.color * self.intensity * n_dot_l * solid_angle
1278 }
1279}
1280
1281#[derive(Debug, Clone)]
1285pub enum LightAnimation {
1286 Constant,
1288 Pulse { frequency: f32, min_intensity: f32, max_intensity: f32 },
1290 Flicker { speed: f32, depth: f32 },
1292 Strobe { frequency: f32 },
1294 Fade { start: f32, end: f32, duration: f32 },
1296 Math { func: MathFunction, base_intensity: f32, amplitude: f32 },
1298 ColorCycle { speed: f32, saturation: f32, value: f32 },
1300 Heartbeat { bpm: f32, base_intensity: f32 },
1302}
1303
1304impl LightAnimation {
1305 pub fn intensity_factor(&self, t: f32, id_seed: u32) -> f32 {
1307 let seed_offset = (id_seed as f32) * 0.317_f32;
1308 match self {
1309 Self::Constant => 1.0,
1310 Self::Pulse { frequency, min_intensity, max_intensity } => {
1311 let s = (t * frequency * std::f32::consts::TAU).sin() * 0.5 + 0.5;
1312 min_intensity + (max_intensity - min_intensity) * s
1313 }
1314 Self::Flicker { speed, depth } => {
1315 let n1 = (t * speed + seed_offset).sin() * 43758.5453;
1317 let n2 = (t * speed * 1.7 + seed_offset * 2.1).sin() * 23421.631;
1318 let noise = (n1.fract() + n2.fract()) * 0.5;
1319 1.0 - depth * noise.abs()
1320 }
1321 Self::Strobe { frequency } => {
1322 let phase = (t * frequency).fract();
1323 if phase < 0.5 { 1.0 } else { 0.0 }
1324 }
1325 Self::Fade { start, end, duration } => {
1326 let progress = (t / duration.max(1e-4)).clamp(0.0, 1.0);
1327 start + (end - start) * progress
1328 }
1329 Self::Math { func, base_intensity, amplitude } => {
1330 let v = func.evaluate(t, 0.0).clamp(-1.0, 1.0);
1331 (base_intensity + amplitude * v).max(0.0)
1332 }
1333 Self::ColorCycle { .. } => 1.0, Self::Heartbeat { bpm, base_intensity } => {
1335 let beat_t = (t * bpm / 60.0).fract();
1336 let pulse1 = (-((beat_t - 0.05) / 0.03).powi(2) * 8.0).exp();
1337 let pulse2 = (-((beat_t - 0.20) / 0.03).powi(2) * 8.0).exp();
1338 base_intensity + (pulse1 + pulse2 * 0.6) * (1.0 - base_intensity)
1339 }
1340 }
1341 }
1342
1343 pub fn color_at(&self, t: f32, base_color: Vec3) -> Vec3 {
1345 match self {
1346 Self::ColorCycle { speed, saturation, value } => {
1347 let hue = (t * speed).fract();
1348 let h6 = hue * 6.0;
1350 let hi = h6 as u32;
1351 let f = h6.fract();
1352 let p = value * (1.0 - saturation);
1353 let q = value * (1.0 - saturation * f);
1354 let tv = value * (1.0 - saturation * (1.0 - f));
1355 let (r, g, b) = match hi % 6 {
1356 0 => (*value, tv, p),
1357 1 => (q, *value, p),
1358 2 => (p, *value, tv),
1359 3 => (p, q, *value),
1360 4 => (tv, p, *value),
1361 _ => (*value, p, q),
1362 };
1363 Vec3::new(r, g, b)
1364 }
1365 _ => base_color,
1366 }
1367 }
1368}
1369
1370#[derive(Debug, Clone)]
1372pub struct AnimatedPointLight {
1373 pub light: PointLight,
1374 pub animation: LightAnimation,
1375 pub base_intensity: f32,
1377 pub base_color: Vec3,
1379}
1380
1381impl AnimatedPointLight {
1382 pub fn new(light: PointLight, animation: LightAnimation) -> Self {
1383 let base_intensity = light.intensity;
1384 let base_color = light.color;
1385 Self { light, animation, base_intensity, base_color }
1386 }
1387
1388 pub fn update(&mut self, dt: f32, time: f32) {
1389 let factor = self.animation.intensity_factor(time, self.light.id.0);
1390 self.light.intensity = self.base_intensity * factor;
1391 self.light.color = self.animation.color_at(time, self.base_color);
1392 let _ = dt;
1393 }
1394}
1395
1396#[derive(Debug, Clone)]
1398pub struct AnimatedSpotLight {
1399 pub light: SpotLight,
1400 pub animation: LightAnimation,
1401 pub base_intensity: f32,
1402 pub base_color: Vec3,
1403 pub orbit_speed: Option<f32>,
1405 pub orbit_axis: Vec3,
1406 orbit_angle: f32,
1407 base_direction: Vec3,
1408}
1409
1410impl AnimatedSpotLight {
1411 pub fn new(light: SpotLight, animation: LightAnimation) -> Self {
1412 let base_intensity = light.intensity;
1413 let base_color = light.color;
1414 let base_direction = light.direction;
1415 Self {
1416 light,
1417 animation,
1418 base_intensity,
1419 base_color,
1420 orbit_speed: None,
1421 orbit_axis: Vec3::Y,
1422 orbit_angle: 0.0,
1423 base_direction,
1424 }
1425 }
1426
1427 pub fn with_orbit(mut self, speed_rps: f32, axis: Vec3) -> Self {
1428 self.orbit_speed = Some(speed_rps);
1429 self.orbit_axis = axis.normalize_or_zero();
1430 self
1431 }
1432
1433 pub fn update(&mut self, dt: f32, time: f32) {
1434 let factor = self.animation.intensity_factor(time, self.light.id.0);
1435 self.light.intensity = self.base_intensity * factor;
1436 self.light.color = self.animation.color_at(time, self.base_color);
1437
1438 if let Some(speed) = self.orbit_speed {
1439 self.orbit_angle += speed * dt * std::f32::consts::TAU;
1440 let cos_a = self.orbit_angle.cos();
1441 let sin_a = self.orbit_angle.sin();
1442 let axis = self.orbit_axis;
1443 let d = self.base_direction;
1445 self.light.direction = d * cos_a
1446 + axis.cross(d) * sin_a
1447 + axis * axis.dot(d) * (1.0 - cos_a);
1448 }
1449 }
1450}
1451
1452#[derive(Debug, Clone)]
1458pub struct IesProfile {
1459 pub name: String,
1460 pub vertical_angles: Vec<f32>,
1462 pub horizontal_angles: Vec<f32>,
1464 pub candela: Vec<Vec<f32>>,
1466 pub max_candela: f32,
1468}
1469
1470impl IesProfile {
1471 pub fn uniform(name: impl Into<String>) -> Self {
1473 let v_angles = (0..=18).map(|i| i as f32 * 10.0).collect::<Vec<_>>();
1474 let h_angles = vec![0.0, 90.0, 180.0, 270.0, 360.0];
1475 let n_v = v_angles.len();
1476 let n_h = h_angles.len();
1477 let candela = vec![vec![1.0; n_v]; n_h];
1478 Self {
1479 name: name.into(),
1480 vertical_angles: v_angles,
1481 horizontal_angles: h_angles,
1482 candela,
1483 max_candela: 1.0,
1484 }
1485 }
1486
1487 pub fn downlight(name: impl Into<String>) -> Self {
1489 let v_angles = (0..=18).map(|i| i as f32 * 10.0).collect::<Vec<_>>();
1490 let h_angles = vec![0.0, 360.0];
1491 let n_v = v_angles.len();
1492 let n_h = h_angles.len();
1493 let candela = (0..n_h).map(|_| {
1495 v_angles.iter().map(|&angle| {
1496 let t = (angle / 90.0).min(1.0);
1497 (1.0 - t * t).max(0.0)
1498 }).collect::<Vec<_>>()
1499 }).collect::<Vec<_>>();
1500 Self {
1501 name: name.into(),
1502 vertical_angles: v_angles,
1503 horizontal_angles: h_angles,
1504 candela,
1505 max_candela: 1.0,
1506 }
1507 }
1508
1509 pub fn sample(&self, v_angle: f32, h_angle: f32) -> f32 {
1511 let v_angle = v_angle.clamp(0.0, 180.0);
1512 let h_angle = h_angle.rem_euclid(360.0);
1513
1514 let vi = self.vertical_angles.partition_point(|&a| a <= v_angle).min(self.vertical_angles.len() - 1);
1516 let vi0 = vi.saturating_sub(1);
1517 let vi1 = vi;
1518 let vt = if vi0 == vi1 { 0.0 } else {
1519 (v_angle - self.vertical_angles[vi0]) / (self.vertical_angles[vi1] - self.vertical_angles[vi0] + 1e-7)
1520 };
1521
1522 let hi = self.horizontal_angles.partition_point(|&a| a <= h_angle).min(self.horizontal_angles.len() - 1);
1524 let hi0 = hi.saturating_sub(1);
1525 let hi1 = hi % self.horizontal_angles.len();
1526
1527 let row0 = &self.candela[hi0];
1529 let row1 = &self.candela[hi1];
1530 let c00 = row0.get(vi0).copied().unwrap_or(0.0);
1531 let c01 = row0.get(vi1).copied().unwrap_or(0.0);
1532 let c10 = row1.get(vi0).copied().unwrap_or(0.0);
1533 let c11 = row1.get(vi1).copied().unwrap_or(0.0);
1534 let ht = if hi0 == hi1 { 0.0 } else {
1535 (h_angle - self.horizontal_angles[hi0]) / (self.horizontal_angles[hi1] - self.horizontal_angles[hi0] + 1e-7)
1536 };
1537 let c0 = c00 + (c01 - c00) * vt;
1538 let c1 = c10 + (c11 - c10) * vt;
1539 (c0 + (c1 - c0) * ht) / self.max_candela.max(1e-7)
1540 }
1541
1542 pub fn evaluate_direction(&self, light_dir: Vec3, fixture_down: Vec3) -> f32 {
1545 let cos_v = fixture_down.dot(light_dir).clamp(-1.0, 1.0);
1546 let v_angle = cos_v.acos().to_degrees();
1547 self.sample(v_angle, 0.0) }
1549}
1550
1551#[derive(Debug, Clone)]
1555pub struct ShadowCascade {
1556 pub near: f32,
1557 pub far: f32,
1558 pub resolution: u32,
1559 pub bias: f32,
1560 pub view_proj: Mat4,
1562}
1563
1564impl ShadowCascade {
1565 pub fn new(near: f32, far: f32, resolution: u32, bias: f32) -> Self {
1566 Self { near, far, resolution, bias, view_proj: Mat4::IDENTITY }
1567 }
1568
1569 pub fn update_view_proj(
1571 &mut self,
1572 light_dir: Vec3,
1573 camera_pos: Vec3,
1574 camera_forward: Vec3,
1575 camera_fov: f32,
1576 aspect: f32,
1577 ) {
1578 let (sin_h, cos_h) = (camera_fov * 0.5).sin_cos();
1580 let tan_h = sin_h / cos_h.max(1e-7);
1581 let tan_v = tan_h / aspect.max(1e-7);
1582
1583 let right = camera_forward.cross(Vec3::Y).normalize_or_zero();
1584 let up = right.cross(camera_forward).normalize_or_zero();
1585
1586 let corners: Vec<Vec3> = [self.near, self.far].iter().flat_map(|&d| {
1587 [
1588 camera_pos + camera_forward * d + right * tan_h * d + up * tan_v * d,
1589 camera_pos + camera_forward * d - right * tan_h * d + up * tan_v * d,
1590 camera_pos + camera_forward * d + right * tan_h * d - up * tan_v * d,
1591 camera_pos + camera_forward * d - right * tan_h * d - up * tan_v * d,
1592 ]
1593 }).collect();
1594
1595 let light_up = if light_dir.dot(Vec3::Y).abs() < 0.99 { Vec3::Y } else { Vec3::Z };
1597 let center = corners.iter().fold(Vec3::ZERO, |a, &b| a + b) / corners.len() as f32;
1598 let light_view = Mat4::look_at_rh(center - light_dir, center, light_up);
1599
1600 let mut min = Vec3::splat(f32::MAX);
1601 let mut max = Vec3::splat(f32::MIN);
1602 for c in &corners {
1603 let ls = light_view.transform_point3(*c);
1604 min = min.min(ls);
1605 max = max.max(ls);
1606 }
1607 let slack = 2.0;
1608 let proj = Mat4::orthographic_rh(
1609 min.x - slack, max.x + slack,
1610 min.y - slack, max.y + slack,
1611 -max.z - slack, -min.z + slack,
1612 );
1613 self.view_proj = proj * light_view;
1614 }
1615}
1616
1617#[derive(Debug, Clone)]
1619pub struct CsmSystem {
1620 pub cascades: Vec<ShadowCascade>,
1621 pub stabilize: bool, pub blend_band: f32, pub debug_vis: bool,
1624}
1625
1626impl CsmSystem {
1627 pub fn new(cascade_splits: &[f32], base_resolution: u32) -> Self {
1628 let cascades = cascade_splits.windows(2).map(|w| {
1629 ShadowCascade::new(w[0], w[1], base_resolution, 0.005)
1630 }).collect();
1631 Self {
1632 cascades,
1633 stabilize: true,
1634 blend_band: 0.1,
1635 debug_vis: false,
1636 }
1637 }
1638
1639 pub fn default_3_cascade() -> Self {
1640 Self::new(&[0.1, 8.0, 30.0, 100.0], 2048)
1641 }
1642
1643 pub fn update(
1644 &mut self,
1645 light_dir: Vec3,
1646 camera_pos: Vec3,
1647 camera_forward: Vec3,
1648 fov: f32,
1649 aspect: f32,
1650 ) {
1651 for c in &mut self.cascades {
1652 c.update_view_proj(light_dir, camera_pos, camera_forward, fov, aspect);
1653 }
1654 }
1655
1656 pub fn cascade_for_distance(&self, dist: f32) -> Option<usize> {
1658 for (i, c) in self.cascades.iter().enumerate() {
1659 if dist >= c.near && dist < c.far {
1660 return Some(i);
1661 }
1662 }
1663 None
1664 }
1665
1666 pub fn cascade_color(index: usize) -> Vec3 {
1668 match index % 4 {
1669 0 => Vec3::new(1.0, 0.0, 0.0),
1670 1 => Vec3::new(0.0, 1.0, 0.0),
1671 2 => Vec3::new(0.0, 0.0, 1.0),
1672 _ => Vec3::new(1.0, 1.0, 0.0),
1673 }
1674 }
1675}
1676
1677#[derive(Debug, Clone)]
1684pub struct IblEnvironment {
1685 pub name: String,
1686 pub irradiance_sh: [Vec3; 9],
1688 pub specular_mips: Vec<(f32, Vec<Vec3>)>,
1690 pub mip_width: u32,
1691 pub mip_height: u32,
1692 pub brdf_lut: Vec<Vec2>,
1694 pub brdf_lut_size: u32,
1695 pub exposure: f32,
1696 pub rotation_y: f32,
1697}
1698
1699impl IblEnvironment {
1700 pub fn grey(name: impl Into<String>, intensity: f32) -> Self {
1702 let color = Vec3::splat(intensity / std::f32::consts::PI);
1703 let mut sh = [Vec3::ZERO; 9];
1704 sh[0] = color * (1.0 / 0.282_095_f32);
1705 let lut_size = 64u32;
1706 let lut = Self::compute_brdf_lut(lut_size);
1707 Self {
1708 name: name.into(),
1709 irradiance_sh: sh,
1710 specular_mips: Vec::new(),
1711 mip_width: 0,
1712 mip_height: 0,
1713 brdf_lut: lut,
1714 brdf_lut_size: lut_size,
1715 exposure: 1.0,
1716 rotation_y: 0.0,
1717 }
1718 }
1719
1720 pub fn sky_gradient(sky_color: Vec3, ground_color: Vec3, intensity: f32) -> Self {
1722 let mut sh = [Vec3::ZERO; 9];
1723 sh[0] = (sky_color + ground_color) * 0.5 * intensity * (1.0 / 0.282_095_f32);
1725 sh[2] = (sky_color - ground_color) * intensity * (1.0 / 0.488_603_f32);
1727 let lut_size = 64u32;
1728 let lut = Self::compute_brdf_lut(lut_size);
1729 Self {
1730 name: "sky_gradient".to_string(),
1731 irradiance_sh: sh,
1732 specular_mips: Vec::new(),
1733 mip_width: 0,
1734 mip_height: 0,
1735 brdf_lut: lut,
1736 brdf_lut_size: lut_size,
1737 exposure: 1.0,
1738 rotation_y: 0.0,
1739 }
1740 }
1741
1742 pub fn eval_diffuse(&self, normal: Vec3) -> Vec3 {
1744 let n = normal;
1745 let c0 = 0.282_095_f32;
1746 let c1 = 0.488_603_f32;
1747 let c2 = 1.092_548_f32;
1748 let c3 = 0.315_392_f32;
1749 let c4 = 0.546_274_f32;
1750 let sh = &self.irradiance_sh;
1751 let result =
1752 sh[0] * c0
1753 + sh[1] * c1 * n.y
1754 + sh[2] * c1 * n.z
1755 + sh[3] * c1 * n.x
1756 + sh[4] * c2 * n.x * n.y
1757 + sh[5] * c2 * n.y * n.z
1758 + sh[6] * c3 * (3.0 * n.z * n.z - 1.0)
1759 + sh[7] * c2 * n.x * n.z
1760 + sh[8] * c4 * (n.x * n.x - n.y * n.y);
1761 result.max(Vec3::ZERO) * self.exposure
1762 }
1763
1764 pub fn eval_brdf_lut(&self, n_dot_v: f32, roughness: f32) -> Vec2 {
1766 if self.brdf_lut.is_empty() { return Vec2::new(1.0, 0.0); }
1767 let u = n_dot_v.clamp(0.0, 1.0);
1768 let v = roughness.clamp(0.0, 1.0);
1769 let n = self.brdf_lut_size as usize;
1770 let xi = ((u * (n - 1) as f32) as usize).min(n - 1);
1771 let yi = ((v * (n - 1) as f32) as usize).min(n - 1);
1772 self.brdf_lut.get(yi * n + xi).copied().unwrap_or(Vec2::new(1.0, 0.0))
1773 }
1774
1775 fn compute_brdf_lut(size: u32) -> Vec<Vec2> {
1777 let n = size as usize;
1778 let mut lut = vec![Vec2::ZERO; n * n];
1779 for yi in 0..n {
1780 let roughness = (yi as f32 + 0.5) / n as f32;
1781 for xi in 0..n {
1782 let n_dot_v = (xi as f32 + 0.5) / n as f32;
1783 let (scale, bias) = Self::integrate_brdf(n_dot_v, roughness);
1784 lut[yi * n + xi] = Vec2::new(scale, bias);
1785 }
1786 }
1787 lut
1788 }
1789
1790 fn integrate_brdf(n_dot_v: f32, roughness: f32) -> (f32, f32) {
1791 let v = Vec3::new((1.0 - n_dot_v * n_dot_v).sqrt(), 0.0, n_dot_v);
1792 let n = Vec3::Z;
1793 let mut a = 0.0_f32;
1794 let mut b = 0.0_f32;
1795 let samples = 1024u32;
1796 for i in 0..samples {
1797 let xi = Self::hammersley(i, samples);
1798 let h = Self::importance_sample_ggx(xi, n, roughness);
1799 let l = (2.0 * v.dot(h) * h - v).normalize_or_zero();
1800 let n_dot_l = n.dot(l).max(0.0);
1801 let n_dot_h = n.dot(h).max(0.0);
1802 let v_dot_h = v.dot(h).max(0.0);
1803 if n_dot_l > 0.0 {
1804 let g = PbrLighting::geometry_smith(n_dot_v, n_dot_l, roughness);
1805 let g_vis = (g * v_dot_h) / (n_dot_h * n_dot_v + 1e-7);
1806 let fc = (1.0 - v_dot_h).powi(5);
1807 a += (1.0 - fc) * g_vis;
1808 b += fc * g_vis;
1809 }
1810 }
1811 (a / samples as f32, b / samples as f32)
1812 }
1813
1814 fn hammersley(i: u32, n: u32) -> Vec2 {
1815 let radical = {
1816 let mut bits = i;
1817 bits = (bits << 16) | (bits >> 16);
1818 bits = ((bits & 0x55555555) << 1) | ((bits & 0xAAAAAAAA) >> 1);
1819 bits = ((bits & 0x33333333) << 2) | ((bits & 0xCCCCCCCC) >> 2);
1820 bits = ((bits & 0x0F0F0F0F) << 4) | ((bits & 0xF0F0F0F0) >> 4);
1821 bits = ((bits & 0x00FF00FF) << 8) | ((bits & 0xFF00FF00) >> 8);
1822 bits as f32 * 2.328_306_4e-10
1823 };
1824 Vec2::new(i as f32 / n as f32, radical)
1825 }
1826
1827 fn importance_sample_ggx(xi: Vec2, n: Vec3, roughness: f32) -> Vec3 {
1828 let a = roughness * roughness;
1829 let phi = 2.0 * std::f32::consts::PI * xi.x;
1830 let cos_theta = ((1.0 - xi.y) / (1.0 + (a * a - 1.0) * xi.y)).sqrt().clamp(0.0, 1.0);
1831 let sin_theta = (1.0 - cos_theta * cos_theta).sqrt();
1832 let h_local = Vec3::new(sin_theta * phi.cos(), sin_theta * phi.sin(), cos_theta);
1833 let up = if n.z.abs() < 0.999 { Vec3::Z } else { Vec3::X };
1835 let right = up.cross(n).normalize_or_zero();
1836 let up2 = n.cross(right);
1837 (right * h_local.x + up2 * h_local.y + n * h_local.z).normalize_or_zero()
1838 }
1839
1840 pub fn shade_ibl(&self, normal: Vec3, view_dir: Vec3, mat: &PbrMaterial) -> Vec3 {
1842 let n_dot_v = normal.dot(view_dir).clamp(0.0, 1.0);
1843 let diffuse = self.eval_diffuse(normal) * mat.albedo * (1.0 - mat.metallic);
1844 let f0 = mat.f0();
1845 let f = PbrLighting::fresnel_schlick(n_dot_v, f0 + Vec3::splat(mat.roughness * 0.5));
1846 let brdf_lut = self.eval_brdf_lut(n_dot_v, mat.roughness);
1847 let specular = f * brdf_lut.x + Vec3::splat(brdf_lut.y);
1848 (diffuse + specular) * mat.ao
1849 }
1850}
1851
1852#[derive(Debug, Clone)]
1856pub struct ExposureSettings {
1857 pub ev100: f32, pub auto_exposure: bool,
1859 pub auto_min_ev: f32,
1860 pub auto_max_ev: f32,
1861 pub auto_adapt_speed: f32, pub tonemap_mode: ToneMapMode,
1863 pub white_point: f32,
1864}
1865
1866#[derive(Debug, Clone, Copy, PartialEq)]
1867pub enum ToneMapMode {
1868 Linear,
1869 Reinhard,
1870 ReinhardLuminance,
1871 Aces,
1872 AcesApprox,
1873 Uncharted2,
1874 Hejl,
1875 Custom { a: f32, b: f32, c: f32, d: f32, e: f32, f: f32 },
1876}
1877
1878impl Default for ExposureSettings {
1879 fn default() -> Self {
1880 Self {
1881 ev100: 0.0,
1882 auto_exposure: false,
1883 auto_min_ev: -4.0,
1884 auto_max_ev: 12.0,
1885 auto_adapt_speed: 2.0,
1886 tonemap_mode: ToneMapMode::AcesApprox,
1887 white_point: 1.0,
1888 }
1889 }
1890}
1891
1892impl ExposureSettings {
1893 pub fn exposure_factor(&self) -> f32 {
1894 let iso = 100.0_f32;
1896 let n_shutter = 1.0_f32;
1897 let aperture = 1.0_f32;
1898 let ev = self.ev100 + (iso / 100.0).log2();
1899 let lmax = (aperture * aperture / n_shutter) * (100.0 / iso) * 12.5;
1900 1.0 / (lmax * (2.0_f32).powf(ev) * std::f32::consts::PI)
1901 }
1902
1903 pub fn tonemap(&self, color: Vec3) -> Vec3 {
1905 let c = color * self.exposure_factor();
1906 match self.tonemap_mode {
1907 ToneMapMode::Linear => c.clamp(Vec3::ZERO, Vec3::ONE),
1908 ToneMapMode::Reinhard => c / (c + Vec3::ONE),
1909 ToneMapMode::ReinhardLuminance => {
1910 let lum = c.dot(Vec3::new(0.2126, 0.7152, 0.0722));
1911 c * ((lum + 1.0) / (lum * (1.0 + lum / (self.white_point * self.white_point)) + 1.0))
1912 }
1913 ToneMapMode::Aces => Self::aces_filmic(c),
1914 ToneMapMode::AcesApprox => Self::aces_approx(c),
1915 ToneMapMode::Uncharted2 => Self::uncharted2(c, self.white_point),
1916 ToneMapMode::Hejl => {
1917 let a = (c * (6.2 * c + 0.5)) / (c * (6.2 * c + 1.7) + 0.06);
1918 a.clamp(Vec3::ZERO, Vec3::ONE)
1919 }
1920 ToneMapMode::Custom { a, b, c: cc, d, e, f } => {
1921 let x = c;
1922 ((x * (a * x + Vec3::splat(b))) + Vec3::splat(d))
1923 / ((x * (a * x + Vec3::splat(cc))) + Vec3::splat(e))
1924 - Vec3::splat(f / cc)
1925 }
1926 }
1927 }
1928
1929 fn aces_filmic(x: Vec3) -> Vec3 {
1930 let a = 2.51_f32;
1931 let b = 0.03_f32;
1932 let c = 2.43_f32;
1933 let d = 0.59_f32;
1934 let e = 0.14_f32;
1935 ((x * (a * x + Vec3::splat(b))) / (x * (c * x + Vec3::splat(d)) + Vec3::splat(e)))
1936 .clamp(Vec3::ZERO, Vec3::ONE)
1937 }
1938
1939 fn aces_approx(x: Vec3) -> Vec3 {
1940 let x = x * 0.6;
1941 let a = 2.51_f32;
1942 let b = 0.03_f32;
1943 let c = 2.43_f32;
1944 let d = 0.59_f32;
1945 let e = 0.14_f32;
1946 ((x * (a * x + Vec3::splat(b))) / (x * (c * x + Vec3::splat(d)) + Vec3::splat(e)))
1947 .clamp(Vec3::ZERO, Vec3::ONE)
1948 }
1949
1950 fn uncharted2(x: Vec3, white: f32) -> Vec3 {
1951 fn curve(v: Vec3) -> Vec3 {
1952 let a = 0.15_f32; let b = 0.50_f32; let c = 0.10_f32;
1953 let d = 0.20_f32; let e = 0.02_f32; let f = 0.30_f32;
1954 (v * (a * v + Vec3::splat(c * b)) + Vec3::splat(d * e))
1955 / (v * (a * v + Vec3::splat(b)) + Vec3::splat(d * f))
1956 - Vec3::splat(e / f)
1957 }
1958 let curr = curve(x * 2.0);
1959 let white_sc = curve(Vec3::splat(white));
1960 (curr / white_sc).clamp(Vec3::ZERO, Vec3::ONE)
1961 }
1962
1963 pub fn auto_expose(&mut self, scene_luminance: f32, dt: f32) {
1965 if !self.auto_exposure { return; }
1966 let target_ev = scene_luminance.max(1e-7).log2() + 3.0;
1967 let target_ev = target_ev.clamp(self.auto_min_ev, self.auto_max_ev);
1968 let delta = (target_ev - self.ev100).clamp(-self.auto_adapt_speed * dt, self.auto_adapt_speed * dt);
1969 self.ev100 += delta;
1970 }
1971}
1972
1973pub struct LightBaker {
1980 pub sample_count: u32,
1981 pub hemisphere_samples: Vec<Vec3>,
1982}
1983
1984#[derive(Debug, Clone)]
1986pub struct LightMap {
1987 pub width: u32,
1988 pub height: u32,
1989 pub texels: Vec<Vec3>,
1990}
1991
1992impl LightMap {
1993 pub fn new(width: u32, height: u32) -> Self {
1994 Self { width, height, texels: vec![Vec3::ZERO; (width * height) as usize] }
1995 }
1996
1997 pub fn set(&mut self, x: u32, y: u32, color: Vec3) {
1998 let idx = (y * self.width + x) as usize;
1999 if idx < self.texels.len() {
2000 self.texels[idx] = color;
2001 }
2002 }
2003
2004 pub fn get(&self, x: u32, y: u32) -> Vec3 {
2005 self.texels.get((y * self.width + x) as usize).copied().unwrap_or(Vec3::ZERO)
2006 }
2007
2008 pub fn sample_bilinear(&self, u: f32, v: f32) -> Vec3 {
2009 let px = (u * self.width as f32 - 0.5).max(0.0);
2010 let py = (v * self.height as f32 - 0.5).max(0.0);
2011 let x0 = px as u32;
2012 let y0 = py as u32;
2013 let x1 = (x0 + 1).min(self.width - 1);
2014 let y1 = (y0 + 1).min(self.height - 1);
2015 let fx = px.fract();
2016 let fy = py.fract();
2017 let c00 = self.get(x0, y0);
2018 let c10 = self.get(x1, y0);
2019 let c01 = self.get(x0, y1);
2020 let c11 = self.get(x1, y1);
2021 let cx0 = c00.lerp(c10, fx);
2022 let cx1 = c01.lerp(c11, fx);
2023 cx0.lerp(cx1, fy)
2024 }
2025
2026 pub fn blur(&self, radius: u32) -> LightMap {
2028 let mut out = LightMap::new(self.width, self.height);
2029 let r = radius as i32;
2030 for y in 0..self.height {
2031 for x in 0..self.width {
2032 let mut sum = Vec3::ZERO;
2033 let mut count = 0_u32;
2034 for dy in -r..=r {
2035 for dx in -r..=r {
2036 let nx = x as i32 + dx;
2037 let ny = y as i32 + dy;
2038 if nx >= 0 && nx < self.width as i32 && ny >= 0 && ny < self.height as i32 {
2039 sum += self.get(nx as u32, ny as u32);
2040 count += 1;
2041 }
2042 }
2043 }
2044 out.set(x, y, if count > 0 { sum / count as f32 } else { Vec3::ZERO });
2045 }
2046 }
2047 out
2048 }
2049}
2050
2051impl LightBaker {
2052 pub fn new(sample_count: u32) -> Self {
2053 let hemisphere_samples = Self::generate_hemisphere_samples(sample_count);
2054 Self { sample_count, hemisphere_samples }
2055 }
2056
2057 fn generate_hemisphere_samples(n: u32) -> Vec<Vec3> {
2058 (0..n).map(|i| {
2059 let xi0 = (i as f32 + 0.5) / n as f32;
2060 let xi1 = {
2061 let mut bits = i;
2062 bits = (bits << 16) | (bits >> 16);
2063 bits = ((bits & 0x55555555) << 1) | ((bits & 0xAAAAAAAA) >> 1);
2064 bits as f32 * 2.328_306_4e-10
2065 };
2066 let phi = 2.0 * std::f32::consts::PI * xi1;
2067 let cos_theta = xi0.sqrt();
2068 let sin_theta = (1.0 - cos_theta * cos_theta).sqrt();
2069 Vec3::new(sin_theta * phi.cos(), sin_theta * phi.sin(), cos_theta)
2070 }).collect()
2071 }
2072
2073 pub fn bake_point_ibl(&self, position: Vec3, normal: Vec3, env: &IblEnvironment) -> Vec3 {
2075 env.eval_diffuse(normal)
2076 }
2077
2078 pub fn bake_point_direct(&self, position: Vec3, normal: Vec3, manager: &LightManager) -> Vec3 {
2080 manager.evaluate_cpu(position, normal)
2081 }
2082
2083 pub fn bake_samples(
2085 &self,
2086 samples: &[(Vec3, Vec3)], manager: &LightManager,
2088 env: &IblEnvironment,
2089 ) -> Vec<Vec3> {
2090 samples.iter().map(|&(pos, nor)| {
2091 self.bake_point_direct(pos, nor, manager)
2092 + self.bake_point_ibl(pos, nor, env)
2093 }).collect()
2094 }
2095
2096 pub fn bake_plane(
2098 &self,
2099 width: u32,
2100 height: u32,
2101 origin: Vec3,
2102 u_axis: Vec3, v_axis: Vec3, normal: Vec3,
2105 manager: &LightManager,
2106 env: &IblEnvironment,
2107 ) -> LightMap {
2108 let mut map = LightMap::new(width, height);
2109 for y in 0..height {
2110 let tv = (y as f32 + 0.5) / height as f32;
2111 for x in 0..width {
2112 let tu = (x as f32 + 0.5) / width as f32;
2113 let pos = origin + u_axis * tu + v_axis * tv;
2114 let irr = self.bake_point_direct(pos, normal, manager)
2115 + self.bake_point_ibl(pos, normal, env);
2116 map.set(x, y, irr);
2117 }
2118 }
2119 map
2120 }
2121}
2122
2123impl LightManager {
2126 pub fn update_animated(&mut self, _animated_points: &mut [AnimatedPointLight], _animated_spots: &mut [AnimatedSpotLight], time: f32, dt: f32) {
2128 for ap in _animated_points.iter_mut() {
2129 ap.update(dt, time);
2130 }
2131 for asp in _animated_spots.iter_mut() {
2132 asp.update(dt, time);
2133 }
2134 }
2135
2136 pub fn generate_glsl_uniforms(&self) -> String {
2138 let mut s = String::new();
2139 s.push_str(&format!(
2140 "uniform int u_num_point_lights;\n\
2141 uniform int u_num_spot_lights;\n\
2142 uniform int u_has_directional;\n"
2143 ));
2144 for (i, l) in self.point_lights.iter().take(64).enumerate() {
2145 s.push_str(&format!(
2146 "uniform vec3 u_point_pos[{i}];\n\
2147 uniform vec3 u_point_color[{i}];\n\
2148 uniform float u_point_intensity[{i}];\n\
2149 uniform float u_point_range[{i}];\n"
2150 ));
2151 }
2152 s
2153 }
2154
2155 pub fn shadow_caster_count(&self) -> usize {
2157 self.point_lights.iter().filter(|l| l.cast_shadow).count()
2158 + self.spot_lights.iter().filter(|l| l.cast_shadow).count()
2159 + if self.directional.as_ref().map(|d| d.cast_shadow).unwrap_or(false) { 1 } else { 0 }
2160 }
2161
2162 pub fn serialize_compact(&self) -> Vec<u8> {
2164 let mut buf = Vec::new();
2165 let push_f32 = |buf: &mut Vec<u8>, v: f32| buf.extend_from_slice(&v.to_le_bytes());
2166 let push_v3 = |buf: &mut Vec<u8>, v: Vec3| {
2167 buf.extend_from_slice(&v.x.to_le_bytes());
2168 buf.extend_from_slice(&v.y.to_le_bytes());
2169 buf.extend_from_slice(&v.z.to_le_bytes());
2170 };
2171 buf.extend_from_slice(&(self.point_lights.len() as u32).to_le_bytes());
2173 buf.extend_from_slice(&(self.spot_lights.len() as u32).to_le_bytes());
2174 for l in &self.point_lights {
2175 push_v3(&mut buf, l.position);
2176 push_v3(&mut buf, l.color);
2177 push_f32(&mut buf, l.intensity);
2178 push_f32(&mut buf, l.range);
2179 }
2180 for l in &self.spot_lights {
2181 push_v3(&mut buf, l.position);
2182 push_v3(&mut buf, l.direction);
2183 push_v3(&mut buf, l.color);
2184 push_f32(&mut buf, l.intensity);
2185 push_f32(&mut buf, l.range);
2186 push_f32(&mut buf, l.inner_angle);
2187 push_f32(&mut buf, l.outer_angle);
2188 }
2189 buf
2190 }
2191
2192 pub fn add_torch(&mut self, position: Vec3) -> LightId {
2194 self.add_point_light(
2195 PointLight::new(position, Vec3::new(1.0, 0.5, 0.2), 2.5, 6.0)
2196 .with_tag("torch"),
2197 )
2198 }
2199
2200 pub fn add_fluorescent(&mut self, position: Vec3) -> LightId {
2202 self.add_point_light(
2203 PointLight::new(position, Vec3::new(0.85, 0.9, 1.0), 3.5, 12.0)
2204 .with_tag("fluorescent"),
2205 )
2206 }
2207
2208 pub fn add_candle(&mut self, position: Vec3) -> LightId {
2210 self.add_point_light(
2211 PointLight::new(position, Vec3::new(1.0, 0.65, 0.3), 0.8, 3.0)
2212 .with_attenuation(Attenuation::InverseSquare)
2213 .with_tag("candle"),
2214 )
2215 }
2216
2217 pub fn add_led_strip(&mut self, from: Vec3, to: Vec3, color: Vec3, segment_count: u32) -> Vec<LightId> {
2219 (0..segment_count).map(|i| {
2220 let t = (i as f32 + 0.5) / segment_count as f32;
2221 let p = from.lerp(to, t);
2222 self.add_point_light(
2223 PointLight::new(p, color, 1.5, 2.0).with_tag("led_strip"),
2224 )
2225 }).collect()
2226 }
2227}
2228
2229#[cfg(test)]
2232mod tests {
2233 use super::*;
2234
2235 #[test]
2236 fn test_attenuation_falloff() {
2237 let a = Attenuation::InverseSquare;
2238 assert!((a.evaluate(1.0, 10.0) - 1.0).abs() < 1e-4);
2239 assert!((a.evaluate(2.0, 10.0) - 0.25).abs() < 1e-4);
2240 }
2241
2242 #[test]
2243 fn test_point_light_contribution() {
2244 let light = PointLight::new(Vec3::new(0.0, 5.0, 0.0), Vec3::ONE, 1.0, 20.0);
2245 let surface = Vec3::ZERO;
2246 let normal = Vec3::Y;
2247 let contrib = light.contribution(surface, normal);
2248 assert!(contrib.length() > 0.0);
2249 }
2250
2251 #[test]
2252 fn test_spot_light_cone() {
2253 let mut light = SpotLight::new(Vec3::new(0.0, 5.0, 0.0), -Vec3::Y, Vec3::ONE, 1.0, 20.0);
2254 light.inner_angle = 0.2;
2255 light.outer_angle = 0.5;
2256 let directly_below = Vec3::ZERO;
2257 let c = light.contribution(directly_below, Vec3::Y);
2258 assert!(c.length() > 0.0);
2259 }
2260
2261 #[test]
2262 fn test_pbr_material_f0() {
2263 let mat = PbrMaterial::dielectric(Vec3::ONE, 0.5);
2264 let f0 = mat.f0();
2265 assert!(f0.x > 0.0 && f0.x < 1.0);
2266 let metal = PbrMaterial::metal(Vec3::new(0.8, 0.7, 0.1), 0.2);
2267 assert!((metal.f0().x - 0.8).abs() < 1e-5);
2269 }
2270
2271 #[test]
2272 fn test_pbr_brdf_zero_behind() {
2273 let mat = PbrMaterial::dielectric(Vec3::ONE, 0.5);
2274 let result = PbrLighting::brdf(Vec3::Y, Vec3::Y, -Vec3::Y, &mat);
2276 assert_eq!(result, Vec3::ZERO);
2277 }
2278
2279 #[test]
2280 fn test_light_manager_presets() {
2281 let m = LightManager::preset_daylight();
2282 assert!(m.directional.is_some());
2283 let m = LightManager::preset_dungeon();
2284 assert!(m.directional.is_none());
2285 }
2286
2287 #[test]
2288 fn test_ambient_hemisphere() {
2289 let amb = AmbientLight::hemisphere(Vec3::new(0.5, 0.6, 0.9), Vec3::new(0.2, 0.2, 0.1), 1.0);
2290 let top = amb.evaluate(Vec3::Y);
2291 let bottom = amb.evaluate(-Vec3::Y);
2292 assert!(top.x > bottom.x || top.z > bottom.z);
2293 }
2294
2295 #[test]
2296 fn test_sh_probe() {
2297 let probe = LightProbe::from_uniform_color(Vec3::ZERO, 5.0, Vec3::ONE);
2298 let result = probe.evaluate_sh(Vec3::Y);
2299 assert!(result.length() > 0.0);
2300 }
2301
2302 #[test]
2303 fn test_ibl_diffuse_grey() {
2304 let ibl = IblEnvironment::grey("test", 1.0);
2305 let result = ibl.eval_diffuse(Vec3::Y);
2306 assert!(result.length() > 0.0 && result.length() < 10.0);
2307 }
2308
2309 #[test]
2310 fn test_tonemap_modes() {
2311 let settings = ExposureSettings { ev100: 0.0, ..Default::default() };
2312 let hdr = Vec3::new(2.0, 1.5, 0.8);
2313 let mapped = settings.tonemap(hdr);
2314 assert!(mapped.x <= 1.0 && mapped.y <= 1.0 && mapped.z <= 1.0);
2315 }
2316
2317 #[test]
2318 fn test_lightmap_blur() {
2319 let mut map = LightMap::new(8, 8);
2320 map.set(4, 4, Vec3::ONE);
2321 let blurred = map.blur(1);
2322 assert!(blurred.get(3, 4).length() > 0.0);
2323 }
2324
2325 #[test]
2326 fn test_csm_cascade_find() {
2327 let csm = CsmSystem::default_3_cascade();
2328 assert_eq!(csm.cascade_for_distance(5.0), Some(0));
2329 assert_eq!(csm.cascade_for_distance(200.0), None);
2330 }
2331
2332 #[test]
2333 fn test_animated_light_flicker() {
2334 let anim = LightAnimation::Flicker { speed: 10.0, depth: 0.3 };
2335 let f0 = anim.intensity_factor(0.0, 0);
2336 let f1 = anim.intensity_factor(0.1, 0);
2337 assert!(f0 >= 0.7 && f0 <= 1.0);
2339 assert!(f1 >= 0.7 && f1 <= 1.0);
2340 }
2341
2342 #[test]
2343 fn test_ies_profile_downlight() {
2344 let ies = IesProfile::downlight("test");
2345 let v0 = ies.sample(0.0, 0.0);
2347 let v90 = ies.sample(90.0, 0.0);
2349 assert!(v0 > v90);
2350 }
2351
2352 #[test]
2353 fn test_rect_light_irradiance() {
2354 let rl = RectLight::new(
2355 Vec3::new(0.0, 5.0, 0.0),
2356 Vec3::new(2.0, 0.0, 0.0),
2357 Vec3::new(0.0, 0.0, 2.0),
2358 Vec3::ONE, 5.0,
2359 );
2360 let irr = rl.irradiance_at(Vec3::ZERO, Vec3::Y);
2361 assert!(irr.length() > 0.0);
2362 }
2363
2364 #[test]
2365 fn test_light_baker_plane() {
2366 let manager = LightManager::preset_daylight();
2367 let env = IblEnvironment::grey("grey", 0.5);
2368 let baker = LightBaker::new(64);
2369 let map = baker.bake_plane(
2370 4, 4,
2371 Vec3::ZERO, Vec3::X * 4.0, Vec3::Z * 4.0, Vec3::Y,
2372 &manager, &env,
2373 );
2374 assert_eq!(map.texels.len(), 16);
2375 assert!(map.texels.iter().any(|c| c.length() > 0.0));
2376 }
2377
2378 #[test]
2379 fn test_manager_serialize() {
2380 let mut m = LightManager::new();
2381 m.add_point_light(PointLight::new(Vec3::ZERO, Vec3::ONE, 1.0, 5.0));
2382 let bytes = m.serialize_compact();
2383 assert!(!bytes.is_empty());
2384 }
2385
2386 #[test]
2387 fn test_exposure_auto_adapt() {
2388 let mut settings = ExposureSettings {
2389 auto_exposure: true,
2390 auto_min_ev: -4.0,
2391 auto_max_ev: 12.0,
2392 auto_adapt_speed: 10.0,
2393 ev100: 0.0,
2394 ..Default::default()
2395 };
2396 settings.auto_expose(100.0, 1.0);
2397 assert!(settings.ev100 != 0.0);
2398 }
2399}