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}