@plasius/gpu-lighting 0.2.5 → 0.2.7

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/src/index.js CHANGED
@@ -40,6 +40,15 @@ const techniqueSpecs = {
40
40
  denoise: "denoise.job.wgsl",
41
41
  },
42
42
  },
43
+ wavefront: {
44
+ description:
45
+ "Renderer-aligned wavefront lighting jobs for terminal radiance and continuation scattering.",
46
+ prelude: "prelude.wgsl",
47
+ jobs: {
48
+ accumulateTerminalRadiance: "accumulate-terminal-radiance.job.wgsl",
49
+ scatterContinuations: "scatter-continuations.job.wgsl",
50
+ },
51
+ },
43
52
  volumetrics: {
44
53
  description:
45
54
  "Froxel volumetric lighting for fog, shafts, and participating media shadows.",
@@ -1128,6 +1137,775 @@ export function createWavefrontEnvironmentLightingOptions(options = {}) {
1128
1137
  });
1129
1138
  }
1130
1139
 
1140
+ export const lightingWavefrontSchemaVersion = 1;
1141
+ export const lightingWavefrontQueuePairStrategy = "ping-pong-active-next";
1142
+ export const lightingWavefrontHitTypes = Object.freeze([
1143
+ "surface",
1144
+ "emissive",
1145
+ "environment",
1146
+ "transparent",
1147
+ "miss",
1148
+ ]);
1149
+ export const lightingWavefrontRayKinds = Object.freeze([
1150
+ "path",
1151
+ "visibility-probe",
1152
+ ]);
1153
+ export const lightingWavefrontVisibilityProbeModes = Object.freeze([
1154
+ "disabled",
1155
+ "mis-balanced",
1156
+ "exclusive-emissive",
1157
+ ]);
1158
+ export const lightingWavefrontTerminalHitTypes = Object.freeze([
1159
+ "emissive",
1160
+ "environment",
1161
+ "miss",
1162
+ ]);
1163
+ export const lightingWavefrontContinuationHitTypes = Object.freeze([
1164
+ "surface",
1165
+ "transparent",
1166
+ ]);
1167
+ export const lightingWavefrontPassOrder = Object.freeze([
1168
+ "accumulateTerminalRadiance",
1169
+ "scatterContinuations",
1170
+ ]);
1171
+ export const lightingRequiredRendererWavefrontPassOrder = Object.freeze([
1172
+ "generatePrimaryRays",
1173
+ "intersectActiveQueue",
1174
+ "resolveSurfaceRecords",
1175
+ "accumulateTerminalRadiance",
1176
+ "scatterContinuations",
1177
+ "compactAndSwapQueues",
1178
+ ]);
1179
+
1180
+ function createLightingWavefrontField(name, type, description) {
1181
+ return Object.freeze({
1182
+ name,
1183
+ type,
1184
+ description,
1185
+ });
1186
+ }
1187
+
1188
+ function createLightingWavefrontRecordContract(recordName, fields) {
1189
+ return Object.freeze({
1190
+ schemaVersion: lightingWavefrontSchemaVersion,
1191
+ recordName,
1192
+ fields: Object.freeze(fields),
1193
+ });
1194
+ }
1195
+
1196
+ export const lightingWavefrontBufferContracts = Object.freeze({
1197
+ ray: createLightingWavefrontRecordContract(
1198
+ "RayRecord",
1199
+ [
1200
+ createLightingWavefrontField("rayId", "u32", "Stable ray identifier."),
1201
+ createLightingWavefrontField("parentRayId", "u32", "Parent ray identifier."),
1202
+ createLightingWavefrontField("sourcePixelId", "u32", "Source pixel/sample owner."),
1203
+ createLightingWavefrontField("sampleId", "u32", "Per-pixel sample slot."),
1204
+ createLightingWavefrontField("bounce", "u32", "Current bounce depth."),
1205
+ createLightingWavefrontField("mediumRefId", "u32", "Current medium reference."),
1206
+ createLightingWavefrontField(
1207
+ "mediumStackDepth",
1208
+ "u32",
1209
+ "Depth of the bounded nested medium stack."
1210
+ ),
1211
+ createLightingWavefrontField(
1212
+ "flags",
1213
+ "u32",
1214
+ "Renderer-owned ray flags. Low bits may encode ray kind metadata."
1215
+ ),
1216
+ createLightingWavefrontField(
1217
+ "mediumStack0",
1218
+ "vec4<u32>",
1219
+ "Lower half of the bounded nested medium stack."
1220
+ ),
1221
+ createLightingWavefrontField(
1222
+ "mediumStack1",
1223
+ "vec4<u32>",
1224
+ "Upper half of the bounded nested medium stack."
1225
+ ),
1226
+ createLightingWavefrontField(
1227
+ "spectralState",
1228
+ "vec4<f32>",
1229
+ "Spectral transport payload for wavelength-driven reference validation."
1230
+ ),
1231
+ createLightingWavefrontField("origin", "vec3<f32>", "Ray origin."),
1232
+ createLightingWavefrontField("direction", "vec3<f32>", "Normalized ray direction."),
1233
+ createLightingWavefrontField("throughput", "vec3<f32>", "Accumulated path throughput."),
1234
+ ]
1235
+ ),
1236
+ hit: createLightingWavefrontRecordContract(
1237
+ "HitRecord",
1238
+ [
1239
+ createLightingWavefrontField("rayId", "u32", "Stable ray identifier."),
1240
+ createLightingWavefrontField("sourcePixelId", "u32", "Source pixel/sample owner."),
1241
+ createLightingWavefrontField("hitType", "u32", "Surface, emissive, environment, transparent, or miss."),
1242
+ createLightingWavefrontField("distance", "f32", "Nearest-hit distance."),
1243
+ createLightingWavefrontField("entityId", "u32", "Owning entity identifier."),
1244
+ createLightingWavefrontField("instanceId", "u32", "Owning instance identifier."),
1245
+ createLightingWavefrontField("primitiveId", "u32", "Primitive identifier."),
1246
+ createLightingWavefrontField("materialId", "u32", "Resolved material identifier."),
1247
+ createLightingWavefrontField("barycentrics", "vec3<f32>", "Triangle barycentrics."),
1248
+ createLightingWavefrontField("uv", "vec2<f32>", "Resolved UV coordinates."),
1249
+ createLightingWavefrontField("geometricNormal", "vec3<f32>", "Geometric surface normal."),
1250
+ createLightingWavefrontField("shadingNormal", "vec3<f32>", "Interpolated shading normal."),
1251
+ createLightingWavefrontField("frontFace", "u32", "Front-face classification."),
1252
+ ]
1253
+ ),
1254
+ surface: createLightingWavefrontRecordContract(
1255
+ "SurfaceRecord",
1256
+ [
1257
+ createLightingWavefrontField("rayId", "u32", "Stable ray identifier."),
1258
+ createLightingWavefrontField("entityId", "u32", "Owning entity identifier."),
1259
+ createLightingWavefrontField("materialRefId", "u32", "Renderer material reference id."),
1260
+ createLightingWavefrontField("mediumRefId", "u32", "Renderer medium reference id."),
1261
+ createLightingWavefrontField("geometricNormal", "vec3<f32>", "Geometric surface normal."),
1262
+ createLightingWavefrontField("shadingNormal", "vec3<f32>", "Interpolated shading normal."),
1263
+ createLightingWavefrontField("uv", "vec2<f32>", "Resolved UV coordinates."),
1264
+ createLightingWavefrontField("tangentFrame", "mat3x3<f32>", "Resolved tangent frame."),
1265
+ ]
1266
+ ),
1267
+ materialReference: createLightingWavefrontRecordContract(
1268
+ "MaterialReferenceRecord",
1269
+ [
1270
+ createLightingWavefrontField("materialRefId", "u32", "Lighting-visible material reference id."),
1271
+ createLightingWavefrontField("materialId", "u32", "Source material identifier."),
1272
+ createLightingWavefrontField("shadingModel", "u32", "Renderer shading-model discriminator."),
1273
+ createLightingWavefrontField("textureSetId", "u32", "Resolved texture-set identifier."),
1274
+ createLightingWavefrontField("flags", "u32", "Renderer material flags."),
1275
+ ]
1276
+ ),
1277
+ mediumReference: createLightingWavefrontRecordContract(
1278
+ "MediumReferenceRecord",
1279
+ [
1280
+ createLightingWavefrontField("mediumRefId", "u32", "Lighting-visible medium reference id."),
1281
+ createLightingWavefrontField("mediumId", "u32", "Source medium identifier."),
1282
+ createLightingWavefrontField("phaseModel", "u32", "Phase-function discriminator."),
1283
+ createLightingWavefrontField("absorption", "vec3<f32>", "Medium absorption coefficients."),
1284
+ createLightingWavefrontField("scattering", "vec3<f32>", "Medium scattering coefficients."),
1285
+ ]
1286
+ ),
1287
+ accumulation: createLightingWavefrontRecordContract(
1288
+ "AccumulationRecord",
1289
+ [
1290
+ createLightingWavefrontField("sourcePixelId", "u32", "Source pixel/sample owner."),
1291
+ createLightingWavefrontField("sampleCount", "u32", "Accumulated sample count."),
1292
+ createLightingWavefrontField("radiance", "vec3<f32>", "Accumulated radiance."),
1293
+ createLightingWavefrontField("throughput", "vec3<f32>", "Last surviving throughput."),
1294
+ createLightingWavefrontField("resetEpoch", "u32", "Renderer accumulation reset epoch."),
1295
+ ]
1296
+ ),
1297
+ });
1298
+
1299
+ export const lightingWavefrontTerminationPolicy = Object.freeze({
1300
+ terminalHitTypes: lightingWavefrontTerminalHitTypes,
1301
+ continuationHitTypes: lightingWavefrontContinuationHitTypes,
1302
+ emissive: Object.freeze({
1303
+ action: "accumulate-and-stop",
1304
+ contributesRadiance: true,
1305
+ }),
1306
+ environment: Object.freeze({
1307
+ action: "accumulate-and-stop",
1308
+ contributesRadiance: true,
1309
+ }),
1310
+ miss: Object.freeze({
1311
+ action: "accumulate-environment-or-dark-stop",
1312
+ contributesRadiance: true,
1313
+ }),
1314
+ });
1315
+
1316
+ const defaultWavefrontDarkRadiance = Object.freeze([0.0001, 0.0001, 0.0001]);
1317
+ const wavefrontEventKinds = Object.freeze([
1318
+ "diffuse",
1319
+ "reflection",
1320
+ "refraction",
1321
+ "transparency",
1322
+ "terminate",
1323
+ ]);
1324
+ const wavefrontRayKindFlagMask = 0x3;
1325
+ const wavefrontRayKindFlagValues = Object.freeze({
1326
+ path: 0,
1327
+ "visibility-probe": 1,
1328
+ });
1329
+
1330
+ function normalizeWavefrontRayKind(value) {
1331
+ return lightingWavefrontRayKinds.includes(value) ? value : "path";
1332
+ }
1333
+
1334
+ function normalizeWavefrontVisibilityProbeMode(value) {
1335
+ return lightingWavefrontVisibilityProbeModes.includes(value)
1336
+ ? value
1337
+ : "disabled";
1338
+ }
1339
+
1340
+ function normalizeWavefrontHitType(value) {
1341
+ return lightingWavefrontHitTypes.includes(value) ? value : "surface";
1342
+ }
1343
+
1344
+ function normalizeWavefrontEventKind(value) {
1345
+ return wavefrontEventKinds.includes(value) ? value : null;
1346
+ }
1347
+
1348
+ function normalizeVec3(value, fallback = [0, 0, 0]) {
1349
+ if (!Array.isArray(value) || value.length < 3) {
1350
+ return [...fallback];
1351
+ }
1352
+ return [
1353
+ Number.isFinite(value[0]) ? value[0] : fallback[0],
1354
+ Number.isFinite(value[1]) ? value[1] : fallback[1],
1355
+ Number.isFinite(value[2]) ? value[2] : fallback[2],
1356
+ ];
1357
+ }
1358
+
1359
+ function clampUnit(value, fallback = 0) {
1360
+ return Math.max(0, Math.min(1, readFinite(value, fallback)));
1361
+ }
1362
+
1363
+ function saturateVec3(value) {
1364
+ return value.map((component) => Math.max(0, component));
1365
+ }
1366
+
1367
+ function scaleVec3(value, scalar) {
1368
+ return value.map((component) => component * scalar);
1369
+ }
1370
+
1371
+ function addVec3(left, right) {
1372
+ return left.map((component, index) => component + right[index]);
1373
+ }
1374
+
1375
+ function multiplyVec3(left, right) {
1376
+ return left.map((component, index) => component * right[index]);
1377
+ }
1378
+
1379
+ function dotVec3(left, right) {
1380
+ return left[0] * right[0] + left[1] * right[1] + left[2] * right[2];
1381
+ }
1382
+
1383
+ function lengthVec3(value) {
1384
+ return Math.hypot(value[0], value[1], value[2]);
1385
+ }
1386
+
1387
+ function normalizeDirection(value, fallback = [0, 1, 0]) {
1388
+ const vector = normalizeVec3(value, fallback);
1389
+ const length = lengthVec3(vector);
1390
+ if (!Number.isFinite(length) || length <= 0.000001) {
1391
+ return [...fallback];
1392
+ }
1393
+ return vector.map((component) => component / length);
1394
+ }
1395
+
1396
+ function mixVec3(left, right, factor) {
1397
+ return left.map((component, index) =>
1398
+ component * (1 - factor) + right[index] * factor
1399
+ );
1400
+ }
1401
+
1402
+ function reflectDirection(direction, normal) {
1403
+ const scale = 2 * dotVec3(direction, normal);
1404
+ return normalizeDirection(
1405
+ [
1406
+ direction[0] - scale * normal[0],
1407
+ direction[1] - scale * normal[1],
1408
+ direction[2] - scale * normal[2],
1409
+ ],
1410
+ normal
1411
+ );
1412
+ }
1413
+
1414
+ function refractDirection(direction, normal, etaRatio) {
1415
+ const cosTheta = Math.min(-dotVec3(direction, normal), 1);
1416
+ const rOutPerp = scaleVec3(addVec3(direction, scaleVec3(normal, cosTheta)), etaRatio);
1417
+ const rOutPerpLengthSquared = dotVec3(rOutPerp, rOutPerp);
1418
+ const parallelFactor = 1 - rOutPerpLengthSquared;
1419
+ if (parallelFactor <= 0) {
1420
+ return null;
1421
+ }
1422
+ const rOutParallel = scaleVec3(normal, -Math.sqrt(parallelFactor));
1423
+ return normalizeDirection(addVec3(rOutPerp, rOutParallel), direction);
1424
+ }
1425
+
1426
+ function normalizeMediumRefId(value) {
1427
+ const mediumRefId = Math.max(0, Math.trunc(readFinite(value, 0)));
1428
+ return Number.isFinite(mediumRefId) ? mediumRefId : 0;
1429
+ }
1430
+
1431
+ function normalizeMediumStack(value) {
1432
+ if (!Array.isArray(value)) {
1433
+ return [];
1434
+ }
1435
+
1436
+ return value
1437
+ .map((entry) => normalizeMediumRefId(entry))
1438
+ .filter((entry, index, stack) => entry > 0 && stack.indexOf(entry) === index)
1439
+ .slice(0, 4);
1440
+ }
1441
+
1442
+ function createWavefrontMediumStatePayload(currentMediumRefId, stack) {
1443
+ const normalizedStack = normalizeMediumStack(stack);
1444
+ const stackSlots = [0, 0, 0, 0];
1445
+ normalizedStack.forEach((entry, index) => {
1446
+ stackSlots[index] = entry;
1447
+ });
1448
+ return Object.freeze({
1449
+ currentMediumRefId,
1450
+ stackDepth: normalizedStack.length,
1451
+ stack: Object.freeze(normalizedStack),
1452
+ stackSlots: Object.freeze(stackSlots),
1453
+ });
1454
+ }
1455
+
1456
+ export function evaluateWavefrontMediumState(options = {}) {
1457
+ const currentMediumRefId = normalizeMediumRefId(
1458
+ options.currentMediumRefId ?? options.mediumRefId
1459
+ );
1460
+ const surfaceMediumRefId = normalizeMediumRefId(options.surfaceMediumRefId);
1461
+ const stack = normalizeMediumStack(options.mediumStack);
1462
+ const frontFace = options.frontFace !== false;
1463
+ const eventKind =
1464
+ normalizeWavefrontEventKind(options.eventKind) ?? "transparency";
1465
+
1466
+ if (
1467
+ surfaceMediumRefId === 0 ||
1468
+ (eventKind !== "refraction" && eventKind !== "transparency")
1469
+ ) {
1470
+ return Object.freeze({
1471
+ ...createWavefrontMediumStatePayload(currentMediumRefId, stack),
1472
+ enteredMediumRefId: 0,
1473
+ exitedMediumRefId: 0,
1474
+ });
1475
+ }
1476
+
1477
+ let nextStack = [...stack];
1478
+ let nextMediumRefId = currentMediumRefId;
1479
+ let enteredMediumRefId = 0;
1480
+ let exitedMediumRefId = 0;
1481
+ const stackTop = nextStack.at(-1) ?? 0;
1482
+
1483
+ if (frontFace) {
1484
+ if (stackTop !== surfaceMediumRefId) {
1485
+ nextStack.push(surfaceMediumRefId);
1486
+ nextStack = nextStack.slice(-4);
1487
+ }
1488
+ nextMediumRefId = surfaceMediumRefId;
1489
+ enteredMediumRefId = surfaceMediumRefId;
1490
+ } else if (stackTop === surfaceMediumRefId) {
1491
+ nextStack.pop();
1492
+ nextMediumRefId = nextStack.at(-1) ?? 0;
1493
+ exitedMediumRefId = surfaceMediumRefId;
1494
+ }
1495
+
1496
+ return Object.freeze({
1497
+ ...createWavefrontMediumStatePayload(nextMediumRefId, nextStack),
1498
+ enteredMediumRefId,
1499
+ exitedMediumRefId,
1500
+ });
1501
+ }
1502
+
1503
+ function encodeWavefrontRayFlags(flags, rayKind) {
1504
+ const normalizedFlags = Math.max(0, Math.trunc(readFinite(flags, 0)));
1505
+ const rayKindValue = wavefrontRayKindFlagValues[rayKind];
1506
+ return (normalizedFlags & ~wavefrontRayKindFlagMask) | rayKindValue;
1507
+ }
1508
+
1509
+ export function createWavefrontRayPayload(options = {}) {
1510
+ const rayKind = normalizeWavefrontRayKind(options.rayKind);
1511
+ const mediumState = evaluateWavefrontMediumState({
1512
+ currentMediumRefId: options.mediumRefId,
1513
+ mediumStack: options.mediumStack,
1514
+ });
1515
+ const spectralState = Object.freeze(
1516
+ normalizeVec3(options.spectralState, [550, 1, 0]).concat(
1517
+ readFinite(options.spectralWeight, 0)
1518
+ )
1519
+ );
1520
+ const mediumStack0 = Object.freeze([
1521
+ mediumState.stackSlots[0],
1522
+ mediumState.stackSlots[1],
1523
+ mediumState.stackSlots[2],
1524
+ mediumState.stackSlots[3],
1525
+ ]);
1526
+ const mediumStack1 = Object.freeze([0, 0, 0, 0]);
1527
+ return Object.freeze({
1528
+ rayId: Math.max(0, Math.trunc(readFinite(options.rayId, 0))),
1529
+ parentRayId: Math.max(0, Math.trunc(readFinite(options.parentRayId, 0))),
1530
+ sourcePixelId: Math.max(0, Math.trunc(readFinite(options.sourcePixelId, 0))),
1531
+ sampleId: Math.max(0, Math.trunc(readFinite(options.sampleId, 0))),
1532
+ bounce: Math.max(0, Math.trunc(readFinite(options.bounce, 0))),
1533
+ origin: Object.freeze(normalizeVec3(options.origin, [0, 0, 0])),
1534
+ direction: Object.freeze(normalizeDirection(options.direction, [0, 0, -1])),
1535
+ throughput: Object.freeze(
1536
+ saturateVec3(normalizeVec3(options.throughput, [1, 1, 1]))
1537
+ ),
1538
+ mediumRefId: mediumState.currentMediumRefId,
1539
+ mediumStackDepth: mediumState.stackDepth,
1540
+ mediumStack: mediumState.stack,
1541
+ mediumStackSlots: mediumState.stackSlots,
1542
+ mediumStack0,
1543
+ mediumStack1,
1544
+ spectralState,
1545
+ rayKind,
1546
+ flags: encodeWavefrontRayFlags(options.flags, rayKind),
1547
+ });
1548
+ }
1549
+
1550
+ export function createWavefrontVisibilityProbeRay(options = {}) {
1551
+ return createWavefrontRayPayload({
1552
+ ...options,
1553
+ rayKind: "visibility-probe",
1554
+ });
1555
+ }
1556
+
1557
+ export function createWavefrontLightingPlan(options = {}) {
1558
+ const maxDepth = Math.max(1, Math.trunc(readFinite(options.maxDepth, 4)));
1559
+ const queueCapacity = Math.max(
1560
+ 1,
1561
+ Math.trunc(readFinite(options.queueCapacity, 4096))
1562
+ );
1563
+ const explicitLightSampling = Boolean(options.explicitLightSampling);
1564
+ const visibilityProbeMode = normalizeWavefrontVisibilityProbeMode(
1565
+ options.visibilityProbeMode ??
1566
+ (explicitLightSampling ? "mis-balanced" : "disabled")
1567
+ );
1568
+ const accumulationResetEpoch = Math.max(
1569
+ 0,
1570
+ Math.trunc(readFinite(options.accumulationResetEpoch, 0))
1571
+ );
1572
+
1573
+ return Object.freeze({
1574
+ schemaVersion: lightingWavefrontSchemaVersion,
1575
+ maxDepth,
1576
+ queueCapacity,
1577
+ explicitLightSampling,
1578
+ visibilityProbeMode,
1579
+ rayKinds: lightingWavefrontRayKinds,
1580
+ accumulationResetEpoch,
1581
+ queueLayout: Object.freeze({
1582
+ strategy: lightingWavefrontQueuePairStrategy,
1583
+ compactAfterScatter: true,
1584
+ queues: Object.freeze([
1585
+ Object.freeze({ name: "active", role: "current-bounce" }),
1586
+ Object.freeze({ name: "next", role: "next-bounce" }),
1587
+ ]),
1588
+ }),
1589
+ bufferContracts: lightingWavefrontBufferContracts,
1590
+ terminationPolicy: lightingWavefrontTerminationPolicy,
1591
+ requiredRendererPassOrder: lightingRequiredRendererWavefrontPassOrder,
1592
+ lightingPasses: Object.freeze([
1593
+ Object.freeze({
1594
+ key: "accumulateTerminalRadiance",
1595
+ stage: "accumulateTerminalRadiance",
1596
+ reads: Object.freeze([
1597
+ "ray",
1598
+ "hit",
1599
+ "surface",
1600
+ "materialReference",
1601
+ "mediumReference",
1602
+ ]),
1603
+ writes: Object.freeze(["accumulation"]),
1604
+ terminalHitTypes: lightingWavefrontTerminalHitTypes,
1605
+ }),
1606
+ Object.freeze({
1607
+ key: "scatterContinuations",
1608
+ stage: "scatterContinuations",
1609
+ reads: Object.freeze([
1610
+ "ray",
1611
+ "hit",
1612
+ "surface",
1613
+ "materialReference",
1614
+ "mediumReference",
1615
+ ]),
1616
+ writes: Object.freeze(["ray"]),
1617
+ continuationHitTypes: lightingWavefrontContinuationHitTypes,
1618
+ explicitLightSampling,
1619
+ visibilityProbeMode,
1620
+ }),
1621
+ ]),
1622
+ });
1623
+ }
1624
+
1625
+ export function evaluateWavefrontTerminalRadiance(options = {}) {
1626
+ const hitType = normalizeWavefrontHitType(options.hitType);
1627
+ const throughput = saturateVec3(normalizeVec3(options.throughput, [1, 1, 1]));
1628
+ const emission = saturateVec3(normalizeVec3(options.emission, [0, 0, 0]));
1629
+ const environmentRadiance = saturateVec3(
1630
+ normalizeVec3(options.environmentRadiance, [0, 0, 0])
1631
+ );
1632
+ const missRadiance = saturateVec3(
1633
+ normalizeVec3(options.missRadiance, defaultWavefrontDarkRadiance)
1634
+ );
1635
+ const environmentLuminance = colorLuminance(environmentRadiance);
1636
+ let source = "none";
1637
+ let rawRadiance = [0, 0, 0];
1638
+
1639
+ if (hitType === "emissive") {
1640
+ source = "emissive";
1641
+ rawRadiance = emission;
1642
+ } else if (hitType === "environment") {
1643
+ source = "environment";
1644
+ rawRadiance = environmentRadiance;
1645
+ } else if (hitType === "miss") {
1646
+ source = environmentLuminance > 0.000001 ? "environment" : "dark";
1647
+ rawRadiance = environmentLuminance > 0.000001 ? environmentRadiance : missRadiance;
1648
+ }
1649
+
1650
+ const radiance = multiplyVec3(throughput, rawRadiance);
1651
+ const terminated = lightingWavefrontTerminalHitTypes.includes(hitType);
1652
+
1653
+ return Object.freeze({
1654
+ hitType,
1655
+ source,
1656
+ terminated,
1657
+ radiance: Object.freeze(radiance),
1658
+ nearDarkSample:
1659
+ source === "dark" && colorLuminance(radiance) <= colorLuminance(defaultWavefrontDarkRadiance),
1660
+ });
1661
+ }
1662
+
1663
+ export function evaluateWavefrontContinuationEvent(options = {}) {
1664
+ const hitType = normalizeWavefrontHitType(options.hitType);
1665
+ const bounceIndex = Math.max(0, Math.trunc(readFinite(options.bounceIndex, 0)));
1666
+ const maxDepth = Math.max(1, Math.trunc(readFinite(options.maxDepth, 4)));
1667
+ const throughput = saturateVec3(normalizeVec3(options.throughput, [1, 1, 1]));
1668
+ const albedo = saturateVec3(normalizeVec3(options.albedo, [0.8, 0.8, 0.8]));
1669
+ const transmission = saturateVec3(
1670
+ normalizeVec3(options.transmission, [0, 0, 0])
1671
+ );
1672
+ const shadingNormal = normalizeDirection(options.shadingNormal, [0, 1, 0]);
1673
+ const viewDirection = normalizeDirection(options.viewDirection, [0, 0, 1]);
1674
+ const incomingDirection = normalizeDirection(
1675
+ scaleVec3(viewDirection, -1),
1676
+ [0, 0, -1]
1677
+ );
1678
+ const frontFace = options.frontFace !== false;
1679
+ const orientedNormal = frontFace
1680
+ ? shadingNormal
1681
+ : scaleVec3(shadingNormal, -1);
1682
+ const metalness = clampUnit(options.metalness, 0);
1683
+ const roughness = clampUnit(options.roughness, 0.5);
1684
+ const opacity = clampUnit(options.opacity, 1);
1685
+ const refractiveIndex = Math.max(1, readFinite(options.refractiveIndex ?? options.ior, 1.45));
1686
+ const transmissionStrength = Math.max(...transmission);
1687
+ const requestedEventKind =
1688
+ normalizeWavefrontEventKind(options.eventKind) ??
1689
+ (hitType === "transparent" || opacity < 0.999
1690
+ ? "transparency"
1691
+ : transmissionStrength > 0.001
1692
+ ? "refraction"
1693
+ : metalness >= 0.5 || roughness <= 0.2
1694
+ ? "reflection"
1695
+ : "diffuse");
1696
+ let eventKind = requestedEventKind;
1697
+ let totalInternalReflection = false;
1698
+
1699
+ let nextDirection = incomingDirection;
1700
+ let attenuation;
1701
+
1702
+ if (!lightingWavefrontContinuationHitTypes.includes(hitType) || bounceIndex >= maxDepth - 1) {
1703
+ eventKind = "terminate";
1704
+ attenuation = [0, 0, 0];
1705
+ } else if (eventKind === "reflection") {
1706
+ nextDirection = reflectDirection(incomingDirection, orientedNormal);
1707
+ attenuation = mixVec3([0.04, 0.04, 0.04], albedo, metalness);
1708
+ } else if (eventKind === "refraction") {
1709
+ const etaRatio = frontFace ? 1 / refractiveIndex : refractiveIndex;
1710
+ const refractedDirection = refractDirection(
1711
+ incomingDirection,
1712
+ orientedNormal,
1713
+ etaRatio
1714
+ );
1715
+ if (refractedDirection) {
1716
+ nextDirection = refractedDirection;
1717
+ attenuation = transmissionStrength > 0.001 ? transmission : [1, 1, 1];
1718
+ } else {
1719
+ totalInternalReflection = true;
1720
+ eventKind = "reflection";
1721
+ nextDirection = reflectDirection(incomingDirection, orientedNormal);
1722
+ attenuation = mixVec3([0.04, 0.04, 0.04], albedo, metalness);
1723
+ }
1724
+ } else if (eventKind === "transparency") {
1725
+ nextDirection = incomingDirection;
1726
+ const transparencyWeight = Math.max(1 - opacity, transmissionStrength, 0.05);
1727
+ attenuation = transmissionStrength > 0.001
1728
+ ? transmission
1729
+ : [transparencyWeight, transparencyWeight, transparencyWeight];
1730
+ } else {
1731
+ nextDirection = normalizeDirection(
1732
+ addVec3(orientedNormal, albedo.map((component) => component - 0.5)),
1733
+ orientedNormal
1734
+ );
1735
+ attenuation = scaleVec3(albedo, Math.max(0.05, 1 - metalness));
1736
+ }
1737
+
1738
+ const nextThroughput = multiplyVec3(throughput, saturateVec3(attenuation));
1739
+ const continueTracing =
1740
+ eventKind !== "terminate" && colorLuminance(nextThroughput) > 0.0001;
1741
+ const mediumState = evaluateWavefrontMediumState({
1742
+ currentMediumRefId: options.currentMediumRefId ?? options.mediumRefId,
1743
+ surfaceMediumRefId: options.surfaceMediumRefId,
1744
+ mediumStack: options.mediumStack,
1745
+ frontFace,
1746
+ eventKind,
1747
+ });
1748
+
1749
+ return Object.freeze({
1750
+ hitType,
1751
+ requestedEventKind,
1752
+ eventKind,
1753
+ continueTracing,
1754
+ totalInternalReflection,
1755
+ nextDirection: Object.freeze(nextDirection),
1756
+ attenuation: Object.freeze(saturateVec3(attenuation)),
1757
+ nextThroughput: Object.freeze(nextThroughput),
1758
+ mediumState,
1759
+ explicitLightSamplingEnabled: Boolean(options.explicitLightSampling),
1760
+ });
1761
+ }
1762
+
1763
+ export function evaluateWavefrontVisibilityProbe(options = {}) {
1764
+ const probeMode = normalizeWavefrontVisibilityProbeMode(
1765
+ options.probeMode ??
1766
+ (options.explicitLightSampling ? "mis-balanced" : "disabled")
1767
+ );
1768
+ const probeRay = createWavefrontVisibilityProbeRay({
1769
+ ...options.probeRay,
1770
+ throughput: options.throughput ?? options.probeRay?.throughput,
1771
+ direction: options.direction ?? options.probeRay?.direction,
1772
+ mediumRefId: options.currentMediumRefId ?? options.probeRay?.mediumRefId,
1773
+ mediumStack: options.mediumStack ?? options.probeRay?.mediumStack,
1774
+ });
1775
+ const transparentSegments = Array.isArray(options.transparentSegments)
1776
+ ? options.transparentSegments
1777
+ : [];
1778
+ const transmittance = transparentSegments.reduce(
1779
+ (current, segment) =>
1780
+ multiplyVec3(current, saturateVec3(normalizeVec3(segment, [1, 1, 1]))),
1781
+ [1, 1, 1]
1782
+ );
1783
+ const emissiveRadiance = saturateVec3(
1784
+ normalizeVec3(options.emissiveRadiance, [0, 0, 0])
1785
+ );
1786
+ const environmentRadiance = saturateVec3(
1787
+ normalizeVec3(options.environmentRadiance, [0, 0, 0])
1788
+ );
1789
+ const activeEmissiveRadiance = saturateVec3(
1790
+ normalizeVec3(options.activeEmissiveRadiance, [0, 0, 0])
1791
+ );
1792
+ const prefersEnvironment = Boolean(options.prefersEnvironment);
1793
+ const sourceRadiance =
1794
+ prefersEnvironment || colorLuminance(emissiveRadiance) <= 0.000001
1795
+ ? environmentRadiance
1796
+ : emissiveRadiance;
1797
+ const rawContribution = multiplyVec3(
1798
+ probeRay.throughput,
1799
+ multiplyVec3(sourceRadiance, transmittance)
1800
+ );
1801
+ const activeEmissiveVisible =
1802
+ colorLuminance(activeEmissiveRadiance) > 0.000001;
1803
+ const misWeight =
1804
+ probeMode === "mis-balanced" && activeEmissiveVisible ? 0.5 : 1;
1805
+ const contribution =
1806
+ probeMode === "exclusive-emissive" && activeEmissiveVisible
1807
+ ? [0, 0, 0]
1808
+ : scaleVec3(rawContribution, misWeight);
1809
+
1810
+ return Object.freeze({
1811
+ probeMode,
1812
+ probeRay,
1813
+ transmittance: Object.freeze(transmittance),
1814
+ misWeight,
1815
+ doubleCountPrevented:
1816
+ activeEmissiveVisible && probeMode !== "disabled",
1817
+ contribution: Object.freeze(contribution),
1818
+ });
1819
+ }
1820
+
1821
+ export function evaluateWavefrontMaterialReference(options = {}) {
1822
+ const material = Object.freeze({
1823
+ albedo: Object.freeze(
1824
+ saturateVec3(normalizeVec3(options.albedo, [0.8, 0.8, 0.8]))
1825
+ ),
1826
+ emission: Object.freeze(
1827
+ saturateVec3(normalizeVec3(options.emission, [0, 0, 0]))
1828
+ ),
1829
+ roughness: clampUnit(options.roughness, 0.5),
1830
+ metalness: clampUnit(options.metalness, 0),
1831
+ opacity: clampUnit(options.opacity, 1),
1832
+ transmission: Object.freeze(
1833
+ saturateVec3(normalizeVec3(options.transmission, [0, 0, 0]))
1834
+ ),
1835
+ refractiveIndex: Math.max(
1836
+ 1,
1837
+ readFinite(options.refractiveIndex ?? options.ior, 1.45)
1838
+ ),
1839
+ });
1840
+ const continuation = evaluateWavefrontContinuationEvent({
1841
+ ...options,
1842
+ albedo: material.albedo,
1843
+ roughness: material.roughness,
1844
+ metalness: material.metalness,
1845
+ opacity: material.opacity,
1846
+ transmission: material.transmission,
1847
+ refractiveIndex: material.refractiveIndex,
1848
+ });
1849
+ const terminal = evaluateWavefrontTerminalRadiance({
1850
+ ...options,
1851
+ emission: material.emission,
1852
+ });
1853
+
1854
+ return Object.freeze({
1855
+ material,
1856
+ terminal,
1857
+ continuation,
1858
+ throughputUpdate: continuation.nextThroughput,
1859
+ });
1860
+ }
1861
+
1862
+ export function createWavefrontReferenceFixture(options = {}) {
1863
+ const tolerance = Math.max(0.0001, readFinite(options.tolerance, 0.0005));
1864
+ const ray = createWavefrontRayPayload({
1865
+ rayId: options.rayId,
1866
+ parentRayId: options.parentRayId,
1867
+ sourcePixelId: options.sourcePixelId,
1868
+ sampleId: options.sampleId,
1869
+ bounce: options.bounceIndex ?? options.bounce,
1870
+ origin: options.origin,
1871
+ direction: options.direction ?? scaleVec3(options.viewDirection ?? [0, 0, 1], -1),
1872
+ throughput: options.throughput,
1873
+ mediumRefId: options.currentMediumRefId ?? options.mediumRefId,
1874
+ mediumStack: options.mediumStack,
1875
+ });
1876
+ const reference = evaluateWavefrontMaterialReference(options);
1877
+ const visibilityProbe =
1878
+ options.visibilityProbe === undefined
1879
+ ? null
1880
+ : evaluateWavefrontVisibilityProbe({
1881
+ ...options.visibilityProbe,
1882
+ throughput: options.visibilityProbe.throughput ?? ray.throughput,
1883
+ });
1884
+ const accumulationRadiance = addVec3(
1885
+ reference.terminal.radiance,
1886
+ visibilityProbe?.contribution ?? [0, 0, 0]
1887
+ );
1888
+
1889
+ return Object.freeze({
1890
+ tolerance,
1891
+ ray,
1892
+ material: reference.material,
1893
+ continuation: reference.continuation,
1894
+ terminal: reference.terminal,
1895
+ visibilityProbe,
1896
+ accumulation: Object.freeze({
1897
+ sourcePixelId: ray.sourcePixelId,
1898
+ sampleCount: reference.terminal.terminated ? 1 : 0,
1899
+ radiance: Object.freeze(accumulationRadiance),
1900
+ throughput: reference.continuation.nextThroughput,
1901
+ resetEpoch: Math.max(
1902
+ 0,
1903
+ Math.trunc(readFinite(options.accumulationResetEpoch, 0))
1904
+ ),
1905
+ }),
1906
+ });
1907
+ }
1908
+
1131
1909
  const lightingImportanceLevels = Object.freeze([
1132
1910
  "low",
1133
1911
  "medium",
@@ -1725,6 +2503,91 @@ const lightingWorkerSpecPresets = {
1725
2503
  },
1726
2504
  },
1727
2505
  },
