1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
use *;
/// Computes the Lambertian (diffuse) contribution of a single light at a
/// shaded surface point.
///
/// ```text
/// color * intensity * albedo * max(0, dot(normal, light_dir))
/// ```
///
/// `light_dir` is the unit direction from the surface toward the light;
/// any falloff must already be folded into a scaled intensity by the
/// caller.
///
/// A thin wrapper over [`lambert_term`], which takes the same quantities
/// unpacked instead of through a [`Light`], so a direct call and the same
/// light routed through [`LightingUniforms::shade_with_visibility`] share
/// one implementation.
///
/// # Arguments
///
/// - `&Light` - The light source being evaluated.
/// - `Vector3D` - The surface normal (expected to be unit length).
/// - `&Material` - The material at the shaded point.
///
/// # Returns
///
/// - `Vector3D` - The diffuse contribution of this light.
/// The Lambertian term shared by [`compute_lambert`] and
/// [`LightingUniforms::shade_with_visibility`].
///
/// The light arrives as a [`LightSample`] rather than as a [`Light`]
/// because the shading loop has already resolved it — one distance, one
/// direction, one falloff — into the colour, intensity, and direction this
/// term reads. Passing that sample on keeps the [`Light`] un-cloned and
/// stops each term from re-deriving what the loop measured.
///
/// # Arguments
///
/// - `&LightSample` - The resolved colour, intensity, and direction of the
/// light being evaluated.
/// - `Vector3D` - The surface normal (unit length).
/// - `&Material` - The material at the shaded point; its diffuse albedo is
/// read here.
///
/// # Returns
///
/// - `Vector3D` - `color * albedo * intensity * max(0, dot(normal, light_dir))`.
pub
/// Computes the Phong specular contribution of a single light.
///
/// ```text
/// color * intensity * specular * pow(max(0, dot(reflect(-l, n), v)), shininess)
/// ```
///
/// where `l` is the unit direction from the surface toward the light, `n`
/// the surface normal, and `v` the unit view direction from the shaded
/// point toward the eye. A thin wrapper over [`phong_specular_term`], so
/// a direct call and the same light routed through
/// [`LightingUniforms::shade_with_visibility`] share one implementation.
///
/// # Arguments
///
/// - `&Light` - The light source. Its `direction` must point from the
/// surface toward the light.
/// - `Vector3D` - The surface normal (unit length).
/// - `Vector3D` - The unit view direction (from the surface toward the eye).
/// - `&Material` - The material at the shaded point.
///
/// # Returns
///
/// - `Vector3D` - The specular contribution of this light.
/// The Phong specular term shared by [`compute_phong`] and
/// [`LightingUniforms::shade_with_visibility`]. Returns the zero vector
/// unless the surface faces the light, so the lobe cannot survive on the
/// back side of a surface.
///
/// Like [`lambert_term`], it reads a [`LightSample`] the caller already
/// resolved and takes the two material scalars through the [`Material`]
/// itself, so neither the light nor the material is rebuilt per term.
///
/// # Arguments
///
/// - `&LightSample` - The resolved colour, intensity, and direction of the
/// light being evaluated.
/// - `Vector3D` - The surface normal (unit length).
/// - `Vector3D` - The unit view direction (from the surface toward the eye).
/// - `&Material` - The material at the shaded point; its specular intensity
/// and shininess are read here.
///
/// # Returns
///
/// - `Vector3D` - `color * intensity * specular * spec_factor`.
pub
/// Computes the Phong specular factor in the range 0.0..=1.0.
///
/// The factor is `pow(max(0, dot(reflect(-l, n), v)), shininess)`.
///
/// ## Why `-l` is reflected, not `l`
///
/// `reflect(i, n) = i - 2 * dot(i, n) * n` mirrors an **incident** ray `i`
/// about the normal. The incident ray travels from the light into the
/// surface, so it is `-l`, and the expansion is
///
/// ```text
/// reflect(-l, n) = -l + 2 * dot(n, l) * n
/// ```
///
/// Reflecting `l` instead yields a vector pointing back at the lamp, so
/// `dot(reflect, v)` peaks only where the eye is *behind* the surface and
/// the highlight lands on the unlit side.
///
/// With `n` and `l` both unit length the reflection is already unit
/// length — `|2 * n * d - l|² = 4d² - 4d² + 1 = 1` for `d = dot(n, l)` —
/// so this needs no normalization, no division, and has no degenerate
/// case when `l` opposes `n`. The lobe peaks at `v == 2 * n * dot(n, l) -
/// l`, i.e. off-axis unless the light is exactly head-on.
///
/// Returns `0.0` — never `NaN` — when `dot(n, l) <= 0.0`: a surface lit
/// from behind must not flash a highlight.
///
/// # Arguments
///
/// - `Vector3D` - The unit direction from the surface toward the light.
/// - `Vector3D` - The surface normal (unit length).
/// - `Vector3D` - The unit view direction (from the surface toward the eye).
/// - `f64` - The Phong specular exponent.
///
/// # Returns
///
/// - `f64` - The specular factor.
pub
/// Applies the inverse-square falloff formula
/// `1.0 / (1.0 + falloff * d²)`, clamped to a non-negative result.
///
/// # Arguments
///
/// - `f64` - The distance from the light source. Negative values are
/// treated as zero.
/// - `f64` - The falloff coefficient (0.0 disables falloff).
///
/// # Returns
///
/// - `f64` - A non-negative scalar in the range 0.0..=1.0.
/// Computes a Schlick fresnel factor for the [`MaterialKind::Pbr`] branch of
/// [`LightingUniforms::shade`].
///
/// Schlick's approximation is `F = F0 + (1 - F0) * (1 - cos)^5` where
/// `cos` is the cosine between the surface normal and the half-vector
/// between the light and the eye, and `F0` is the normal-incidence
/// reflectance. Returns a scalar in the range
/// [`LIGHTING_PBR_ENERGY_CONSERVATION`]..=1.0: it is minimal when the
/// half-vector aligns with the normal and grows as the half-vector turns
/// away from it, so a lit silhouette picks up a rim.
///
/// This is a single-lobe approximation, not a microfacet BRDF: it ignores
/// roughness, multiple scattering, and per-channel Fresnel, so it should be
/// read as "diffuse plus a fresnel rim" rather than as a physically based
/// reflectance model.
///
/// # Arguments
///
/// - `Vector3D` - The unit direction from the surface toward the light.
/// - `Vector3D` - The unit view direction (from the surface toward the eye).
/// - `Vector3D` - The surface normal (unit length).
///
/// # Returns
///
/// - `f64` - The fresnel reflectance scalar.
/// Returns the unit-length form of `vector`, or the zero vector when it is
/// too short to have a meaningful direction.
///
/// The threshold is [`EPSILON`], so a degenerate vector normalises to
/// zero rather than to `NaN` from a division by a vanishing magnitude.
/// A zero direction is safe downstream: it fails both the facing test and
/// the cone test.
///
/// # Arguments
///
/// - `Vector3D` - The vector to normalize.
///
/// # Returns
///
/// - `Vector3D` - The unit vector, or the zero vector at or below [`EPSILON`].
pub
/// Returns `(unit(vector), length)` while taking the magnitude **once**.
///
/// Shading needs both the unit direction and the original length of the
/// same vector; measuring it twice would duplicate the square root on the
/// hot path. A vector at or below [`EPSILON`] yields
/// `(zero, 0.0)`.
///
/// # Arguments
///
/// - `Vector3D` - The vector to normalize.
///
/// # Returns
///
/// - `(Vector3D, f64)` - The unit vector and the vector's own length.
pub
/// Returns whether `light` can reach a surface point at all, before any
/// visibility query: the spot cone test plus a zero-intensity reject.
///
/// These are the only two rejects a caller can make without geometry, so
/// [`LightingUniforms::shade_with_visibility`] applies them before
/// invoking the visibility callback.
///
/// ## Why the cone test compares against `-spot_cos`
///
/// A spot light stores `direction` as the cone axis pointing **outward**
/// along the beam, i.e. the way the light travels. `light_dir` points the
/// other way, from the surface **into** the lamp, so for a surface on the
/// axis `dot(light_dir, axis)` is `-1.0`, not `+1.0`. Being inside the cone
/// is therefore
///
/// ```text
/// dot(light_dir, axis) <= -cos(half_angle) == -spot_cos
/// ```
///
/// Testing `dot(light_dir, axis) >= spot_cos` instead would invert the
/// cone: every point on the beam is rejected and every point behind the
/// lamp is lit.
///
/// When the surface sits exactly on the lamp `light_dir` is zero, whose dot
/// product with any axis is `0.0`; for a cone of half-angle 90° or narrower
/// that would read as out of cone. The degenerate case is accepted
/// explicitly — a surface coincident with a lamp sits at the cone's apex
/// and is inside it by definition — and the zero `light_dir` makes the
/// facing test reject it regardless.
///
/// # Arguments
///
/// - `&Light` - The light being tested.
/// - `Vector3D` - The normalized direction from the surface toward the light.
///
/// # Returns
///
/// - `bool` - `false` when the light is out of cone or carries no intensity.
pub
/// Intersects a ray with a sphere centered at `center` with radius `radius`.
///
/// Uses the standard quadratic-form ray-sphere test.
///
/// # Arguments
///
/// - `Vector3D` - The ray origin.
/// - `Vector3D` - The ray direction (expected to be unit length).
/// - `Vector3D` - The sphere center.
/// - `f64` - The sphere radius (must be positive).
///
/// # Returns
///
/// - `Option<(f64, Vector3D)>` - The hit distance and surface normal, or
/// `None` on miss.
/// Intersects a ray with an axis-aligned bounding box using the slab method.
///
/// # Arguments
///
/// - `Vector3D` - The ray origin.
/// - `Vector3D` - The ray direction (expected to be unit length).
/// - `Vector3D` - The AABB minimum corner.
/// - `Vector3D` - The AABB maximum corner.
///
/// # Returns
///
/// - `Option<(f64, f64, Vector3D)>` - `(t_near, t_far, normal)` on hit, or
/// `None` if the ray misses or is parallel to the slab.
/// Computes a visibility factor in the range 0.0..=1.0 for the segment from
/// `origin` to `light_pos`.
///
/// Despite the historical name this is **binary**, not soft: `1.0` when
/// nothing blocks the segment, `0.0` when anything does. No penumbra is
/// sampled. Callers that hold real geometry should use
/// [`LightingUniforms::shade_with_visibility`] with a callback instead;
/// this is the bounded fallback for callers holding only
/// `(center, radius)` bounding spheres, which necessarily over-shadow a
/// ray passing through empty space beside an occluder.
///
/// # Arguments
///
/// - `Vector3D` - The surface point casting the shadow ray.
/// - `Vector3D` - The light position to test against.
/// - `&[(Vector3D, f64)]` - A slice of `(center, radius)` occluder spheres.
///
/// # Returns
///
/// - `f64` - 1.0 if the light is visible, 0.0 if fully occluded.
/// Returns whether either ray-sphere root lands strictly inside the open
/// segment `(EPSILON, distance - EPSILON)`.
///
/// **Both roots are tested, not just the near one.** A shadow ray is very
/// often cast from the surface point of one of the occluders themselves:
/// for that ray the near root sits at `t ≈ 0`, inside the [`EPSILON`]
/// self-hit threshold, while the far root sits at `t ≈ 2r`. Testing only
/// the nearest root would report the light as visible through the very
/// sphere the ray starts inside.
///
/// The segment arrives as a [`ShadowSegment`] so the same start, unit
/// direction, and distance measured once by [`soft_shadow_factor`] reach
/// every occluder in the loop instead of being restated per call.
///
/// # Arguments
///
/// - `&ShadowSegment` - The segment start, unit direction toward the light,
/// and distance from the segment start to the light.
/// - `Vector3D` - The sphere center.
/// - `f64` - The sphere radius.
///
/// # Returns
///
/// - `bool` - `true` when the segment passes through the sphere's interior.
pub