@plasius/gpu-renderer 0.1.15 → 0.2.1

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/dist/index.cjs CHANGED
@@ -31,22 +31,29 @@ __export(index_exports, {
31
31
  createWavefrontMeshAcceleration: () => createWavefrontMeshAcceleration,
32
32
  createWavefrontPathTracingComputeConfig: () => createWavefrontPathTracingComputeConfig,
33
33
  createWavefrontPathTracingComputeRenderer: () => createWavefrontPathTracingComputeRenderer,
34
+ createWavefrontPathTracingComputeShaderSource: () => createWavefrontPathTracingComputeShaderSource,
34
35
  createWavefrontPathTracingPlan: () => createWavefrontPathTracingPlan,
36
+ createWavefrontReferenceRay: () => createWavefrontReferenceRay,
35
37
  defaultRendererClearColor: () => defaultRendererClearColor,
36
38
  defaultRendererWorkerProfile: () => defaultRendererWorkerProfile,
37
39
  estimateWavefrontPathTracingMemory: () => estimateWavefrontPathTracingMemory,
38
40
  getRendererWorkerManifest: () => getRendererWorkerManifest,
39
41
  getRendererWorkerProfile: () => getRendererWorkerProfile,
42
+ intersectWavefrontReferenceTriangle: () => intersectWavefrontReferenceTriangle,
40
43
  normalizeWavefrontMesh: () => normalizeWavefrontMesh,
41
44
  normalizeWavefrontSceneObject: () => normalizeWavefrontSceneObject,
42
45
  packWavefrontBvhNodes: () => packWavefrontBvhNodes,
43
46
  packWavefrontSceneObjects: () => packWavefrontSceneObjects,
44
47
  packWavefrontTriangles: () => packWavefrontTriangles,
48
+ renderWavefrontPathTracingComputeFrame: () => renderWavefrontPathTracingComputeFrame,
45
49
  rendererAccelerationStructureUpdateClasses: () => rendererAccelerationStructureUpdateClasses,
46
50
  rendererDebugOwner: () => rendererDebugOwner,
47
51
  rendererRayTracingStageOrder: () => rendererRayTracingStageOrder,
48
52
  rendererRepresentationBands: () => rendererRepresentationBands,
49
53
  rendererWavefrontBufferSchemaVersion: () => rendererWavefrontBufferSchemaVersion,
54
+ rendererWavefrontComputeMode: () => rendererWavefrontComputeMode,
55
+ rendererWavefrontComputeStatsStride: () => rendererWavefrontComputeStatsStride,
56
+ rendererWavefrontComputeWorkgroupSize: () => rendererWavefrontComputeWorkgroupSize,
50
57
  rendererWavefrontHitTypes: () => rendererWavefrontHitTypes,
51
58
  rendererWavefrontPassOrder: () => rendererWavefrontPassOrder,
52
59
  rendererWavefrontQueuePairStrategy: () => rendererWavefrontQueuePairStrategy,
@@ -56,6 +63,7 @@ __export(index_exports, {
56
63
  rendererWorkerQueueClass: () => rendererWorkerQueueClass,
57
64
  supportsWavefrontPathTracingCompute: () => supportsWavefrontPathTracingCompute,
58
65
  supportsWebGpu: () => supportsWebGpu,
66
+ traceWavefrontReferenceTriangles: () => traceWavefrontReferenceTriangles,
59
67
  wavefrontMaterialKinds: () => wavefrontMaterialKinds,
60
68
  wavefrontPathTracingComputeLimits: () => wavefrontPathTracingComputeLimits,
61
69
  wavefrontSceneObjectKinds: () => wavefrontSceneObjectKinds
@@ -71,6 +79,9 @@ var DEFAULT_SAMPLES_PER_PIXEL = 1;
71
79
  var DEFAULT_SCENE_OBJECT_CAPACITY = 128;
72
80
  var DEFAULT_ENVIRONMENT_PORTAL_CAPACITY = 32;
73
81
  var WORKGROUP_SIZE = 64;
82
+ var rendererWavefrontComputeMode = "webgpu-compute";
83
+ var rendererWavefrontComputeWorkgroupSize = WORKGROUP_SIZE;
84
+ var rendererWavefrontComputeStatsStride = 8;
74
85
  var RAY_RECORD_BYTES = 80;
75
86
  var HIT_RECORD_BYTES = 208;
76
87
  var SCENE_OBJECT_RECORD_BYTES = 96;
@@ -83,7 +94,9 @@ var EMISSIVE_TRIANGLE_INDEX_BYTES = 4;
83
94
  var ENVIRONMENT_PORTAL_RECORD_BYTES = 96;
84
95
  var ACCUMULATION_RECORD_BYTES = 16;
85
96
  var CONFIG_BUFFER_BYTES = 272;
86
- var COUNTER_BUFFER_BYTES = 16;
97
+ var COUNTER_DISPATCH_ARGS_OFFSET = 16;
98
+ var INDIRECT_DISPATCH_ARGS_BYTES = 12;
99
+ var COUNTER_BUFFER_BYTES = 32;
87
100
  var TRACE_STORAGE_BUFFER_BINDINGS = 9;
88
101
  var MATERIAL_DIFFUSE = 0;
89
102
  var MATERIAL_METAL = 1;
@@ -123,7 +136,9 @@ var wavefrontPathTracingComputeLimits = Object.freeze({
123
136
  emissiveTriangleIndexBytes: EMISSIVE_TRIANGLE_INDEX_BYTES,
124
137
  emissiveTriangleMetadataRecordBytes: BVH_NODE_RECORD_BYTES,
125
138
  environmentPortalRecordBytes: ENVIRONMENT_PORTAL_RECORD_BYTES,
126
- accumulationRecordBytes: ACCUMULATION_RECORD_BYTES
139
+ accumulationRecordBytes: ACCUMULATION_RECORD_BYTES,
140
+ counterRecordBytes: COUNTER_BUFFER_BYTES,
141
+ indirectDispatchRecordBytes: INDIRECT_DISPATCH_ARGS_BYTES
127
142
  });
128
143
  var wavefrontSceneObjectKinds = Object.freeze({
129
144
  sphere: OBJECT_KIND_SPHERE,
@@ -231,6 +246,25 @@ function normalize(value, fallback = [0, 0, 1]) {
231
246
  }
232
247
  return [value[0] / length, value[1] / length, value[2] / length];
233
248
  }
249
+ function hashUint32(value) {
250
+ let x = value >>> 0;
251
+ x = ((x >>> 16 ^ x) >>> 0) * 73244475 >>> 0;
252
+ x = ((x >>> 16 ^ x) >>> 0) * 73244475 >>> 0;
253
+ return (x >>> 16 ^ x) >>> 0;
254
+ }
255
+ function mixSeed(pixelId, sampleId, bounce, frameIndex, dimension) {
256
+ let x = (pixelId >>> 0) * 747796405 ^ (sampleId >>> 0) * 2891336453 ^ (bounce >>> 0) * 277803737 ^ (frameIndex >>> 0) * 1442695041 ^ (dimension >>> 0) * 1597334677;
257
+ x >>>= 0;
258
+ x ^= x >>> 16;
259
+ x = x * 2146121005 >>> 0;
260
+ x ^= x >>> 15;
261
+ x = x * 2221713035 >>> 0;
262
+ x ^= x >>> 16;
263
+ return x >>> 0;
264
+ }
265
+ function random01FromSeed(seed) {
266
+ return (hashUint32(seed) & 16777215) / 16777215;
267
+ }
234
268
  function getArrayLikeLength(value) {
235
269
  return Array.isArray(value) || ArrayBuffer.isView(value) ? value.length : 0;
236
270
  }
@@ -829,6 +863,29 @@ function resolveEnvironmentLighting(input, environmentColor, ambientColor) {
829
863
  exposure: Math.max(1e-4, readFiniteNumber("environmentLighting.exposure", source.exposure, DEFAULT_ENVIRONMENT_LIGHTING.exposure))
830
864
  });
831
865
  }
866
+ function evaluateReferenceEnvironmentRadiance(config, origin, direction) {
867
+ void origin;
868
+ const rayDirection = normalize(direction, [0, 1, 0]);
869
+ const upFactor = clamp(rayDirection[1] * 0.5 + 0.5, 0, 1);
870
+ const sunDirection = normalize(
871
+ config.environmentLighting?.sunDirection ?? DEFAULT_ENVIRONMENT_LIGHTING.sunDirection,
872
+ DEFAULT_ENVIRONMENT_LIGHTING.sunDirection
873
+ );
874
+ const sunGlow = Math.pow(clamp(dot(rayDirection, sunDirection), 0, 1), 192);
875
+ const horizonColor = config.environmentLighting?.horizonColor ?? DEFAULT_ENVIRONMENT_LIGHTING.horizonColor;
876
+ const zenithColor = config.environmentLighting?.zenithColor ?? DEFAULT_ENVIRONMENT_LIGHTING.zenithColor;
877
+ const sunColor = config.environmentLighting?.sunColor ?? DEFAULT_ENVIRONMENT_LIGHTING.sunColor;
878
+ const intensity = Math.max(
879
+ 1e-4,
880
+ Number(config.environmentLighting?.intensity ?? DEFAULT_ENVIRONMENT_LIGHTING.intensity)
881
+ );
882
+ return Object.freeze([
883
+ (horizonColor[0] * (1 - upFactor) + zenithColor[0] * upFactor + sunColor[0] * sunGlow) * intensity,
884
+ (horizonColor[1] * (1 - upFactor) + zenithColor[1] * upFactor + sunColor[1] * sunGlow) * intensity,
885
+ (horizonColor[2] * (1 - upFactor) + zenithColor[2] * upFactor + sunColor[2] * sunGlow) * intensity,
886
+ 1
887
+ ]);
888
+ }
832
889
  function resolveEnvironmentPortalMode(value, hasPortals) {
833
890
  if (value === void 0 || value === null) {
834
891
  return hasPortals ? 2 : 0;
@@ -1032,7 +1089,8 @@ function estimateWavefrontPathTracingMemory(options = {}) {
1032
1089
  environmentPortalBytes,
1033
1090
  configBytes: CONFIG_BUFFER_BYTES,
1034
1091
  counterBytes: COUNTER_BUFFER_BYTES,
1035
- totalHotBufferBytes: queueBytes * 2 + hitBytes + accumulationBytes + sceneObjectBytes + triangleBytes + bvhNodeBytes + bvhLeafReferenceBytes + emissiveTriangleMetadataBytes + environmentPortalBytes + CONFIG_BUFFER_BYTES + COUNTER_BUFFER_BYTES
1092
+ indirectDispatchBytes: INDIRECT_DISPATCH_ARGS_BYTES,
1093
+ totalHotBufferBytes: queueBytes * 2 + hitBytes + accumulationBytes + sceneObjectBytes + triangleBytes + bvhNodeBytes + bvhLeafReferenceBytes + emissiveTriangleMetadataBytes + environmentPortalBytes + CONFIG_BUFFER_BYTES + COUNTER_BUFFER_BYTES + INDIRECT_DISPATCH_ARGS_BYTES
1036
1094
  });
1037
1095
  }
1038
1096
  function createWavefrontPathTracingComputeConfig(options = {}) {
@@ -1109,6 +1167,7 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
1109
1167
  environmentPortals.length > 0
1110
1168
  );
1111
1169
  return Object.freeze({
1170
+ mode: rendererWavefrontComputeMode,
1112
1171
  width,
1113
1172
  height,
1114
1173
  maxDepth,
@@ -1163,6 +1222,9 @@ function getGpuUsageConstants() {
1163
1222
  if (typeof GPUBufferUsage === "undefined" || typeof GPUTextureUsage === "undefined" || typeof GPUShaderStage === "undefined") {
1164
1223
  throw new Error("WebGPU runtime unavailable. Required GPU constants are missing.");
1165
1224
  }
1225
+ if (typeof GPUBufferUsage.INDIRECT !== "number") {
1226
+ throw new Error("WebGPU runtime unavailable. GPUBufferUsage.INDIRECT is missing.");
1227
+ }
1166
1228
  return {
1167
1229
  buffer: GPUBufferUsage,
1168
1230
  texture: GPUTextureUsage,
@@ -1357,6 +1419,209 @@ function createTiles(width, height, tileSize) {
1357
1419
  }
1358
1420
  return Object.freeze(tiles);
1359
1421
  }
1422
+ function normalizeReferenceTile(config, tileInput = {}) {
1423
+ const tileX = clamp(
1424
+ readNonNegativeInteger("tile.x", tileInput.x, 0),
1425
+ 0,
1426
+ Math.max(0, config.width - 1)
1427
+ );
1428
+ const tileY = clamp(
1429
+ readNonNegativeInteger("tile.y", tileInput.y, 0),
1430
+ 0,
1431
+ Math.max(0, config.height - 1)
1432
+ );
1433
+ const tileWidth = clamp(
1434
+ readPositiveInteger("tile.width", tileInput.width, config.width - tileX),
1435
+ 1,
1436
+ config.width - tileX
1437
+ );
1438
+ const tileHeight = clamp(
1439
+ readPositiveInteger("tile.height", tileInput.height, config.height - tileY),
1440
+ 1,
1441
+ config.height - tileY
1442
+ );
1443
+ return Object.freeze({
1444
+ x: tileX,
1445
+ y: tileY,
1446
+ width: tileWidth,
1447
+ height: tileHeight
1448
+ });
1449
+ }
1450
+ function repairReferenceShadingNormal(geometricNormal, shadingNormal) {
1451
+ const normal = normalize(shadingNormal, geometricNormal);
1452
+ return dot(normal, geometricNormal) < 0 ? scale(normal, -1) : normal;
1453
+ }
1454
+ function readOptionalMaxDistance(value) {
1455
+ if (value === void 0 || value === null) {
1456
+ return Number.POSITIVE_INFINITY;
1457
+ }
1458
+ const numeric = Number(value);
1459
+ if (!Number.isFinite(numeric) || numeric <= 0) {
1460
+ throw new Error("maxDistance must be a positive finite number when provided.");
1461
+ }
1462
+ return numeric;
1463
+ }
1464
+ function createWavefrontReferenceRay(config, options = {}) {
1465
+ if (!config || typeof config !== "object") {
1466
+ throw new Error("config must be a wavefront path tracing config.");
1467
+ }
1468
+ const tile = normalizeReferenceTile(config, options.tile);
1469
+ const tilePixelCount = tile.width * tile.height;
1470
+ const pixelIndex = readNonNegativeInteger("pixelIndex", options.pixelIndex, 0);
1471
+ if (pixelIndex >= tilePixelCount) {
1472
+ throw new Error(`pixelIndex ${pixelIndex} exceeds tile capacity ${tilePixelCount}.`);
1473
+ }
1474
+ const sampleIndex = readNonNegativeInteger("sampleIndex", options.sampleIndex, 0);
1475
+ const frameIndex = readNonNegativeInteger("frameIndex", options.frameIndex, config.frameIndex ?? 0);
1476
+ const jitterScale = clamp(readFiniteNumber("jitterScale", options.jitterScale, 0.35), 0, 1);
1477
+ const localX = pixelIndex % tile.width;
1478
+ const localY = Math.floor(pixelIndex / tile.width);
1479
+ const pixelX = tile.x + localX;
1480
+ const pixelY = tile.y + localY;
1481
+ const sourcePixelId = pixelY * config.width + pixelX;
1482
+ const jitterX = random01FromSeed(mixSeed(sourcePixelId, sampleIndex, 0, frameIndex, 1)) - 0.5;
1483
+ const jitterY = random01FromSeed(mixSeed(sourcePixelId, sampleIndex, 0, frameIndex, 2)) - 0.5;
1484
+ const ndcX = (pixelX + 0.5 + jitterX * jitterScale) / config.width * 2 - 1;
1485
+ const ndcY = 1 - (pixelY + 0.5 + jitterY * jitterScale) / config.height * 2;
1486
+ const viewX = ndcX * config.camera.tanHalfFovY * config.camera.aspect;
1487
+ const viewY = ndcY * config.camera.tanHalfFovY;
1488
+ const direction = normalize(
1489
+ add(
1490
+ add(config.camera.forward, scale(config.camera.right, viewX)),
1491
+ scale(config.camera.up, viewY)
1492
+ ),
1493
+ config.camera.forward
1494
+ );
1495
+ return Object.freeze({
1496
+ rayId: pixelIndex,
1497
+ parentRayId: 4294967295,
1498
+ sourcePixelId,
1499
+ sampleId: sampleIndex,
1500
+ bounce: 0,
1501
+ mediumRefId: 0,
1502
+ flags: 0,
1503
+ origin: Object.freeze([...config.camera.position]),
1504
+ direction: Object.freeze(direction),
1505
+ throughput: Object.freeze([1, 1, 1, 1]),
1506
+ pixelX,
1507
+ pixelY
1508
+ });
1509
+ }
1510
+ function intersectWavefrontReferenceTriangle(ray, triangle, options = {}) {
1511
+ if (!ray || typeof ray !== "object") {
1512
+ throw new Error("ray must be a wavefront reference ray.");
1513
+ }
1514
+ if (!triangle || typeof triangle !== "object") {
1515
+ throw new Error("triangle must be a wavefront triangle record.");
1516
+ }
1517
+ const maxDistance = readOptionalMaxDistance(options.maxDistance);
1518
+ const triangleIndex = readNonNegativeInteger("triangleIndex", options.triangleIndex, 0);
1519
+ const edge1 = subtract(triangle.v1, triangle.v0);
1520
+ const edge2 = subtract(triangle.v2, triangle.v0);
1521
+ const pvec = cross(ray.direction, edge2);
1522
+ const determinant = dot(edge1, pvec);
1523
+ if (Math.abs(determinant) < 1e-7) {
1524
+ return null;
1525
+ }
1526
+ const invDet = 1 / determinant;
1527
+ const tvec = subtract(ray.origin, triangle.v0);
1528
+ const u = dot(tvec, pvec) * invDet;
1529
+ if (u < 0 || u > 1) {
1530
+ return null;
1531
+ }
1532
+ const qvec = cross(tvec, edge1);
1533
+ const v = dot(ray.direction, qvec) * invDet;
1534
+ if (v < 0 || u + v > 1) {
1535
+ return null;
1536
+ }
1537
+ const distance = dot(edge2, qvec) * invDet;
1538
+ if (distance <= 1e-3 || distance > maxDistance) {
1539
+ return null;
1540
+ }
1541
+ const geometric = normalize(cross(edge1, edge2), [0, 1, 0]);
1542
+ const frontFace = dot(ray.direction, geometric) < 0;
1543
+ const orientedGeometric = frontFace ? geometric : scale(geometric, -1);
1544
+ const w = 1 - u - v;
1545
+ const interpolated = [
1546
+ triangle.n0[0] * w + triangle.n1[0] * u + triangle.n2[0] * v,
1547
+ triangle.n0[1] * w + triangle.n1[1] * u + triangle.n2[1] * v,
1548
+ triangle.n0[2] * w + triangle.n1[2] * u + triangle.n2[2] * v
1549
+ ];
1550
+ const shadingNormal = repairReferenceShadingNormal(orientedGeometric, interpolated);
1551
+ const uv = [
1552
+ triangle.uv0[0] * w + triangle.uv1[0] * u + triangle.uv2[0] * v,
1553
+ triangle.uv0[1] * w + triangle.uv1[1] * u + triangle.uv2[1] * v
1554
+ ];
1555
+ const position = add(ray.origin, scale(ray.direction, distance));
1556
+ return Object.freeze({
1557
+ hitType: "surface",
1558
+ rayId: ray.rayId,
1559
+ sourcePixelId: ray.sourcePixelId,
1560
+ distance,
1561
+ entityId: triangle.meshId,
1562
+ instanceId: 0,
1563
+ primitiveId: triangle.triangleId,
1564
+ materialId: triangle.materialKind,
1565
+ materialRefId: triangle.materialRefId,
1566
+ mediumRefId: triangle.mediumRefId,
1567
+ barycentrics: Object.freeze([w, u, v]),
1568
+ uv: Object.freeze(uv),
1569
+ geometricNormal: Object.freeze(orientedGeometric),
1570
+ shadingNormal: Object.freeze(shadingNormal),
1571
+ frontFace,
1572
+ triangleIndex,
1573
+ triangleId: triangle.triangleId,
1574
+ position: Object.freeze(position),
1575
+ color: triangle.color,
1576
+ emission: triangle.emission,
1577
+ material: triangle.material
1578
+ });
1579
+ }
1580
+ function createWavefrontReferenceEnvironmentHit(config, ray) {
1581
+ const radiance = evaluateReferenceEnvironmentRadiance(config, ray.origin, ray.direction);
1582
+ return Object.freeze({
1583
+ hitType: "environment",
1584
+ rayId: ray.rayId,
1585
+ sourcePixelId: ray.sourcePixelId,
1586
+ distance: -1,
1587
+ entityId: 0,
1588
+ instanceId: 0,
1589
+ primitiveId: 0,
1590
+ materialId: 0,
1591
+ materialRefId: 0,
1592
+ mediumRefId: 0,
1593
+ barycentrics: Object.freeze([0, 0, 0]),
1594
+ uv: Object.freeze([0, 0]),
1595
+ geometricNormal: Object.freeze(scale(ray.direction, -1)),
1596
+ shadingNormal: Object.freeze(scale(ray.direction, -1)),
1597
+ frontFace: true,
1598
+ triangleIndex: -1,
1599
+ triangleId: -1,
1600
+ position: Object.freeze(add(ray.origin, scale(ray.direction, 1e3))),
1601
+ color: Object.freeze([0, 0, 0, 0]),
1602
+ emission: radiance,
1603
+ material: Object.freeze([1, 0, 1, 1])
1604
+ });
1605
+ }
1606
+ function traceWavefrontReferenceTriangles(config, ray, triangles, options = {}) {
1607
+ if (!config || typeof config !== "object") {
1608
+ throw new Error("config must be a wavefront path tracing config.");
1609
+ }
1610
+ const source = Array.isArray(triangles) ? triangles : [];
1611
+ let nearestHit = null;
1612
+ let nearestDistance = readOptionalMaxDistance(options.maxDistance);
1613
+ source.forEach((triangle, index) => {
1614
+ const hit = intersectWavefrontReferenceTriangle(ray, triangle, {
1615
+ maxDistance: Number.isFinite(nearestDistance) ? nearestDistance : void 0,
1616
+ triangleIndex: index
1617
+ });
1618
+ if (hit && hit.distance < nearestDistance) {
1619
+ nearestDistance = hit.distance;
1620
+ nearestHit = hit;
1621
+ }
1622
+ });
1623
+ return nearestHit ?? createWavefrontReferenceEnvironmentHit(config, ray);
1624
+ }
1360
1625
  function clampTileSizeForDevice(config, device) {
1361
1626
  const limit = Number(device?.limits?.maxStorageBufferBindingSize);
1362
1627
  if (!Number.isFinite(limit) || limit <= 0) {
@@ -1596,6 +1861,10 @@ struct Counters {
1596
1861
  nextCount: atomic<u32>,
1597
1862
  terminatedCount: atomic<u32>,
1598
1863
  hitCount: atomic<u32>,
1864
+ dispatchX: u32,
1865
+ dispatchY: u32,
1866
+ dispatchZ: u32,
1867
+ dispatchPad: u32,
1599
1868
  };
1600
1869
 
1601
1870
  struct Candidate {
@@ -1758,6 +2027,18 @@ fn gated_environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f
1758
2027
  return environment_radiance(origin, direction);
1759
2028
  }
1760
2029
 
2030
+ fn terminal_surface_environment_contribution(ray: RayRecord, hit: HitRecord) -> vec3<f32> {
2031
+ let normal = safe_normalize(hit.shadingNormal.xyz, vec3<f32>(0.0, 1.0, 0.0));
2032
+ let surfaceColor = max(hit.color.xyz, config.ambientColor.xyz);
2033
+ let normalEnvironment = gated_environment_radiance(
2034
+ hit.position.xyz + normal * 0.003,
2035
+ normal
2036
+ );
2037
+ let environmentFloor = max(config.ambientColor.xyz, normalEnvironment * 0.12);
2038
+ let materialFloor = select(0.7, 1.0, hit.materialKind == 0u || hit.materialKind == 3u);
2039
+ return clamp_sample_radiance(ray.throughput.xyz * surfaceColor * environmentFloor * materialFloor);
2040
+ }
2041
+
1761
2042
  fn default_mesh_range() -> MeshRange {
1762
2043
  return MeshRange(
1763
2044
  0u,
@@ -2329,6 +2610,17 @@ fn tone_map_radiance(value: vec3<f32>) -> vec3<f32> {
2329
2610
  return pow(clamp(mapped, vec3<f32>(0.0), vec3<f32>(1.0)), vec3<f32>(1.0 / 2.2));
2330
2611
  }
2331
2612
 
2613
+ fn ray_workgroups_for_count(rayCount: u32) -> u32 {
2614
+ return max(1u, (rayCount + 63u) / 64u);
2615
+ }
2616
+
2617
+ fn write_active_dispatch_args(activeCount: u32) {
2618
+ counters.dispatchX = ray_workgroups_for_count(activeCount);
2619
+ counters.dispatchY = 1u;
2620
+ counters.dispatchZ = 1u;
2621
+ counters.dispatchPad = 0u;
2622
+ }
2623
+
2332
2624
  fn denoise_range_space(value: vec3<f32>) -> vec3<f32> {
2333
2625
  return value / (vec3<f32>(1.0) + value);
2334
2626
  }
@@ -2341,6 +2633,7 @@ fn generatePrimaryRays(@builtin(global_invocation_id) globalId: vec3<u32>) {
2341
2633
  atomicStore(&counters.nextCount, 0u);
2342
2634
  atomicStore(&counters.terminatedCount, 0u);
2343
2635
  atomicStore(&counters.hitCount, 0u);
2636
+ write_active_dispatch_args(config.tilePixelCount);
2344
2637
  }
2345
2638
  if (index >= config.tilePixelCount) {
2346
2639
  return;
@@ -2616,9 +2909,9 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
2616
2909
  }
2617
2910
 
2618
2911
  if (ray.bounce + 1u >= config.maxDepth) {
2912
+ let terminalEnvironment = terminal_surface_environment_contribution(ray, hit);
2619
2913
  accumulation[ray.rayId] =
2620
- accumulation[ray.rayId] +
2621
- vec4<f32>(ray.throughput.xyz * config.ambientColor.xyz * sample_weight(), 1.0);
2914
+ accumulation[ray.rayId] + vec4<f32>(terminalEnvironment * sample_weight(), 1.0);
2622
2915
  atomicAdd(&counters.terminatedCount, 1u);
2623
2916
  return;
2624
2917
  }
@@ -2627,6 +2920,10 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
2627
2920
  let scatter = scatter_direction(ray, hit, seed);
2628
2921
  let nextIndex = atomicAdd(&counters.nextCount, 1u);
2629
2922
  if (nextIndex >= config.tilePixelCount) {
2923
+ let overflowEnvironment = terminal_surface_environment_contribution(ray, hit);
2924
+ accumulation[ray.rayId] =
2925
+ accumulation[ray.rayId] + vec4<f32>(overflowEnvironment * sample_weight(), 1.0);
2926
+ atomicAdd(&counters.terminatedCount, 1u);
2630
2927
  return;
2631
2928
  }
2632
2929
  let color = clamp(hit.color.xyz, vec3<f32>(0.0), vec3<f32>(1.0));
@@ -2655,8 +2952,10 @@ fn compactAndSwapQueues(@builtin(global_invocation_id) globalId: vec3<u32>) {
2655
2952
  return;
2656
2953
  }
2657
2954
  let nextCount = atomicLoad(&counters.nextCount);
2658
- atomicStore(&counters.activeCount, min(nextCount, config.tilePixelCount));
2955
+ let activeCount = min(nextCount, config.tilePixelCount);
2956
+ atomicStore(&counters.activeCount, activeCount);
2659
2957
  atomicStore(&counters.nextCount, 0u);
2958
+ write_active_dispatch_args(activeCount);
2660
2959
  }
2661
2960
 
2662
2961
  @compute @workgroup_size(64)
@@ -2934,10 +3233,16 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
2934
3233
  );
2935
3234
  const counterBuffer = createBuffer(
2936
3235
  device,
2937
- constants.buffer.STORAGE | constants.buffer.COPY_DST,
3236
+ constants.buffer.STORAGE | constants.buffer.COPY_DST | constants.buffer.COPY_SRC,
2938
3237
  COUNTER_BUFFER_BYTES,
2939
3238
  "plasius.wavefront.counters"
2940
3239
  );
3240
+ const activeDispatchBuffer = createBuffer(
3241
+ device,
3242
+ constants.buffer.INDIRECT | constants.buffer.COPY_DST,
3243
+ INDIRECT_DISPATCH_ARGS_BYTES,
3244
+ "plasius.wavefront.activeDispatchArgs"
3245
+ );
2941
3246
  let packedScene = packWavefrontSceneObjects(config.sceneObjects, config.sceneObjectCapacity);
2942
3247
  device.queue.writeBuffer(sceneObjectBuffer, 0, packedScene.buffer);
2943
3248
  const packedTriangles = packWavefrontTriangles(
@@ -3359,24 +3664,34 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3359
3664
  return true;
3360
3665
  }
3361
3666
  function encodeTileSample(encoder, tile, configOffset) {
3362
- const passEncoder = encoder.beginComputePass({
3363
- label: "plasius.wavefront.computePass"
3667
+ const generatePass = encoder.beginComputePass({
3668
+ label: "plasius.wavefront.generatePrimaryRaysPass"
3364
3669
  });
3365
3670
  const tileWorkgroups = Math.ceil(tile.width * tile.height / WORKGROUP_SIZE);
3366
- const capacityWorkgroups = Math.ceil(config.tilePixelCapacity / WORKGROUP_SIZE);
3367
- passEncoder.setBindGroup(0, bindGroups[0], [configOffset]);
3368
- passEncoder.setPipeline(pipelines.generatePrimaryRays);
3369
- passEncoder.dispatchWorkgroups(tileWorkgroups);
3671
+ generatePass.setBindGroup(0, bindGroups[0], [configOffset]);
3672
+ generatePass.setPipeline(pipelines.generatePrimaryRays);
3673
+ generatePass.dispatchWorkgroups(tileWorkgroups);
3674
+ generatePass.end();
3370
3675
  for (let bounceIndex = 0; bounceIndex < config.maxDepth; bounceIndex += 1) {
3676
+ encoder.copyBufferToBuffer(
3677
+ counterBuffer,
3678
+ COUNTER_DISPATCH_ARGS_OFFSET,
3679
+ activeDispatchBuffer,
3680
+ 0,
3681
+ INDIRECT_DISPATCH_ARGS_BYTES
3682
+ );
3683
+ const passEncoder = encoder.beginComputePass({
3684
+ label: `plasius.wavefront.bounce.${bounceIndex}`
3685
+ });
3371
3686
  passEncoder.setBindGroup(0, bindGroups[bounceIndex % 2], [configOffset]);
3372
3687
  passEncoder.setPipeline(pipelines.intersectActiveQueue);
3373
- passEncoder.dispatchWorkgroups(capacityWorkgroups);
3688
+ passEncoder.dispatchWorkgroupsIndirect(activeDispatchBuffer, 0);
3374
3689
  passEncoder.setPipeline(pipelines.resolveSurfaceRecords);
3375
- passEncoder.dispatchWorkgroups(capacityWorkgroups);
3690
+ passEncoder.dispatchWorkgroupsIndirect(activeDispatchBuffer, 0);
3376
3691
  passEncoder.setPipeline(pipelines.compactAndSwapQueues);
3377
3692
  passEncoder.dispatchWorkgroups(1);
3693
+ passEncoder.end();
3378
3694
  }
3379
- passEncoder.end();
3380
3695
  }
3381
3696
  function encodeTileOutput(encoder, tile, configOffset) {
3382
3697
  const passEncoder = encoder.beginComputePass({
@@ -3519,6 +3834,28 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3519
3834
  luminance: (0.2126 * bytes[0] + 0.7152 * bytes[1] + 0.0722 * bytes[2]) / 255
3520
3835
  });
3521
3836
  }
3837
+ async function renderFrame(renderOptions = {}) {
3838
+ const frameStats = renderOnce();
3839
+ const probe = renderOptions.readOutputProbe === false ? null : await readOutputProbe(renderOptions.probe);
3840
+ const maxChannel = probe ? Math.max(...probe.rgba.slice(0, 3)) : 0;
3841
+ return Object.freeze({
3842
+ ...frameStats,
3843
+ outputProbe: probe ? Object.freeze({
3844
+ ...probe,
3845
+ sampledPixels: 1,
3846
+ nonZeroSamples: maxChannel > 0 ? 1 : 0,
3847
+ maxChannel
3848
+ }) : null,
3849
+ bounces: [],
3850
+ termination: Object.freeze({
3851
+ emissive: 0,
3852
+ environment: 0,
3853
+ ambientFallback: 0,
3854
+ maxDepth: 0
3855
+ }),
3856
+ queueOverflow: 0
3857
+ });
3858
+ }
3522
3859
  function updateSceneObjects(sceneObjects) {
3523
3860
  const nextPackedScene = packWavefrontSceneObjects(sceneObjects, config.sceneObjectCapacity);
3524
3861
  packedScene = nextPackedScene;
@@ -3577,6 +3914,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3577
3914
  configBuffer.destroy?.();
3578
3915
  bvhBuildConfigBuffer.destroy?.();
3579
3916
  counterBuffer.destroy?.();
3917
+ activeDispatchBuffer.destroy?.();
3580
3918
  radianceTexture.destroy?.();
3581
3919
  denoiseScratchTexture.destroy?.();
3582
3920
  outputTexture.destroy?.();
@@ -3589,12 +3927,32 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3589
3927
  format,
3590
3928
  config,
3591
3929
  renderOnce,
3930
+ renderFrame,
3592
3931
  readOutputProbe,
3593
3932
  updateSceneObjects,
3594
3933
  getSnapshot,
3595
3934
  destroy
3596
3935
  });
3597
3936
  }
3937
+ async function renderWavefrontPathTracingComputeFrame(options = {}) {
3938
+ const renderer = await createWavefrontPathTracingComputeRenderer(options);
3939
+ try {
3940
+ return await renderer.renderFrame(options);
3941
+ } finally {
3942
+ renderer.destroy();
3943
+ }
3944
+ }
3945
+ function createWavefrontPathTracingComputeShaderSource(options = {}) {
3946
+ const workgroupSize = readPositiveInteger(
3947
+ "workgroupSize",
3948
+ options.workgroupSize ?? rendererWavefrontComputeWorkgroupSize,
3949
+ rendererWavefrontComputeWorkgroupSize
3950
+ );
3951
+ if (workgroupSize !== rendererWavefrontComputeWorkgroupSize) {
3952
+ throw new Error(`wavefront mesh compute currently requires workgroupSize=${rendererWavefrontComputeWorkgroupSize}.`);
3953
+ }
3954
+ return WAVEFRONT_COMPUTE_WGSL;
3955
+ }
3598
3956
 
3599
3957
  // src/index.js
3600
3958
  var DEFAULT_CLEAR_COLOR = Object.freeze([0.07, 0.11, 0.18, 1]);
@@ -4895,22 +5253,29 @@ var defaultRendererClearColor = DEFAULT_CLEAR_COLOR;
4895
5253
  createWavefrontMeshAcceleration,
4896
5254
  createWavefrontPathTracingComputeConfig,
4897
5255
  createWavefrontPathTracingComputeRenderer,
5256
+ createWavefrontPathTracingComputeShaderSource,
4898
5257
  createWavefrontPathTracingPlan,
5258
+ createWavefrontReferenceRay,
4899
5259
  defaultRendererClearColor,
4900
5260
  defaultRendererWorkerProfile,
4901
5261
  estimateWavefrontPathTracingMemory,
4902
5262
  getRendererWorkerManifest,
4903
5263
  getRendererWorkerProfile,
5264
+ intersectWavefrontReferenceTriangle,
4904
5265
  normalizeWavefrontMesh,
4905
5266
  normalizeWavefrontSceneObject,
4906
5267
  packWavefrontBvhNodes,
4907
5268
  packWavefrontSceneObjects,
4908
5269
  packWavefrontTriangles,
5270
+ renderWavefrontPathTracingComputeFrame,
4909
5271
  rendererAccelerationStructureUpdateClasses,
4910
5272
  rendererDebugOwner,
4911
5273
  rendererRayTracingStageOrder,
4912
5274
  rendererRepresentationBands,
4913
5275
  rendererWavefrontBufferSchemaVersion,
5276
+ rendererWavefrontComputeMode,
5277
+ rendererWavefrontComputeStatsStride,
5278
+ rendererWavefrontComputeWorkgroupSize,
4914
5279
  rendererWavefrontHitTypes,
4915
5280
  rendererWavefrontPassOrder,
4916
5281
  rendererWavefrontQueuePairStrategy,
@@ -4920,6 +5285,7 @@ var defaultRendererClearColor = DEFAULT_CLEAR_COLOR;
4920
5285
  rendererWorkerQueueClass,
4921
5286
  supportsWavefrontPathTracingCompute,
4922
5287
  supportsWebGpu,
5288
+ traceWavefrontReferenceTriangles,
4923
5289
  wavefrontMaterialKinds,
4924
5290
  wavefrontPathTracingComputeLimits,
4925
5291
  wavefrontSceneObjectKinds