2506
+ wavefront: {
2507
+ suggestedAllocationIds: [
2508
+ "lighting.wavefront.active-queue",
2509
+ "lighting.wavefront.next-queue",
2510
+ "lighting.wavefront.accumulation",
2511
+ ],
2512
+ jobs: {
2513
+ accumulateTerminalRadiance: {
2514
+ domain: "lighting",
2515
+ importance: "critical",
2516
+ levels: buildWorkerBudgetLevels(
2517
+ "lighting.wavefront.accumulateTerminalRadiance",
2518
+ lightingWorkerQueueClass,
2519
+ {
2520
+ low: {
2521
+ estimatedCostMs: 0.7,
2522
+ maxDispatchesPerFrame: 1,
2523
+ maxJobsPerDispatch: 32,
2524
+ cadenceDivisor: 2,
2525
+ workgroupScale: 0.5,
2526
+ maxQueueDepth: 96,
2527
+ },
2528
+ medium: {
2529
+ estimatedCostMs: 1.2,
2530
+ maxDispatchesPerFrame: 1,
2531
+ maxJobsPerDispatch: 64,
2532
+ cadenceDivisor: 1,
2533
+ workgroupScale: 0.75,
2534
+ maxQueueDepth: 192,
2535
+ },
2536
+ high: {
2537
+ estimatedCostMs: 1.8,
2538
+ maxDispatchesPerFrame: 2,
2539
+ maxJobsPerDispatch: 128,
2540
+ cadenceDivisor: 1,
2541
+ workgroupScale: 1,
2542
+ maxQueueDepth: 256,
2543
+ },
2544
+ }
2545
+ ),
2546
+ suggestedAllocationIds: [
2547
+ "lighting.wavefront.accumulation",
2548
+ "lighting.wavefront.active-queue",
2549
+ ],
2550
+ },
2551
+ scatterContinuations: {
2552
+ domain: "lighting",
2553
+ importance: "critical",
2554
+ levels: buildWorkerBudgetLevels(
2555
+ "lighting.wavefront.scatterContinuations",
2556
+ lightingWorkerQueueClass,
2557
+ {
2558
+ low: {
2559
+ estimatedCostMs: 0.8,
2560
+ maxDispatchesPerFrame: 1,
2561
+ maxJobsPerDispatch: 32,
2562
+ cadenceDivisor: 2,
2563
+ workgroupScale: 0.5,
2564
+ maxQueueDepth: 96,
2565
+ },
2566
+ medium: {
2567
+ estimatedCostMs: 1.4,
2568
+ maxDispatchesPerFrame: 1,
2569
+ maxJobsPerDispatch: 64,
2570
+ cadenceDivisor: 1,
2571
+ workgroupScale: 0.75,
2572
+ maxQueueDepth: 192,
2573
+ },
2574
+ high: {
2575
+ estimatedCostMs: 2.1,
2576
+ maxDispatchesPerFrame: 2,
2577
+ maxJobsPerDispatch: 128,
2578
+ cadenceDivisor: 1,
2579
+ workgroupScale: 1,
2580
+ maxQueueDepth: 256,
2581
+ },
2582
+ }
2583
+ ),
2584
+ suggestedAllocationIds: [
2585
+ "lighting.wavefront.active-queue",
2586
+ "lighting.wavefront.next-queue",
2587
+ ],
2588
+ },
2589
+ },
2590
+ },
1728
2591
  volumetrics: {
1729
2592
  suggestedAllocationIds: [
1730
2593
  "lighting.volumetrics.froxel-grid",
@@ -1935,6 +2798,13 @@ const lightingWorkerDagSpecs = {
1935
2798
  accumulate: { priority: 3, dependencies: ["pathTrace"] },
1936
2799
  denoise: { priority: 2, dependencies: ["accumulate"] },
1937
2800
  },
2801
+ wavefront: {
2802
+ accumulateTerminalRadiance: { priority: 3, dependencies: [] },
2803
+ scatterContinuations: {
2804
+ priority: 2,
2805
+ dependencies: ["accumulateTerminalRadiance"],
2806
+ },
2807
+ },
1938
2808
  volumetrics: {
1939
2809
  volumetricShadow: { priority: 3, dependencies: [] },
1940
2810
  froxelIntegrate: { priority: 2, dependencies: ["volumetricShadow"] },
@@ -1969,6 +2839,16 @@ function resolveLightingQualityDimensions(techniqueName, jobKey) {
1969
2839
  "pathtracer.pathTrace": { rayTracing: 1, lightingSamples: 1 },
1970
2840
  "pathtracer.accumulate": { temporalReuse: 1, updateCadence: 0.4 },
1971
2841
  "pathtracer.denoise": { temporalReuse: 1, shading: 0.4 },
2842
+ "wavefront.accumulateTerminalRadiance": {
2843
+ rayTracing: 1,
2844
+ lightingSamples: 1,
2845
+ temporalReuse: 0.4,
2846
+ },
2847
+ "wavefront.scatterContinuations": {
2848
+ rayTracing: 1,
2849
+ shading: 0.7,
2850
+ updateCadence: 0.5,
2851
+ },
1972
2852
  "volumetrics.froxelIntegrate": {
1973
2853
  lightingSamples: 0.6,
1974
2854
  shading: 0.4,
@@ -2014,6 +2894,15 @@ function resolveLightingImportanceSignals(techniqueName, jobKey) {
2014
2894
  },
2015
2895
  "pathtracer.accumulate": { visible: true },
2016
2896
  "pathtracer.denoise": { visible: true },
2897
+ "wavefront.accumulateTerminalRadiance": {
2898
+ visible: true,
2899
+ shadowSignificance: "high",
2900
+ reflectionSignificance: "high",
2901
+ },
2902
+ "wavefront.scatterContinuations": {
2903
+ visible: true,
2904
+ reflectionSignificance: "high",
2905
+ },
2017
2906
  "volumetrics.froxelIntegrate": { visible: true },
2018
2907
  "volumetrics.volumetricShadow": { visible: true, shadowSignificance: "high" },
2019
2908
  "hdri.irradianceConvolution": { visible: false },