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/// };
40/// ```
41#[derive(Debug, Clone, Copy, PartialEq)]
42pub struct DirectionalLight {
43    /// The direction the light is pointing (normalized).
44    ///
45    /// This vector points from the light source towards the scene.
46    /// For a sun at noon, this would be `(0, -1, 0)`.
47    pub direction: Vec3,
48
49    /// The color of the light in linear RGB space.
50    pub color: LinearRgba,
51
52    /// The intensity multiplier for the light.
53    ///
54    /// A value of 1.0 represents standard intensity.
55    /// Higher values create brighter lights, useful for HDR rendering.
56    pub intensity: f32,
57}
58
59impl Default for DirectionalLight {
60    fn default() -> Self {
61        Self {
62            // Default: light coming from above and slightly forward
63            direction: Vec3::new(0.0, -1.0, -0.5).normalize(),
64            color: LinearRgba::WHITE,
65            intensity: 1.0,
66        }
67    }
68}
69
70/// A point light source that emits light in all directions from a single point.
71///
72/// Point lights simulate local light sources like light bulbs or candles.
73/// They have a position (provided by the entity's transform) and attenuate
74/// with distance according to the inverse-square law.
75///
76/// # Examples
77///
78/// ```
79/// use khora_core::renderer::light::PointLight;
80/// use khora_core::math::LinearRgba;
81///
82/// // Create a warm indoor light
83/// let lamp = PointLight {
84///     color: LinearRgba::new(1.0, 0.9, 0.7, 1.0),
85///     intensity: 100.0,
86///     range: 10.0,
87/// };
88/// ```
89#[derive(Debug, Clone, Copy, PartialEq)]
90pub struct PointLight {
91    /// The color of the light in linear RGB space.
92    pub color: LinearRgba,
93
94    /// The intensity of the light in lumens.
95    ///
96    /// Higher values create brighter lights. This is used in conjunction
97    /// with the physically-based attenuation formula.
98    pub intensity: f32,
99
100    /// The maximum range of the light in world units.
101    ///
102    /// Beyond this distance, the light has no effect. This is used for
103    /// performance optimization to cull lights that won't contribute
104    /// to a fragment's lighting.
105    pub range: f32,
106}
107
108impl Default for PointLight {
109    fn default() -> Self {
110        Self {
111            color: LinearRgba::WHITE,
112            intensity: 100.0,
113            range: 10.0,
114        }
115    }
116}
117
118/// A spot light source that emits light in a cone from a single point.
119///
120/// Spot lights are like point lights but restricted to a cone of influence.
121/// They're useful for flashlights, stage lights, and car headlights.
122///
123/// # Examples
124///
125/// ```
126/// use khora_core::renderer::light::SpotLight;
127/// use khora_core::math::{Vec3, LinearRgba};
128///
129/// // Create a flashlight
130/// let flashlight = SpotLight {
131///     direction: Vec3::new(0.0, 0.0, -1.0),
132///     color: LinearRgba::WHITE,
133///     intensity: 200.0,
134///     range: 20.0,
135///     inner_cone_angle: 15.0_f32.to_radians(),
136///     outer_cone_angle: 30.0_f32.to_radians(),
137/// };
138/// ```
139#[derive(Debug, Clone, Copy, PartialEq)]
140pub struct SpotLight {
141    /// The direction the spotlight is pointing (normalized).
142    pub direction: Vec3,
143
144    /// The color of the light in linear RGB space.
145    pub color: LinearRgba,
146
147    /// The intensity of the light in lumens.
148    pub intensity: f32,
149
150    /// The maximum range of the light in world units.
151    pub range: f32,
152
153    /// The angle in radians at which the light begins to fall off.
154    ///
155    /// Within this angle from the center of the cone, the light is at full intensity.
156    pub inner_cone_angle: f32,
157
158    /// The angle in radians at which the light is fully attenuated.
159    ///
160    /// Beyond this angle from the center of the cone, there is no light.
161    /// The region between inner and outer cone angles has smooth falloff.
162    pub outer_cone_angle: f32,
163}
164
165impl Default for SpotLight {
166    fn default() -> Self {
167        Self {
168            direction: Vec3::new(0.0, -1.0, 0.0),
169            color: LinearRgba::WHITE,
170            intensity: 200.0,
171            range: 15.0,
172            inner_cone_angle: 20.0_f32.to_radians(),
173            outer_cone_angle: 35.0_f32.to_radians(),
174        }
175    }
176}
177
178/// An enumeration of all supported light types.
179///
180/// This enum allows a single `Light` component to represent any type of light source.
181/// The render lanes use this to determine how to calculate lighting contributions.
182#[derive(Debug, Clone, Copy, PartialEq)]
183pub enum LightType {
184    /// A directional light (sun-like, infinite distance, no falloff).
185    Directional(DirectionalLight),
186    /// A point light (omni-directional with distance falloff).
187    Point(PointLight),
188    /// A spotlight (cone-shaped with distance and angular falloff).
189    Spot(SpotLight),
190}
191
192impl Default for LightType {
193    fn default() -> Self {
194        LightType::Directional(DirectionalLight::default())
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201    use crate::math::EPSILON;
202
203    fn approx_eq(a: f32, b: f32) -> bool {
204        (a - b).abs() < EPSILON
205    }
206
207    #[test]
208    fn test_directional_light_default() {
209        let light = DirectionalLight::default();
210        assert_eq!(light.color, LinearRgba::WHITE);
211        assert!(approx_eq(light.intensity, 1.0));
212        // Direction should be normalized
213        assert!(approx_eq(light.direction.length(), 1.0));
214    }
215
216    #[test]
217    fn test_directional_light_custom() {
218        let direction = Vec3::new(1.0, -1.0, 0.0).normalize();
219        let light = DirectionalLight {
220            direction,
221            color: LinearRgba::new(1.0, 0.5, 0.0, 1.0),
222            intensity: 2.0,
223        };
224        assert!(approx_eq(light.direction.length(), 1.0));
225        assert!(approx_eq(light.intensity, 2.0));
226    }
227
228    #[test]
229    fn test_point_light_default() {
230        let light = PointLight::default();
231        assert_eq!(light.color, LinearRgba::WHITE);
232        assert!(approx_eq(light.intensity, 100.0));
233        assert!(approx_eq(light.range, 10.0));
234    }
235
236    #[test]
237    fn test_point_light_custom() {
238        let light = PointLight {
239            color: LinearRgba::new(0.0, 1.0, 0.0, 1.0),
240            intensity: 50.0,
241            range: 5.0,
242        };
243        assert!(approx_eq(light.intensity, 50.0));
244        assert!(approx_eq(light.range, 5.0));
245    }
246
247    #[test]
248    fn test_spot_light_default() {
249        let light = SpotLight::default();
250        assert_eq!(light.color, LinearRgba::WHITE);
251        assert!(light.inner_cone_angle < light.outer_cone_angle);
252        assert!(approx_eq(light.direction.length(), 1.0));
253    }
254
255    #[test]
256    fn test_spot_light_cone_angles() {
257        let light = SpotLight {
258            inner_cone_angle: 10.0_f32.to_radians(),
259            outer_cone_angle: 45.0_f32.to_radians(),
260            ..Default::default()
261        };
262        assert!(light.inner_cone_angle < light.outer_cone_angle);
263        assert!(light.outer_cone_angle < std::f32::consts::FRAC_PI_2);
264    }
265
266    #[test]
267    fn test_light_type_default() {
268        let light = LightType::default();
269        match light {
270            LightType::Directional(_) => {}
271            _ => panic!("Expected Directional light as default"),
272        }
273    }
274
275    #[test]
276    fn test_light_type_variants() {
277        let dir = LightType::Directional(DirectionalLight::default());
278        let point = LightType::Point(PointLight::default());
279        let spot = LightType::Spot(SpotLight::default());
280
281        assert!(matches!(dir, LightType::Directional(_)));
282        assert!(matches!(point, LightType::Point(_)));
283        assert!(matches!(spot, LightType::Spot(_)));
284    }
285}