indicatrix 0.6.0

A physically-based spectral gemstone renderer that turns GemCAD-style cutting instructions into rendered output.
Documentation
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
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
//! Tests for the illuminant spectral-power curves, the preset label/index round trip,
//! the backdrop fill, the lit models' observer head shadow, and the studio rig's own
//! golden-bits regression pin.

use super::*;
use std::hint::black_box;

/// Pins [`d65_relative_spectral_power`] against the tabulated CIE D65 values
/// directly, and against a genuine Planckian 6500K curve to confirm this is not
/// just a relabelled blackbody -- D65's real, measured blue-green irregularity
/// means the two must disagree at some wavelengths.
#[test]
fn d65_relative_spectral_power_matches_the_cie_table_at_450_and_550nm() {
    // Exact table entries, normalized by /100.0 (560nm's entry) like the function
    // under test does.
    let expected_450 = 117.008 / 100.0;
    let expected_550 = 104.046 / 100.0;

    let got_450 = d65_relative_spectral_power(450.0);
    let got_550 = d65_relative_spectral_power(550.0);
    assert!(
        (got_450 - expected_450).abs() < 1e-4,
        "450nm: got {got_450}, expected {expected_450} from the CIE D65 table"
    );
    assert!(
        (got_550 - expected_550).abs() < 1e-4,
        "550nm: got {got_550}, expected {expected_550} from the CIE D65 table"
    );

    // The real CIE table has 450nm's power exceeding 550nm's (a measured
    // irregularity, not something a smooth blackbody curve produces at 6500K).
    assert!(
        got_450 > got_550,
        "D65's real spectrum has more relative power at 450nm than 550nm; \
         got 450nm={got_450}, 550nm={got_550}"
    );

    // Distinguishes this from a plain 6500K Planckian: a genuine blackbody curve
    // is smooth (450nm/380nm ratio close to 1, both on the same gently-rising
    // Wien tail), while the real D65 table rises much more steeply there.
    let d65_ratio = d65_relative_spectral_power(450.0) / d65_relative_spectral_power(380.0);
    let blackbody_ratio = blackbody_spectrum(450.0, 6500.0) / blackbody_spectrum(380.0, 6500.0);
    assert!(
        d65_ratio > blackbody_ratio * 1.5,
        "test premise: the real D65 table's 450nm/380nm rise ({d65_ratio:.3}) must \
         be much steeper than a smooth 6500K Planckian's ({blackbody_ratio:.3}), \
         confirming the Daylight preset is no longer just a relabelled blackbody"
    );
}

/// `sample_studio_environment`'s Daylight preset must route through the D65 table,
/// not `blackbody_spectrum` -- exercised end to end through the full lighting-rig
/// function, not just the standalone table lookup above.
#[test]
fn daylight_preset_studio_environment_uses_the_d65_table_not_a_blackbody() {
    // Straight down from above (`Vec3::Y`) misses every directional light term
    // (key/fill/ring all `.max(0.0)`-clamped dot products that can legitimately
    // land at/near zero here), leaving only the ambient backdrop term -- which is
    // exactly `bg_val * spec_power`, isolating `spec_power` cleanly.
    let dir = Vec3::new(0.0, -1.0, 0.0);
    let exposure = 1.0;

    let daylight_450 =
        sample_studio_environment(dir, 450.0, LightingPreset::Daylight, exposure, 0.0, 0.0);
    let daylight_550 =
        sample_studio_environment(dir, 550.0, LightingPreset::Daylight, exposure, 0.0, 0.0);
    // A genuine Planckian 6500K curve has 450nm < 550nm (see the standalone table
    // test above); the real D65 table has the opposite ordering. If this preset
    // were still silently using `blackbody_spectrum`, this assertion would fail.
    assert!(
        daylight_450 > daylight_550,
        "Daylight preset must reflect D65's own 450nm > 550nm ordering end to \
         end, got 450nm={daylight_450}, 550nm={daylight_550}"
    );
}

#[test]
fn backdrop_fills_the_camera_ray_only_when_set() {
    let lambdas = [450.0f32, 550.0, 650.0];
    let mut radiance = [0.0f32; 3];
    let plain = LightingPreset::LightTent.studio(1.0, 0.4, 0.35);
    assert!(!fill_backdrop(plain, &lambdas, &mut radiance));
    assert_eq!(radiance, [0.0; 3]);

    let carded = plain.with_backdrop(BACKDROP_GREY);
    assert!(fill_backdrop(carded, &lambdas, &mut radiance));
    for (&value, &lambda_nm) in radiance.iter().zip(&lambdas) {
        let expected = BACKDROP_GREY * LightingPreset::LightTent.spectral_power(lambda_nm);
        assert!(
            (value - expected).abs() < 1e-6,
            "backdrop at {lambda_nm} nm: got {value}, expected {expected}"
        );
    }
}

