Skip to main content

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