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 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 — in particular, read it as
141    /// "diffuse plus a fresnel rim", not as a physical BRDF.
142    ///
143    /// # Arguments
144    ///
145    /// - `Vector3D` - The diffuse albedo color.
146    ///
147    /// # Returns
148    ///
149    /// - `Material` - A PBR-approximation material.
150    pub fn pbr(albedo: Vector3D) -> Material {
151        Material::new(
152            MaterialKind::Pbr,
153            albedo,
154            0.0,
155            LIGHTING_DEFAULT_SHININESS,
156            Vector3D::zero(),
157        )
158    }
159
160    /// Creates a purely emissive material (light source with no shading).
161    ///
162    /// # Arguments
163    ///
164    /// - `Vector3D` - The self-illumination color.
165    ///
166    /// # Returns
167    ///
168    /// - `Material` - An emissive material.
169    pub fn emissive(color: Vector3D) -> Material {
170        Material::new(MaterialKind::Lambert, Vector3D::zero(), 0.0, 0.0, color)
171    }
172}
173
174/// Implements [`LightingUniforms`] builders and the shading entry points
175/// used by the ray tracer and by any external visibility provider.
176impl LightingUniforms {
177    /// Creates a uniform set with an empty light list, default ambient,
178    /// and the supplied eye position.
179    ///
180    /// # Arguments
181    ///
182    /// - `Vector3D` - The view position used for specular calculations.
183    ///
184    /// # Returns
185    ///
186    /// - `LightingUniforms` - The new uniform set.
187    pub fn with_eye(eye: Vector3D) -> LightingUniforms {
188        LightingUniforms::new(Vec::new(), LIGHTING_DEFAULT_AMBIENT, eye)
189    }
190
191    /// Adds a light to the uniform set.
192    ///
193    /// # Arguments
194    ///
195    /// - `Light` - The light to append.
196    pub fn add_light(&mut self, light: Light) {
197        self.get_mut_lights().push(light);
198    }
199
200    /// Shades a surface point against `(center, radius)` shadow bounding
201    /// spheres, the pre-existing entry point.
202    ///
203    /// A compatibility shim over
204    /// [`LightingUniforms::shade_with_visibility`] that supplies a callback
205    /// backed by [`soft_shadow_factor`], retained so existing ray-tracing
206    /// callers keep compiling unchanged. New code holding real geometry
207    /// should call [`LightingUniforms::shade_with_visibility`] directly and
208    /// get exact shadows instead of bounding-sphere ones.
209    ///
210    /// Directional lights report full visibility here, unchanged from
211    /// before: a bounding sphere stores a position but no direction, so
212    /// there is no finite segment to cast along and a callback using one
213    /// would have to invent a far bound. That far bound is
214    /// [`LIGHTING_DIRECTIONAL_VISIBILITY_DISTANCE`] and belongs to
215    /// [`LightingUniforms::shade_with_visibility`], where the caller
216    /// supplies its own geometry.
217    ///
218    /// # Arguments
219    ///
220    /// - `Vector3D` - The world-space position of the shaded point.
221    /// - `Vector3D` - The surface normal (unit length).
222    /// - `&Material` - The material at the shaded point.
223    /// - `&[(Vector3D, f64)]` - `(center, radius)` occluder bounding
224    ///   spheres, one per occluder.
225    ///
226    /// # Returns
227    ///
228    /// - `Vector3D` - The final shaded color.
229    pub fn shade(
230        &self,
231        position: Vector3D,
232        normal: Vector3D,
233        material: &Material,
234        occluders: &[(Vector3D, f64)],
235    ) -> Vector3D {
236        self.shade_with_visibility(
237            position,
238            normal,
239            material,
240            |light: &Light, _dir: Vector3D, _max_distance: f64| -> f64 {
241                match light.get_kind() {
242                    LightType::Directional => 1.0,
243                    LightType::Point | LightType::Spot => {
244                        soft_shadow_factor(position, light.get_position(), occluders)
245                    }
246                }
247            },
248        )
249    }
250
251    /// Shades a surface point, asking `visibility` whether each light is
252    /// unoccluded along the segment to that light.
253    ///
254    /// The entry point for real shadow geometry. The callback receives the
255    /// light, the unit direction from the shaded point toward the light,
256    /// and the maximum distance to cast along that direction, and returns a
257    /// visibility factor that multiplies the light's whole contribution.
258    /// Returning `1.0` for every light reproduces the unshadowed result
259    /// exactly.
260    ///
261    /// ## The callback contract
262    ///
263    /// ```text
264    /// F: FnMut(&Light, surface_to_light_dir: Vector3D, max_distance: f64) -> f64
265    /// ```
266    ///
267    /// - `surface_to_light_dir` is the **normalized** direction from
268    ///   `position` toward the light. For a spot light this points *into*
269    ///   the lamp (it is the negation of the cone axis), matching the
270    ///   shading convention used for every other term — cast your shadow
271    ///   ray along this vector and it runs surface→light.
272    /// - `max_distance` is the far bound for that ray: the true
273    ///   surface→light distance for point and spot lights, so the ray can
274    ///   never reach past the lamp, and
275    ///   [`LIGHTING_DIRECTIONAL_VISIBILITY_DISTANCE`] (1000.0, matching
276    ///   the ray tracer's `RAYTRACE_DEFAULT_T_MAX`) for directional
277    ///   lights, which have no distance of their own.
278    /// - The returned factor is clamped to `0.0..=1.0`.
279    ///
280    /// ## When the callback runs
281    ///
282    /// At most once per light, and only for lights that can actually
283    /// contribute. Lights are skipped before any callback work when they
284    /// carry no intensity, fall outside their spot cone, or face away from
285    /// the surface (`dot(normal, light_dir) <= 0`). Those tests are free,
286    /// and skipping them is the point of the ordering: back-facing and
287    /// out-of-cone lights are the common reject path. A light that is fully
288    /// occluded still costs one callback — only the callback knows the
289    /// geometry.
290    ///
291    /// ## Evaluation order
292    ///
293    /// Per light: cone test → facing test → compute `light_dir`, `dist`,
294    /// and `view_dir` **once** → visibility callback → falloff →
295    /// [`MaterialKind`] dispatch. The [`Light`] itself is only read, never
296    /// cloned: a point or spot light's derived direction and attenuating
297    /// intensity live in locals for the duration of the iteration, so the
298    /// diffuse, specular, and fresnel terms all read the same values.
299    ///
300    /// [`MaterialKind`] selects which terms run per light:
301    /// [`MaterialKind::Lambert`] contributes diffuse only,
302    /// [`MaterialKind::Phong`] adds a Phong specular lobe that is zero
303    /// unless the surface faces the light, and [`MaterialKind::Pbr`]
304    /// scales the diffuse term by a Schlick fresnel factor and adds no
305    /// separate specular lobe.
306    ///
307    /// Ambient is added once and is never shadowed; the material's
308    /// emissive term is added last and is likewise unaffected by
309    /// visibility.
310    ///
311    /// # Arguments
312    ///
313    /// - `Vector3D` - The world-space position of the shaded point.
314    /// - `Vector3D` - The surface normal (unit length).
315    /// - `&Material` - The material at the shaded point.
316    /// - `F` - The visibility provider, called once per contributing
317    ///   light.
318    ///
319    /// # Returns
320    ///
321    /// - `Vector3D` - The final shaded color.
322    pub fn shade_with_visibility<F>(
323        &self,
324        position: Vector3D,
325        normal: Vector3D,
326        material: &Material,
327        mut visibility: F,
328    ) -> Vector3D
329    where
330        F: FnMut(&Light, Vector3D, f64) -> f64,
331    {
332        let mut color: Vector3D = self.get_ambient();
333        let view_dir: Vector3D = unit_vector(self.get_eye() - position);
334        let material_kind: MaterialKind = material.get_kind();
335        for light in self.get_lights().iter() {
336            let kind: LightType = light.get_kind();
337            // Derived once per light and shared by every term below. The
338            // magnitude is taken once, alongside the unit direction, since
339            // both come from the same vector. The `Light` itself is only
340            // read, never cloned or mutated.
341            let (light_dir, distance): (Vector3D, f64) = match kind {
342                LightType::Directional => (light.get_direction(), 0.0),
343                LightType::Point | LightType::Spot => {
344                    unit_vector_and_length(light.get_position() - position)
345                }
346            };
347            // Free rejects first: a light that cannot reach this surface
348            // costs no visibility callback at all.
349            if !light_reaches_position(light, light_dir) {
350                continue;
351            }
352            // A back-facing surface has zero Lambert and zero specular, so
353            // culling here saves a shadow ray per light.
354            if normal.dot(light_dir) <= 0.0 {
355                continue;
356            }
357            // `max_distance` is the **true** surface→light distance, so a
358            // shadow ray can never reach past the lamp. Only the falloff
359            // distance is floored at `LIGHTING_POINT_LIGHT_MIN_DISTANCE`,
360            // to keep a coincident surface from dividing by zero; a
361            // directional light attenuates not at all and its shadow ray
362            // runs to the ray tracer's far bound.
363            let (falloff_factor, shadow_distance): (f64, f64) = match kind {
364                LightType::Directional => (1.0, LIGHTING_DIRECTIONAL_VISIBILITY_DISTANCE),
365                LightType::Point | LightType::Spot => (
366                    apply_falloff(
367                        distance.max(LIGHTING_POINT_LIGHT_MIN_DISTANCE),
368                        light.get_falloff(),
369                    ),
370                    distance,
371                ),
372            };
373            let shadow: f64 = visibility(light, light_dir, shadow_distance).clamp(0.0, 1.0);
374            if shadow <= 0.0 {
375                continue;
376            }
377            // One resolved light, shared by every term below: the diffuse
378            // and specular lobes and the fresnel scale all read this one
379            // colour, the one attenuated intensity, and the one derived
380            // direction, instead of each re-reading the `Light`.
381            let sample: LightSample = LightSample {
382                color: light.get_color(),
383                intensity: light.get_intensity() * falloff_factor,
384                direction: light_dir,
385            };
386            let diffuse: Vector3D = match material_kind {
387                MaterialKind::Pbr => {
388                    let fresnel: f64 = apply_schlick_fresnel(light_dir, view_dir, normal);
389                    lambert_term(&sample, normal, material).scaled(
390                        LIGHTING_PBR_ENERGY_CONSERVATION
391                            + (1.0 - LIGHTING_PBR_ENERGY_CONSERVATION) * fresnel,
392                    )
393                }
394                MaterialKind::Lambert | MaterialKind::Phong => {
395                    lambert_term(&sample, normal, material)
396                }
397            };
398            let specular: Vector3D = match material_kind {
399                MaterialKind::Lambert | MaterialKind::Pbr => Vector3D::zero(),
400                MaterialKind::Phong => phong_specular_term(&sample, normal, view_dir, material),
401            };
402            color += (diffuse + specular).scaled(shadow);
403        }
404        color += material.get_emissive();
405        color
406    }
407}