Skip to main content

euv_engine/lighting/
impl.rs

1use super::*;
2
3/// Implements factory constructors and shading for [`Light`], [`Material`],
4/// and [`LightingUniforms`].
5impl Light {
6    /// Creates a new directional light pointing in `direction` with `color`.
7    ///
8    /// The `direction` is normalized internally; intensity defaults to 1.0.
9    /// Falloff and spot half-angle are unused for directional lights.
10    ///
11    /// # Arguments
12    ///
13    /// - `Vector3D` - The unit direction toward the light source.
14    /// - `Vector3D` - The RGB intensity multiplier.
15    ///
16    /// # Returns
17    ///
18    /// - `Light` - The new directional light.
19    pub fn new_directional(direction: Vector3D, color: Vector3D) -> Light {
20        Light::new(
21            LightType::Directional,
22            Vector3D::zero(),
23            direction.normalized(),
24            color,
25            1.0,
26            0.0,
27            0.0,
28        )
29    }
30
31    /// Creates a new point light at `position` with `color` and `intensity`.
32    ///
33    /// Falloff defaults to 1.0 (inverse-square). The `direction` field is
34    /// unused for point lights and is set to the zero vector.
35    ///
36    /// # Arguments
37    ///
38    /// - `Vector3D` - The world-space position of the light.
39    /// - `Vector3D` - The RGB intensity multiplier.
40    /// - `f64` - The intensity scalar.
41    ///
42    /// # Returns
43    ///
44    /// - `Light` - The new point light.
45    pub fn new_point(position: Vector3D, color: Vector3D, intensity: f64) -> Light {
46        Light::new(
47            LightType::Point,
48            position,
49            Vector3D::zero(),
50            color,
51            intensity,
52            1.0,
53            0.0,
54        )
55    }
56
57    /// Creates a new spotlight at `position` shining in `direction`.
58    ///
59    /// The cone is defined by `half_angle_rad`; the cosine of that angle
60    /// is stored for fast cone-test comparisons during shading.
61    ///
62    /// # Arguments
63    ///
64    /// - `Vector3D` - The world-space position of the light.
65    /// - `Vector3D` - The unit direction the cone opens along.
66    /// - `Vector3D` - The RGB intensity multiplier.
67    /// - `f64` - The intensity scalar.
68    /// - `f64` - The half-angle of the cone in radians.
69    ///
70    /// # Returns
71    ///
72    /// - `Light` - The new spotlight.
73    pub fn new_spot(
74        position: Vector3D,
75        direction: Vector3D,
76        color: Vector3D,
77        intensity: f64,
78        half_angle_rad: f64,
79    ) -> Light {
80        Light::new(
81            LightType::Spot,
82            position,
83            direction.normalized(),
84            color,
85            intensity,
86            1.0,
87            half_angle_rad.cos(),
88        )
89    }
90}
91
92/// Implements factory constructors for [`Material`].
93impl Material {
94    /// Creates a pure-Lambert material with the given albedo.
95    ///
96    /// # Arguments
97    ///
98    /// - `Vector3D` - The diffuse albedo color.
99    ///
100    /// # Returns
101    ///
102    /// - `Material` - A Lambertian material.
103    pub fn lambert(albedo: Vector3D) -> Material {
104        Material::new(
105            MaterialKind::Lambert,
106            albedo,
107            0.0,
108            LIGHTING_DEFAULT_SHININESS,
109            Vector3D::zero(),
110        )
111    }
112
113    /// Creates a Blinn-Phong material with the given albedo, specular
114    /// strength, and specular exponent.
115    ///
116    /// # Arguments
117    ///
118    /// - `Vector3D` - The diffuse albedo color.
119    /// - `f64` - The specular intensity in the range 0.0..=1.0.
120    /// - `f64` - The Phong specular exponent.
121    ///
122    /// # Returns
123    ///
124    /// - `Material` - A Phong material.
125    pub fn phong(albedo: Vector3D, specular: f64, shininess: f64) -> Material {
126        Material::new(
127            MaterialKind::Phong,
128            albedo,
129            specular,
130            shininess,
131            Vector3D::zero(),
132        )
133    }
134
135    /// Creates a material using the [`MaterialKind::Pbr`] approximation.
136    ///
137    /// The resulting material is shaded as a Lambertian diffuse term scaled
138    /// by a Schlick fresnel factor; its `specular` and `shininess` fields are
139    /// not consulted on that path. See [`MaterialKind::Pbr`] for what this
140    /// model does and does not include.
141    ///
142    /// # Arguments
143    ///
144    /// - `Vector3D` - The diffuse albedo color.
145    ///
146    /// # Returns
147    ///
148    /// - `Material` - A PBR-approximation material.
149    pub fn pbr(albedo: Vector3D) -> Material {
150        Material::new(
151            MaterialKind::Pbr,
152            albedo,
153            0.0,
154            LIGHTING_DEFAULT_SHININESS,
155            Vector3D::zero(),
156        )
157    }
158
159    /// Creates a purely emissive material (light source with no shading).
160    ///
161    /// # Arguments
162    ///
163    /// - `Vector3D` - The self-illumination color.
164    ///
165    /// # Returns
166    ///
167    /// - `Material` - An emissive material.
168    pub fn emissive(color: Vector3D) -> Material {
169        Material::new(MaterialKind::Lambert, Vector3D::zero(), 0.0, 0.0, color)
170    }
171}
172
173/// Implements [`LightingUniforms`] builders and the [`LightingUniforms::shade`]
174/// entry point used by the ray tracer.
175impl LightingUniforms {
176    /// Creates a uniform set with an empty light list, default ambient,
177    /// and the supplied eye position.
178    ///
179    /// # Arguments
180    ///
181    /// - `Vector3D` - The view position used for specular calculations.
182    ///
183    /// # Returns
184    ///
185    /// - `LightingUniforms` - The new uniform set.
186    pub fn with_eye(eye: Vector3D) -> LightingUniforms {
187        LightingUniforms::new(Vec::new(), LIGHTING_DEFAULT_AMBIENT, eye)
188    }
189
190    /// Adds a light to the uniform set.
191    ///
192    /// # Arguments
193    ///
194    /// - `Light` - The light to append.
195    pub fn add_light(&mut self, light: Light) {
196        self.get_mut_lights().push(light);
197    }
198
199    /// Shades a surface point by summing ambient, per-light Lambertian, and
200    /// per-light Phong contributions, gated by a soft shadow factor.
201    ///
202    /// The [`MaterialKind`] selects which terms are evaluated per light:
203    /// [`MaterialKind::Lambert`] contributes diffuse only,
204    /// [`MaterialKind::Phong`] adds a Blinn-Phong specular lobe, and
205    /// [`MaterialKind::Pbr`] adds an energy-conserving Schlick fresnel
206    /// factor to the diffuse term and no separate specular lobe. See
207    /// [`MaterialKind::Pbr`] for why that is an approximation and not a
208    /// full physically based model.
209    ///
210    /// # Arguments
211    ///
212    /// - `Vector3D` - The world-space position of the shaded point.
213    /// - `Vector3D` - The surface normal (unit length).
214    /// - `&Material` - The material at the shaded point.
215    /// - `&[(Vector3D, f64)]` - `(center, radius)` occluder tuples used by
216    ///   [`soft_shadow_factor`].
217    ///
218    /// # Returns
219    ///
220    /// - `Vector3D` - The final shaded color.
221    pub fn shade(
222        &self,
223        position: Vector3D,
224        normal: Vector3D,
225        material: &Material,
226        occluders: &[(Vector3D, f64)],
227    ) -> Vector3D {
228        let mut color: Vector3D = self.get_ambient();
229        let eye: Vector3D = self.get_eye();
230        let to_eye: Vector3D = eye - position;
231        let view_dist: f64 = to_eye.magnitude();
232        let view_dir: Vector3D = if view_dist > EPSILON {
233            to_eye.scaled(1.0 / view_dist)
234        } else {
235            Vector3D::zero()
236        };
237        for light in self.get_lights().iter() {
238            let kind: LightType = light.get_kind();
239            let shadow: f64 = match kind {
240                LightType::Directional => 1.0,
241                LightType::Point | LightType::Spot => {
242                    soft_shadow_factor(position, light.get_position(), occluders)
243                }
244            };
245            if shadow <= 0.0 {
246                continue;
247            }
248            let mut lambert_input: Light = light.clone();
249            match kind {
250                LightType::Directional => {}
251                LightType::Point | LightType::Spot => {
252                    let to_light: Vector3D = light.get_position() - position;
253                    let dist: f64 = to_light.magnitude().max(LIGHTING_POINT_LIGHT_MIN_DISTANCE);
254                    let dir: Vector3D = to_light.scaled(1.0 / dist);
255                    lambert_input.set_direction(dir);
256                }
257            }
258            let material_kind: MaterialKind = material.get_kind();
259            let diffuse: Vector3D = match material_kind {
260                MaterialKind::Pbr => {
261                    let base: Vector3D = compute_lambert(&lambert_input, normal, material);
262                    let fresnel: f64 =
263                        apply_schlick_fresnel(lambert_input.get_direction(), view_dir, normal);
264                    base.scaled(
265                        LIGHTING_PBR_ENERGY_CONSERVATION
266                            + (1.0 - LIGHTING_PBR_ENERGY_CONSERVATION) * fresnel,
267                    )
268                }
269                MaterialKind::Lambert | MaterialKind::Phong => {
270                    compute_lambert(&lambert_input, normal, material)
271                }
272            };
273            let specular: Vector3D = match material_kind {
274                MaterialKind::Lambert | MaterialKind::Pbr => Vector3D::zero(),
275                MaterialKind::Phong => {
276                    let mut spec_input: Light = lambert_input.clone();
277                    spec_input.set_intensity(
278                        light.get_intensity() * apply_falloff(view_dist, light.get_falloff()),
279                    );
280                    compute_phong(&spec_input, normal, view_dir, material)
281                }
282            };
283            let mut contribution: Vector3D = diffuse + specular;
284            contribution = contribution.scaled(shadow);
285            color += contribution;
286        }
287        let emissive: Vector3D = material.get_emissive();
288        color += emissive;
289        color
290    }
291}