#[test]
fn label_and_index_round_trip_for_every_preset() {
    for (pos, &p) in LightingPreset::ALL.iter().enumerate() {
        assert_eq!(LightingPreset::from_label(p.label()), p);
        assert_eq!(LightingPreset::from_index(p.index()), p);
        assert_eq!(p.index(), pos as i32);
    }
}

#[test]
fn iso_hemisphere_is_one_above_and_zero_below() {
    let val_up =
        sample_studio_environment(Vec3::Y, 550.0, LightingPreset::IsoHemisphere, 1.0, 0.0, 0.0);
    let expected = d65_relative_spectral_power(550.0);
    assert!(
        (val_up - expected).abs() < 1e-4,
        "up direction should match d65_relative_spectral_power(550): got {val_up}, expected {expected}"
    );

    let val_down = sample_studio_environment(
        -Vec3::Y,
        550.0,
        LightingPreset::IsoHemisphere,
        1.0,
        0.0,
        0.0,
    );
    assert_eq!(val_down, 0.0, "down direction should be 0");
}

/// The `Studio` arm ignores the observer; the lit models go fully dark exactly at
/// the eye direction and are untouched 20 degrees away from it.
#[test]
fn observer_head_shadow_darkens_only_the_lit_models() {
    let observer = Vec3::new(0.2, 0.9, -0.3).normalize();
    // 20 degrees away from the observer, outside the 18 degree cone.
    let perp = observer.cross(Vec3::X).normalize();
    let (sin_a, cos_a) = 20.0f32.to_radians().sin_cos();
    let outside = observer.mul_add(Vec3::splat(cos_a), perp * sin_a);

    let ring_with = sample_studio_environment_observed(
        observer,
        560.0,
        LightingPreset::RingLights,
        1.0,
        0.4,
        0.35,
        observer,
    );
    let ring_without =
        sample_studio_environment(observer, 560.0, LightingPreset::RingLights, 1.0, 0.4, 0.35);
    assert_eq!(
        ring_with.to_bits(),
        ring_without.to_bits(),
        "the studio rig must ignore the observer"
    );

    for preset in [
        LightingPreset::IsoHemisphere,
        LightingPreset::LightTent,
        LightingPreset::DaylightDome,
    ] {
        let at_eye =
            sample_studio_environment_observed(observer, 560.0, preset, 1.0, 0.4, 0.35, observer);
        assert_eq!(
            at_eye, 0.0,
            "{preset:?}: the eye direction must be fully shadowed"
        );
        let clear =
            sample_studio_environment_observed(outside, 560.0, preset, 1.0, 0.4, 0.35, observer);
        let unobserved = sample_studio_environment(outside, 560.0, preset, 1.0, 0.4, 0.35);
        assert_eq!(
            clear.to_bits(),
            unobserved.to_bits(),
            "{preset:?}: outside the cone the observer must not matter"
        );
        assert!(
            unobserved > 0.0,
            "{preset:?}: a lit direction above the girdle must be lit"
        );
    }
}

#[test]
fn legacy_lit_model_labels_resolve_to_the_renamed_presets() {
    assert_eq!(
        LightingPreset::from_label("ISO hemisphere"),
        LightingPreset::IsoHemisphere
    );
    assert_eq!(
        LightingPreset::from_label("ISO hemisphere (GemRay-style)"),
        LightingPreset::IsoHemisphere,
        "a settings file saved before the GemRay-style label was reworded must still \
         load as IsoHemisphere"
    );
    assert_eq!(
        LightingPreset::from_label("Soft dome + ring lights"),
        LightingPreset::LightTent
    );
    assert_eq!(
        LightingPreset::from_label("Daylight dome + sun"),
        LightingPreset::DaylightDome
    );
}

