khora_core/renderer/
light.rs

1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Defines light types for the rendering system.
16//!
17//! This module provides the data structures for representing different light sources
18//! in a scene. These types are used by the ECS components in `khora-data` and by
19//! the render lanes in `khora-lanes` to calculate lighting during rendering.
20
21use crate::math::{LinearRgba, Vec3};
22
23/// A directional light source that illuminates from a uniform direction.
24///
25/// Directional lights simulate infinitely distant light sources like the sun.
26/// They have no position, only a direction, and cast parallel rays with no falloff.
27///
28/// # Examples
29///
30/// ```
31/// use khora_core::renderer::light::DirectionalLight;
32/// use khora_core::math::{Vec3, LinearRgba};
33///
34/// // Create a warm sunlight
35/// let sun = DirectionalLight {
36///     direction: Vec3::new(-0.5, -1.0, -0.3).normalize(),
37///     color: LinearRgba::new(1.0, 0.95, 0.8, 1.0),
38///     intensity: 1.0,
39///     shadow_enabled: true,
40///     shadow_bias: 0.005,
41///     shadow_normal_bias: 0.02,
42/// };
43/// ```
44#[derive(Debug, Clone, Copy, PartialEq)]
45pub struct DirectionalLight {
46    /// The direction the light is pointing (normalized).
47    ///
48    /// This vector points from the light source towards the scene.
49    /// For a sun at noon, this would be `(0, -1, 0)`.
50    pub direction: Vec3,
51
52    /// The color of the light in linear RGB space.
53    pub color: LinearRgba,
54
55    /// The intensity multiplier for the light.
56    ///
57    /// A value of 1.0 represents standard intensity.
58    /// Higher values create brighter lights, useful for HDR rendering.
59    pub intensity: f32,
60
61    /// Whether this light casts shadows.
62    pub shadow_enabled: bool,
63    /// Constant bias to apply to depth values to prevent shadow acne.
64    pub shadow_bias: f32,
65    /// Normal-based bias to apply to prevent shadow acne on sloped surfaces.
66    pub shadow_normal_bias: f32,
67}
68
69impl Default for DirectionalLight {
70    fn default() -> Self {
71        Self {
72            // Default: light coming from above and slightly forward
73            direction: Vec3::new(0.0, -1.0, -0.5).normalize(),
74            color: LinearRgba::WHITE,
75            intensity: 1.0,
76            shadow_enabled: false,
77            shadow_bias: 0.005,
78            shadow_normal_bias: 0.0,
79        }
80    }
81}
82
83/// A point light source that emits light in all directions from a single point.
84///
85/// Point lights simulate local light sources like light bulbs or candles.
86/// They have a position (provided by the entity's transform) and attenuate
87/// with distance according to the inverse-square law.
88///
89/// # Examples
90///
91/// ```
92/// use khora_core::renderer::light::PointLight;
93/// use khora_core::math::LinearRgba;
94///
95/// // Create a warm indoor light
96/// let lamp = PointLight {
97///     color: LinearRgba::new(1.0, 0.9, 0.7, 1.0),
98///     intensity: 100.0,
99///     range: 10.0,
100///     shadow_enabled: false,
101///     shadow_bias: 0.01,
102///     shadow_normal_bias: 0.0,
103/// };
104/// ```
105#[derive(Debug, Clone, Copy, PartialEq)]
106pub struct PointLight {
107    /// The color of the light in linear RGB space.
108    pub color: LinearRgba,
109
110    /// The intensity of the light in lumens.
111    ///
112    /// Higher values create brighter lights. This is used in conjunction
113    /// with the physically-based attenuation formula.
114    pub intensity: f32,
115
116    /// The maximum range of the light in world units.
117    ///
118    /// Beyond this distance, the light has no effect. This is used for
119    /// performance optimization to cull lights that won't contribute
120    /// to a fragment's lighting.
121    pub range: f32,
122
123    /// Whether this light casts shadows.
124    pub shadow_enabled: bool,
125    /// Constant bias to apply to depth values to prevent shadow acne.
126    pub shadow_bias: f32,
127    /// Normal-based bias to apply to prevent shadow acne on sloped surfaces.
128    pub shadow_normal_bias: f32,
129}
130
131impl Default for PointLight {
132    fn default() -> Self {
133        Self {
134            color: LinearRgba::WHITE,
135            intensity: 100.0,
136            range: 10.0,
137            shadow_enabled: false,
138            shadow_bias: 0.01,
139            shadow_normal_bias: 0.0,
140        }
141    }
142}
143
144/// A spot light source that emits light in a cone from a single point.
145///
146/// Spot lights are like point lights but restricted to a cone of influence.
147/// They're useful for flashlights, stage lights, and car headlights.
148///
149/// # Examples
150///
151/// ```
152/// use khora_core::renderer::light::SpotLight;
153/// use khora_core::math::{Vec3, LinearRgba};
154///
155/// // Create a flashlight
156/// let flashlight = SpotLight {
157///     direction: Vec3::new(0.0, 0.0, -1.0),
158///     color: LinearRgba::WHITE,
159///     intensity: 200.0,
160///     range: 20.0,
161///     inner_cone_angle: 15.0_f32.to_radians(),
162///     outer_cone_angle: 30.0_f32.to_radians(),
163///     shadow_enabled: false,
164///     shadow_bias: 0.01,
165///     shadow_normal_bias: 0.0,
166/// };
167/// ```
168#[derive(Debug, Clone, Copy, PartialEq)]
169pub struct SpotLight {
170    /// The direction the spotlight is pointing (normalized).
171    pub direction: Vec3,
172
173    /// The color of the light in linear RGB space.
174    pub color: LinearRgba,
175
176    /// The intensity of the light in lumens.
177    pub intensity: f32,
178
179    /// The maximum range of the light in world units.
180    pub range: f32,
181
182    /// The angle in radians at which the light begins to fall off.
183    ///
184    /// Within this angle from the center of the cone, the light is at full intensity.
185    pub inner_cone_angle: f32,
186
187    /// The angle in radians at which the light is fully attenuated.
188    ///
189    /// Beyond this angle from the center of the cone, there is no light.
190    /// The region between inner and outer cone angles has smooth falloff.
191    pub outer_cone_angle: f32,
192
193    /// Whether this light casts shadows.
194    pub shadow_enabled: bool,
195    /// Constant bias to apply to depth values to prevent shadow acne.
196    pub shadow_bias: f32,
197    /// Normal-based bias to apply to prevent shadow acne on sloped surfaces.
198    pub shadow_normal_bias: f32,
199}
200
201impl Default for SpotLight {
202    fn default() -> Self {
203        Self {
204            direction: Vec3::new(0.0, -1.0, 0.0),
205            color: LinearRgba::WHITE,
206            intensity: 200.0,
207            range: 15.0,
208            inner_cone_angle: 20.0_f32.to_radians(),
209            outer_cone_angle: 35.0_f32.to_radians(),
210            shadow_enabled: false,
211            shadow_bias: 0.01,
212            shadow_normal_bias: 0.0,
213        }
214    }
215}
216
217/// An enumeration of all supported light types.
218///
219/// This enum allows a single `Light` component to represent any type of light source.
220/// The render lanes use this to determine how to calculate lighting contributions.
221#[derive(Debug, Clone, Copy, PartialEq)]
222pub enum LightType {
223    /// A directional light (sun-like, infinite distance, no falloff).
224    Directional(DirectionalLight),
225    /// A point light (omni-directional with distance falloff).
226    Point(PointLight),
227    /// A spotlight (cone-shaped with distance and angular falloff).
228    Spot(SpotLight),
229}
230
231impl Default for LightType {
232    fn default() -> Self {
233        LightType::Directional(DirectionalLight::default())
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240    use crate::math::EPSILON;
241
242    fn approx_eq(a: f32, b: f32) -> bool {
243        (a - b).abs() < EPSILON
244    }
245
246    #[test]
247    fn test_directional_light_default() {
248        let light = DirectionalLight::default();
249        assert_eq!(light.color, LinearRgba::WHITE);
250        assert!(approx_eq(light.intensity, 1.0));
251        // Direction should be normalized
252        assert!(approx_eq(light.direction.length(), 1.0));
253    }
254
255    #[test]
256    fn test_directional_light_custom() {
257        let direction = Vec3::new(1.0, -1.0, 0.0).normalize();
258        let light = DirectionalLight {
259            direction,
260            color: LinearRgba::new(1.0, 0.5, 0.0, 1.0),
261            intensity: 2.0,
262            shadow_enabled: false,
263            shadow_bias: 0.005,
264            shadow_normal_bias: 0.0,
265        };
266        assert!(approx_eq(light.direction.length(), 1.0));
267        assert!(approx_eq(light.intensity, 2.0));
268    }
269
270    #[test]
271    fn test_point_light_default() {
272        let light = PointLight::default();
273        assert_eq!(light.color, LinearRgba::WHITE);
274        assert!(approx_eq(light.intensity, 100.0));
275        assert!(approx_eq(light.range, 10.0));
276    }
277
278    #[test]
279    fn test_point_light_custom() {
280        let light = PointLight {
281            color: LinearRgba::new(0.0, 1.0, 0.0, 1.0),
282            intensity: 50.0,
283            range: 5.0,
284            shadow_enabled: false,
285            shadow_bias: 0.01,
286            shadow_normal_bias: 0.0,
287        };
288        assert!(approx_eq(light.intensity, 50.0));
289        assert!(approx_eq(light.range, 5.0));
290    }
291
292    #[test]
293    fn test_spot_light_default() {
294        let light = SpotLight::default();
295        assert_eq!(light.color, LinearRgba::WHITE);
296        assert!(light.inner_cone_angle < light.outer_cone_angle);
297        assert!(approx_eq(light.direction.length(), 1.0));
298    }
299
300    #[test]
301    fn test_spot_light_cone_angles() {
302        let light = SpotLight {
303            inner_cone_angle: 10.0_f32.to_radians(),
304            outer_cone_angle: 45.0_f32.to_radians(),
305            ..Default::default()
306        };
307        assert!(light.inner_cone_angle < light.outer_cone_angle);
308        assert!(light.outer_cone_angle < std::f32::consts::FRAC_PI_2);
309    }
310
311    #[test]
312    fn test_light_type_default() {
313        let light = LightType::default();
314        match light {
315            LightType::Directional(_) => {}
316            _ => panic!("Expected Directional light as default"),
317        }
318    }
319
320    #[test]
321    fn test_light_type_variants() {
322        let dir = LightType::Directional(DirectionalLight::default());
323        let point = LightType::Point(PointLight::default());
324        let spot = LightType::Spot(SpotLight::default());
325
326        assert!(matches!(dir, LightType::Directional(_)));
327        assert!(matches!(point, LightType::Point(_)));
328        assert!(matches!(spot, LightType::Spot(_)));
329    }
330}