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
//! The frosted-facet BSDF.
//!
//! [`apply_frosted_bounce`]'s diffuse reflect/transmit dispatch, its cosine-weighted
//! hemisphere direction sampler, and the exterior-side next-event-estimation
//! contribution for the two outcomes that land outside the gem.
use ;
use crateStokesVector;
use Vec3;
/// Cosine-weighted hemisphere direction about `n`, from two independent uniform
/// `[0, 1)` randoms (Malley's method -- polar mapping, not the concentric-disk variant,
/// for a direct WGSL trig-call translation). See `apply_frosted_bounce` for why its
/// call sites need no explicit pdf division.
///
/// `pub(crate)`: `renderer::gpu::transport_check`'s Tier 2 ULP check compares this real
/// function against `shaders/transport_physics.wgsl`'s translation. Visibility only.
pub
/// Next-event estimation for the two [`apply_frosted_bounce`] outcomes whose
/// sampled hemisphere lands on the EXTERIOR side of the gem -- see that function's own
/// doc comment ("Sign convention for NEE eligibility") for exactly which two branches
/// those are and why.
///
/// # Why a convex stone needs no shadow ray, unlike `nee_contribution_hg_scatter`
///
/// (A concave stone does: see "Concave stones" below.)
///
/// [`nee_contribution_hg_scatter`](super::nee_contribution_hg_scatter)'s scattering
/// point sits somewhere in the INTERIOR of the (necessarily convex) polyhedron, so it
/// must trace a shadow ray to find which exit facet the light-sampled direction would
/// leave through, and pay that facet's own Fresnel transmittance. This function's
/// bounce point, by contrast, is already ON the polyhedron's own surface, and
/// `ext_normal` is that facet's true (mathematical) outward-facing normal for this
/// specific outcome. A convex solid's own surface point, moving into the outward
/// half-space about its own true normal, never re-enters the solid for any positive
/// distance -- so the environment is unconditionally visible along any `dir` with
/// `dir.dot(ext_normal) > 0`, with no occlusion test and no second Fresnel interface to
/// cross (the interface AT this point was already paid for by the caller's
/// `r_unpol`/`t_unpol` branch-selection division before `stokes` reached here).
///
/// # Concave stones
///
/// With tools the convexity argument fails: a point on a groove wall moving along its
/// outward normal enters the cavity and can meet the opposite wall. So when
/// `nee.tools` is non-empty this casts a real shadow probe from `origin` against the
/// whole stone and deposits only on a miss. Any hit, even one past a cavity, is an
/// occluder: the escaping BSDF-sampled continuation sees the same visibility, so the two
/// MIS techniques stay consistent. The probe runs after the RNG draws, so the stream
/// consumption is the same as for a convex stone.
///
/// # Simplification, mirroring `nee_contribution_hg_scatter`
///
/// One broadband cosine-weighted-hemisphere ("Lambertian") BSDF density
/// (`cos_light / pi`) stands in for the true frosted BSDF's competing technique here,
/// exactly like [`apply_frosted_bounce`] itself already uses one broadband `r_unpol`/
/// `t_unpol` split for every channel (see that function's own "The model, honestly"
/// section) -- consistent with, not an additional approximation on top of, the rest of
/// this BSDF's treatment.
///
/// Consumes exactly one new 2D RNG draw ([`FROSTED_NEE_ENV_DIR_U_STREAM`]/
/// [`FROSTED_NEE_ENV_DIR_V_STREAM`]) whenever `nee.enabled` -- callers gate the call
/// itself on eligibility (see [`apply_frosted_bounce`]), so a bounce that lands on the
/// interior side, or any bounce with NEE disabled, never touches this stream at all.
///
/// `stokes` must already carry this branch's own throughput (post `/r_unpol` or
/// `/t_unpol` rescale, exactly like
/// [`nee_contribution_hg_scatter`](super::nee_contribution_hg_scatter)'s identical
/// precondition on its own `stokes` argument).
pub
/// The replacement for the specular TIR/partial-reflect/refract dispatch at a
/// `FacetFinish::Frosted` facet.
///
/// # The model, honestly
///
/// A real bruted/ground surface's reflectance doesn't carry the polished interface's
/// sharp per-wavelength Fresnel structure -- roughness averages over local micro-facet
/// angles, washing out the fine structure -- so this uses ONE broadband reflect/
/// transmit fraction (`r_unpol`, hero channel only) for every spectral channel, unlike
/// the polished path's per-channel `r_unpol_k`. Every channel shares both the sampled
/// direction (roughness scattering isn't meaningfully dispersive) and the reflect/
/// transmit split probability (a modeling simplification).
///
/// Direction is drawn from the cosine-weighted hemisphere about the correct macroscopic
/// normal (`normal` reflect, `-normal` transmit) -- standard importance sampling for a
/// Lambertian BRDF/BTDF, so `f * cos(theta) / pdf` is the constant albedo (`1.0`). The
/// branch itself is drawn from `FRESNEL_BRANCH_STREAM` with probability `r_unpol` (or
/// `t_unpol`), and `path_pdf[k] *= r_unpol` (or `t_unpol`) below divides by exactly that
/// selection probability -- the intensity itself is left unscaled (weight `1`, not
/// `1 / r_unpol`): the branch's own energy fraction and its own selection probability
/// are the same number, so they cancel and no throughput division belongs here.
///
/// Depolarizes every channel (`StokesVector::unpolarized`): multiple internal
/// micro-scattering events scramble the coherent phase relationship polarization
/// depends on.
///
/// # Sign convention for NEE eligibility
///
/// `normal` arrives here ALREADY resolved by the caller (`transport::apply_interior_segment`)
/// to the facet's outward normal flipped to face the current medium: unflipped (the raw,
/// truly outward-pointing facet normal) when `!inside_gem` (this bounce is happening at a
/// ray currently in air, about to enter or bounce off the gem from outside), and flipped
/// to point INWARD when `inside_gem` (this bounce is happening at a ray currently inside
/// the crystal, hitting a facet from the inside). Given that:
///
/// - **`!inside_gem`, reflect branch** (hemisphere about `normal`, i.e. the true outward
/// normal): the ray was already outside and bounces back outside. `inside_gem` is
/// unchanged (`false`) -- EXTERIOR side. NEE-eligible.
/// - **`!inside_gem`, transmit branch** (hemisphere about `-normal`, i.e. pointing
/// inward): the ray enters the crystal for the first time through this frosted facet.
/// `inside_gem` flips to `true` -- INTERIOR side. Not handled here: a light sample
/// drawn into the interior hemisphere would need to find where THIS path eventually
/// exits the solid and pay that exit facet's own Fresnel transmittance, exactly the
/// shadow-ray search `nee_contribution_hg_scatter` already does from an interior
/// scattering point. Deliberately not implemented (documented gap, not an oversight):
/// entry-facet frosted transmission is the less common case in practice (a frosted
/// girdle scattering light back OUT to the camera -- the exterior-transmit case below
/// -- is the one every existing frosted-girdle test/scene exercises), and adding it
/// would double this function's shadow-ray-tracing surface for comparatively little
/// variance-reduction benefit. `pending_light_mis` is `None` for this branch, so a
/// phase-sampled continuation that (rarely, since it heads inward) still escapes
/// directly next bounce keeps its full weight rather than being MIS-weighted
/// against a light sample this function never drew.
/// - **`inside_gem`, forced-reflect (TIR) branch** (hemisphere about `normal`, i.e.
/// ALREADY flipped inward): TIR is only reachable with `inside_gem == true` (`n1 > n2`
/// for the hero channel -- same premise `dispatch_bounce`'s polished-path doc comment
/// states), so this hemisphere stays interior. `inside_gem` unchanged (`true`) --
/// INTERIOR side. Not NEE-eligible (an internal reflection has no direct environment
/// visibility at this point).
/// - **`inside_gem`, reflect branch** (hemisphere about `normal`, flipped inward): an
/// internal reflection off a frosted facet, staying inside. INTERIOR side. Not
/// NEE-eligible, for the same reason as the TIR branch above.
/// - **`inside_gem`, transmit branch** (hemisphere about `-normal`, i.e. the true
/// outward normal once the inward flip is undone): the ray exits the crystal to the
/// environment through this frosted facet -- the frosted-girdle scene every existing
/// test exercises. `inside_gem` flips to `false` -- EXTERIOR side. NEE-eligible.
///
/// In short: NEE fires exactly when this bounce's own `inside_gem` input and its
/// resulting output disagree in the "becoming exterior" direction reflect can never
/// take (`!inside_gem` reflect) or transmit does take (`inside_gem` transmit) -- both
/// cases share the property that the branch's own hemisphere normal (`normal` for the
/// first, `-normal` for the second) is the TRUE, un-flipped outward-facing normal.
///
/// # Estimator composition -- why this needs no chromatic termination
///
/// The polished path drops a companion channel to exactly zero when its specular
/// direction misses the hero-driven direction, because a delta BSDF has zero density
/// elsewhere. Neither premise holds here: direction is shared and the BSDF is a smooth
/// hemisphere, so `path_pdf[k] *= r_unpol` (or `t_unpol`) uses the same factor for
/// every channel.
///
/// Reuses `FRESNEL_BRANCH_STREAM` for the reflect-vs-transmit branch (divides by its
/// own selection probability, disjoint energy). `BIREFRINGENT_SPLIT_STREAM` on an
/// air->crystal entry picks which ~0.5 energy SHARE this path becomes, so it is NOT
/// divided by its own 0.5 (same reasoning as `apply_refract_bounce`). Mode
/// re-coupling is applied by the caller.
///
/// # Why the exterior NEE deposit needs no separate `nee_xyz` accumulator (F-09b)
///
/// `nee_contribution_frosted_exterior` deposits straight into the caller's shared
/// `radiance` array (weighted, like every other channel's contribution, by the FINAL
/// post-loop `path_pdf` at `trace_spectral_ray_inner`'s integration step) rather than
/// into a separate `nee_xyz` accumulator integrated with THIS moment's `path_pdf`, the
/// fix `try_scatter_step`'s identical deposit needed (see that function's own "NEE
/// spectral weighting" doc section). That asymmetry is safe, not an oversight: both
/// NEE-eligible outcomes here are, by construction, cases where `new_dir` lies in the
/// open half-space strictly outward of this bounce point's TRUE outward normal (a
/// cosine-weighted hemisphere about `normal` for the `!inside_gem` reflect branch,
/// about `-normal` for the `inside_gem` transmit branch -- see "Sign convention for NEE
/// eligibility" above). A convex polyhedron's own surface point moving into that exact
/// half-space can never re-intersect the solid at any positive distance (the same
/// convexity property `nee_contribution_hg_scatter`'s and `try_split_exit_channel`'s own
/// doc comments already lean on) -- so the VERY NEXT bounce-loop iteration is guaranteed
/// to find no facet hit and escape directly, meaning no further `path_pdf[k] *=` update
/// can happen between this deposit and the trace's final integration. The FINAL
/// `path_pdf` this deposit is (eventually) weighted by is therefore always identical to
/// the `path_pdf` live at the moment of the deposit itself, so riding the shared
/// `radiance` array is exact here, unlike the HG scattering-point case (an interior
/// point with an unbounded number of further bounces still ahead of it).
///
/// # Return value
///
/// The fourth tuple element is the `pending_light_mis` carry, mirroring
/// `ScatterStepOutcome::ScatteredAndSurvived`'s identical carry for the
/// Henyey-Greenstein path: `Some(cos(theta_new) / pi)` -- the cosine-weighted-hemisphere
/// BSDF's own density at the just-sampled continuation direction `new_dir` -- whenever
/// `nee.enabled` and this outcome is NEE-eligible (see above), `None` otherwise
/// (including every trace with NEE disabled). The caller stashes this exactly like the
/// HG path's own carry: consumed only by the VERY NEXT bounce-loop iteration's escape
/// check, and only if that iteration's ray escapes directly.
// `pub(crate)`: `renderer::gpu::transport_check`'s Tier 2 ULP check compares this real
// function against `shaders/transport_physics.wgsl`'s translation. Visibility only.
pub