#[test]
fn new_models_never_exceed_their_documented_peak() {
    let mut max_iso = 0.0f32;
    let mut max_soft = 0.0f32;
    let mut max_daylight = 0.0f32;

    for i in 0..2000 {
        let phi = (i as f32 + 0.5) * (std::f32::consts::PI * (3.0 - 5.0f32.sqrt()));
        let y = (i as f32 + 0.5).mul_add(-(2.0 / 2000.0), 1.0);
        let r = y.mul_add(-y, 1.0).max(0.0).sqrt();
        let dir = Vec3::new(r * phi.cos(), y, r * phi.sin());

        let v_iso =
            sample_studio_environment(dir, 560.0, LightingPreset::IsoHemisphere, 1.0, 0.4, 0.35);
        let v_soft =
            sample_studio_environment(dir, 560.0, LightingPreset::LightTent, 1.0, 0.4, 0.35);
        let v_daylight =
            sample_studio_environment(dir, 560.0, LightingPreset::DaylightDome, 1.0, 0.4, 0.35);

        max_iso = max_iso.max(v_iso);
        max_soft = max_soft.max(v_soft);
        max_daylight = max_daylight.max(v_daylight);
    }

    assert!(
        max_iso <= 1.0 + 1e-5,
        "ISO peak must not exceed 1.0, got {max_iso}"
    );
    // Spark (5.0) on the tent walls (<= 0.22) is the tent's brightest direction; the
    // key softbox (1.4) never coincides with it.
    assert!(
        max_soft <= 5.5,
        "Light tent peak must not exceed 5.5, got {max_soft}"
    );
    // Sun (10.0) + aureole (0.30) + sky (<= 0.18) at the sun centre.
    assert!(
        max_daylight <= 10.5,
        "Daylight dome peak must not exceed 10.5, got {max_daylight}"
    );
}

