Skip to main content

rusterix/map/
light.rs

1use crate::{Value, ValueContainer};
2use theframework::prelude::*;
3use vek::{Vec2, Vec3};
4
5/// Parameters for flickering
6#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
7pub enum LightType {
8    Point,
9    Ambient,
10    AmbientDaylight,
11    Spot,
12    Area,
13    Daylight,
14}
15
16impl LightType {
17    /// Returns the name of the light type as a string slice
18    pub fn name(&self) -> &'static str {
19        match self {
20            LightType::Point => "Point",
21            LightType::Ambient => "Ambient",
22            LightType::AmbientDaylight => "Ambient Daylight",
23            LightType::Spot => "Spot",
24            LightType::Area => "Area",
25            LightType::Daylight => "Daylight",
26        }
27    }
28}
29
30#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
31pub struct Light {
32    pub light_type: LightType,
33    pub properties: ValueContainer,
34    pub active: bool,
35}
36
37impl Light {
38    pub fn new(light_type: LightType) -> Self {
39        Self {
40            light_type,
41            properties: ValueContainer::default(),
42            active: true,
43        }
44    }
45
46    /// Set the position with the builder pattern.
47    pub fn with_position(mut self, position: Vec3<f32>) -> Self {
48        self.set_position(position);
49        self
50    }
51
52    /// Set the color with the builder pattern.
53    pub fn with_color(mut self, color: [f32; 3]) -> Self {
54        self.set_color(color);
55        self
56    }
57
58    /// Set the intensity with the builder pattern.
59    pub fn with_intensity(mut self, intensity: f32) -> Self {
60        self.set_intensity(intensity);
61        self
62    }
63
64    /// Set the start distance with the builder pattern.
65    pub fn with_start_distance(mut self, start: f32) -> Self {
66        self.set_start_distance(start);
67        self
68    }
69
70    /// Set the end distance with the builder pattern.
71    pub fn with_end_distance(mut self, end: f32) -> Self {
72        self.set_end_distance(end);
73        self
74    }
75
76    /// Set the flicker with the builder pattern.
77    pub fn with_flicker(mut self, flicker: f32) -> Self {
78        self.set_flicker(flicker);
79        self
80    }
81
82    /// Helper: get the position from the ValueContainer (defaults to [0,0,0] if not found)
83    fn get_position(&self) -> Vec3<f32> {
84        let p = self
85            .properties
86            .get_vec3("position")
87            .unwrap_or([0.0, 0.0, 0.0]);
88        Vec3::new(p[0], p[1], p[2])
89    }
90
91    /// Helper: get color (defaults to white if not found)
92    pub fn get_color(&self) -> [f32; 3] {
93        self.properties.get_vec3("color").unwrap_or([1.0, 1.0, 1.0])
94    }
95
96    /// Helper: get intensity (defaults to 1.0 if not found)
97    pub fn get_intensity(&self) -> f32 {
98        self.properties.get_float_default("intensity", 1.0)
99    }
100
101    /// Helper: get start distance (defaults to 3.0 if not found)
102    pub fn get_start_distance(&self) -> f32 {
103        self.properties.get_float_default("start_distance", 1.0)
104    }
105
106    /// Helper: get end distance (defaults to 5.0 if not found)
107    pub fn get_end_distance(&self) -> f32 {
108        self.properties.get_float_default("end_distance", 2.0)
109    }
110
111    /// Helper: get flicker
112    pub fn get_flicker(&self) -> f32 {
113        self.properties.get_float_default("flicker", 0.0)
114    }
115
116    /// Returns the position of the light (3D)
117    pub fn position(&self) -> Vec3<f32> {
118        self.get_position()
119    }
120
121    /// Returns the position of the light in 2D (x, z)
122    pub fn position_2d(&self) -> Vec2<f32> {
123        let p = self.position();
124        Vec2::new(p.x, p.z)
125    }
126
127    /// Loads and caches all the parameters from the value container into a CompiledLight.
128    pub fn compile(&self) -> CompiledLight {
129        // Common parameters
130        let position = {
131            let p = self
132                .properties
133                .get_vec3("position")
134                .unwrap_or([0.0, 0.0, 0.0]);
135            Vec3::new(p[0], p[1], p[2])
136        };
137        let color = self.properties.get_vec3("color").unwrap_or([1.0, 1.0, 1.0]);
138        let intensity = self.properties.get_float_default("intensity", 1.0);
139
140        // For Point and Spot lights (if used)
141        let start_distance = self.properties.get_float_default("start_distance", 1.0);
142        let end_distance = self.properties.get_float_default("end_distance", 2.0);
143
144        let flicker = self.properties.get_float_default("flicker", 0.0);
145
146        // For spot lights:
147        let direction = {
148            let d = self
149                .properties
150                .get_vec3("direction")
151                .unwrap_or([0.0, 0.0, -1.0]);
152            Vec3::new(d[0], d[1], d[2]).normalized()
153        };
154        let cone_angle = self
155            .properties
156            .get_float_default("cone_angle", std::f32::consts::FRAC_PI_4);
157
158        // For area lights:
159        let normal = {
160            let n = self
161                .properties
162                .get_vec3("normal")
163                .unwrap_or([0.0, 1.0, 0.0]);
164            Vec3::new(n[0], n[1], n[2]).normalized()
165        };
166        let width = self.properties.get_float_default("width", 1.0);
167        let height = self.properties.get_float_default("height", 1.0);
168        let emitting = self.properties.get_bool_default("emitting", true);
169
170        let from_linedef = self.properties.get_bool_default("from_linedef", false);
171
172        CompiledLight {
173            light_type: self.light_type,
174            // common
175            position,
176            color,
177            intensity,
178            emitting,
179            // point/spot
180            start_distance,
181            end_distance,
182            flicker,
183            // spot
184            direction,
185            cone_angle,
186            // area
187            normal,
188            width,
189            height,
190
191            from_linedef,
192        }
193    }
194
195    /// Set the position of the light
196    pub fn set_position(&mut self, position: Vec3<f32>) {
197        self.properties.set(
198            "position",
199            Value::Vec3([position.x, position.y, position.z]),
200        );
201    }
202
203    /// Sets the color of the light
204    pub fn set_color(&mut self, new_color: [f32; 3]) {
205        self.properties.set("color", Value::Vec3(new_color));
206    }
207
208    /// Sets the intensity of the light
209    pub fn set_intensity(&mut self, new_intensity: f32) {
210        self.properties
211            .set("intensity", Value::Float(new_intensity));
212    }
213
214    /// Sets the start distance (for Point or Spot)
215    pub fn set_start_distance(&mut self, new_start_distance: f32) {
216        self.properties
217            .set("start_distance", Value::Float(new_start_distance));
218    }
219
220    /// Sets the end distance (for Point or Spot)
221    pub fn set_end_distance(&mut self, new_end_distance: f32) {
222        self.properties
223            .set("end_distance", Value::Float(new_end_distance));
224    }
225
226    /// Set flicker frequency and amplitude
227    pub fn set_flicker(&mut self, flicker: f32) {
228        self.properties.set("flicker", Value::Float(flicker));
229    }
230
231    /// Create a copy of the light and adjust position and direction from the linedef attributes.
232    pub fn from_linedef(&self, p1: Vec2<f32>, p2: Vec2<f32>, height: f32) -> Self {
233        let position = (p1 + p2) / 2.0; // Midpoint of the line
234        let direction = (p2 - p1).normalized(); // Direction of the line
235        let normal = Vec2::new(direction.y, -direction.x); // Perpendicular normal
236        let width = (p2 - p1).magnitude(); // Line segment length
237        let offset = 0.1;
238        let position = position + normal * offset;
239
240        match self.light_type {
241            LightType::Point => {
242                let mut light = Light::new(LightType::Point);
243                light.set_position(Vec3::new(position.x, height, position.y));
244
245                if let Some(start_distance) = self.properties.get("start_distance") {
246                    light
247                        .properties
248                        .set("start_distance", start_distance.clone());
249                }
250                if let Some(end_distance) = self.properties.get("end_distance") {
251                    light.properties.set("end_distance", end_distance.clone());
252                }
253                if let Some(intensity) = self.properties.get("intensity") {
254                    light.properties.set("intensity", intensity.clone());
255                }
256                if let Some(color) = self.properties.get("color") {
257                    light.properties.set("color", color.clone());
258                }
259
260                light
261            }
262            LightType::Ambient | LightType::AmbientDaylight => self.clone(),
263            LightType::Spot => {
264                let mut light = Light::new(LightType::Spot);
265                light.set_position(Vec3::new(position.x, height, position.y));
266
267                light
268                    .properties
269                    .set("direction", Value::Vec3([normal.x, 0.0, normal.y]));
270
271                if let Some(cone_angle) = self.properties.get("cone_angle") {
272                    light.properties.set("cone_angle", cone_angle.clone());
273                }
274                if let Some(intensity) = self.properties.get("intensity") {
275                    light.properties.set("intensity", intensity.clone());
276                }
277                if let Some(color) = self.properties.get("color") {
278                    light.properties.set("color", color.clone());
279                }
280                if let Some(start_distance) = self.properties.get("start_distance") {
281                    light
282                        .properties
283                        .set("start_distance", start_distance.clone());
284                }
285                if let Some(end_distance) = self.properties.get("end_distance") {
286                    light.properties.set("end_distance", end_distance.clone());
287                }
288
289                light
290            }
291            LightType::Area => {
292                let mut light = Light::new(LightType::Area);
293                light.properties.set("from_linedef", Value::Bool(true));
294                light.set_position(Vec3::new(position.x, height, position.y));
295
296                light
297                    .properties
298                    .set("normal", Value::Vec3([normal.x, 0.0, normal.y]));
299
300                // Set the width to match the line segment
301                light.properties.set("width", Value::Float(width));
302                light.properties.set("height", Value::Float(1.0));
303
304                if let Some(intensity) = self.properties.get("intensity") {
305                    light.properties.set("intensity", intensity.clone());
306                }
307                if let Some(color) = self.properties.get("color") {
308                    light.properties.set("color", color.clone());
309                }
310                if let Some(start_distance) = self.properties.get("start_distance") {
311                    light
312                        .properties
313                        .set("start_distance", start_distance.clone());
314                }
315                if let Some(end_distance) = self.properties.get("end_distance") {
316                    light.properties.set("end_distance", end_distance.clone());
317                }
318
319                light
320            }
321            LightType::Daylight => {
322                let mut light = Light::new(LightType::Area);
323                light.set_position(Vec3::new(position.x, height, position.y));
324
325                if let Some(intensity) = self.properties.get("intensity") {
326                    light.properties.set("intensity", intensity.clone());
327                }
328                if let Some(color) = self.properties.get("color") {
329                    light.properties.set("color", color.clone());
330                }
331                if let Some(start_distance) = self.properties.get("start_distance") {
332                    light
333                        .properties
334                        .set("start_distance", start_distance.clone());
335                }
336                if let Some(end_distance) = self.properties.get("end_distance") {
337                    light.properties.set("end_distance", end_distance.clone());
338                }
339
340                light
341            }
342        }
343    }
344
345    /// Create a copy of the light and adjust position and direction based on the sectors center and normal.
346    pub fn from_sector(&self, center: Vec3<f32>, size: Vec2<f32>) -> Self {
347        let normal = Vec3::new(0.0, 1.0, 0.0);
348        let offset = 0.1; // Small forward push to avoid occlusion
349        let position = center + normal * offset;
350
351        match self.light_type {
352            LightType::Point => {
353                let mut light = Light::new(LightType::Point);
354                light.set_position(position);
355
356                // Copy common properties
357                if let Some(start_distance) = self.properties.get("start_distance") {
358                    light
359                        .properties
360                        .set("start_distance", start_distance.clone());
361                }
362                if let Some(end_distance) = self.properties.get("end_distance") {
363                    light.properties.set("end_distance", end_distance.clone());
364                }
365                if let Some(intensity) = self.properties.get("intensity") {
366                    light.properties.set("intensity", intensity.clone());
367                }
368                if let Some(color) = self.properties.get("color") {
369                    light.properties.set("color", color.clone());
370                }
371
372                light
373            }
374            LightType::Ambient | LightType::AmbientDaylight => self.clone(),
375            LightType::Spot => {
376                let mut light = Light::new(LightType::Spot);
377                light.set_position(position);
378
379                light
380                    .properties
381                    .set("direction", Value::Vec3([0.0, 1.0, 0.0]));
382
383                if let Some(cone_angle) = self.properties.get("cone_angle") {
384                    light.properties.set("cone_angle", cone_angle.clone());
385                }
386                if let Some(start_distance) = self.properties.get("start_distance") {
387                    light
388                        .properties
389                        .set("start_distance", start_distance.clone());
390                }
391                if let Some(color) = self.properties.get("color") {
392                    light.properties.set("color", color.clone());
393                }
394                if let Some(end_distance) = self.properties.get("end_distance") {
395                    light.properties.set("end_distance", end_distance.clone());
396                }
397                if let Some(intensity) = self.properties.get("intensity") {
398                    light.properties.set("intensity", intensity.clone());
399                }
400
401                light
402            }
403            LightType::Area => {
404                let mut light = Light::new(LightType::Area);
405                light.properties.set("from_sector", Value::Bool(true));
406                light.set_position(position);
407
408                light
409                    .properties
410                    .set("normal", Value::Vec3([normal.x, normal.y, normal.z]));
411
412                light.properties.set("width", Value::Float(size.x));
413                light.properties.set("height", Value::Float(size.y));
414
415                if let Some(color) = self.properties.get("color") {
416                    light.properties.set("color", color.clone());
417                }
418                if let Some(start_distance) = self.properties.get("start_distance") {
419                    light
420                        .properties
421                        .set("start_distance", start_distance.clone());
422                }
423                if let Some(end_distance) = self.properties.get("end_distance") {
424                    light.properties.set("end_distance", end_distance.clone());
425                }
426                if let Some(intensity) = self.properties.get("intensity") {
427                    light.properties.set("intensity", intensity.clone());
428                }
429
430                light
431            }
432            LightType::Daylight => {
433                let mut light = Light::new(LightType::Area);
434                light.set_position(position);
435
436                if let Some(intensity) = self.properties.get("intensity") {
437                    light.properties.set("intensity", intensity.clone());
438                }
439                if let Some(color) = self.properties.get("color") {
440                    light.properties.set("color", color.clone());
441                }
442                if let Some(end_distance) = self.properties.get("end_distance") {
443                    light.properties.set("end_distance", end_distance.clone());
444                }
445                if let Some(intensity) = self.properties.get("intensity") {
446                    light.properties.set("intensity", intensity.clone());
447                }
448
449                light
450            }
451        }
452    }
453}
454
455/// A “compiled” version of Light that caches all values needed for rendering.
456#[derive(Debug, Clone)]
457pub struct CompiledLight {
458    pub light_type: LightType,
459    // common parameters
460    pub position: Vec3<f32>,
461    pub color: [f32; 3],
462    pub intensity: f32,
463    pub emitting: bool,
464    // for point and spot lights
465    pub start_distance: f32,
466    pub end_distance: f32,
467    pub flicker: f32,
468    // for spot lights
469    pub direction: Vec3<f32>,
470    pub cone_angle: f32,
471    // for area lights
472    pub normal: Vec3<f32>,
473    pub width: f32,
474    pub height: f32,
475
476    pub from_linedef: bool,
477}
478
479impl CompiledLight {
480    /// Returns the 3D position of the light.
481    pub fn position(&self) -> Vec3<f32> {
482        self.position
483    }
484
485    /// Returns the 2D position of the light (x, z).
486    pub fn position_2d(&self) -> Vec2<f32> {
487        Vec2::new(self.position.x, self.position.z)
488    }
489
490    /// Calculate the light's intensity and color at a given point.
491    pub fn color_at(&self, point: Vec3<f32>, hash: &u32, d2: bool) -> Option<[f32; 3]> {
492        if !self.emitting {
493            return None;
494        };
495        match self.light_type {
496            LightType::Point => self.calculate_point_light(point, hash),
497            LightType::Ambient | LightType::AmbientDaylight => self.calculate_ambient_light(hash),
498            LightType::Spot => self.calculate_spot_light(point, hash),
499            LightType::Area => self.calculate_area_light(point, hash, d2),
500            LightType::Daylight => self.calculate_daylight_light(point, hash),
501        }
502    }
503
504    pub fn radiance_at(
505        &self,
506        point: Vec3<f32>,
507        surface_normal: Option<Vec3<f32>>,
508        hash: u32,
509    ) -> Option<Vec3<f32>> {
510        let incoming = match self.color_at(point, &hash, false) {
511            Some(c) => Vec3::new(c[0], c[1], c[2]),
512            None => return None,
513        };
514
515        // For ambient lights, skip Lambert shading
516        if matches!(
517            self.light_type,
518            LightType::Ambient | LightType::AmbientDaylight | LightType::Daylight
519        ) {
520            return Some(incoming);
521        }
522
523        // If no surface normal, just return the light color
524        let n = match surface_normal {
525            Some(n) => n,
526            None => return Some(incoming),
527        };
528
529        // Lambert: scale by cosine of angle
530        let dir_to_light = (self.position - point).normalized();
531        let lambert = n.dot(dir_to_light).max(0.0);
532        Some(incoming * lambert)
533    }
534
535    fn calculate_point_light(&self, point: Vec3<f32>, hash: &u32) -> Option<[f32; 3]> {
536        let distance = (point - self.position).magnitude();
537
538        // Beyond end_distance => no intensity
539        if distance >= self.end_distance {
540            return None;
541        }
542
543        // Within start_distance => full intensity
544        if distance <= self.start_distance {
545            return Some(self.apply_flicker(self.color, self.intensity, self.flicker, hash));
546        }
547
548        // Smooth attenuation between start and end
549        let attenuation = self.smoothstep(self.end_distance, self.start_distance, distance);
550        let adjusted_intensity = self.intensity * attenuation;
551        Some(self.apply_flicker(self.color, adjusted_intensity, self.flicker, hash))
552    }
553
554    fn calculate_ambient_light(&self, hash: &u32) -> Option<[f32; 3]> {
555        // Ambient light does not attenuate by distance.
556        Some(self.apply_flicker(self.color, self.intensity, self.flicker, hash))
557    }
558
559    fn calculate_spot_light(&self, point: Vec3<f32>, hash: &u32) -> Option<[f32; 3]> {
560        let distance = (point - self.position).magnitude();
561        if distance >= self.end_distance {
562            return None;
563        }
564
565        let attenuation = if distance <= self.start_distance {
566            1.0
567        } else {
568            1.0 - ((distance - self.start_distance) / (self.end_distance - self.start_distance))
569        };
570
571        // Check if the point is within the spot cone
572        let direction_to_point = (point - self.position).normalized();
573        let angle = self.direction.dot(direction_to_point).acos();
574        if angle > self.cone_angle {
575            return None;
576        }
577
578        let adjusted_intensity = self.intensity * attenuation;
579        Some(self.apply_flicker(self.color, adjusted_intensity, self.flicker, hash))
580    }
581
582    fn calculate_area_light(&self, point: Vec3<f32>, _hash: &u32, d2: bool) -> Option<[f32; 3]> {
583        let to_point = point - self.position;
584        let distance = to_point.magnitude();
585
586        if distance >= self.end_distance {
587            return None;
588        }
589
590        if distance < 0.1 {
591            return Some(self.color);
592        }
593
594        let distance_attenuation = if distance <= self.start_distance {
595            1.0
596        } else {
597            self.smoothstep(self.end_distance, self.start_distance, distance)
598        };
599        let area = self.width * self.height;
600
601        let direction = to_point.normalized();
602
603        if self.from_linedef {
604            // let angle_attenuation = self.normal.dot(direction).max(0.0);
605            let attenuation = /*angle_attenuation **/ distance_attenuation * area * self.intensity;
606            Some([
607                self.color[0] * attenuation,
608                self.color[1] * attenuation,
609                self.color[2] * attenuation,
610            ])
611        } else {
612            let attenuation = if d2 {
613                let distance_x = (to_point.x / (self.width * 0.5)).abs(); // Normalize by half-width
614                let distance_y = (to_point.y / (self.height * 0.5)).abs(); // Normalize by half-height
615                let attenuation_x = (1.0 - distance_x).max(0.0);
616                let attenuation_y = (1.0 - distance_y).max(0.0);
617                attenuation_x * attenuation_y * distance_attenuation * self.intensity
618            } else {
619                let angle_attenuation = self.normal.dot(direction).max(0.0);
620                angle_attenuation * distance_attenuation * area * self.intensity
621            };
622            Some([
623                self.color[0] * attenuation,
624                self.color[1] * attenuation,
625                self.color[2] * attenuation,
626            ])
627        }
628    }
629
630    fn calculate_daylight_light(&self, point: Vec3<f32>, _hash: &u32) -> Option<[f32; 3]> {
631        let to_point = point - self.position;
632        let distance = to_point.magnitude();
633
634        // If outside the end distance, return no light
635        if distance >= self.end_distance {
636            return None;
637        }
638
639        let direction = to_point.normalized();
640        let angle_attenuation = self.normal.dot(direction).max(0.0);
641        let distance_attenuation = if distance <= self.start_distance {
642            1.0
643        } else {
644            self.smoothstep(self.end_distance, self.start_distance, distance)
645        };
646        let attenuation = angle_attenuation * distance_attenuation * self.intensity;
647
648        Some([
649            self.color[0] * attenuation,
650            self.color[1] * attenuation,
651            self.color[2] * attenuation,
652        ])
653    }
654
655    /// Applies flicker effect to the light color.
656    fn apply_flicker(&self, color: [f32; 3], intensity: f32, flicker: f32, hash: &u32) -> [f32; 3] {
657        let flicker_factor = if flicker > 0.0 {
658            let combined_hash = hash.wrapping_add(
659                (self.position.x as u32 + self.position.y as u32 + self.position.z as u32) * 100,
660            );
661            let flicker_value = (combined_hash as f32 / u32::MAX as f32).clamp(0.0, 1.0);
662            1.0 - flicker_value * flicker
663        } else {
664            1.0
665        };
666
667        [
668            color[0] * intensity * flicker_factor,
669            color[1] * intensity * flicker_factor,
670            color[2] * intensity * flicker_factor,
671        ]
672    }
673
674    fn smoothstep(&self, edge0: f32, edge1: f32, x: f32) -> f32 {
675        let t = ((x - edge0) / (edge1 - edge0)).clamp(0.0, 1.0);
676        t * t * (3.0 - 2.0 * t)
677    }
678}