@plasius/gpu-renderer 0.1.14 → 0.2.0

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
@@ -32,11 +32,13 @@ __export(index_exports, {
32
32
  createWavefrontPathTracingComputeConfig: () => createWavefrontPathTracingComputeConfig,
33
33
  createWavefrontPathTracingComputeRenderer: () => createWavefrontPathTracingComputeRenderer,
34
34
  createWavefrontPathTracingPlan: () => createWavefrontPathTracingPlan,
35
+ createWavefrontReferenceRay: () => createWavefrontReferenceRay,
35
36
  defaultRendererClearColor: () => defaultRendererClearColor,
36
37
  defaultRendererWorkerProfile: () => defaultRendererWorkerProfile,
37
38
  estimateWavefrontPathTracingMemory: () => estimateWavefrontPathTracingMemory,
38
39
  getRendererWorkerManifest: () => getRendererWorkerManifest,
39
40
  getRendererWorkerProfile: () => getRendererWorkerProfile,
41
+ intersectWavefrontReferenceTriangle: () => intersectWavefrontReferenceTriangle,
40
42
  normalizeWavefrontMesh: () => normalizeWavefrontMesh,
41
43
  normalizeWavefrontSceneObject: () => normalizeWavefrontSceneObject,
42
44
  packWavefrontBvhNodes: () => packWavefrontBvhNodes,
@@ -56,6 +58,7 @@ __export(index_exports, {
56
58
  rendererWorkerQueueClass: () => rendererWorkerQueueClass,
57
59
  supportsWavefrontPathTracingCompute: () => supportsWavefrontPathTracingCompute,
58
60
  supportsWebGpu: () => supportsWebGpu,
61
+ traceWavefrontReferenceTriangles: () => traceWavefrontReferenceTriangles,
59
62
  wavefrontMaterialKinds: () => wavefrontMaterialKinds,
60
63
  wavefrontPathTracingComputeLimits: () => wavefrontPathTracingComputeLimits,
61
64
  wavefrontSceneObjectKinds: () => wavefrontSceneObjectKinds
@@ -69,6 +72,7 @@ var DEFAULT_MAX_DEPTH = 6;
69
72
  var DEFAULT_TILE_SIZE = 128;
70
73
  var DEFAULT_SAMPLES_PER_PIXEL = 1;
71
74
  var DEFAULT_SCENE_OBJECT_CAPACITY = 128;
75
+ var DEFAULT_ENVIRONMENT_PORTAL_CAPACITY = 32;
72
76
  var WORKGROUP_SIZE = 64;
73
77
  var RAY_RECORD_BYTES = 80;
74
78
  var HIT_RECORD_BYTES = 208;
@@ -79,9 +83,11 @@ var TRIANGLE_RECORD_BYTES = 208;
79
83
  var BVH_NODE_RECORD_BYTES = 48;
80
84
  var BVH_LEAF_REF_RECORD_BYTES = 16;
81
85
  var EMISSIVE_TRIANGLE_INDEX_BYTES = 4;
86
+ var ENVIRONMENT_PORTAL_RECORD_BYTES = 96;
82
87
  var ACCUMULATION_RECORD_BYTES = 16;
83
- var CONFIG_BUFFER_BYTES = 256;
88
+ var CONFIG_BUFFER_BYTES = 272;
84
89
  var COUNTER_BUFFER_BYTES = 16;
90
+ var TRACE_STORAGE_BUFFER_BINDINGS = 9;
85
91
  var MATERIAL_DIFFUSE = 0;
86
92
  var MATERIAL_METAL = 1;
87
93
  var MATERIAL_DIELECTRIC = 2;
@@ -108,6 +114,7 @@ var DEFAULT_ENVIRONMENT_LIGHTING = Object.freeze({
108
114
  });
109
115
  var wavefrontPathTracingComputeLimits = Object.freeze({
110
116
  workgroupSize: WORKGROUP_SIZE,
117
+ traceStorageBufferBindings: TRACE_STORAGE_BUFFER_BINDINGS,
111
118
  rayRecordBytes: RAY_RECORD_BYTES,
112
119
  hitRecordBytes: HIT_RECORD_BYTES,
113
120
  sceneObjectRecordBytes: SCENE_OBJECT_RECORD_BYTES,
@@ -118,6 +125,7 @@ var wavefrontPathTracingComputeLimits = Object.freeze({
118
125
  bvhLeafReferenceRecordBytes: BVH_LEAF_REF_RECORD_BYTES,
119
126
  emissiveTriangleIndexBytes: EMISSIVE_TRIANGLE_INDEX_BYTES,
120
127
  emissiveTriangleMetadataRecordBytes: BVH_NODE_RECORD_BYTES,
128
+ environmentPortalRecordBytes: ENVIRONMENT_PORTAL_RECORD_BYTES,
121
129
  accumulationRecordBytes: ACCUMULATION_RECORD_BYTES
122
130
  });
123
131
  var wavefrontSceneObjectKinds = Object.freeze({
@@ -209,6 +217,9 @@ function subtract(a, b) {
209
217
  function scale(a, scalar) {
210
218
  return [a[0] * scalar, a[1] * scalar, a[2] * scalar];
211
219
  }
220
+ function dot(a, b) {
221
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
222
+ }
212
223
  function cross(a, b) {
213
224
  return [
214
225
  a[1] * b[2] - a[2] * b[1],
@@ -223,6 +234,25 @@ function normalize(value, fallback = [0, 0, 1]) {
223
234
  }
224
235
  return [value[0] / length, value[1] / length, value[2] / length];
225
236
  }
237
+ function hashUint32(value) {
238
+ let x = value >>> 0;
239
+ x = ((x >>> 16 ^ x) >>> 0) * 73244475 >>> 0;
240
+ x = ((x >>> 16 ^ x) >>> 0) * 73244475 >>> 0;
241
+ return (x >>> 16 ^ x) >>> 0;
242
+ }
243
+ function mixSeed(pixelId, sampleId, bounce, frameIndex, dimension) {
244
+ let x = (pixelId >>> 0) * 747796405 ^ (sampleId >>> 0) * 2891336453 ^ (bounce >>> 0) * 277803737 ^ (frameIndex >>> 0) * 1442695041 ^ (dimension >>> 0) * 1597334677;
245
+ x >>>= 0;
246
+ x ^= x >>> 16;
247
+ x = x * 2146121005 >>> 0;
248
+ x ^= x >>> 15;
249
+ x = x * 2221713035 >>> 0;
250
+ x ^= x >>> 16;
251
+ return x >>> 0;
252
+ }
253
+ function random01FromSeed(seed) {
254
+ return (hashUint32(seed) & 16777215) / 16777215;
255
+ }
226
256
  function getArrayLikeLength(value) {
227
257
  return Array.isArray(value) || ArrayBuffer.isView(value) ? value.length : 0;
228
258
  }
@@ -821,6 +851,150 @@ function resolveEnvironmentLighting(input, environmentColor, ambientColor) {
821
851
  exposure: Math.max(1e-4, readFiniteNumber("environmentLighting.exposure", source.exposure, DEFAULT_ENVIRONMENT_LIGHTING.exposure))
822
852
  });
823
853
  }
854
+ function evaluateReferenceEnvironmentRadiance(config, origin, direction) {
855
+ void origin;
856
+ const rayDirection = normalize(direction, [0, 1, 0]);
857
+ const upFactor = clamp(rayDirection[1] * 0.5 + 0.5, 0, 1);
858
+ const sunDirection = normalize(
859
+ config.environmentLighting?.sunDirection ?? DEFAULT_ENVIRONMENT_LIGHTING.sunDirection,
860
+ DEFAULT_ENVIRONMENT_LIGHTING.sunDirection
861
+ );
862
+ const sunGlow = Math.pow(clamp(dot(rayDirection, sunDirection), 0, 1), 192);
863
+ const horizonColor = config.environmentLighting?.horizonColor ?? DEFAULT_ENVIRONMENT_LIGHTING.horizonColor;
864
+ const zenithColor = config.environmentLighting?.zenithColor ?? DEFAULT_ENVIRONMENT_LIGHTING.zenithColor;
865
+ const sunColor = config.environmentLighting?.sunColor ?? DEFAULT_ENVIRONMENT_LIGHTING.sunColor;
866
+ const intensity = Math.max(
867
+ 1e-4,
868
+ Number(config.environmentLighting?.intensity ?? DEFAULT_ENVIRONMENT_LIGHTING.intensity)
869
+ );
870
+ return Object.freeze([
871
+ (horizonColor[0] * (1 - upFactor) + zenithColor[0] * upFactor + sunColor[0] * sunGlow) * intensity,
872
+ (horizonColor[1] * (1 - upFactor) + zenithColor[1] * upFactor + sunColor[1] * sunGlow) * intensity,
873
+ (horizonColor[2] * (1 - upFactor) + zenithColor[2] * upFactor + sunColor[2] * sunGlow) * intensity,
874
+ 1
875
+ ]);
876
+ }
877
+ function resolveEnvironmentPortalMode(value, hasPortals) {
878
+ if (value === void 0 || value === null) {
879
+ return hasPortals ? 2 : 0;
880
+ }
881
+ if (Number.isInteger(value) && value >= 0 && value <= 2) {
882
+ return value;
883
+ }
884
+ if (value === "disabled") {
885
+ return 0;
886
+ }
887
+ if (value === "guide") {
888
+ return 1;
889
+ }
890
+ if (value === "guide-and-gate" || value === "gate") {
891
+ return 2;
892
+ }
893
+ throw new Error(
894
+ "environmentPortalMode must be disabled, guide, guide-and-gate, or an integer between 0 and 2."
895
+ );
896
+ }
897
+ function orthogonalPortalTangent(normal) {
898
+ if (Math.abs(normal[1]) < 0.92) {
899
+ return normalize(cross([0, 1, 0], normal), [1, 0, 0]);
900
+ }
901
+ return normalize(cross([1, 0, 0], normal), [0, 0, 1]);
902
+ }
903
+ function resolvePortalTangent(value, normal) {
904
+ const fallback = orthogonalPortalTangent(normal);
905
+ const tangent = asUnitVec3(value, fallback);
906
+ const projected = subtract(tangent, scale(normal, dot(tangent, normal)));
907
+ return normalize(projected, fallback);
908
+ }
909
+ function readPositiveFiniteNumber(name, value, fallback) {
910
+ const numeric = readFiniteNumber(name, value, fallback);
911
+ if (numeric <= 0) {
912
+ throw new Error(`${name} must be a positive finite number.`);
913
+ }
914
+ return numeric;
915
+ }
916
+ function readPortalExtent(name, value, halfName, halfValue) {
917
+ if (value !== void 0 && value !== null) {
918
+ return readPositiveFiniteNumber(name, value, 1);
919
+ }
920
+ return readPositiveFiniteNumber(halfName, halfValue, 0.5) * 2;
921
+ }
922
+ function normalizeEnvironmentPortal(portal, index) {
923
+ if (!portal || typeof portal !== "object") {
924
+ throw new Error(`environmentPortals[${index}] must be an object.`);
925
+ }
926
+ const shape = portal.shape ?? portal.kind ?? "rectangle";
927
+ if (shape !== "rectangle") {
928
+ throw new Error(`environmentPortals[${index}].shape must be "rectangle".`);
929
+ }
930
+ const position = asVec3(portal.position ?? portal.center, [0, 0, 0]);
931
+ const normal = asUnitVec3(portal.normal, [0, 0, 1]);
932
+ const tangent = resolvePortalTangent(portal.tangent, normal);
933
+ const bitangent = normalize(cross(normal, tangent), [0, 1, 0]);
934
+ const width = readPortalExtent(
935
+ `environmentPortals[${index}].width`,
936
+ portal.width,
937
+ `environmentPortals[${index}].halfWidth`,
938
+ portal.halfWidth
939
+ );
940
+ const height = readPortalExtent(
941
+ `environmentPortals[${index}].height`,
942
+ portal.height,
943
+ `environmentPortals[${index}].halfHeight`,
944
+ portal.halfHeight
945
+ );
946
+ const radianceScale = Math.max(
947
+ 0,
948
+ readFiniteNumber(
949
+ `environmentPortals[${index}].radianceScale`,
950
+ portal.radianceScale ?? portal.intensity,
951
+ 1
952
+ )
953
+ );
954
+ return Object.freeze({
955
+ kind: 1,
956
+ flags: portal.twoSided === false ? 0 : 1,
957
+ position: Object.freeze([position[0], position[1], position[2], width * height]),
958
+ normal: Object.freeze([normal[0], normal[1], normal[2], radianceScale]),
959
+ tangent: Object.freeze([tangent[0], tangent[1], tangent[2], width * 0.5]),
960
+ bitangent: Object.freeze([bitangent[0], bitangent[1], bitangent[2], height * 0.5]),
961
+ color: Object.freeze(asColor(portal.color, [1, 1, 1, 1]))
962
+ });
963
+ }
964
+ function normalizeEnvironmentPortals(value) {
965
+ if (value === void 0 || value === null) {
966
+ return Object.freeze([]);
967
+ }
968
+ if (!Array.isArray(value)) {
969
+ throw new Error("environmentPortals must be an array when provided.");
970
+ }
971
+ return Object.freeze(value.map(normalizeEnvironmentPortal));
972
+ }
973
+ function packEnvironmentPortals(portals, capacity) {
974
+ const bytes = new ArrayBuffer(capacity * ENVIRONMENT_PORTAL_RECORD_BYTES);
975
+ const data = new DataView(bytes);
976
+ const floatView = new Float32Array(bytes);
977
+ portals.forEach((portal, index) => {
978
+ const byteOffset = index * ENVIRONMENT_PORTAL_RECORD_BYTES;
979
+ const floatOffset = byteOffset / Float32Array.BYTES_PER_ELEMENT;
980
+ data.setUint32(byteOffset, portal.kind, true);
981
+ data.setUint32(byteOffset + 4, portal.flags, true);
982
+ data.setUint32(byteOffset + 8, 0, true);
983
+ data.setUint32(byteOffset + 12, 0, true);
984
+ writeVec4(floatView, floatOffset + 4, portal.position);
985
+ writeVec4(floatView, floatOffset + 8, portal.normal);
986
+ writeVec4(floatView, floatOffset + 12, portal.tangent);
987
+ writeVec4(floatView, floatOffset + 16, portal.bitangent);
988
+ writeVec4(floatView, floatOffset + 20, portal.color);
989
+ });
990
+ return Object.freeze({
991
+ buffer: bytes,
992
+ portals,
993
+ count: portals.length,
994
+ capacity,
995
+ recordBytes: ENVIRONMENT_PORTAL_RECORD_BYTES
996
+ });
997
+ }
824
998
  function getCanvasDimension(canvas, key, fallback) {
825
999
  const value = Number(canvas?.[key]);
826
1000
  if (Number.isFinite(value) && value > 0) {
@@ -876,6 +1050,11 @@ function estimateWavefrontPathTracingMemory(options = {}) {
876
1050
  options.emissiveTriangleCapacity,
877
1051
  0
878
1052
  );
1053
+ const environmentPortalCapacity = readNonNegativeInteger(
1054
+ "environmentPortalCapacity",
1055
+ options.environmentPortalCapacity,
1056
+ 0
1057
+ );
879
1058
  const queueBytes = tilePixelCapacity * RAY_RECORD_BYTES;
880
1059
  const hitBytes = tilePixelCapacity * HIT_RECORD_BYTES;
881
1060
  const accumulationBytes = tilePixelCapacity * ACCUMULATION_RECORD_BYTES;
@@ -884,6 +1063,7 @@ function estimateWavefrontPathTracingMemory(options = {}) {
884
1063
  const bvhNodeBytes = bvhNodeCapacity * BVH_NODE_RECORD_BYTES;
885
1064
  const bvhLeafReferenceBytes = bvhLeafSortCapacity * BVH_LEAF_REF_RECORD_BYTES;
886
1065
  const emissiveTriangleMetadataBytes = emissiveTriangleCapacity * BVH_NODE_RECORD_BYTES;
1066
+ const environmentPortalBytes = environmentPortalCapacity * ENVIRONMENT_PORTAL_RECORD_BYTES;
887
1067
  return Object.freeze({
888
1068
  queueBytes,
889
1069
  queuePairBytes: queueBytes * 2,
@@ -894,9 +1074,10 @@ function estimateWavefrontPathTracingMemory(options = {}) {
894
1074
  bvhNodeBytes,
895
1075
  bvhLeafReferenceBytes,
896
1076
  emissiveTriangleMetadataBytes,
1077
+ environmentPortalBytes,
897
1078
  configBytes: CONFIG_BUFFER_BYTES,
898
1079
  counterBytes: COUNTER_BUFFER_BYTES,
899
- totalHotBufferBytes: queueBytes * 2 + hitBytes + accumulationBytes + sceneObjectBytes + triangleBytes + bvhNodeBytes + bvhLeafReferenceBytes + emissiveTriangleMetadataBytes + CONFIG_BUFFER_BYTES + COUNTER_BUFFER_BYTES
1080
+ totalHotBufferBytes: queueBytes * 2 + hitBytes + accumulationBytes + sceneObjectBytes + triangleBytes + bvhNodeBytes + bvhLeafReferenceBytes + emissiveTriangleMetadataBytes + environmentPortalBytes + CONFIG_BUFFER_BYTES + COUNTER_BUFFER_BYTES
900
1081
  });
901
1082
  }
902
1083
  function createWavefrontPathTracingComputeConfig(options = {}) {
@@ -957,6 +1138,21 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
957
1138
  environmentColor,
958
1139
  ambientColor
959
1140
  );
1141
+ const environmentPortals = normalizeEnvironmentPortals(
1142
+ options.environmentPortals ?? options.environmentLightPortals ?? options.environmentLighting?.environmentPortals
1143
+ );
1144
+ const environmentPortalCapacity = Math.max(
1145
+ environmentPortals.length,
1146
+ readNonNegativeInteger(
1147
+ "environmentPortalCapacity",
1148
+ options.environmentPortalCapacity,
1149
+ DEFAULT_ENVIRONMENT_PORTAL_CAPACITY
1150
+ )
1151
+ );
1152
+ const environmentPortalMode = resolveEnvironmentPortalMode(
1153
+ options.environmentPortalMode ?? options.portalMode ?? options.environmentLighting?.environmentPortalMode,
1154
+ environmentPortals.length > 0
1155
+ );
960
1156
  return Object.freeze({
961
1157
  width,
962
1158
  height,
@@ -985,6 +1181,10 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
985
1181
  environmentColor: environmentLighting.environmentColor,
986
1182
  ambientColor: environmentLighting.ambientColor,
987
1183
  environmentLighting,
1184
+ environmentPortals,
1185
+ environmentPortalCount: environmentPortals.length,
1186
+ environmentPortalCapacity,
1187
+ environmentPortalMode,
988
1188
  displayQuality: options.displayQuality === true,
989
1189
  requiresMeshBvhForDisplayQuality: true,
990
1190
  denoise: options.denoise !== false,
@@ -995,7 +1195,8 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
995
1195
  triangleCapacity,
996
1196
  bvhNodeCapacity,
997
1197
  bvhLeafSortCapacity,
998
- emissiveTriangleCapacity: emissiveTriangleIndices.capacity
1198
+ emissiveTriangleCapacity: emissiveTriangleIndices.capacity,
1199
+ environmentPortalCapacity
999
1200
  })
1000
1201
  });
1001
1202
  }
@@ -1179,6 +1380,10 @@ function createConfigPayload(config, tile, frameIndex, buildRange = {}) {
1179
1380
  data.setUint32(244, buildRange.count ?? 0, true);
1180
1381
  data.setUint32(248, buildRange.sortItemCount ?? 0, true);
1181
1382
  data.setUint32(252, config.emissiveTriangleCount ?? 0, true);
1383
+ data.setUint32(256, config.environmentPortalCount ?? 0, true);
1384
+ data.setUint32(260, config.environmentPortalMode ?? 0, true);
1385
+ data.setUint32(264, 0, true);
1386
+ data.setUint32(268, 0, true);
1182
1387
  return bytes;
1183
1388
  }
1184
1389
  function createTiles(width, height, tileSize) {
@@ -1197,6 +1402,209 @@ function createTiles(width, height, tileSize) {
1197
1402
  }
1198
1403
  return Object.freeze(tiles);
1199
1404
  }
1405
+ function normalizeReferenceTile(config, tileInput = {}) {
1406
+ const tileX = clamp(
1407
+ readNonNegativeInteger("tile.x", tileInput.x, 0),
1408
+ 0,
1409
+ Math.max(0, config.width - 1)
1410
+ );
1411
+ const tileY = clamp(
1412
+ readNonNegativeInteger("tile.y", tileInput.y, 0),
1413
+ 0,
1414
+ Math.max(0, config.height - 1)
1415
+ );
1416
+ const tileWidth = clamp(
1417
+ readPositiveInteger("tile.width", tileInput.width, config.width - tileX),
1418
+ 1,
1419
+ config.width - tileX
1420
+ );
1421
+ const tileHeight = clamp(
1422
+ readPositiveInteger("tile.height", tileInput.height, config.height - tileY),
1423
+ 1,
1424
+ config.height - tileY
1425
+ );
1426
+ return Object.freeze({
1427
+ x: tileX,
1428
+ y: tileY,
1429
+ width: tileWidth,
1430
+ height: tileHeight
1431
+ });
1432
+ }
1433
+ function repairReferenceShadingNormal(geometricNormal, shadingNormal) {
1434
+ const normal = normalize(shadingNormal, geometricNormal);
1435
+ return dot(normal, geometricNormal) < 0 ? scale(normal, -1) : normal;
1436
+ }
1437
+ function readOptionalMaxDistance(value) {
1438
+ if (value === void 0 || value === null) {
1439
+ return Number.POSITIVE_INFINITY;
1440
+ }
1441
+ const numeric = Number(value);
1442
+ if (!Number.isFinite(numeric) || numeric <= 0) {
1443
+ throw new Error("maxDistance must be a positive finite number when provided.");
1444
+ }
1445
+ return numeric;
1446
+ }
1447
+ function createWavefrontReferenceRay(config, options = {}) {
1448
+ if (!config || typeof config !== "object") {
1449
+ throw new Error("config must be a wavefront path tracing config.");
1450
+ }
1451
+ const tile = normalizeReferenceTile(config, options.tile);
1452
+ const tilePixelCount = tile.width * tile.height;
1453
+ const pixelIndex = readNonNegativeInteger("pixelIndex", options.pixelIndex, 0);
1454
+ if (pixelIndex >= tilePixelCount) {
1455
+ throw new Error(`pixelIndex ${pixelIndex} exceeds tile capacity ${tilePixelCount}.`);
1456
+ }
1457
+ const sampleIndex = readNonNegativeInteger("sampleIndex", options.sampleIndex, 0);
1458
+ const frameIndex = readNonNegativeInteger("frameIndex", options.frameIndex, config.frameIndex ?? 0);
1459
+ const jitterScale = clamp(readFiniteNumber("jitterScale", options.jitterScale, 0.35), 0, 1);
1460
+ const localX = pixelIndex % tile.width;
1461
+ const localY = Math.floor(pixelIndex / tile.width);
1462
+ const pixelX = tile.x + localX;
1463
+ const pixelY = tile.y + localY;
1464
+ const sourcePixelId = pixelY * config.width + pixelX;
1465
+ const jitterX = random01FromSeed(mixSeed(sourcePixelId, sampleIndex, 0, frameIndex, 1)) - 0.5;
1466
+ const jitterY = random01FromSeed(mixSeed(sourcePixelId, sampleIndex, 0, frameIndex, 2)) - 0.5;
1467
+ const ndcX = (pixelX + 0.5 + jitterX * jitterScale) / config.width * 2 - 1;
1468
+ const ndcY = 1 - (pixelY + 0.5 + jitterY * jitterScale) / config.height * 2;
1469
+ const viewX = ndcX * config.camera.tanHalfFovY * config.camera.aspect;
1470
+ const viewY = ndcY * config.camera.tanHalfFovY;
1471
+ const direction = normalize(
1472
+ add(
1473
+ add(config.camera.forward, scale(config.camera.right, viewX)),
1474
+ scale(config.camera.up, viewY)
1475
+ ),
1476
+ config.camera.forward
1477
+ );
1478
+ return Object.freeze({
1479
+ rayId: pixelIndex,
1480
+ parentRayId: 4294967295,
1481
+ sourcePixelId,
1482
+ sampleId: sampleIndex,
1483
+ bounce: 0,
1484
+ mediumRefId: 0,
1485
+ flags: 0,
1486
+ origin: Object.freeze([...config.camera.position]),
1487
+ direction: Object.freeze(direction),
1488
+ throughput: Object.freeze([1, 1, 1, 1]),
1489
+ pixelX,
1490
+ pixelY
1491
+ });
1492
+ }
1493
+ function intersectWavefrontReferenceTriangle(ray, triangle, options = {}) {
1494
+ if (!ray || typeof ray !== "object") {
1495
+ throw new Error("ray must be a wavefront reference ray.");
1496
+ }
1497
+ if (!triangle || typeof triangle !== "object") {
1498
+ throw new Error("triangle must be a wavefront triangle record.");
1499
+ }
1500
+ const maxDistance = readOptionalMaxDistance(options.maxDistance);
1501
+ const triangleIndex = readNonNegativeInteger("triangleIndex", options.triangleIndex, 0);
1502
+ const edge1 = subtract(triangle.v1, triangle.v0);
1503
+ const edge2 = subtract(triangle.v2, triangle.v0);
1504
+ const pvec = cross(ray.direction, edge2);
1505
+ const determinant = dot(edge1, pvec);
1506
+ if (Math.abs(determinant) < 1e-7) {
1507
+ return null;
1508
+ }
1509
+ const invDet = 1 / determinant;
1510
+ const tvec = subtract(ray.origin, triangle.v0);
1511
+ const u = dot(tvec, pvec) * invDet;
1512
+ if (u < 0 || u > 1) {
1513
+ return null;
1514
+ }
1515
+ const qvec = cross(tvec, edge1);
1516
+ const v = dot(ray.direction, qvec) * invDet;
1517
+ if (v < 0 || u + v > 1) {
1518
+ return null;
1519
+ }
1520
+ const distance = dot(edge2, qvec) * invDet;
1521
+ if (distance <= 1e-3 || distance > maxDistance) {
1522
+ return null;
1523
+ }
1524
+ const geometric = normalize(cross(edge1, edge2), [0, 1, 0]);
1525
+ const frontFace = dot(ray.direction, geometric) < 0;
1526
+ const orientedGeometric = frontFace ? geometric : scale(geometric, -1);
1527
+ const w = 1 - u - v;
1528
+ const interpolated = [
1529
+ triangle.n0[0] * w + triangle.n1[0] * u + triangle.n2[0] * v,
1530
+ triangle.n0[1] * w + triangle.n1[1] * u + triangle.n2[1] * v,
1531
+ triangle.n0[2] * w + triangle.n1[2] * u + triangle.n2[2] * v
1532
+ ];
1533
+ const shadingNormal = repairReferenceShadingNormal(orientedGeometric, interpolated);
1534
+ const uv = [
1535
+ triangle.uv0[0] * w + triangle.uv1[0] * u + triangle.uv2[0] * v,
1536
+ triangle.uv0[1] * w + triangle.uv1[1] * u + triangle.uv2[1] * v
1537
+ ];
1538
+ const position = add(ray.origin, scale(ray.direction, distance));
1539
+ return Object.freeze({
1540
+ hitType: "surface",
1541
+ rayId: ray.rayId,
1542
+ sourcePixelId: ray.sourcePixelId,
1543
+ distance,
1544
+ entityId: triangle.meshId,
1545
+ instanceId: 0,
1546
+ primitiveId: triangle.triangleId,
1547
+ materialId: triangle.materialKind,
1548
+ materialRefId: triangle.materialRefId,
1549
+ mediumRefId: triangle.mediumRefId,
1550
+ barycentrics: Object.freeze([w, u, v]),
1551
+ uv: Object.freeze(uv),
1552
+ geometricNormal: Object.freeze(orientedGeometric),
1553
+ shadingNormal: Object.freeze(shadingNormal),
1554
+ frontFace,
1555
+ triangleIndex,
1556
+ triangleId: triangle.triangleId,
1557
+ position: Object.freeze(position),
1558
+ color: triangle.color,
1559
+ emission: triangle.emission,
1560
+ material: triangle.material
1561
+ });
1562
+ }
1563
+ function createWavefrontReferenceEnvironmentHit(config, ray) {
1564
+ const radiance = evaluateReferenceEnvironmentRadiance(config, ray.origin, ray.direction);
1565
+ return Object.freeze({
1566
+ hitType: "environment",
1567
+ rayId: ray.rayId,
1568
+ sourcePixelId: ray.sourcePixelId,
1569
+ distance: -1,
1570
+ entityId: 0,
1571
+ instanceId: 0,
1572
+ primitiveId: 0,
1573
+ materialId: 0,
1574
+ materialRefId: 0,
1575
+ mediumRefId: 0,
1576
+ barycentrics: Object.freeze([0, 0, 0]),
1577
+ uv: Object.freeze([0, 0]),
1578
+ geometricNormal: Object.freeze(scale(ray.direction, -1)),
1579
+ shadingNormal: Object.freeze(scale(ray.direction, -1)),
1580
+ frontFace: true,
1581
+ triangleIndex: -1,
1582
+ triangleId: -1,
1583
+ position: Object.freeze(add(ray.origin, scale(ray.direction, 1e3))),
1584
+ color: Object.freeze([0, 0, 0, 0]),
1585
+ emission: radiance,
1586
+ material: Object.freeze([1, 0, 1, 1])
1587
+ });
1588
+ }
1589
+ function traceWavefrontReferenceTriangles(config, ray, triangles, options = {}) {
1590
+ if (!config || typeof config !== "object") {
1591
+ throw new Error("config must be a wavefront path tracing config.");
1592
+ }
1593
+ const source = Array.isArray(triangles) ? triangles : [];
1594
+ let nearestHit = null;
1595
+ let nearestDistance = readOptionalMaxDistance(options.maxDistance);
1596
+ source.forEach((triangle, index) => {
1597
+ const hit = intersectWavefrontReferenceTriangle(ray, triangle, {
1598
+ maxDistance: Number.isFinite(nearestDistance) ? nearestDistance : void 0,
1599
+ triangleIndex: index
1600
+ });
1601
+ if (hit && hit.distance < nearestDistance) {
1602
+ nearestDistance = hit.distance;
1603
+ nearestHit = hit;
1604
+ }
1605
+ });
1606
+ return nearestHit ?? createWavefrontReferenceEnvironmentHit(config, ray);
1607
+ }
1200
1608
  function clampTileSizeForDevice(config, device) {
1201
1609
  const limit = Number(device?.limits?.maxStorageBufferBindingSize);
1202
1610
  if (!Number.isFinite(limit) || limit <= 0) {
@@ -1425,6 +1833,10 @@ struct FrameConfig {
1425
1833
  bvhBuildNodeCount: u32,
1426
1834
  bvhSortItemCount: u32,
1427
1835
  emissiveTriangleCount: u32,
1836
+ environmentPortalCount: u32,
1837
+ environmentPortalMode: u32,
1838
+ _portalPad0: u32,
1839
+ _portalPad1: u32,
1428
1840
  };
1429
1841
 
1430
1842
  struct Counters {
@@ -1448,6 +1860,18 @@ struct Candidate {
1448
1860
  mediumRefId: u32,
1449
1861
  };
1450
1862
 
1863
+ struct EnvironmentPortal {
1864
+ kind: u32,
1865
+ flags: u32,
1866
+ _pad0: u32,
1867
+ _pad1: u32,
1868
+ position: vec4<f32>,
1869
+ normal: vec4<f32>,
1870
+ tangent: vec4<f32>,
1871
+ bitangent: vec4<f32>,
1872
+ color: vec4<f32>,
1873
+ };
1874
+
1451
1875
  @group(0) @binding(0) var<storage, read_write> activeQueue: array<RayRecord>;
1452
1876
  @group(0) @binding(1) var<storage, read_write> nextQueue: array<RayRecord>;
1453
1877
  @group(0) @binding(2) var<storage, read_write> hits: array<HitRecord>;
@@ -1467,6 +1891,7 @@ struct Candidate {
1467
1891
  @group(0) @binding(16) var radianceImage: texture_storage_2d<rgba16float, write>;
1468
1892
  @group(0) @binding(17) var finalDenoiseInputRadiance: texture_2d<f32>;
1469
1893
  @group(0) @binding(18) var denoisedOutputImage: texture_storage_2d<rgba8unorm, write>;
1894
+ @group(0) @binding(19) var<storage, read> environmentPortals: array<EnvironmentPortal>;
1470
1895
 
1471
1896
  fn hash_u32(value: u32) -> u32 {
1472
1897
  var x = value;
@@ -1507,7 +1932,48 @@ fn saturate(value: f32) -> f32 {
1507
1932
  return clamp(value, 0.0, 1.0);
1508
1933
  }
1509
1934
 
1510
- fn environment_radiance(direction: vec3<f32>) -> vec3<f32> {
1935
+ fn max_component(value: vec3<f32>) -> f32 {
1936
+ return max(max(value.x, value.y), value.z);
1937
+ }
1938
+
1939
+ fn environment_portal_radiance_scale(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
1940
+ if (config.environmentPortalCount == 0u || config.environmentPortalMode == 0u) {
1941
+ return vec3<f32>(1.0);
1942
+ }
1943
+ var scale = vec3<f32>(0.0);
1944
+ for (var portalIndex = 0u; portalIndex < config.environmentPortalCount; portalIndex = portalIndex + 1u) {
1945
+ let portal = environmentPortals[portalIndex];
1946
+ if (portal.kind == 1u) {
1947
+ let portalNormal = safe_normalize(portal.normal.xyz, vec3<f32>(0.0, 0.0, 1.0));
1948
+ let denominator = dot(direction, portalNormal);
1949
+ let twoSided = (portal.flags & 1u) != 0u;
1950
+ var facing = abs(denominator) > 0.0001;
1951
+ if (!twoSided && denominator <= 0.0001) {
1952
+ facing = false;
1953
+ }
1954
+ if (facing) {
1955
+ let distance = dot(portal.position.xyz - origin, portalNormal) / denominator;
1956
+ if (distance > 0.001) {
1957
+ let hitPosition = origin + direction * distance;
1958
+ let local = hitPosition - portal.position.xyz;
1959
+ let tangent = safe_normalize(portal.tangent.xyz, vec3<f32>(1.0, 0.0, 0.0));
1960
+ let bitangent = safe_normalize(portal.bitangent.xyz, vec3<f32>(0.0, 1.0, 0.0));
1961
+ let u = dot(local, tangent);
1962
+ let v = dot(local, bitangent);
1963
+ if (abs(u) <= portal.tangent.w && abs(v) <= portal.bitangent.w) {
1964
+ let areaWeight = clamp(sqrt(max(portal.position.w, 0.0001)), 0.25, 4.0);
1965
+ let angleWeight = max(abs(denominator), 0.08);
1966
+ let portalScale = portal.color.rgb * portal.normal.w * portal.color.a * areaWeight * angleWeight;
1967
+ scale = max(scale, portalScale);
1968
+ }
1969
+ }
1970
+ }
1971
+ }
1972
+ }
1973
+ return scale;
1974
+ }
1975
+
1976
+ fn environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
1511
1977
  let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
1512
1978
  let upFactor = saturate(rayDirection.y * 0.5 + 0.5);
1513
1979
  let sunDirection = safe_normalize(
@@ -1518,10 +1984,26 @@ fn environment_radiance(direction: vec3<f32>) -> vec3<f32> {
1518
1984
  let gradient =
1519
1985
  config.environmentHorizonColor.xyz * (1.0 - upFactor) +
1520
1986
  config.environmentZenithColor.xyz * upFactor;
1987
+ let portalScale = environment_portal_radiance_scale(origin, rayDirection);
1988
+ let portalHit = max_component(portalScale) > 0.0001;
1521
1989
  return (
1522
1990
  gradient +
1523
1991
  config.environmentSunColor.xyz * sunGlow
1524
- ) * max(config.environmentSunDirectionIntensity.w, 0.0001);
1992
+ ) *
1993
+ max(config.environmentSunDirectionIntensity.w, 0.0001) *
1994
+ select(vec3<f32>(1.0), portalScale, portalHit);
1995
+ }
1996
+
1997
+ fn gated_environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
1998
+ let portalScale = environment_portal_radiance_scale(origin, safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0)));
1999
+ if (
2000
+ config.environmentPortalCount > 0u &&
2001
+ config.environmentPortalMode == 2u &&
2002
+ max_component(portalScale) <= 0.0001
2003
+ ) {
2004
+ return config.ambientColor.xyz * 0.65;
2005
+ }
2006
+ return environment_radiance(origin, direction);
1525
2007
  }
1526
2008
 
1527
2009
  fn default_mesh_range() -> MeshRange {
@@ -1792,7 +2274,7 @@ fn make_ray(pixelIndex: u32) -> RayRecord {
1792
2274
  }
1793
2275
 
1794
2276
  fn make_miss(ray: RayRecord) -> HitRecord {
1795
- let radiance = environment_radiance(ray.direction.xyz);
2277
+ let radiance = gated_environment_radiance(ray.origin.xyz, ray.direction.xyz);
1796
2278
  return HitRecord(
1797
2279
  ray.rayId,
1798
2280
  ray.sourcePixelId,
@@ -2278,6 +2760,21 @@ fn sample_emissive_triangle_direction(hit: HitRecord, seed: u32, fallback: vec3<
2278
2760
  return safe_normalize(lightPoint - hit.position.xyz, fallback);
2279
2761
  }
2280
2762
 
2763
+ fn sample_environment_portal_direction(hit: HitRecord, seed: u32, fallback: vec3<f32>) -> vec3<f32> {
2764
+ if (config.environmentPortalCount == 0u || config.environmentPortalMode == 0u) {
2765
+ return fallback;
2766
+ }
2767
+ let portalSlot = min(
2768
+ u32(random01(seed + 211u) * f32(config.environmentPortalCount)),
2769
+ config.environmentPortalCount - 1u
2770
+ );
2771
+ let portal = environmentPortals[portalSlot];
2772
+ let u = (random01(seed + 223u) * 2.0 - 1.0) * portal.tangent.w;
2773
+ let v = (random01(seed + 227u) * 2.0 - 1.0) * portal.bitangent.w;
2774
+ let portalTarget = portal.position.xyz + portal.tangent.xyz * u + portal.bitangent.xyz * v;
2775
+ return safe_normalize(portalTarget - hit.position.xyz, fallback);
2776
+ }
2777
+
2281
2778
  fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult {
2282
2779
  let roughness = clamp(hit.material.x, 0.0, 1.0);
2283
2780
  if (hit.materialKind == 1u) {
@@ -2317,8 +2814,17 @@ fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult
2317
2814
  let canSampleLight = dot(hit.shadingNormal.xyz, guidedLight) > -0.04;
2318
2815
  let guideProbability = select(0.38, 0.72, ray.bounce == 0u);
2319
2816
  let useGuidedLight = canSampleLight && random01(seed + 37u) < guideProbability;
2817
+ let guidedPortal = sample_environment_portal_direction(hit, seed, randomDiffuse);
2818
+ let canSamplePortal = dot(hit.shadingNormal.xyz, guidedPortal) > -0.04;
2819
+ let useGuidedPortal =
2820
+ !useGuidedLight &&
2821
+ canSamplePortal &&
2822
+ config.environmentPortalCount > 0u &&
2823
+ config.environmentPortalMode > 0u &&
2824
+ random01(seed + 89u) < 0.58;
2825
+ let guidedDirection = select(randomDiffuse, guidedPortal, useGuidedPortal);
2320
2826
  return ScatterResult(
2321
- vec4<f32>(select(randomDiffuse, guidedLight, useGuidedLight), 0.0),
2827
+ vec4<f32>(select(guidedDirection, guidedLight, useGuidedLight), 0.0),
2322
2828
  select(0u, RAY_FLAG_GUIDED_EMISSIVE, useGuidedLight),
2323
2829
  0u,
2324
2830
  0u,
@@ -2525,6 +3031,29 @@ fn fragmentMain(in: VertexOut) -> @location(0) vec4<f32> {
2525
3031
  return textureSample(renderTexture, renderSampler, in.uv);
2526
3032
  }
2527
3033
  `;
3034
+ function createWavefrontDeviceDescriptor(adapter, options = {}) {
3035
+ const requiredLimits = { ...options.requiredLimits ?? {} };
3036
+ const exposedStorageBufferLimit = Number(adapter?.limits?.maxStorageBuffersPerShaderStage);
3037
+ if (Number.isFinite(exposedStorageBufferLimit)) {
3038
+ if (exposedStorageBufferLimit < TRACE_STORAGE_BUFFER_BINDINGS) {
3039
+ throw new Error(
3040
+ `Wavefront mesh tracing requires maxStorageBuffersPerShaderStage>=${TRACE_STORAGE_BUFFER_BINDINGS}, but this adapter exposes ${exposedStorageBufferLimit}.`
3041
+ );
3042
+ }
3043
+ requiredLimits.maxStorageBuffersPerShaderStage = Math.max(
3044
+ Number(requiredLimits.maxStorageBuffersPerShaderStage ?? 0),
3045
+ TRACE_STORAGE_BUFFER_BINDINGS
3046
+ );
3047
+ }
3048
+ const descriptor = { ...options.deviceDescriptor ?? {} };
3049
+ if (Object.keys(requiredLimits).length > 0) {
3050
+ descriptor.requiredLimits = {
3051
+ ...descriptor.requiredLimits ?? {},
3052
+ ...requiredLimits
3053
+ };
3054
+ }
3055
+ return Object.keys(descriptor).length > 0 ? descriptor : void 0;
3056
+ }
2528
3057
  async function createWavefrontPathTracingComputeRenderer(options = {}) {
2529
3058
  assertAnalyticDisplayQualityPolicy(options);
2530
3059
  const constants = getGpuUsageConstants();
@@ -2543,7 +3072,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
2543
3072
  if (!adapter) {
2544
3073
  throw new Error("Unable to acquire a WebGPU adapter for wavefront path tracing.");
2545
3074
  }
2546
- const device = await adapter.requestDevice();
3075
+ const device = await adapter.requestDevice(createWavefrontDeviceDescriptor(adapter, options));
2547
3076
  const context = canvas.getContext("webgpu");
2548
3077
  if (!context || typeof context.configure !== "function") {
2549
3078
  throw new Error("Canvas WebGPU context does not support configure().");
@@ -2616,23 +3145,34 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
2616
3145
  Math.max(1, config.gpuMeshSource.meshes.count) * MESH_RANGE_RECORD_BYTES,
2617
3146
  "plasius.wavefront.meshRanges"
2618
3147
  );
3148
+ const environmentPortalBuffer = createBuffer(
3149
+ device,
3150
+ constants.buffer.STORAGE | constants.buffer.COPY_DST,
3151
+ Math.max(1, config.environmentPortalCapacity) * ENVIRONMENT_PORTAL_RECORD_BYTES,
3152
+ "plasius.wavefront.environmentPortals"
3153
+ );
2619
3154
  const bvhLeafRefBuffer = createBuffer(
2620
3155
  device,
2621
3156
  constants.buffer.STORAGE | constants.buffer.COPY_DST,
2622
3157
  Math.max(1, config.bvhLeafSortCapacity) * BVH_LEAF_REF_RECORD_BYTES,
2623
3158
  "plasius.wavefront.bvhLeafRefs"
2624
3159
  );
2625
- const configBuffer = createBuffer(
2626
- device,
2627
- constants.buffer.UNIFORM | constants.buffer.COPY_DST,
2628
- CONFIG_BUFFER_BYTES,
2629
- "plasius.wavefront.frameConfig"
2630
- );
3160
+ const tiles = createTiles(config.width, config.height, config.tileSize);
2631
3161
  const uniformOffsetAlignment = Number(device?.limits?.minUniformBufferOffsetAlignment);
2632
3162
  const configBufferStride = alignTo(
2633
3163
  CONFIG_BUFFER_BYTES,
2634
3164
  Number.isFinite(uniformOffsetAlignment) && uniformOffsetAlignment > 0 ? uniformOffsetAlignment : CONFIG_BUFFER_BYTES
2635
3165
  );
3166
+ const frameConfigSlotCount = Math.max(
3167
+ 1,
3168
+ tiles.length * config.samplesPerPixel + tiles.length + (config.denoise ? 1 : 0)
3169
+ );
3170
+ const configBuffer = createBuffer(
3171
+ device,
3172
+ constants.buffer.UNIFORM | constants.buffer.COPY_DST,
3173
+ frameConfigSlotCount * configBufferStride,
3174
+ "plasius.wavefront.frameConfig"
3175
+ );
2636
3176
  const bvhBuildConfigSlots = 1 + config.bvhSortStages.length + config.bvhBuildLevels.length;
2637
3177
  const bvhBuildConfigBuffer = createBuffer(
2638
3178
  device,
@@ -2666,6 +3206,11 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
2666
3206
  device.queue.writeBuffer(meshVertexBuffer, 0, config.gpuMeshSource.vertices.buffer);
2667
3207
  device.queue.writeBuffer(meshIndexBuffer, 0, config.gpuMeshSource.indices.buffer);
2668
3208
  device.queue.writeBuffer(meshRangeBuffer, 0, config.gpuMeshSource.meshes.buffer);
3209
+ const packedEnvironmentPortals = packEnvironmentPortals(
3210
+ config.environmentPortals,
3211
+ Math.max(1, config.environmentPortalCapacity)
3212
+ );
3213
+ device.queue.writeBuffer(environmentPortalBuffer, 0, packedEnvironmentPortals.buffer);
2669
3214
  const radianceTexture = device.createTexture({
2670
3215
  label: "plasius.wavefront.radiance",
2671
3216
  size: { width: config.width, height: config.height },
@@ -2717,7 +3262,8 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
2717
3262
  binding: 16,
2718
3263
  visibility: constants.shader.COMPUTE,
2719
3264
  storageTexture: { access: "write-only", format: "rgba16float" }
2720
- }
3265
+ },
3266
+ { binding: 19, visibility: constants.shader.COMPUTE, buffer: { type: "read-only-storage" } }
2721
3267
  ]
2722
3268
  });
2723
3269
  const accelerationBindGroupLayout = device.createBindGroupLayout({
@@ -2890,7 +3436,8 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
2890
3436
  { binding: 7, resource: outputView },
2891
3437
  { binding: 8, resource: { buffer: triangleBuffer } },
2892
3438
  { binding: 9, resource: { buffer: bvhNodeBuffer } },
2893
- { binding: 16, resource: radianceView }
3439
+ { binding: 16, resource: radianceView },
3440
+ { binding: 19, resource: { buffer: environmentPortalBuffer } }
2894
3441
  ]
2895
3442
  });
2896
3443
  }
@@ -2977,10 +3524,27 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
2977
3524
  ]
2978
3525
  });
2979
3526
  let frame = 0;
2980
- const tiles = createTiles(config.width, config.height, config.tileSize);
3527
+ let accelerationBuilt = !config.gpuAccelerationBuildRequired;
3528
+ let accelerationBuildCount = 0;
3529
+ function createFrameConfigWriter(frameIndex) {
3530
+ let slot = 0;
3531
+ return (tile, buildRange = {}) => {
3532
+ if (slot >= frameConfigSlotCount) {
3533
+ throw new Error("Wavefront frame config slot capacity exceeded.");
3534
+ }
3535
+ const offset = slot * configBufferStride;
3536
+ slot += 1;
3537
+ device.queue.writeBuffer(
3538
+ configBuffer,
3539
+ offset,
3540
+ createConfigPayload(config, tile, frameIndex, buildRange)
3541
+ );
3542
+ return offset;
3543
+ };
3544
+ }
2981
3545
  function dispatchGpuAccelerationBuild(frameIndex) {
2982
- if (!config.gpuAccelerationBuildRequired) {
2983
- return;
3546
+ if (!config.gpuAccelerationBuildRequired || accelerationBuilt) {
3547
+ return false;
2984
3548
  }
2985
3549
  const buildTile = tiles[0] ?? { x: 0, y: 0, width: 1, height: 1 };
2986
3550
  const encoder = device.createCommandEncoder({
@@ -3038,27 +3602,21 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3038
3602
  }
3039
3603
  passEncoder.end();
3040
3604
  device.queue.submit([encoder.finish()]);
3605
+ accelerationBuilt = true;
3606
+ accelerationBuildCount += 1;
3607
+ return true;
3041
3608
  }
3042
- function dispatchTileSample(tile, frameIndex, sampleIndex) {
3043
- const sampleWeight = 1 / config.samplesPerPixel;
3044
- const configPayload = createConfigPayload(config, tile, frameIndex, {
3045
- sampleIndex,
3046
- sampleWeight
3047
- });
3048
- device.queue.writeBuffer(configBuffer, 0, configPayload);
3049
- const encoder = device.createCommandEncoder({
3050
- label: `plasius.wavefront.frame.${frameIndex}.tile.${tile.x}.${tile.y}.sample.${sampleIndex}`
3051
- });
3609
+ function encodeTileSample(encoder, tile, configOffset) {
3052
3610
  const passEncoder = encoder.beginComputePass({
3053
3611
  label: "plasius.wavefront.computePass"
3054
3612
  });
3055
3613
  const tileWorkgroups = Math.ceil(tile.width * tile.height / WORKGROUP_SIZE);
3056
3614
  const capacityWorkgroups = Math.ceil(config.tilePixelCapacity / WORKGROUP_SIZE);
3057
- passEncoder.setBindGroup(0, bindGroups[0], [0]);
3615
+ passEncoder.setBindGroup(0, bindGroups[0], [configOffset]);
3058
3616
  passEncoder.setPipeline(pipelines.generatePrimaryRays);
3059
3617
  passEncoder.dispatchWorkgroups(tileWorkgroups);
3060
3618
  for (let bounceIndex = 0; bounceIndex < config.maxDepth; bounceIndex += 1) {
3061
- passEncoder.setBindGroup(0, bindGroups[bounceIndex % 2], [0]);
3619
+ passEncoder.setBindGroup(0, bindGroups[bounceIndex % 2], [configOffset]);
3062
3620
  passEncoder.setPipeline(pipelines.intersectActiveQueue);
3063
3621
  passEncoder.dispatchWorkgroups(capacityWorkgroups);
3064
3622
  passEncoder.setPipeline(pipelines.resolveSurfaceRecords);
@@ -3067,71 +3625,38 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3067
3625
  passEncoder.dispatchWorkgroups(1);
3068
3626
  }
3069
3627
  passEncoder.end();
3070
- device.queue.submit([encoder.finish()]);
3071
3628
  }
3072
- function dispatchTileOutput(tile, frameIndex) {
3073
- const configPayload = createConfigPayload(config, tile, frameIndex, {
3074
- sampleIndex: 0,
3075
- sampleWeight: 1 / config.samplesPerPixel
3076
- });
3077
- device.queue.writeBuffer(configBuffer, 0, configPayload);
3078
- const encoder = device.createCommandEncoder({
3079
- label: `plasius.wavefront.frame.${frameIndex}.tile.${tile.x}.${tile.y}.output`
3080
- });
3629
+ function encodeTileOutput(encoder, tile, configOffset) {
3081
3630
  const passEncoder = encoder.beginComputePass({
3082
3631
  label: "plasius.wavefront.outputPass"
3083
3632
  });
3084
3633
  const tileWorkgroups = Math.ceil(tile.width * tile.height / WORKGROUP_SIZE);
3085
- passEncoder.setBindGroup(0, bindGroups[0], [0]);
3634
+ passEncoder.setBindGroup(0, bindGroups[0], [configOffset]);
3086
3635
  passEncoder.setPipeline(pipelines.accumulateTerminalRadiance);
3087
3636
  passEncoder.dispatchWorkgroups(tileWorkgroups);
3088
3637
  passEncoder.end();
3089
- device.queue.submit([encoder.finish()]);
3090
- }
3091
- function dispatchTile(tile, frameIndex) {
3092
- for (let sampleIndex = 0; sampleIndex < config.samplesPerPixel; sampleIndex += 1) {
3093
- dispatchTileSample(tile, frameIndex, sampleIndex);
3094
- }
3095
- dispatchTileOutput(tile, frameIndex);
3096
3638
  }
3097
- function dispatchDenoise(frameIndex) {
3639
+ function encodeDenoise(encoder, configOffset) {
3098
3640
  if (!config.denoise) {
3099
3641
  return;
3100
3642
  }
3101
- device.queue.writeBuffer(
3102
- configBuffer,
3103
- 0,
3104
- createConfigPayload(
3105
- config,
3106
- { x: 0, y: 0, width: config.width, height: config.height },
3107
- frameIndex,
3108
- { sampleIndex: 0, sampleWeight: 1 / config.samplesPerPixel }
3109
- )
3110
- );
3111
- const encoder = device.createCommandEncoder({
3112
- label: `plasius.wavefront.frame.${frameIndex}.denoise`
3113
- });
3114
3643
  const radiancePass = encoder.beginComputePass({
3115
3644
  label: "plasius.wavefront.denoiseRadiancePass"
3116
3645
  });
3117
- radiancePass.setBindGroup(0, denoiseRadianceBindGroup, [0]);
3646
+ radiancePass.setBindGroup(0, denoiseRadianceBindGroup, [configOffset]);
3118
3647
  radiancePass.setPipeline(pipelines.denoiseLinearRadiance);
3119
3648
  radiancePass.dispatchWorkgroups(Math.ceil(config.width / 8), Math.ceil(config.height / 8));
3120
3649
  radiancePass.end();
3121
3650
  const resolvePass = encoder.beginComputePass({
3122
3651
  label: "plasius.wavefront.denoiseResolvePass"
3123
3652
  });
3124
- resolvePass.setBindGroup(0, denoiseResolveBindGroup, [0]);
3653
+ resolvePass.setBindGroup(0, denoiseResolveBindGroup, [configOffset]);
3125
3654
  resolvePass.setPipeline(pipelines.resolveDenoisedOutputImage);
3126
3655
  resolvePass.dispatchWorkgroups(Math.ceil(config.width / 8), Math.ceil(config.height / 8));
3127
3656
  resolvePass.end();
3128
- device.queue.submit([encoder.finish()]);
3129
3657
  }
3130
- function present() {
3658
+ function encodePresent(encoder) {
3131
3659
  const texture = context.getCurrentTexture();
3132
- const encoder = device.createCommandEncoder({
3133
- label: `plasius.wavefront.present.${frame}`
3134
- });
3135
3660
  const passEncoder = encoder.beginRenderPass({
3136
3661
  label: "plasius.wavefront.presentPass",
3137
3662
  colorAttachments: [
@@ -3147,16 +3672,42 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3147
3672
  passEncoder.setBindGroup(0, presentBindGroup);
3148
3673
  passEncoder.draw(3);
3149
3674
  passEncoder.end();
3675
+ }
3676
+ function dispatchFrame(frameIndex) {
3677
+ const writeFrameConfig = createFrameConfigWriter(frameIndex);
3678
+ const encoder = device.createCommandEncoder({
3679
+ label: `plasius.wavefront.frame.${frameIndex}.batched`
3680
+ });
3681
+ for (const tile of tiles) {
3682
+ for (let sampleIndex = 0; sampleIndex < config.samplesPerPixel; sampleIndex += 1) {
3683
+ const configOffset = writeFrameConfig(tile, {
3684
+ sampleIndex,
3685
+ sampleWeight: 1 / config.samplesPerPixel
3686
+ });
3687
+ encodeTileSample(encoder, tile, configOffset);
3688
+ }
3689
+ const outputConfigOffset = writeFrameConfig(tile, {
3690
+ sampleIndex: 0,
3691
+ sampleWeight: 1 / config.samplesPerPixel
3692
+ });
3693
+ encodeTileOutput(encoder, tile, outputConfigOffset);
3694
+ }
3695
+ if (config.denoise) {
3696
+ const denoiseConfigOffset = writeFrameConfig(
3697
+ { x: 0, y: 0, width: config.width, height: config.height },
3698
+ { sampleIndex: 0, sampleWeight: 1 / config.samplesPerPixel }
3699
+ );
3700
+ encodeDenoise(encoder, denoiseConfigOffset);
3701
+ }
3702
+ encodePresent(encoder);
3150
3703
  device.queue.submit([encoder.finish()]);
3704
+ return 1;
3151
3705
  }
3152
3706
  function renderOnce() {
3153
3707
  frame += 1;
3154
- dispatchGpuAccelerationBuild(frame + config.frameIndex);
3155
- for (const tile of tiles) {
3156
- dispatchTile(tile, frame + config.frameIndex);
3157
- }
3158
- dispatchDenoise(frame + config.frameIndex);
3159
- present();
3708
+ const frameIndex = frame + config.frameIndex;
3709
+ const accelerationBuildSubmitted = dispatchGpuAccelerationBuild(frameIndex);
3710
+ const frameSubmissionCount = dispatchFrame(frameIndex);
3160
3711
  return Object.freeze({
3161
3712
  frame,
3162
3713
  width: config.width,
@@ -3170,10 +3721,17 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3170
3721
  sceneObjectCount: config.sceneObjectCount,
3171
3722
  triangleCount: config.triangleCount,
3172
3723
  emissiveTriangleCount: config.emissiveTriangleCount,
3724
+ environmentPortalCount: config.environmentPortalCount,
3725
+ environmentPortalMode: config.environmentPortalMode,
3173
3726
  bvhNodeCount: config.bvhNodeCount,
3174
3727
  displayQuality: config.displayQuality,
3175
3728
  accelerationBuildMode: config.accelerationBuildMode,
3176
3729
  gpuAccelerationBuildRequired: config.gpuAccelerationBuildRequired,
3730
+ accelerationBuildSubmitted,
3731
+ accelerationBuilt,
3732
+ accelerationBuildCount,
3733
+ commandSubmissions: frameSubmissionCount + (accelerationBuildSubmitted ? 1 : 0),
3734
+ frameConfigSlots: frameConfigSlotCount,
3177
3735
  memory: config.memory
3178
3736
  });
3179
3737
  }
@@ -3239,10 +3797,15 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3239
3797
  sceneObjectCount: config.sceneObjectCount,
3240
3798
  triangleCount: config.triangleCount,
3241
3799
  emissiveTriangleCount: config.emissiveTriangleCount,
3800
+ environmentPortalCount: config.environmentPortalCount,
3801
+ environmentPortalMode: config.environmentPortalMode,
3242
3802
  bvhNodeCount: config.bvhNodeCount,
3243
3803
  displayQuality: config.displayQuality,
3244
3804
  accelerationBuildMode: config.accelerationBuildMode,
3245
3805
  gpuAccelerationBuildRequired: config.gpuAccelerationBuildRequired,
3806
+ accelerationBuilt,
3807
+ accelerationBuildCount,
3808
+ frameConfigSlots: frameConfigSlotCount,
3246
3809
  memory: config.memory
3247
3810
  });
3248
3811
  }
@@ -3257,6 +3820,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3257
3820
  meshVertexBuffer.destroy?.();
3258
3821
  meshIndexBuffer.destroy?.();
3259
3822
  meshRangeBuffer.destroy?.();
3823
+ environmentPortalBuffer.destroy?.();
3260
3824
  bvhLeafRefBuffer.destroy?.();
3261
3825
  configBuffer.destroy?.();
3262
3826
  bvhBuildConfigBuffer.destroy?.();
@@ -4580,11 +5144,13 @@ var defaultRendererClearColor = DEFAULT_CLEAR_COLOR;
4580
5144
  createWavefrontPathTracingComputeConfig,
4581
5145
  createWavefrontPathTracingComputeRenderer,
4582
5146
  createWavefrontPathTracingPlan,
5147
+ createWavefrontReferenceRay,
4583
5148
  defaultRendererClearColor,
4584
5149
  defaultRendererWorkerProfile,
4585
5150
  estimateWavefrontPathTracingMemory,
4586
5151
  getRendererWorkerManifest,
4587
5152
  getRendererWorkerProfile,
5153
+ intersectWavefrontReferenceTriangle,
4588
5154
  normalizeWavefrontMesh,
4589
5155
  normalizeWavefrontSceneObject,
4590
5156
  packWavefrontBvhNodes,
@@ -4604,6 +5170,7 @@ var defaultRendererClearColor = DEFAULT_CLEAR_COLOR;
4604
5170
  rendererWorkerQueueClass,
4605
5171
  supportsWavefrontPathTracingCompute,
4606
5172
  supportsWebGpu,
5173
+ traceWavefrontReferenceTriangles,
4607
5174
  wavefrontMaterialKinds,
4608
5175
  wavefrontPathTracingComputeLimits,
4609
5176
  wavefrontSceneObjectKinds