/// Largest tolerated bit-pattern distance between a freshly computed value and
/// its golden. Every baseline value here is positive and finite, so `u32` bit
/// patterns increase monotonically with the represented value exactly like ULP
/// distance does -- a signed difference of the raw bit patterns IS the ULP
/// distance, no separate distance function needed. See the doc comment above
/// for why this must stay tight rather than a wider, looser bound.
const MAX_ULP_DIFF: i64 = 2;
// One row per [`LightingPreset::ALL`] entry, in that order (Daylight, Incandescent,
// RingLights, DarkSpotlight, IsoHemisphere, LightTent, DaylightDome) -- see
// "Regenerating the baseline" above. IsoHemisphere's row is legitimately full of
// exact `0` bit patterns for its lower hemisphere (see
// `iso_hemisphere_is_one_above_and_zero_below` above), not a probe artifact.
#[allow(
    clippy::unreadable_literal,
    reason = "bit-pattern baseline table is pinned verbatim as printed"
)]
const BASELINE_BITS: [[u32; 192]; 7] = [
    [
        1025162801, 1024039367, 1020505008, 1025092110, 1023976507, 1020408312, 1025021648,
        1023913851, 1020311927, 1024951087, 1023851107, 1020215409, 1025346055, 1024202320,
        1020755680, 1025666613, 1024487368, 1021194168, 1024738758, 1023662299, 1019924966,
        1032212597, 1030676166, 1027062275, 1044013601, 1042660459, 1040118652, 1024533502,
        1023479781, 1019644200, 1024456252, 1023411089, 1019538532, 1024385406, 1023286006,
        1019441621, 1065648146, 1064019172, 1060454089, 1082551552, 1081020810, 1077403919,
        1024272461, 1023085141, 1019287126, 1049820964, 1048753769, 1044976412, 1077087441,
        1075787537, 1073015688, 1092050504, 1090951570, 1087311350, 1099112643, 1097413659,
        1093885500, 1099524362, 1098145878, 1094448685, 1097428217, 1095733547, 1092593244,
        1098879375, 1097023947, 1093585753, 1071105434, 1069538931, 1066636126, 1079221057,
        1077684794, 1074838028, 1083370228, 1082303605, 1078523774, 1085806508, 1084469997,
        1081856327, 1076808764, 1075539732, 1072634491, 1048797347, 1047111092, 1043576219,
        1051158443, 1049943083, 1046805930, 1043203236, 1041939864, 1039010164, 1023023449,
        1021207730, 1017843119, 1022534055, 1020772551, 1017508402, 1022392475, 1020646654,
        1017411569, 1078815957, 1077324571, 1074560963, 1066793382, 1065704562, 1062020640,
        1021968452, 1020269604, 1017121561, 1021918760, 1020225417, 1017087574, 1021686103,
        1020018533, 1016928451, 1032860469, 1031813576, 1027948490, 1021493497, 1019847265,
        1016796720, 1021261748, 1019641188, 1016638216, 1042717161, 1041507636, 1038345269,
        1022678992, 1020901431, 1017607530, 1020837726, 1019264139, 1016348210, 1020696384,
        1019138455, 1016251539, 1020555042, 1019012770, 1016154870, 1030483910, 1028771010,
        1025596927, 1020290565, 1018777592, 1015973983, 1020131020, 1018635721, 1015864863,
        1020003539, 1018522363, 1015777674, 1019848338, 1018384354, 1015671524, 1019707337,
        1018258973, 1015575087, 1019565656, 1018132987, 1015478186, 1019424315, 1018007304,
        1015381517, 1019317772, 1017912564, 1015308648, 1019141634, 1017755938, 1015188179,
        1019000292, 1017630254, 1015091509, 1018858951, 1017504570, 1014968112, 1018717611,
        1017378888, 1014774776, 1018576269, 1017253204, 1014581436, 1018434928, 1017127520,
        1014388097, 1018293587, 1017001837, 1014194759, 1018152247, 1016876154, 1014001422,
        1018010905, 1016750470, 1013808082,
    ],
    [
        1012776288, 1023024255, 1027534148, 1012674986, 1022909992, 1027446924, 1012574016,
        1022796103, 1027359984, 1012472997, 1022682158, 1027273003, 1013038877, 1023320442,
        1027760248, 1013743885, 1023762916, 1028367289, 1012168648, 1022338868, 1027010946,
        1019246433, 1029248933, 1034270883, 1033569102, 1043257544, 1048934636, 1011874526,
        1022007112, 1026757696, 1011763880, 1021882310, 1026662425, 1011662310, 1021767744,
        1026574969, 1052630511, 1062611215, 1067678634, 1072092693, 1082273685, 1086767714,
        1011528857, 1021617217, 1026460061, 1039523329, 1049274807, 1054061465, 1067033203,
        1076710087, 1082411289, 1082258257, 1091736542, 1096796415, 1088513955, 1098792656,
        1103238439, 1089221965, 1099249452, 1103848064, 1086345145, 1096346346, 1101371006,
        1088137268, 1098367772, 1102914096, 1060695451, 1070634742, 1075788551, 1068868734,
        1078780474, 1083991755, 1073502184, 1083068602, 1087981342, 1075082870, 1084716386,
        1090497071, 1066795188, 1076441617, 1082206348, 1037920448, 1048165643, 1052681319,
        1039155781, 1049067518, 1053744991, 1032925538, 1042531635, 1048185002, 1009987769,
        1019878946, 1025133122, 1009637163, 1019483480, 1024831236, 1009535690, 1019369023,
        1024743864, 1066610359, 1076233140, 1081963975, 1056991862, 1066457279, 1071457399,
        1009231887, 1019026349, 1024482277, 1009196283, 1018986190, 1024451622, 1009029637,
        1018798222, 1024308133, 1020174804, 1030296090, 1035070248, 1008904547, 1018657126,
        1024200425, 1008725550, 1018455225, 1024046300, 1030667111, 1040622478, 1045270225,
        1009984565, 1019875332, 1025130364, 1008421748, 1018112552, 1023784715, 1008320479,
        1017998326, 1023697518, 1008219211, 1017884100, 1023610322, 1017711380, 1027517471,
        1032949141, 1008032328, 1017673306, 1023449409, 1007915408, 1017541427, 1023287297,
        1007824072, 1017438403, 1023130007, 1007712873, 1017312977, 1022938515, 1007611848,
        1017199027, 1022764543, 1007510338, 1017084528, 1022589733, 1007409070, 1016970303,
        1022415342, 1007332735, 1016884200, 1022283886, 1007206536, 1016741854, 1022066561,
        1007105267, 1016627628, 1021892168, 1007004000, 1016513403, 1021717777, 1006902733,
        1016399180, 1021543388, 1006801464, 1016284954, 1021368995, 1006700197, 1016170729,
        1021194604, 1006564898, 1016056504, 1021020213, 1006362365, 1015942280, 1020845824,
        1006159828, 1015828055, 1020671431,
    ],
    [
        1021303258, 1023655294, 1023453223, 1021200980, 1023595102, 1023378703, 1021099049,
        1023535114, 1023261534, 1020997253, 1023475205, 1023144520, 1021568336, 1023811298,
        1023605575, 1022776057, 1024522062, 1024299704, 1020689822, 1023178377, 1022791133,
        1027755127, 1030009411, 1029658622, 1044895793, 1047214420, 1046853619, 1020392899,
        1022828889, 1022449825, 1020281301, 1022697534, 1022321544, 1020178664, 1022576726,
        1022203564, 1061137584, 1063361422, 1063015371, 1083905861, 1085705250, 1085425247,
        1020101264, 1022485625, 1022114595, 1050651329, 1052503811, 1052215547, 1078871876,
        1081265151, 1080892734, 1093572623, 1095598290, 1095283076, 1100456709, 1102216023,
        1101942257, 1100933244, 1102776921, 1102490027, 1098170626, 1099958974, 1099738034,
        1100203294, 1101917744, 1101650959, 1073213026, 1074915693, 1074691885, 1081344407,
        1083152919, 1082932654, 1084855932, 1086823517, 1086517342, 1085755938, 1087882856,
        1087551887, 1078554162, 1080891191, 1080527526, 1049704878, 1051389805, 1051127614,
        1047743926, 1049571387, 1049351755, 1044118990, 1046300096, 1045960695, 1018488179,
        1020586967, 1020260376, 1018134305, 1020170445, 1019853601, 1018031797, 1020049789,
        1019735769, 1075090799, 1076814691, 1076546437, 1068234283, 1070229409, 1069918948,
        1017725102, 1019688798, 1019383227, 1017689159, 1019646492, 1019341912, 1017521023,
        1019448590, 1019148641, 1028692337, 1031112541, 1030735933, 1017420802, 1019330627,
        1019033440, 1017213944, 1019087147, 1018795658, 1039204672, 1041094120, 1040876564,
        1018976767, 1021162052, 1020822000, 1016907249, 1018726157, 1018443117, 1016805017,
        1018605826, 1018325602, 1016702784, 1018485494, 1018208087, 1026205460, 1028185399,
        1027877301, 1016519388, 1018269632, 1017997277, 1016396089, 1018124504, 1017855546,
        1016303883, 1018015974, 1017749556, 1016191626, 1017883843, 1017620518, 1016089640,
        1017763802, 1017503286, 1015987162, 1017643183, 1017385490, 1015884931, 1017522852,
        1017267976, 1015807868, 1017432147, 1017179394, 1015680468, 1017282193, 1017032949,
        1015578235, 1017161861, 1016915434, 1015476004, 1017041531, 1016797920, 1015373773,
        1016921202, 1016680407, 1015271540, 1016800870, 1016562892, 1015169308, 1016680540,
        1016445378, 1015067077, 1016560210, 1016327864, 1014908123, 1016439880, 1016210351,
        1014703658, 1016319549, 1016092836,
    ],
    [
        1024021274, 1023786218, 1021689627, 1023958517, 1023725100, 1021584632, 1023895985,
        1023664201, 1021480016, 1023833763, 1023603604, 1021375915, 1024183876, 1023944572,
        1021961661, 1025533326, 1025258778, 1023814745, 1023644939, 1023419712, 1021060011,
        1030644714, 1030236666, 1028090467, 1051708076, 1051407176, 1049824545, 1023462788,
        1023074462, 1020755269, 1023378726, 1022941356, 1020640940, 1023252549, 1022818475,
        1020535392, 1063988146, 1063585608, 1061468395, 1091467345, 1091223481, 1089362643,
        1023298250, 1022862984, 1020573622, 1057854789, 1057612444, 1055710969, 1086150395,
        1085826306, 1084121703, 1101028617, 1100754126, 1099310394, 1108048401, 1107809661,
        1105811680, 1108486935, 1108236742, 1106545356, 1104428687, 1104065392, 1102154582,
        1107815301, 1107582649, 1105421698, 1080245403, 1079856446, 1077810661, 1088427318,
        1088043758, 1086026368, 1092342943, 1092076210, 1090673287, 1092189867, 1091927133,
        1090545238, 1085860486, 1085543969, 1083879191, 1057105622, 1056801079, 1054457599,
        1050030049, 1049772976, 1048265722, 1051075579, 1050791200, 1049295456, 1021178449,
        1020798547, 1018800389, 1020744440, 1020375873, 1018437337, 1020618500, 1020253223,
        1018331987, 1077300520, 1076988477, 1075347240, 1075669867, 1075400415, 1073983185,
        1020242208, 1019886758, 1018017215, 1020198109, 1019843812, 1017980326, 1019992054,
        1019643138, 1017807959, 1031794603, 1031356522, 1029052359, 1019933041, 1019585667,
        1017758595, 1019615054, 1019275985, 1017492595, 1041488130, 1041235062, 1039620622,
        1022984770, 1022557690, 1020311392, 1019238762, 1018909521, 1017177824, 1019113330,
        1018787366, 1017072899, 1018987899, 1018665210, 1016967974, 1028743387, 1028384998,
        1026499990, 1018775810, 1018458661, 1016790560, 1018611607, 1018298746, 1016653203,
        1018498476, 1018188570, 1016558568, 1018360745, 1018054436, 1016443355, 1018235615,
        1017932576, 1016338683, 1018109883, 1017810127, 1016233507, 1017984452, 1017687972,
        1016128583, 1017889902, 1017595891, 1016049491, 1017733592, 1017443663, 1015918736,
        1017608160, 1017321507, 1015813811, 1017482729, 1017199352, 1015708887, 1017357299,
        1017077198, 1015603963, 1017231867, 1016955043, 1015499039, 1017106436, 1016832888,
        1015394115, 1016981005, 1016710733, 1015289190, 1016855575, 1016588579, 1015184267,
        1016730143, 1016466423, 1015079342,
    ],
    [
        1068743020, 1067438222, 1064687523, 1068743020, 1067438222, 1064687523, 1068743020,
        1067438222, 1064687523, 1068743020, 1067438222, 1064687523, 1068743020, 1067438222,
        1064687523, 1068743020, 1067438222, 1064687523, 1068743020, 1067438222, 1064687523,
        1068743020, 1067438222, 1064687523, 1068743020, 1067438222, 1064687523, 1068743020,
        1067438222, 1064687523, 1068743020, 1067438222, 1064687523, 1068743020, 1067438222,
        1064687523, 1068743020, 1067438222, 1064687523, 1068743020, 1067438222, 1064687523,
        1068743020, 1067438222, 1064687523, 1068743020, 1067438222, 1064687523, 1068743020,
        1067438222, 1064687523, 1068743020, 1067438222, 1064687523, 1068743020, 1067438222,
        1064687523, 1068743020, 1067438222, 1064687523, 1068743020, 1067438222, 1064687523,
        1068743020, 1067438222, 1064687523, 1068743020, 1067438222, 1064687523, 1068743020,
        1067438222, 1064687523, 1068743020, 1067438222, 1064687523, 1068743020, 1067438222,
        1064687523, 1068743020, 1067438222, 1064687523, 1068743020, 1067438222, 1064687523,
        1068743020, 1067438222, 1064687523, 1068743020, 1067438222, 1064687523, 1068709232,
        1067408177, 1064641305, 1065524519, 1063799308, 1060284980, 1053061403, 1051635236,
        1048992481, 998505479, 996850192, 993298986, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    ],
    [
        1046707576, 1048961477, 1048756121, 1046537190, 1048861202, 1048658193, 1046366804,
        1048760927, 1048544529, 1046196418, 1048660652, 1048348672, 1046026031, 1048544752,
        1048152815, 1045855645, 1048344202, 1047956958, 1045685259, 1048143651, 1047761102,
        1045514873, 1047943101, 1047565245, 1062085442, 1064477086, 1064104923, 1045174101,
        1047542000, 1047173532, 1045003714, 1047341449, 1046977675, 1043057514, 1045050703,
        1044740543, 1044662942, 1046940348, 1046585962, 1070478363, 1072870771, 1072498489,
        1044322170, 1046539248, 1046194249, 1016515362, 1018264893, 1017992649, 1045444339,
        1047860080, 1047484167, 1016242744, 1017944012, 1017679278, 1043640625, 1045737045,
        1045410822, 1015970126, 1017623132, 1017365908, 1043299853, 1045335944, 1045019109,
        1070307977, 1072670220, 1072302632, 1042959080, 1044934842, 1044627395, 1015424889,
        1016981369, 1016739165, 1042618308, 1044533742, 1044235682, 1031830099, 1033320720,
        1033088765, 1069272620, 1071451568, 1071112503, 1042107149, 1043932091, 1043648112,
        1036814306, 1039187306, 1038818045, 1060910235, 1063093826, 1062754038, 1034249224,
        1036168115, 1035869517, 1038147617, 1040472026, 1040269030, 1025985877, 1027926943,
        1027624894, 1017725367, 1019689111, 1019383534, 1017537677, 1019468193, 1019167786,
        1017537677, 1019468193, 1019167786, 1017537677, 1019468193, 1019167786, 1017537677,
        1019468193, 1019167786, 1017537677, 1019468193, 1019167786, 1017537677, 1019468193,
        1019167786, 1017537677, 1019468193, 1019167786, 1017537677, 1019468193, 1019167786,
        1017537677, 1019468193, 1019167786, 1017537677, 1019468193, 1019167786, 1017537677,
        1019468193, 1019167786, 1017537677, 1019468193, 1019167786, 1017537677, 1019468193,
        1019167786, 1017537677, 1019468193, 1019167786, 1017537677, 1019468193, 1019167786,
        1017537677, 1019468193, 1019167786, 1017537677, 1019468193, 1019167786, 1017537677,
        1019468193, 1019167786, 1017537677, 1019468193, 1019167786, 1017537677, 1019468193,
        1019167786, 1017537677, 1019468193, 1019167786, 1017537677, 1019468193, 1019167786,
        1017537677, 1019468193, 1019167786, 1017537677, 1019468193, 1019167786, 1017537677,
        1019468193, 1019167786, 1017537677, 1019468193, 1019167786, 1017537677, 1019468193,
        1019167786, 1017537677, 1019468193, 1019167786, 1017537677, 1019468193, 1019167786,
        1017537677, 1019468193, 1019167786,
    ],
    [
        1041454910, 1040385217, 1036618654, 1041574866, 1040491884, 1036782741, 1041818147,
        1040708214, 1037115521, 1042301565, 1041138080, 1037776781, 1042281571, 1041120300,
        1037749431, 1044959284, 1043501380, 1040799815, 1042752708, 1041539246, 1038393893,
        1042988275, 1041748717, 1038716123, 1049542017, 1048435446, 1044594843, 1043459412,
        1042167662, 1039360584, 1043910342, 1042568638, 1039977404, 1043955596, 1042608879,
        1040039307, 1044166116, 1042796078, 1040257333, 1055970693, 1054222239, 1050982269,
        1044637253, 1043215024, 1040579564, 1044872821, 1043424496, 1040740679, 1048754496,
        1047034886, 1043517605, 1045343958, 1043843440, 1041062910, 1048746681, 1047020986,
        1043506914, 1045815094, 1044262384, 1041385140, 1046050663, 1044471857, 1041546256,
        1058620798, 1057508048, 1053927529, 1046521799, 1044890802, 1041868486, 1046760146,
        1045102744, 1042031501, 1047124530, 1045426762, 1042280719, 1047228503, 1045519218,
        1042351831, 1054664693, 1053060915, 1050089040, 1047699640, 1045938163, 1042674062,
        1047935208, 1046147635, 1042835177, 1051968167, 1050663107, 1047913539, 1048369515,
        1046533830, 1043132218, 1045209710, 1043724065, 1040971092, 1038157808, 1036524083,
        1033496714, 1030249305, 1028562395, 1025436471, 1030097935, 1028427794, 1025332942,
        1030097935, 1028427794, 1025332942, 1030097935, 1028427794, 1025332942, 1030097935,
        1028427794, 1025332942, 1030097935, 1028427794, 1025332942, 1030097935, 1028427794,
        1025332942, 1030097935, 1028427794, 1025332942, 1030097935, 1028427794, 1025332942,
        1030097935, 1028427794, 1025332942, 1030097935, 1028427794, 1025332942, 1030097935,
        1028427794, 1025332942, 1030097935, 1028427794, 1025332942, 1030097935, 1028427794,
        1025332942, 1030097935, 1028427794, 1025332942, 1030097935, 1028427794, 1025332942,
        1030097935, 1028427794, 1025332942, 1030097935, 1028427794, 1025332942, 1030097935,
        1028427794, 1025332942, 1030097935, 1028427794, 1025332942, 1030097935, 1028427794,
        1025332942, 1030097935, 1028427794, 1025332942, 1030097935, 1028427794, 1025332942,
        1030097935, 1028427794, 1025332942, 1030097935, 1028427794, 1025332942, 1030097935,
        1028427794, 1025332942, 1030097935, 1028427794, 1025332942, 1030097935, 1028427794,
        1025332942, 1030097935, 1028427794, 1025332942, 1030097935, 1028427794, 1025332942,
        1030097935, 1028427794, 1025332942,
    ],
];

