1use super::*;
2
3impl Light {
6 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 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 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
92impl Material {
94 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 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 pub fn emissive(color: Vector3D) -> Material {
145 Material::new(MaterialKind::Lambert, Vector3D::zero(), 0.0, 0.0, color)
146 }
147}
148
149impl LightingUniforms {
152 pub fn with_eye(eye: Vector3D) -> LightingUniforms {
163 LightingUniforms::new(Vec::new(), LIGHTING_DEFAULT_AMBIENT, eye)
164 }
165
166 pub fn add_light(&mut self, light: Light) {
172 self.get_mut_lights().push(light);
173 }
174
175 pub fn shade(
190 &self,
191 position: Vector3D,
192 normal: Vector3D,
193 material: &Material,
194 occluders: &[(Vector3D, f64)],
195 ) -> Vector3D {
196 let mut color: Vector3D = self.get_ambient();
197 let eye: Vector3D = self.get_eye();
198 let to_eye: Vector3D = eye - position;
199 let view_dist: f64 = to_eye.magnitude();
200 let view_dir: Vector3D = if view_dist > EPSILON {
201 to_eye.scaled(1.0 / view_dist)
202 } else {
203 Vector3D::zero()
204 };
205 for light in self.get_lights().iter() {
206 let kind: LightType = light.get_kind();
207 let shadow: f64 = match kind {
208 LightType::Directional => 1.0,
209 LightType::Point | LightType::Spot => {
210 soft_shadow_factor(position, light.get_position(), occluders)
211 }
212 };
213 if shadow <= 0.0 {
214 continue;
215 }
216 let mut lambert_input: Light = light.clone();
217 match kind {
218 LightType::Directional => {}
219 LightType::Point | LightType::Spot => {
220 let to_light: Vector3D = light.get_position() - position;
221 let dist: f64 = to_light.magnitude().max(LIGHTING_POINT_LIGHT_MIN_DISTANCE);
222 let dir: Vector3D = to_light.scaled(1.0 / dist);
223 lambert_input.set_direction(dir);
224 }
225 }
226 let diffuse: Vector3D = compute_lambert(&lambert_input, normal, material);
227 let mut spec_input: Light = lambert_input.clone();
228 spec_input.set_intensity(
229 light.get_intensity() * apply_falloff(view_dist, light.get_falloff()),
230 );
231 let specular: Vector3D = compute_phong(&spec_input, normal, view_dir, material);
232 let mut contribution: Vector3D = diffuse + specular;
233 contribution = contribution.scaled(shadow);
234 color += contribution;
235 }
236 let emissive: Vector3D = material.get_emissive();
237 color += emissive;
238 color
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245
246 #[test]
249 fn lambert_diffuse_face_normal() {
250 let light: Light =
251 Light::new_directional(Vector3D::new(0.0, 1.0, 0.0), Vector3D::new(1.0, 0.0, 0.0));
252 let material: Material = Material::lambert(Vector3D::new(0.5, 0.5, 0.5));
253 let normal: Vector3D = Vector3D::new(0.0, 1.0, 0.0);
254 let result: Vector3D = compute_lambert(&light, normal, &material);
255 let expected: f64 = 1.0 * 1.0 * 1.0 * 0.5;
256 assert!(
257 (result.get_x() - expected).abs() < EPSILON,
258 "expected red channel {expected}, got {}",
259 result.get_x(),
260 );
261 assert!(
262 result.get_y().abs() < EPSILON,
263 "expected green channel 0.0, got {}",
264 result.get_y(),
265 );
266 assert!(
267 result.get_z().abs() < EPSILON,
268 "expected blue channel 0.0, got {}",
269 result.get_z(),
270 );
271 }
272
273 #[test]
276 fn phong_specular_peak() {
277 let normal: Vector3D = Vector3D::new(0.0, 1.0, 0.0);
278 let light_dir: Vector3D = Vector3D::new(0.0, -1.0, 0.0);
279 let view_dir: Vector3D = Vector3D::new(0.0, 1.0, 0.0);
280 let light: Light = Light::new(
281 LightType::Directional,
282 Vector3D::zero(),
283 light_dir,
284 Vector3D::new(1.0, 1.0, 1.0),
285 1.0,
286 0.0,
287 0.0,
288 );
289 let material: Material = Material::phong(Vector3D::new(1.0, 1.0, 1.0), 1.0, 32.0);
290 let result: Vector3D = compute_phong(&light, normal, view_dir, &material);
291 assert!(
292 (result.get_x() - 1.0).abs() < EPSILON,
293 "expected specular peak ~1.0, got {}",
294 result.get_x(),
295 );
296 assert!(
297 (result.get_y() - 1.0).abs() < EPSILON,
298 "expected specular peak ~1.0, got {}",
299 result.get_y(),
300 );
301 assert!(
302 (result.get_z() - 1.0).abs() < EPSILON,
303 "expected specular peak ~1.0, got {}",
304 result.get_z(),
305 );
306 }
307
308 #[test]
311 fn point_light_falloff_distance() {
312 let falloff: f64 = 1.0;
313 let f0: f64 = apply_falloff(0.0, falloff);
314 let f1: f64 = apply_falloff(1.0, falloff);
315 let f2: f64 = apply_falloff(2.0, falloff);
316 assert!((f0 - 1.0).abs() < EPSILON, "d=0 should yield 1.0, got {f0}");
317 assert!(
318 (f1 - 1.0 / (1.0 + 1.0)).abs() < EPSILON,
319 "d=1 should yield 0.5, got {f1}",
320 );
321 assert!(
322 (f2 - 1.0 / (1.0 + 4.0)).abs() < EPSILON,
323 "d=2 should yield 0.2, got {f2}",
324 );
325 }
326
327 #[test]
329 fn ray_sphere_intersect_hit_miss_inside() {
330 let origin: Vector3D = Vector3D::new(0.0, 0.0, 5.0);
332 let dir: Vector3D = Vector3D::new(0.0, 0.0, -1.0);
333 let center: Vector3D = Vector3D::zero();
334 let radius: f64 = 1.0;
335 let hit: Option<(f64, Vector3D)> = ray_sphere_intersect(origin, dir, center, radius);
336 assert!(hit.is_some(), "ray from outside should hit sphere");
337 let (t, normal): (f64, Vector3D) = hit.unwrap();
338 assert!((t - 4.0).abs() < EPSILON, "expected t=4, got {t}");
339 assert!(
340 (normal.get_z() - 1.0).abs() < EPSILON,
341 "expected normal (0,0,1), got (0,0,{})",
342 normal.get_z(),
343 );
344
345 let origin_miss: Vector3D = Vector3D::new(10.0, 0.0, 5.0);
347 let dir_miss: Vector3D = Vector3D::new(0.0, 0.0, -1.0);
348 let miss: Option<(f64, Vector3D)> =
349 ray_sphere_intersect(origin_miss, dir_miss, center, radius);
350 assert!(miss.is_none(), "ray far from sphere should miss");
351
352 let origin_in: Vector3D = Vector3D::zero();
354 let dir_in: Vector3D = Vector3D::new(1.0, 0.0, 0.0);
355 let inside: Option<(f64, Vector3D)> =
356 ray_sphere_intersect(origin_in, dir_in, center, radius);
357 assert!(
358 inside.is_some(),
359 "ray from inside should still hit exit point"
360 );
361 let (t_in, normal_in): (f64, Vector3D) = inside.unwrap();
362 assert!(
363 (t_in - 1.0).abs() < EPSILON,
364 "expected t=1 (exit through +x), got {t_in}",
365 );
366 assert!(
367 (normal_in.get_x() - 1.0).abs() < EPSILON,
368 "expected exit normal (1,0,0), got ({},0,0)",
369 normal_in.get_x(),
370 );
371 }
372
373 #[test]
375 fn soft_shadow_no_occluder_returns_one() {
376 let origin: Vector3D = Vector3D::zero();
377 let light_pos: Vector3D = Vector3D::new(0.0, 0.0, 10.0);
378 let occluders: [(Vector3D, f64); 0] = [];
379 let v: f64 = soft_shadow_factor(origin, light_pos, &occluders);
380 assert!(
381 (v - 1.0).abs() < EPSILON,
382 "empty occluders should yield 1.0, got {v}"
383 );
384 }
385}