@plasius/gpu-lighting 0.2.6 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -20,6 +20,36 @@ The format is based on **[Keep a Changelog](https://keepachangelog.com/en/1.1.0/
20
20
  - **Security**
21
21
  - (placeholder)
22
22
 
23
+ ## [0.2.8] - 2026-06-20
24
+
25
+ - **Added**
26
+ - Extended the wavefront lighting contract with compact medium-state carry,
27
+ visibility-probe ray helpers, MIS/exclusive-emissive probe controls, and
28
+ deterministic CPU reference fixtures for continuation validation.
29
+
30
+ - **Changed**
31
+ - Wavefront ray-record documentation now mirrors the current renderer payload
32
+ shape, including medium-stack and spectral-state fields used for transport
33
+ validation.
34
+ - The Eames validation page now defaults display-quality captures to
35
+ `accelerationBuildMode=cpu-upload` while still allowing explicit GPU BVH
36
+ validation through the query parameter.
37
+
38
+ - **Fixed**
39
+ - Wavefront continuation helpers now report total internal reflection
40
+ explicitly and keep refraction/transparency medium transitions stable in the
41
+ published reference contract.
42
+ - The Eames validation mesh transform now floor-aligns scaled product meshes
43
+ by their actual lower bound instead of centering them through the analytic
44
+ floor plane, preventing chair geometry from rendering mostly below ground.
45
+ - Eames validation captures at 4 SPP and 8 SPP no longer depend on the
46
+ corrupted display-quality CPU-upload material path or the broken high-SPP
47
+ tile scheduling order, so browser-driven runtime screenshots now render the
48
+ chair coherently instead of producing striped/blocked artifact regions.
49
+
50
+ - **Security**
51
+ - (placeholder)
52
+
23
53
  ## [0.2.6] - 2026-06-16
24
54
 
25
55
  - **Added**
@@ -475,7 +505,8 @@ The format is based on **[Keep a Changelog](https://keepachangelog.com/en/1.1.0/
475
505
  [0.1.16]: https://github.com/Plasius-LTD/gpu-lighting/releases/tag/v0.1.16
476
506
  [0.1.17]: https://github.com/Plasius-LTD/gpu-lighting/releases/tag/v0.1.17
477
507
  [0.1.19]: https://github.com/Plasius-LTD/gpu-lighting/releases/tag/v0.1.19
478
- [Unreleased]: https://github.com/Plasius-LTD/gpu-lighting/compare/v0.2.6...HEAD
508
+ [Unreleased]: https://github.com/Plasius-LTD/gpu-lighting/compare/v0.2.8...HEAD
479
509
  [0.2.0]: https://github.com/Plasius-LTD/gpu-lighting/releases/tag/v0.2.0
480
510
  [0.2.2]: https://github.com/Plasius-LTD/gpu-lighting/releases/tag/v0.2.2
481
511
  [0.2.6]: https://github.com/Plasius-LTD/gpu-lighting/releases/tag/v0.2.6
512
+ [0.2.8]: https://github.com/Plasius-LTD/gpu-lighting/releases/tag/v0.2.8
package/README.md CHANGED
@@ -173,7 +173,11 @@ radiance accumulation and continuation scattering.
173
173
  ```js
174
174
  import {
175
175
  createWavefrontLightingPlan,
176
+ createWavefrontReferenceFixture,
177
+ createWavefrontVisibilityProbeRay,
178
+ evaluateWavefrontMaterialReference,
176
179
  evaluateWavefrontTerminalRadiance,
180
+ evaluateWavefrontVisibilityProbe,
177
181
  loadLightingTechniqueWorkerBundle,
178
182
  } from "@plasius/gpu-lighting";
179
183
 
@@ -181,6 +185,7 @@ const plan = createWavefrontLightingPlan({
181
185
  maxDepth: 6,
182
186
  queueCapacity: 4096,
183
187
  explicitLightSampling: true,
188
+ visibilityProbeMode: "mis-balanced",
184
189
  });
185
190
 
186
191
  const bundle = await loadLightingTechniqueWorkerBundle("wavefront");
@@ -189,16 +194,72 @@ const emissive = evaluateWavefrontTerminalRadiance({
189
194
  throughput: [0.5, 0.5, 0.5],
190
195
  emission: [8, 6, 4],
191
196
  });
197
+ const material = evaluateWavefrontMaterialReference({
198
+ hitType: "surface",
199
+ eventKind: "refraction",
200
+ throughput: [1, 1, 1],
201
+ transmission: [0.92, 0.95, 0.98],
202
+ ior: 1.45,
203
+ shadingNormal: [0, 1, 0],
204
+ viewDirection: [0, 1, 0],
205
+ currentMediumRefId: 0,
206
+ surfaceMediumRefId: 7,
207
+ });
208
+ const probeRay = createWavefrontVisibilityProbeRay({
209
+ rayId: 12,
210
+ parentRayId: 4,
211
+ sourcePixelId: 9,
212
+ sampleId: 2,
213
+ bounce: 1,
214
+ origin: [0, 1, 0],
215
+ direction: [0.25, -1, 0.15],
216
+ throughput: [0.8, 0.7, 0.6],
217
+ mediumRefId: 7,
218
+ mediumStack: [7],
219
+ });
220
+ const probe = evaluateWavefrontVisibilityProbe({
221
+ probeRay,
222
+ probeMode: "exclusive-emissive",
223
+ activeEmissiveRadiance: [4, 3, 2],
224
+ emissiveRadiance: [4, 3, 2],
225
+ transparentSegments: [[0.8, 0.8, 0.8]],
226
+ });
227
+ const fixture = createWavefrontReferenceFixture({
228
+ hitType: "emissive",
229
+ throughput: [0.8, 0.7, 0.6],
230
+ emission: [4, 3, 2],
231
+ visibilityProbe: {
232
+ probeMode: "mis-balanced",
233
+ emissiveRadiance: [0.6, 0.3, 0.1],
234
+ transparentSegments: [[0.7, 0.8, 0.9]],
235
+ },
236
+ });
192
237
 
193
238
  console.log(plan.requiredRendererPassOrder);
239
+ console.log(plan.visibilityProbeMode);
194
240
  console.log(bundle.jobs.map((job) => job.label));
195
241
  console.log(emissive.radiance);
242
+ console.log(material.continuation.mediumState);
243
+ console.log(probe.doubleCountPrevented);
244
+ console.log(fixture.tolerance);
196
245
  ```
197
246
 
198
247
  This slice keeps emissive hits, environment hits, and environment-miss dark
199
248
  fallbacks on the lighting package surface without reintroducing a depth-first
200
249
  shader dependency into the renderer-owned wavefront queue model.
201
250
 
251
+ The continuation/reference surface now also exposes:
252
+
253
+ - compact medium-state carry for refraction/transparency events, including
254
+ total-internal-reflection fallback reporting
255
+ - shared-ray payload helpers where visibility probes reuse the base ray layout
256
+ and encode their kind through the low bits of `flags`
257
+ - optional probe contribution helpers with `mis-balanced` and
258
+ `exclusive-emissive` modes so active emissive hits remain correct even when
259
+ explicit light sampling is enabled
260
+ - deterministic reference fixtures that publish buffer-like accumulation outputs
261
+ with a documented default tolerance of `0.0005` for CPU-vs-GPU comparisons
262
+
202
263
  ## Distance-Banded Lighting
203
264
 
204
265
  ```js
package/dist/index.cjs CHANGED
@@ -34,11 +34,17 @@ __export(index_exports, {
34
34
  createLightingProfileModeLadder: () => createLightingProfileModeLadder,
35
35
  createWavefrontEnvironmentLightingOptions: () => createWavefrontEnvironmentLightingOptions,
36
36
  createWavefrontLightingPlan: () => createWavefrontLightingPlan,
37
+ createWavefrontRayPayload: () => createWavefrontRayPayload,
38
+ createWavefrontReferenceFixture: () => createWavefrontReferenceFixture,
39
+ createWavefrontVisibilityProbeRay: () => createWavefrontVisibilityProbeRay,
37
40
  defaultAdaptiveLightingProfilePolicy: () => defaultAdaptiveLightingProfilePolicy,
38
41
  defaultLightingProfile: () => defaultLightingProfile,
39
42
  defaultLightingTechnique: () => defaultLightingTechnique,
40
43
  evaluateWavefrontContinuationEvent: () => evaluateWavefrontContinuationEvent,
44
+ evaluateWavefrontMaterialReference: () => evaluateWavefrontMaterialReference,
45
+ evaluateWavefrontMediumState: () => evaluateWavefrontMediumState,
41
46
  evaluateWavefrontTerminalRadiance: () => evaluateWavefrontTerminalRadiance,
47
+ evaluateWavefrontVisibilityProbe: () => evaluateWavefrontVisibilityProbe,
42
48
  getLightingProfile: () => getLightingProfile,
43
49
  getLightingProfileWorkerManifest: () => getLightingProfileWorkerManifest,
44
50
  getLightingTechnique: () => getLightingTechnique,
@@ -65,9 +71,11 @@ __export(index_exports, {
65
71
  lightingWavefrontHitTypes: () => lightingWavefrontHitTypes,
66
72
  lightingWavefrontPassOrder: () => lightingWavefrontPassOrder,
67
73
  lightingWavefrontQueuePairStrategy: () => lightingWavefrontQueuePairStrategy,
74
+ lightingWavefrontRayKinds: () => lightingWavefrontRayKinds,
68
75
  lightingWavefrontSchemaVersion: () => lightingWavefrontSchemaVersion,
69
76
  lightingWavefrontTerminalHitTypes: () => lightingWavefrontTerminalHitTypes,
70
77
  lightingWavefrontTerminationPolicy: () => lightingWavefrontTerminationPolicy,
78
+ lightingWavefrontVisibilityProbeModes: () => lightingWavefrontVisibilityProbeModes,
71
79
  lightingWorkerManifests: () => lightingWorkerManifests,
72
80
  lightingWorkerQueueClass: () => lightingWorkerQueueClass,
73
81
  loadLightingJobs: () => loadLightingJobs,
@@ -1137,6 +1145,15 @@ var lightingWavefrontHitTypes = Object.freeze([
1137
1145
  "transparent",
1138
1146
  "miss"
1139
1147
  ]);
1148
+ var lightingWavefrontRayKinds = Object.freeze([
1149
+ "path",
1150
+ "visibility-probe"
1151
+ ]);
1152
+ var lightingWavefrontVisibilityProbeModes = Object.freeze([
1153
+ "disabled",
1154
+ "mis-balanced",
1155
+ "exclusive-emissive"
1156
+ ]);
1140
1157
  var lightingWavefrontTerminalHitTypes = Object.freeze([
1141
1158
  "emissive",
1142
1159
  "environment",
@@ -1181,11 +1198,35 @@ var lightingWavefrontBufferContracts = Object.freeze({
1181
1198
  createLightingWavefrontField("sourcePixelId", "u32", "Source pixel/sample owner."),
1182
1199
  createLightingWavefrontField("sampleId", "u32", "Per-pixel sample slot."),
1183
1200
  createLightingWavefrontField("bounce", "u32", "Current bounce depth."),
1201
+ createLightingWavefrontField("mediumRefId", "u32", "Current medium reference."),
1202
+ createLightingWavefrontField(
1203
+ "mediumStackDepth",
1204
+ "u32",
1205
+ "Depth of the bounded nested medium stack."
1206
+ ),
1207
+ createLightingWavefrontField(
1208
+ "flags",
1209
+ "u32",
1210
+ "Renderer-owned ray flags. Low bits may encode ray kind metadata."
1211
+ ),
1212
+ createLightingWavefrontField(
1213
+ "mediumStack0",
1214
+ "vec4<u32>",
1215
+ "Lower half of the bounded nested medium stack."
1216
+ ),
1217
+ createLightingWavefrontField(
1218
+ "mediumStack1",
1219
+ "vec4<u32>",
1220
+ "Upper half of the bounded nested medium stack."
1221
+ ),
1222
+ createLightingWavefrontField(
1223
+ "spectralState",
1224
+ "vec4<f32>",
1225
+ "Spectral transport payload for wavelength-driven reference validation."
1226
+ ),
1184
1227
  createLightingWavefrontField("origin", "vec3<f32>", "Ray origin."),
1185
1228
  createLightingWavefrontField("direction", "vec3<f32>", "Normalized ray direction."),
1186
- createLightingWavefrontField("throughput", "vec3<f32>", "Accumulated path throughput."),
1187
- createLightingWavefrontField("mediumRefId", "u32", "Current medium reference."),
1188
- createLightingWavefrontField("flags", "u32", "Renderer-owned ray flags.")
1229
+ createLightingWavefrontField("throughput", "vec3<f32>", "Accumulated path throughput.")
1189
1230
  ]
1190
1231
  ),
1191
1232
  hit: createLightingWavefrontRecordContract(
@@ -1274,6 +1315,17 @@ var wavefrontEventKinds = Object.freeze([
1274
1315
  "transparency",
1275
1316
  "terminate"
1276
1317
  ]);
1318
+ var wavefrontRayKindFlagMask = 3;
1319
+ var wavefrontRayKindFlagValues = Object.freeze({
1320
+ path: 0,
1321
+ "visibility-probe": 1
1322
+ });
1323
+ function normalizeWavefrontRayKind(value) {
1324
+ return lightingWavefrontRayKinds.includes(value) ? value : "path";
1325
+ }
1326
+ function normalizeWavefrontVisibilityProbeMode(value) {
1327
+ return lightingWavefrontVisibilityProbeModes.includes(value) ? value : "disabled";
1328
+ }
1277
1329
  function normalizeWavefrontHitType(value) {
1278
1330
  return lightingWavefrontHitTypes.includes(value) ? value : "surface";
1279
1331
  }
@@ -1346,6 +1398,118 @@ function refractDirection(direction, normal, etaRatio) {
1346
1398
  const rOutParallel = scaleVec3(normal, -Math.sqrt(parallelFactor));
1347
1399
  return normalizeDirection(addVec3(rOutPerp, rOutParallel), direction);
1348
1400
  }
1401
+ function normalizeMediumRefId(value) {
1402
+ const mediumRefId = Math.max(0, Math.trunc(readFinite(value, 0)));
1403
+ return Number.isFinite(mediumRefId) ? mediumRefId : 0;
1404
+ }
1405
+ function normalizeMediumStack(value) {
1406
+ if (!Array.isArray(value)) {
1407
+ return [];
1408
+ }
1409
+ return value.map((entry) => normalizeMediumRefId(entry)).filter((entry, index, stack) => entry > 0 && stack.indexOf(entry) === index).slice(0, 4);
1410
+ }
1411
+ function createWavefrontMediumStatePayload(currentMediumRefId, stack) {
1412
+ const normalizedStack = normalizeMediumStack(stack);
1413
+ const stackSlots = [0, 0, 0, 0];
1414
+ normalizedStack.forEach((entry, index) => {
1415
+ stackSlots[index] = entry;
1416
+ });
1417
+ return Object.freeze({
1418
+ currentMediumRefId,
1419
+ stackDepth: normalizedStack.length,
1420
+ stack: Object.freeze(normalizedStack),
1421
+ stackSlots: Object.freeze(stackSlots)
1422
+ });
1423
+ }
1424
+ function evaluateWavefrontMediumState(options = {}) {
1425
+ const currentMediumRefId = normalizeMediumRefId(
1426
+ options.currentMediumRefId ?? options.mediumRefId
1427
+ );
1428
+ const surfaceMediumRefId = normalizeMediumRefId(options.surfaceMediumRefId);
1429
+ const stack = normalizeMediumStack(options.mediumStack);
1430
+ const frontFace = options.frontFace !== false;
1431
+ const eventKind = normalizeWavefrontEventKind(options.eventKind) ?? "transparency";
1432
+ if (surfaceMediumRefId === 0 || eventKind !== "refraction" && eventKind !== "transparency") {
1433
+ return Object.freeze({
1434
+ ...createWavefrontMediumStatePayload(currentMediumRefId, stack),
1435
+ enteredMediumRefId: 0,
1436
+ exitedMediumRefId: 0
1437
+ });
1438
+ }
1439
+ let nextStack = [...stack];
1440
+ let nextMediumRefId = currentMediumRefId;
1441
+ let enteredMediumRefId = 0;
1442
+ let exitedMediumRefId = 0;
1443
+ const stackTop = nextStack.at(-1) ?? 0;
1444
+ if (frontFace) {
1445
+ if (stackTop !== surfaceMediumRefId) {
1446
+ nextStack.push(surfaceMediumRefId);
1447
+ nextStack = nextStack.slice(-4);
1448
+ }
1449
+ nextMediumRefId = surfaceMediumRefId;
1450
+ enteredMediumRefId = surfaceMediumRefId;
1451
+ } else if (stackTop === surfaceMediumRefId) {
1452
+ nextStack.pop();
1453
+ nextMediumRefId = nextStack.at(-1) ?? 0;
1454
+ exitedMediumRefId = surfaceMediumRefId;
1455
+ }
1456
+ return Object.freeze({
1457
+ ...createWavefrontMediumStatePayload(nextMediumRefId, nextStack),
1458
+ enteredMediumRefId,
1459
+ exitedMediumRefId
1460
+ });
1461
+ }
1462
+ function encodeWavefrontRayFlags(flags, rayKind) {
1463
+ const normalizedFlags = Math.max(0, Math.trunc(readFinite(flags, 0)));
1464
+ const rayKindValue = wavefrontRayKindFlagValues[rayKind];
1465
+ return normalizedFlags & ~wavefrontRayKindFlagMask | rayKindValue;
1466
+ }
1467
+ function createWavefrontRayPayload(options = {}) {
1468
+ const rayKind = normalizeWavefrontRayKind(options.rayKind);
1469
+ const mediumState = evaluateWavefrontMediumState({
1470
+ currentMediumRefId: options.mediumRefId,
1471
+ mediumStack: options.mediumStack
1472
+ });
1473
+ const spectralState = Object.freeze(
1474
+ normalizeVec3(options.spectralState, [550, 1, 0]).concat(
1475
+ readFinite(options.spectralWeight, 0)
1476
+ )
1477
+ );
1478
+ const mediumStack0 = Object.freeze([
1479
+ mediumState.stackSlots[0],
1480
+ mediumState.stackSlots[1],
1481
+ mediumState.stackSlots[2],
1482
+ mediumState.stackSlots[3]
1483
+ ]);
1484
+ const mediumStack1 = Object.freeze([0, 0, 0, 0]);
1485
+ return Object.freeze({
1486
+ rayId: Math.max(0, Math.trunc(readFinite(options.rayId, 0))),
1487
+ parentRayId: Math.max(0, Math.trunc(readFinite(options.parentRayId, 0))),
1488
+ sourcePixelId: Math.max(0, Math.trunc(readFinite(options.sourcePixelId, 0))),
1489
+ sampleId: Math.max(0, Math.trunc(readFinite(options.sampleId, 0))),
1490
+ bounce: Math.max(0, Math.trunc(readFinite(options.bounce, 0))),
1491
+ origin: Object.freeze(normalizeVec3(options.origin, [0, 0, 0])),
1492
+ direction: Object.freeze(normalizeDirection(options.direction, [0, 0, -1])),
1493
+ throughput: Object.freeze(
1494
+ saturateVec3(normalizeVec3(options.throughput, [1, 1, 1]))
1495
+ ),
1496
+ mediumRefId: mediumState.currentMediumRefId,
1497
+ mediumStackDepth: mediumState.stackDepth,
1498
+ mediumStack: mediumState.stack,
1499
+ mediumStackSlots: mediumState.stackSlots,
1500
+ mediumStack0,
1501
+ mediumStack1,
1502
+ spectralState,
1503
+ rayKind,
1504
+ flags: encodeWavefrontRayFlags(options.flags, rayKind)
1505
+ });
1506
+ }
1507
+ function createWavefrontVisibilityProbeRay(options = {}) {
1508
+ return createWavefrontRayPayload({
1509
+ ...options,
1510
+ rayKind: "visibility-probe"
1511
+ });
1512
+ }
1349
1513
  function createWavefrontLightingPlan(options = {}) {
1350
1514
  const maxDepth = Math.max(1, Math.trunc(readFinite(options.maxDepth, 4)));
1351
1515
  const queueCapacity = Math.max(
@@ -1353,6 +1517,9 @@ function createWavefrontLightingPlan(options = {}) {
1353
1517
  Math.trunc(readFinite(options.queueCapacity, 4096))
1354
1518
  );
1355
1519
  const explicitLightSampling = Boolean(options.explicitLightSampling);
1520
+ const visibilityProbeMode = normalizeWavefrontVisibilityProbeMode(
1521
+ options.visibilityProbeMode ?? (explicitLightSampling ? "mis-balanced" : "disabled")
1522
+ );
1356
1523
  const accumulationResetEpoch = Math.max(
1357
1524
  0,
1358
1525
  Math.trunc(readFinite(options.accumulationResetEpoch, 0))
@@ -1362,6 +1529,8 @@ function createWavefrontLightingPlan(options = {}) {
1362
1529
  maxDepth,
1363
1530
  queueCapacity,
1364
1531
  explicitLightSampling,
1532
+ visibilityProbeMode,
1533
+ rayKinds: lightingWavefrontRayKinds,
1365
1534
  accumulationResetEpoch,
1366
1535
  queueLayout: Object.freeze({
1367
1536
  strategy: lightingWavefrontQueuePairStrategy,
@@ -1400,7 +1569,8 @@ function createWavefrontLightingPlan(options = {}) {
1400
1569
  ]),
1401
1570
  writes: Object.freeze(["ray"]),
1402
1571
  continuationHitTypes: lightingWavefrontContinuationHitTypes,
1403
- explicitLightSampling
1572
+ explicitLightSampling,
1573
+ visibilityProbeMode
1404
1574
  })
1405
1575
  ])
1406
1576
  });
@@ -1460,7 +1630,9 @@ function evaluateWavefrontContinuationEvent(options = {}) {
1460
1630
  const opacity = clampUnit(options.opacity, 1);
1461
1631
  const refractiveIndex = Math.max(1, readFinite(options.refractiveIndex ?? options.ior, 1.45));
1462
1632
  const transmissionStrength = Math.max(...transmission);
1463
- let eventKind = normalizeWavefrontEventKind(options.eventKind) ?? (hitType === "transparent" || opacity < 0.999 ? "transparency" : transmissionStrength > 1e-3 ? "refraction" : metalness >= 0.5 || roughness <= 0.2 ? "reflection" : "diffuse");
1633
+ const requestedEventKind = normalizeWavefrontEventKind(options.eventKind) ?? (hitType === "transparent" || opacity < 0.999 ? "transparency" : transmissionStrength > 1e-3 ? "refraction" : metalness >= 0.5 || roughness <= 0.2 ? "reflection" : "diffuse");
1634
+ let eventKind = requestedEventKind;
1635
+ let totalInternalReflection = false;
1464
1636
  let nextDirection = incomingDirection;
1465
1637
  let attenuation;
1466
1638
  if (!lightingWavefrontContinuationHitTypes.includes(hitType) || bounceIndex >= maxDepth - 1) {
@@ -1471,8 +1643,20 @@ function evaluateWavefrontContinuationEvent(options = {}) {
1471
1643
  attenuation = mixVec3([0.04, 0.04, 0.04], albedo, metalness);
1472
1644
  } else if (eventKind === "refraction") {
1473
1645
  const etaRatio = frontFace ? 1 / refractiveIndex : refractiveIndex;
1474
- nextDirection = refractDirection(incomingDirection, orientedNormal, etaRatio) ?? reflectDirection(incomingDirection, orientedNormal);
1475
- attenuation = transmissionStrength > 1e-3 ? transmission : [1, 1, 1];
1646
+ const refractedDirection = refractDirection(
1647
+ incomingDirection,
1648
+ orientedNormal,
1649
+ etaRatio
1650
+ );
1651
+ if (refractedDirection) {
1652
+ nextDirection = refractedDirection;
1653
+ attenuation = transmissionStrength > 1e-3 ? transmission : [1, 1, 1];
1654
+ } else {
1655
+ totalInternalReflection = true;
1656
+ eventKind = "reflection";
1657
+ nextDirection = reflectDirection(incomingDirection, orientedNormal);
1658
+ attenuation = mixVec3([0.04, 0.04, 0.04], albedo, metalness);
1659
+ }
1476
1660
  } else if (eventKind === "transparency") {
1477
1661
  nextDirection = incomingDirection;
1478
1662
  const transparencyWeight = Math.max(1 - opacity, transmissionStrength, 0.05);
@@ -1486,16 +1670,150 @@ function evaluateWavefrontContinuationEvent(options = {}) {
1486
1670
  }
1487
1671
  const nextThroughput = multiplyVec3(throughput, saturateVec3(attenuation));
1488
1672
  const continueTracing = eventKind !== "terminate" && colorLuminance(nextThroughput) > 1e-4;
1673
+ const mediumState = evaluateWavefrontMediumState({
1674
+ currentMediumRefId: options.currentMediumRefId ?? options.mediumRefId,
1675
+ surfaceMediumRefId: options.surfaceMediumRefId,
1676
+ mediumStack: options.mediumStack,
1677
+ frontFace,
1678
+ eventKind
1679
+ });
1489
1680
  return Object.freeze({
1490
1681
  hitType,
1682
+ requestedEventKind,
1491
1683
  eventKind,
1492
1684
  continueTracing,
1685
+ totalInternalReflection,
1493
1686
  nextDirection: Object.freeze(nextDirection),
1494
1687
  attenuation: Object.freeze(saturateVec3(attenuation)),
1495
1688
  nextThroughput: Object.freeze(nextThroughput),
1689
+ mediumState,
1496
1690
  explicitLightSamplingEnabled: Boolean(options.explicitLightSampling)
1497
1691
  });
1498
1692
  }
1693
+ function evaluateWavefrontVisibilityProbe(options = {}) {
1694
+ const probeMode = normalizeWavefrontVisibilityProbeMode(
1695
+ options.probeMode ?? (options.explicitLightSampling ? "mis-balanced" : "disabled")
1696
+ );
1697
+ const probeRay = createWavefrontVisibilityProbeRay({
1698
+ ...options.probeRay,
1699
+ throughput: options.throughput ?? options.probeRay?.throughput,
1700
+ direction: options.direction ?? options.probeRay?.direction,
1701
+ mediumRefId: options.currentMediumRefId ?? options.probeRay?.mediumRefId,
1702
+ mediumStack: options.mediumStack ?? options.probeRay?.mediumStack
1703
+ });
1704
+ const transparentSegments = Array.isArray(options.transparentSegments) ? options.transparentSegments : [];
1705
+ const transmittance = transparentSegments.reduce(
1706
+ (current, segment) => multiplyVec3(current, saturateVec3(normalizeVec3(segment, [1, 1, 1]))),
1707
+ [1, 1, 1]
1708
+ );
1709
+ const emissiveRadiance = saturateVec3(
1710
+ normalizeVec3(options.emissiveRadiance, [0, 0, 0])
1711
+ );
1712
+ const environmentRadiance = saturateVec3(
1713
+ normalizeVec3(options.environmentRadiance, [0, 0, 0])
1714
+ );
1715
+ const activeEmissiveRadiance = saturateVec3(
1716
+ normalizeVec3(options.activeEmissiveRadiance, [0, 0, 0])
1717
+ );
1718
+ const prefersEnvironment = Boolean(options.prefersEnvironment);
1719
+ const sourceRadiance = prefersEnvironment || colorLuminance(emissiveRadiance) <= 1e-6 ? environmentRadiance : emissiveRadiance;
1720
+ const rawContribution = multiplyVec3(
1721
+ probeRay.throughput,
1722
+ multiplyVec3(sourceRadiance, transmittance)
1723
+ );
1724
+ const activeEmissiveVisible = colorLuminance(activeEmissiveRadiance) > 1e-6;
1725
+ const misWeight = probeMode === "mis-balanced" && activeEmissiveVisible ? 0.5 : 1;
1726
+ const contribution = probeMode === "exclusive-emissive" && activeEmissiveVisible ? [0, 0, 0] : scaleVec3(rawContribution, misWeight);
1727
+ return Object.freeze({
1728
+ probeMode,
1729
+ probeRay,
1730
+ transmittance: Object.freeze(transmittance),
1731
+ misWeight,
1732
+ doubleCountPrevented: activeEmissiveVisible && probeMode !== "disabled",
1733
+ contribution: Object.freeze(contribution)
1734
+ });
1735
+ }
1736
+ function evaluateWavefrontMaterialReference(options = {}) {
1737
+ const material = Object.freeze({
1738
+ albedo: Object.freeze(
1739
+ saturateVec3(normalizeVec3(options.albedo, [0.8, 0.8, 0.8]))
1740
+ ),
1741
+ emission: Object.freeze(
1742
+ saturateVec3(normalizeVec3(options.emission, [0, 0, 0]))
1743
+ ),
1744
+ roughness: clampUnit(options.roughness, 0.5),
1745
+ metalness: clampUnit(options.metalness, 0),
1746
+ opacity: clampUnit(options.opacity, 1),
1747
+ transmission: Object.freeze(
1748
+ saturateVec3(normalizeVec3(options.transmission, [0, 0, 0]))
1749
+ ),
1750
+ refractiveIndex: Math.max(
1751
+ 1,
1752
+ readFinite(options.refractiveIndex ?? options.ior, 1.45)
1753
+ )
1754
+ });
1755
+ const continuation = evaluateWavefrontContinuationEvent({
1756
+ ...options,
1757
+ albedo: material.albedo,
1758
+ roughness: material.roughness,
1759
+ metalness: material.metalness,
1760
+ opacity: material.opacity,
1761
+ transmission: material.transmission,
1762
+ refractiveIndex: material.refractiveIndex
1763
+ });
1764
+ const terminal = evaluateWavefrontTerminalRadiance({
1765
+ ...options,
1766
+ emission: material.emission
1767
+ });
1768
+ return Object.freeze({
1769
+ material,
1770
+ terminal,
1771
+ continuation,
1772
+ throughputUpdate: continuation.nextThroughput
1773
+ });
1774
+ }
1775
+ function createWavefrontReferenceFixture(options = {}) {
1776
+ const tolerance = Math.max(1e-4, readFinite(options.tolerance, 5e-4));
1777
+ const ray = createWavefrontRayPayload({
1778
+ rayId: options.rayId,
1779
+ parentRayId: options.parentRayId,
1780
+ sourcePixelId: options.sourcePixelId,
1781
+ sampleId: options.sampleId,
1782
+ bounce: options.bounceIndex ?? options.bounce,
1783
+ origin: options.origin,
1784
+ direction: options.direction ?? scaleVec3(options.viewDirection ?? [0, 0, 1], -1),
1785
+ throughput: options.throughput,
1786
+ mediumRefId: options.currentMediumRefId ?? options.mediumRefId,
1787
+ mediumStack: options.mediumStack
1788
+ });
1789
+ const reference = evaluateWavefrontMaterialReference(options);
1790
+ const visibilityProbe = options.visibilityProbe === void 0 ? null : evaluateWavefrontVisibilityProbe({
1791
+ ...options.visibilityProbe,
1792
+ throughput: options.visibilityProbe.throughput ?? ray.throughput
1793
+ });
1794
+ const accumulationRadiance = addVec3(
1795
+ reference.terminal.radiance,
1796
+ visibilityProbe?.contribution ?? [0, 0, 0]
1797
+ );
1798
+ return Object.freeze({
1799
+ tolerance,
1800
+ ray,
1801
+ material: reference.material,
1802
+ continuation: reference.continuation,
1803
+ terminal: reference.terminal,
1804
+ visibilityProbe,
1805
+ accumulation: Object.freeze({
1806
+ sourcePixelId: ray.sourcePixelId,
1807
+ sampleCount: reference.terminal.terminated ? 1 : 0,
1808
+ radiance: Object.freeze(accumulationRadiance),
1809
+ throughput: reference.continuation.nextThroughput,
1810
+ resetEpoch: Math.max(
1811
+ 0,
1812
+ Math.trunc(readFinite(options.accumulationResetEpoch, 0))
1813
+ )
1814
+ })
1815
+ });
1816
+ }
1499
1817
  var lightingImportanceLevels = Object.freeze([
1500
1818
  "low",
1501
1819
  "medium",
@@ -2729,11 +3047,17 @@ async function loadLightingProfileWorkerPlan(profileName = defaultLightingProfil
2729
3047
  createLightingProfileModeLadder,
2730
3048
  createWavefrontEnvironmentLightingOptions,
2731
3049
  createWavefrontLightingPlan,
3050
+ createWavefrontRayPayload,
3051
+ createWavefrontReferenceFixture,
3052
+ createWavefrontVisibilityProbeRay,
2732
3053
  defaultAdaptiveLightingProfilePolicy,
2733
3054
  defaultLightingProfile,
2734
3055
  defaultLightingTechnique,
2735
3056
  evaluateWavefrontContinuationEvent,
3057
+ evaluateWavefrontMaterialReference,
3058
+ evaluateWavefrontMediumState,
2736
3059
  evaluateWavefrontTerminalRadiance,
3060
+ evaluateWavefrontVisibilityProbe,
2737
3061
  getLightingProfile,
2738
3062
  getLightingProfileWorkerManifest,
2739
3063
  getLightingTechnique,
@@ -2760,9 +3084,11 @@ async function loadLightingProfileWorkerPlan(profileName = defaultLightingProfil
2760
3084
  lightingWavefrontHitTypes,
2761
3085
  lightingWavefrontPassOrder,
2762
3086
  lightingWavefrontQueuePairStrategy,
3087
+ lightingWavefrontRayKinds,
2763
3088
  lightingWavefrontSchemaVersion,
2764
3089
  lightingWavefrontTerminalHitTypes,
2765
3090
  lightingWavefrontTerminationPolicy,
3091
+ lightingWavefrontVisibilityProbeModes,
2766
3092
  lightingWorkerManifests,
2767
3093
  lightingWorkerQueueClass,
2768
3094
  loadLightingJobs,