/// Regression pin for the rig-sharing split (`sample_studio_environment_with_rig`,
/// which both the ad-hoc callers here and `accumulate_miss_radiance`'s escape lookup
/// share -- the latter borrows the rig the trace built once -- instead of each
/// duplicating the studio rig arithmetic inline):
/// tracing 64 directions x 3 wavelengths through EVERY preset in [`LightingPreset::ALL`]
/// (not just `RingLights`, which is all the original version of this test pinned) must
/// reproduce [`BASELINE_BITS`], to within a small ULP tolerance.
///
/// # Regenerating the baseline
///
/// [`BASELINE_BITS`] is captured from this test's OWN current output (a temporary
/// `eprintln!` of `got_bits` per sample, run once via `cargo
/// test -p indicatrix --lib -- studio_presets_are_unchanged_by_the_split
/// --nocapture`, then removed). [`MAX_ULP_DIFF`] must stay tight: a budget wide
/// enough to absorb reassociation noise is ALSO wide enough to absorb a small
/// coefficient change on the ambient term without the test ever failing, which would
/// mean it isn't actually pinning the formula.
///
/// The table was last captured on 2026-10-01 from the optimised test profile: the
/// workspace manifest gives this crate `opt-level = 3` under `cargo test`
/// (`[profile.test.package.indicatrix]`), and a release build passes this test with
/// it. The inputs of the call go through `black_box` since then, so no build folds any
/// part of it at compile time (a release build with LTO otherwise evaluated the
/// wavelength terms in double precision and landed 3 ULP off). The unoptimised build
/// rounds the studio sampler a few ULP differently, within 2 ULP for most samples but
/// up to 12 ULP at the falloff-cone directions 28/29/38/46 where `key_dot.powi(28)`
/// amplifies the difference, so it no longer passes this test; nothing runs the suite
/// unoptimised since that change.
///
/// The CPU/GPU HDR white-balance step does not touch
/// `studio_rig_lighting`/`sample_studio_environment` at all -- it lives entirely in
/// `trace_spectral_ray_inner`'s post-integration white-balance step -- so nothing on
/// the Studio-rig sampling path this test exercises is affected by it. Before
/// accepting a freshly captured baseline, diff it against the prior one: deltas of a
/// few ULP concentrated in the falloff-cone-edge cluster named below (e.g.
/// 28/29/38/46) match the `key_dot.powi(28)` reassociation-noise mechanism analyzed
/// below and can be folded into the new baseline; a diff that is large, widespread,
/// or inconsistent with that mechanism is a real regression and should be reported
/// rather than pasted over.
///
/// # Why a tolerance at all, not bit-for-bit equality
///
/// Requiring exact equality only ever surfaces the FIRST mismatch (`assert_eq!`
/// aborts the loop immediately), which is what hid the true shape of this the first
/// time: an exhaustive sweep of all 192 samples' bit-pattern deltas (not just the
/// first failure) is needed to tell "reassociation noise" apart from "a real
/// regression". Every non-ambient term in `studio_rig_lighting` (key softbox, fill,
/// ring) is accumulated via `softbox.mul_add(spec_power, radiance)`-style fused
/// multiply-adds, and `key_dot.powi(28)` in particular amplifies a sub-ULP
/// perturbation in `key_dot` into a much larger one in the softbox term for
/// directions near the falloff cone's edge -- exactly where the largest deltas
/// (dir 28/29/38/46) sit. Floating-point multiplication and fused multiply-add are
/// not associative, so evaluating the identical formula through a differently-shaped
/// call chain, or a differently-optimized build, can legitimately round
/// intermediate bits either way, with a `^28` term turning "either way" into
/// "either way, times a large derivative" -- with no change in the actual light
/// transport (10 ULP here is a relative difference near `1e-6`, many orders below
/// anything a path tracer's own sample noise could ever resolve). [`MAX_ULP_DIFF`]'s
/// `2` leaves headroom for exactly that kind of single-ULP-scale noise on the
/// majority of samples while a difference of many thousands of ULP -- what a
/// genuine behavioural regression (a changed exponent, a changed coefficient, a
/// dropped term) would actually produce -- still fails loudly; a handful of the
/// falloff-edge directions may need re-diffing the same way if a future legitimate
/// change (not a regression) shifts them past `2` again.
#[test]
fn studio_presets_are_unchanged_by_the_split() {
    for (preset_idx, &preset) in LightingPreset::ALL.iter().enumerate() {
        let mut idx = 0;
        for k in 0..64 {
            let phi = (k as f32 + 0.5) * (std::f32::consts::PI * (3.0 - 5.0f32.sqrt()));
            let y = (k as f32 + 0.5).mul_add(-(2.0 / 64.0), 1.0);
            let r = y.mul_add(-y, 1.0).max(0.0).sqrt();
            let dir = Vec3::new(r * phi.cos(), y, r * phi.sin());
            for &lambda in &[450.0f32, 550.0, 650.0] {
                // Through `black_box`, so that no build folds part of this call at
                // compile time (a release build with LTO otherwise evaluates the
                // wavelength terms in double precision); the table pins the run-time
                // path the renderer takes.
                let val = sample_studio_environment(
                    black_box(dir),
                    black_box(lambda),
                    black_box(preset),
                    black_box(1.2),
                    black_box(0.4),
                    black_box(0.35),
                );
                let got_bits = val.to_bits();
                let expected_bits = BASELINE_BITS[preset_idx][idx];
                let ulp_diff = (i64::from(got_bits) - i64::from(expected_bits)).abs();
                assert!(
                    ulp_diff <= MAX_ULP_DIFF,
                    "{preset:?}: divergence at dir {k}, lambda {lambda}: got {val} \
                     (bits {got_bits}), expected bits {expected_bits} ({ulp_diff} ULP \
                     away, tolerance {MAX_ULP_DIFF})"
                );
                idx += 1;
            }
        }
    }
}