@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.js CHANGED
@@ -5,6 +5,7 @@ var DEFAULT_MAX_DEPTH = 6;
5
5
  var DEFAULT_TILE_SIZE = 128;
6
6
  var DEFAULT_SAMPLES_PER_PIXEL = 1;
7
7
  var DEFAULT_SCENE_OBJECT_CAPACITY = 128;
8
+ var DEFAULT_ENVIRONMENT_PORTAL_CAPACITY = 32;
8
9
  var WORKGROUP_SIZE = 64;
9
10
  var RAY_RECORD_BYTES = 80;
10
11
  var HIT_RECORD_BYTES = 208;
@@ -15,9 +16,11 @@ var TRIANGLE_RECORD_BYTES = 208;
15
16
  var BVH_NODE_RECORD_BYTES = 48;
16
17
  var BVH_LEAF_REF_RECORD_BYTES = 16;
17
18
  var EMISSIVE_TRIANGLE_INDEX_BYTES = 4;
19
+ var ENVIRONMENT_PORTAL_RECORD_BYTES = 96;
18
20
  var ACCUMULATION_RECORD_BYTES = 16;
19
- var CONFIG_BUFFER_BYTES = 256;
21
+ var CONFIG_BUFFER_BYTES = 272;
20
22
  var COUNTER_BUFFER_BYTES = 16;
23
+ var TRACE_STORAGE_BUFFER_BINDINGS = 9;
21
24
  var MATERIAL_DIFFUSE = 0;
22
25
  var MATERIAL_METAL = 1;
23
26
  var MATERIAL_DIELECTRIC = 2;
@@ -44,6 +47,7 @@ var DEFAULT_ENVIRONMENT_LIGHTING = Object.freeze({
44
47
  });
45
48
  var wavefrontPathTracingComputeLimits = Object.freeze({
46
49
  workgroupSize: WORKGROUP_SIZE,
50
+ traceStorageBufferBindings: TRACE_STORAGE_BUFFER_BINDINGS,
47
51
  rayRecordBytes: RAY_RECORD_BYTES,
48
52
  hitRecordBytes: HIT_RECORD_BYTES,
49
53
  sceneObjectRecordBytes: SCENE_OBJECT_RECORD_BYTES,
@@ -54,6 +58,7 @@ var wavefrontPathTracingComputeLimits = Object.freeze({
54
58
  bvhLeafReferenceRecordBytes: BVH_LEAF_REF_RECORD_BYTES,
55
59
  emissiveTriangleIndexBytes: EMISSIVE_TRIANGLE_INDEX_BYTES,
56
60
  emissiveTriangleMetadataRecordBytes: BVH_NODE_RECORD_BYTES,
61
+ environmentPortalRecordBytes: ENVIRONMENT_PORTAL_RECORD_BYTES,
57
62
  accumulationRecordBytes: ACCUMULATION_RECORD_BYTES
58
63
  });
59
64
  var wavefrontSceneObjectKinds = Object.freeze({
@@ -145,6 +150,9 @@ function subtract(a, b) {
145
150
  function scale(a, scalar) {
146
151
  return [a[0] * scalar, a[1] * scalar, a[2] * scalar];
147
152
  }
153
+ function dot(a, b) {
154
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
155
+ }
148
156
  function cross(a, b) {
149
157
  return [
150
158
  a[1] * b[2] - a[2] * b[1],
@@ -159,6 +167,25 @@ function normalize(value, fallback = [0, 0, 1]) {
159
167
  }
160
168
  return [value[0] / length, value[1] / length, value[2] / length];
161
169
  }
170
+ function hashUint32(value) {
171
+ let x = value >>> 0;
172
+ x = ((x >>> 16 ^ x) >>> 0) * 73244475 >>> 0;
173
+ x = ((x >>> 16 ^ x) >>> 0) * 73244475 >>> 0;
174
+ return (x >>> 16 ^ x) >>> 0;
175
+ }
176
+ function mixSeed(pixelId, sampleId, bounce, frameIndex, dimension) {
177
+ let x = (pixelId >>> 0) * 747796405 ^ (sampleId >>> 0) * 2891336453 ^ (bounce >>> 0) * 277803737 ^ (frameIndex >>> 0) * 1442695041 ^ (dimension >>> 0) * 1597334677;
178
+ x >>>= 0;
179
+ x ^= x >>> 16;
180
+ x = x * 2146121005 >>> 0;
181
+ x ^= x >>> 15;
182
+ x = x * 2221713035 >>> 0;
183
+ x ^= x >>> 16;
184
+ return x >>> 0;
185
+ }
186
+ function random01FromSeed(seed) {
187
+ return (hashUint32(seed) & 16777215) / 16777215;
188
+ }
162
189
  function getArrayLikeLength(value) {
163
190
  return Array.isArray(value) || ArrayBuffer.isView(value) ? value.length : 0;
164
191
  }
@@ -757,6 +784,150 @@ function resolveEnvironmentLighting(input, environmentColor, ambientColor) {
757
784
  exposure: Math.max(1e-4, readFiniteNumber("environmentLighting.exposure", source.exposure, DEFAULT_ENVIRONMENT_LIGHTING.exposure))
758
785
  });
759
786
  }
787
+ function evaluateReferenceEnvironmentRadiance(config, origin, direction) {
788
+ void origin;
789
+ const rayDirection = normalize(direction, [0, 1, 0]);
790
+ const upFactor = clamp(rayDirection[1] * 0.5 + 0.5, 0, 1);
791
+ const sunDirection = normalize(
792
+ config.environmentLighting?.sunDirection ?? DEFAULT_ENVIRONMENT_LIGHTING.sunDirection,
793
+ DEFAULT_ENVIRONMENT_LIGHTING.sunDirection
794
+ );
795
+ const sunGlow = Math.pow(clamp(dot(rayDirection, sunDirection), 0, 1), 192);
796
+ const horizonColor = config.environmentLighting?.horizonColor ?? DEFAULT_ENVIRONMENT_LIGHTING.horizonColor;
797
+ const zenithColor = config.environmentLighting?.zenithColor ?? DEFAULT_ENVIRONMENT_LIGHTING.zenithColor;
798
+ const sunColor = config.environmentLighting?.sunColor ?? DEFAULT_ENVIRONMENT_LIGHTING.sunColor;
799
+ const intensity = Math.max(
800
+ 1e-4,
801
+ Number(config.environmentLighting?.intensity ?? DEFAULT_ENVIRONMENT_LIGHTING.intensity)
802
+ );
803
+ return Object.freeze([
804
+ (horizonColor[0] * (1 - upFactor) + zenithColor[0] * upFactor + sunColor[0] * sunGlow) * intensity,
805
+ (horizonColor[1] * (1 - upFactor) + zenithColor[1] * upFactor + sunColor[1] * sunGlow) * intensity,
806
+ (horizonColor[2] * (1 - upFactor) + zenithColor[2] * upFactor + sunColor[2] * sunGlow) * intensity,
807
+ 1
808
+ ]);
809
+ }
810
+ function resolveEnvironmentPortalMode(value, hasPortals) {
811
+ if (value === void 0 || value === null) {
812
+ return hasPortals ? 2 : 0;
813
+ }
814
+ if (Number.isInteger(value) && value >= 0 && value <= 2) {
815
+ return value;
816
+ }
817
+ if (value === "disabled") {
818
+ return 0;
819
+ }
820
+ if (value === "guide") {
821
+ return 1;
822
+ }
823
+ if (value === "guide-and-gate" || value === "gate") {
824
+ return 2;
825
+ }
826
+ throw new Error(
827
+ "environmentPortalMode must be disabled, guide, guide-and-gate, or an integer between 0 and 2."
828
+ );
829
+ }
830
+ function orthogonalPortalTangent(normal) {
831
+ if (Math.abs(normal[1]) < 0.92) {
832
+ return normalize(cross([0, 1, 0], normal), [1, 0, 0]);
833
+ }
834
+ return normalize(cross([1, 0, 0], normal), [0, 0, 1]);
835
+ }
836
+ function resolvePortalTangent(value, normal) {
837
+ const fallback = orthogonalPortalTangent(normal);
838
+ const tangent = asUnitVec3(value, fallback);
839
+ const projected = subtract(tangent, scale(normal, dot(tangent, normal)));
840
+ return normalize(projected, fallback);
841
+ }
842
+ function readPositiveFiniteNumber(name, value, fallback) {
843
+ const numeric = readFiniteNumber(name, value, fallback);
844
+ if (numeric <= 0) {
845
+ throw new Error(`${name} must be a positive finite number.`);
846
+ }
847
+ return numeric;
848
+ }
849
+ function readPortalExtent(name, value, halfName, halfValue) {
850
+ if (value !== void 0 && value !== null) {
851
+ return readPositiveFiniteNumber(name, value, 1);
852
+ }
853
+ return readPositiveFiniteNumber(halfName, halfValue, 0.5) * 2;
854
+ }
855
+ function normalizeEnvironmentPortal(portal, index) {
856
+ if (!portal || typeof portal !== "object") {
857
+ throw new Error(`environmentPortals[${index}] must be an object.`);
858
+ }
859
+ const shape = portal.shape ?? portal.kind ?? "rectangle";
860
+ if (shape !== "rectangle") {
861
+ throw new Error(`environmentPortals[${index}].shape must be "rectangle".`);
862
+ }
863
+ const position = asVec3(portal.position ?? portal.center, [0, 0, 0]);
864
+ const normal = asUnitVec3(portal.normal, [0, 0, 1]);
865
+ const tangent = resolvePortalTangent(portal.tangent, normal);
866
+ const bitangent = normalize(cross(normal, tangent), [0, 1, 0]);
867
+ const width = readPortalExtent(
868
+ `environmentPortals[${index}].width`,
869
+ portal.width,
870
+ `environmentPortals[${index}].halfWidth`,
871
+ portal.halfWidth
872
+ );
873
+ const height = readPortalExtent(
874
+ `environmentPortals[${index}].height`,
875
+ portal.height,
876
+ `environmentPortals[${index}].halfHeight`,
877
+ portal.halfHeight
878
+ );
879
+ const radianceScale = Math.max(
880
+ 0,
881
+ readFiniteNumber(
882
+ `environmentPortals[${index}].radianceScale`,
883
+ portal.radianceScale ?? portal.intensity,
884
+ 1
885
+ )
886
+ );
887
+ return Object.freeze({
888
+ kind: 1,
889
+ flags: portal.twoSided === false ? 0 : 1,
890
+ position: Object.freeze([position[0], position[1], position[2], width * height]),
891
+ normal: Object.freeze([normal[0], normal[1], normal[2], radianceScale]),
892
+ tangent: Object.freeze([tangent[0], tangent[1], tangent[2], width * 0.5]),
893
+ bitangent: Object.freeze([bitangent[0], bitangent[1], bitangent[2], height * 0.5]),
894
+ color: Object.freeze(asColor(portal.color, [1, 1, 1, 1]))
895
+ });
896
+ }
897
+ function normalizeEnvironmentPortals(value) {
898
+ if (value === void 0 || value === null) {
899
+ return Object.freeze([]);
900
+ }
901
+ if (!Array.isArray(value)) {
902
+ throw new Error("environmentPortals must be an array when provided.");
903
+ }
904
+ return Object.freeze(value.map(normalizeEnvironmentPortal));
905
+ }
906
+ function packEnvironmentPortals(portals, capacity) {
907
+ const bytes = new ArrayBuffer(capacity * ENVIRONMENT_PORTAL_RECORD_BYTES);
908
+ const data = new DataView(bytes);
909
+ const floatView = new Float32Array(bytes);
910
+ portals.forEach((portal, index) => {
911
+ const byteOffset = index * ENVIRONMENT_PORTAL_RECORD_BYTES;
912
+ const floatOffset = byteOffset / Float32Array.BYTES_PER_ELEMENT;
913
+ data.setUint32(byteOffset, portal.kind, true);
914
+ data.setUint32(byteOffset + 4, portal.flags, true);
915
+ data.setUint32(byteOffset + 8, 0, true);
916
+ data.setUint32(byteOffset + 12, 0, true);
917
+ writeVec4(floatView, floatOffset + 4, portal.position);
918
+ writeVec4(floatView, floatOffset + 8, portal.normal);
919
+ writeVec4(floatView, floatOffset + 12, portal.tangent);
920
+ writeVec4(floatView, floatOffset + 16, portal.bitangent);
921
+ writeVec4(floatView, floatOffset + 20, portal.color);
922
+ });
923
+ return Object.freeze({
924
+ buffer: bytes,
925
+ portals,
926
+ count: portals.length,
927
+ capacity,
928
+ recordBytes: ENVIRONMENT_PORTAL_RECORD_BYTES
929
+ });
930
+ }
760
931
  function getCanvasDimension(canvas, key, fallback) {
761
932
  const value = Number(canvas?.[key]);
762
933
  if (Number.isFinite(value) && value > 0) {
@@ -812,6 +983,11 @@ function estimateWavefrontPathTracingMemory(options = {}) {
812
983
  options.emissiveTriangleCapacity,
813
984
  0
814
985
  );
986
+ const environmentPortalCapacity = readNonNegativeInteger(
987
+ "environmentPortalCapacity",
988
+ options.environmentPortalCapacity,
989
+ 0
990
+ );
815
991
  const queueBytes = tilePixelCapacity * RAY_RECORD_BYTES;
816
992
  const hitBytes = tilePixelCapacity * HIT_RECORD_BYTES;
817
993
  const accumulationBytes = tilePixelCapacity * ACCUMULATION_RECORD_BYTES;
@@ -820,6 +996,7 @@ function estimateWavefrontPathTracingMemory(options = {}) {
820
996
  const bvhNodeBytes = bvhNodeCapacity * BVH_NODE_RECORD_BYTES;
821
997
  const bvhLeafReferenceBytes = bvhLeafSortCapacity * BVH_LEAF_REF_RECORD_BYTES;
822
998
  const emissiveTriangleMetadataBytes = emissiveTriangleCapacity * BVH_NODE_RECORD_BYTES;
999
+ const environmentPortalBytes = environmentPortalCapacity * ENVIRONMENT_PORTAL_RECORD_BYTES;
823
1000
  return Object.freeze({
824
1001
  queueBytes,
825
1002
  queuePairBytes: queueBytes * 2,
@@ -830,9 +1007,10 @@ function estimateWavefrontPathTracingMemory(options = {}) {
830
1007
  bvhNodeBytes,
831
1008
  bvhLeafReferenceBytes,
832
1009
  emissiveTriangleMetadataBytes,
1010
+ environmentPortalBytes,
833
1011
  configBytes: CONFIG_BUFFER_BYTES,
834
1012
  counterBytes: COUNTER_BUFFER_BYTES,
835
- totalHotBufferBytes: queueBytes * 2 + hitBytes + accumulationBytes + sceneObjectBytes + triangleBytes + bvhNodeBytes + bvhLeafReferenceBytes + emissiveTriangleMetadataBytes + CONFIG_BUFFER_BYTES + COUNTER_BUFFER_BYTES
1013
+ totalHotBufferBytes: queueBytes * 2 + hitBytes + accumulationBytes + sceneObjectBytes + triangleBytes + bvhNodeBytes + bvhLeafReferenceBytes + emissiveTriangleMetadataBytes + environmentPortalBytes + CONFIG_BUFFER_BYTES + COUNTER_BUFFER_BYTES
836
1014
  });
837
1015
  }
838
1016
  function createWavefrontPathTracingComputeConfig(options = {}) {
@@ -893,6 +1071,21 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
893
1071
  environmentColor,
894
1072
  ambientColor
895
1073
  );
1074
+ const environmentPortals = normalizeEnvironmentPortals(
1075
+ options.environmentPortals ?? options.environmentLightPortals ?? options.environmentLighting?.environmentPortals
1076
+ );
1077
+ const environmentPortalCapacity = Math.max(
1078
+ environmentPortals.length,
1079
+ readNonNegativeInteger(
1080
+ "environmentPortalCapacity",
1081
+ options.environmentPortalCapacity,
1082
+ DEFAULT_ENVIRONMENT_PORTAL_CAPACITY
1083
+ )
1084
+ );
1085
+ const environmentPortalMode = resolveEnvironmentPortalMode(
1086
+ options.environmentPortalMode ?? options.portalMode ?? options.environmentLighting?.environmentPortalMode,
1087
+ environmentPortals.length > 0
1088
+ );
896
1089
  return Object.freeze({
897
1090
  width,
898
1091
  height,
@@ -921,6 +1114,10 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
921
1114
  environmentColor: environmentLighting.environmentColor,
922
1115
  ambientColor: environmentLighting.ambientColor,
923
1116
  environmentLighting,
1117
+ environmentPortals,
1118
+ environmentPortalCount: environmentPortals.length,
1119
+ environmentPortalCapacity,
1120
+ environmentPortalMode,
924
1121
  displayQuality: options.displayQuality === true,
925
1122
  requiresMeshBvhForDisplayQuality: true,
926
1123
  denoise: options.denoise !== false,
@@ -931,7 +1128,8 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
931
1128
  triangleCapacity,
932
1129
  bvhNodeCapacity,
933
1130
  bvhLeafSortCapacity,
934
- emissiveTriangleCapacity: emissiveTriangleIndices.capacity
1131
+ emissiveTriangleCapacity: emissiveTriangleIndices.capacity,
1132
+ environmentPortalCapacity
935
1133
  })
936
1134
  });
937
1135
  }
@@ -1115,6 +1313,10 @@ function createConfigPayload(config, tile, frameIndex, buildRange = {}) {
1115
1313
  data.setUint32(244, buildRange.count ?? 0, true);
1116
1314
  data.setUint32(248, buildRange.sortItemCount ?? 0, true);
1117
1315
  data.setUint32(252, config.emissiveTriangleCount ?? 0, true);
1316
+ data.setUint32(256, config.environmentPortalCount ?? 0, true);
1317
+ data.setUint32(260, config.environmentPortalMode ?? 0, true);
1318
+ data.setUint32(264, 0, true);
1319
+ data.setUint32(268, 0, true);
1118
1320
  return bytes;
1119
1321
  }
1120
1322
  function createTiles(width, height, tileSize) {
@@ -1133,6 +1335,209 @@ function createTiles(width, height, tileSize) {
1133
1335
  }
1134
1336
  return Object.freeze(tiles);
1135
1337
  }
1338
+ function normalizeReferenceTile(config, tileInput = {}) {
1339
+ const tileX = clamp(
1340
+ readNonNegativeInteger("tile.x", tileInput.x, 0),
1341
+ 0,
1342
+ Math.max(0, config.width - 1)
1343
+ );
1344
+ const tileY = clamp(
1345
+ readNonNegativeInteger("tile.y", tileInput.y, 0),
1346
+ 0,
1347
+ Math.max(0, config.height - 1)
1348
+ );
1349
+ const tileWidth = clamp(
1350
+ readPositiveInteger("tile.width", tileInput.width, config.width - tileX),
1351
+ 1,
1352
+ config.width - tileX
1353
+ );
1354
+ const tileHeight = clamp(
1355
+ readPositiveInteger("tile.height", tileInput.height, config.height - tileY),
1356
+ 1,
1357
+ config.height - tileY
1358
+ );
1359
+ return Object.freeze({
1360
+ x: tileX,
1361
+ y: tileY,
1362
+ width: tileWidth,
1363
+ height: tileHeight
1364
+ });
1365
+ }
1366
+ function repairReferenceShadingNormal(geometricNormal, shadingNormal) {
1367
+ const normal = normalize(shadingNormal, geometricNormal);
1368
+ return dot(normal, geometricNormal) < 0 ? scale(normal, -1) : normal;
1369
+ }
1370
+ function readOptionalMaxDistance(value) {
1371
+ if (value === void 0 || value === null) {
1372
+ return Number.POSITIVE_INFINITY;
1373
+ }
1374
+ const numeric = Number(value);
1375
+ if (!Number.isFinite(numeric) || numeric <= 0) {
1376
+ throw new Error("maxDistance must be a positive finite number when provided.");
1377
+ }
1378
+ return numeric;
1379
+ }
1380
+ function createWavefrontReferenceRay(config, options = {}) {
1381
+ if (!config || typeof config !== "object") {
1382
+ throw new Error("config must be a wavefront path tracing config.");
1383
+ }
1384
+ const tile = normalizeReferenceTile(config, options.tile);
1385
+ const tilePixelCount = tile.width * tile.height;
1386
+ const pixelIndex = readNonNegativeInteger("pixelIndex", options.pixelIndex, 0);
1387
+ if (pixelIndex >= tilePixelCount) {
1388
+ throw new Error(`pixelIndex ${pixelIndex} exceeds tile capacity ${tilePixelCount}.`);
1389
+ }
1390
+ const sampleIndex = readNonNegativeInteger("sampleIndex", options.sampleIndex, 0);
1391
+ const frameIndex = readNonNegativeInteger("frameIndex", options.frameIndex, config.frameIndex ?? 0);
1392
+ const jitterScale = clamp(readFiniteNumber("jitterScale", options.jitterScale, 0.35), 0, 1);
1393
+ const localX = pixelIndex % tile.width;
1394
+ const localY = Math.floor(pixelIndex / tile.width);
1395
+ const pixelX = tile.x + localX;
1396
+ const pixelY = tile.y + localY;
1397
+ const sourcePixelId = pixelY * config.width + pixelX;
1398
+ const jitterX = random01FromSeed(mixSeed(sourcePixelId, sampleIndex, 0, frameIndex, 1)) - 0.5;
1399
+ const jitterY = random01FromSeed(mixSeed(sourcePixelId, sampleIndex, 0, frameIndex, 2)) - 0.5;
1400
+ const ndcX = (pixelX + 0.5 + jitterX * jitterScale) / config.width * 2 - 1;
1401
+ const ndcY = 1 - (pixelY + 0.5 + jitterY * jitterScale) / config.height * 2;
1402
+ const viewX = ndcX * config.camera.tanHalfFovY * config.camera.aspect;
1403
+ const viewY = ndcY * config.camera.tanHalfFovY;
1404
+ const direction = normalize(
1405
+ add(
1406
+ add(config.camera.forward, scale(config.camera.right, viewX)),
1407
+ scale(config.camera.up, viewY)
1408
+ ),
1409
+ config.camera.forward
1410
+ );
1411
+ return Object.freeze({
1412
+ rayId: pixelIndex,
1413
+ parentRayId: 4294967295,
1414
+ sourcePixelId,
1415
+ sampleId: sampleIndex,
1416
+ bounce: 0,
1417
+ mediumRefId: 0,
1418
+ flags: 0,
1419
+ origin: Object.freeze([...config.camera.position]),
1420
+ direction: Object.freeze(direction),
1421
+ throughput: Object.freeze([1, 1, 1, 1]),
1422
+ pixelX,
1423
+ pixelY
1424
+ });
1425
+ }
1426
+ function intersectWavefrontReferenceTriangle(ray, triangle, options = {}) {
1427
+ if (!ray || typeof ray !== "object") {
1428
+ throw new Error("ray must be a wavefront reference ray.");
1429
+ }
1430
+ if (!triangle || typeof triangle !== "object") {
1431
+ throw new Error("triangle must be a wavefront triangle record.");
1432
+ }
1433
+ const maxDistance = readOptionalMaxDistance(options.maxDistance);
1434
+ const triangleIndex = readNonNegativeInteger("triangleIndex", options.triangleIndex, 0);
1435
+ const edge1 = subtract(triangle.v1, triangle.v0);
1436
+ const edge2 = subtract(triangle.v2, triangle.v0);
1437
+ const pvec = cross(ray.direction, edge2);
1438
+ const determinant = dot(edge1, pvec);
1439
+ if (Math.abs(determinant) < 1e-7) {
1440
+ return null;
1441
+ }
1442
+ const invDet = 1 / determinant;
1443
+ const tvec = subtract(ray.origin, triangle.v0);
1444
+ const u = dot(tvec, pvec) * invDet;
1445
+ if (u < 0 || u > 1) {
1446
+ return null;
1447
+ }
1448
+ const qvec = cross(tvec, edge1);
1449
+ const v = dot(ray.direction, qvec) * invDet;
1450
+ if (v < 0 || u + v > 1) {
1451
+ return null;
1452
+ }
1453
+ const distance = dot(edge2, qvec) * invDet;
1454
+ if (distance <= 1e-3 || distance > maxDistance) {
1455
+ return null;
1456
+ }
1457
+ const geometric = normalize(cross(edge1, edge2), [0, 1, 0]);
1458
+ const frontFace = dot(ray.direction, geometric) < 0;
1459
+ const orientedGeometric = frontFace ? geometric : scale(geometric, -1);
1460
+ const w = 1 - u - v;
1461
+ const interpolated = [
1462
+ triangle.n0[0] * w + triangle.n1[0] * u + triangle.n2[0] * v,
1463
+ triangle.n0[1] * w + triangle.n1[1] * u + triangle.n2[1] * v,
1464
+ triangle.n0[2] * w + triangle.n1[2] * u + triangle.n2[2] * v
1465
+ ];
1466
+ const shadingNormal = repairReferenceShadingNormal(orientedGeometric, interpolated);
1467
+ const uv = [
1468
+ triangle.uv0[0] * w + triangle.uv1[0] * u + triangle.uv2[0] * v,
1469
+ triangle.uv0[1] * w + triangle.uv1[1] * u + triangle.uv2[1] * v
1470
+ ];
1471
+ const position = add(ray.origin, scale(ray.direction, distance));
1472
+ return Object.freeze({
1473
+ hitType: "surface",
1474
+ rayId: ray.rayId,
1475
+ sourcePixelId: ray.sourcePixelId,
1476
+ distance,
1477
+ entityId: triangle.meshId,
1478
+ instanceId: 0,
1479
+ primitiveId: triangle.triangleId,
1480
+ materialId: triangle.materialKind,
1481
+ materialRefId: triangle.materialRefId,
1482
+ mediumRefId: triangle.mediumRefId,
1483
+ barycentrics: Object.freeze([w, u, v]),
1484
+ uv: Object.freeze(uv),
1485
+ geometricNormal: Object.freeze(orientedGeometric),
1486
+ shadingNormal: Object.freeze(shadingNormal),
1487
+ frontFace,
1488
+ triangleIndex,
1489
+ triangleId: triangle.triangleId,
1490
+ position: Object.freeze(position),
1491
+ color: triangle.color,
1492
+ emission: triangle.emission,
1493
+ material: triangle.material
1494
+ });
1495
+ }
1496
+ function createWavefrontReferenceEnvironmentHit(config, ray) {
1497
+ const radiance = evaluateReferenceEnvironmentRadiance(config, ray.origin, ray.direction);
1498
+ return Object.freeze({
1499
+ hitType: "environment",
1500
+ rayId: ray.rayId,
1501
+ sourcePixelId: ray.sourcePixelId,
1502
+ distance: -1,
1503
+ entityId: 0,
1504
+ instanceId: 0,
1505
+ primitiveId: 0,
1506
+ materialId: 0,
1507
+ materialRefId: 0,
1508
+ mediumRefId: 0,
1509
+ barycentrics: Object.freeze([0, 0, 0]),
1510
+ uv: Object.freeze([0, 0]),
1511
+ geometricNormal: Object.freeze(scale(ray.direction, -1)),
1512
+ shadingNormal: Object.freeze(scale(ray.direction, -1)),
1513
+ frontFace: true,
1514
+ triangleIndex: -1,
1515
+ triangleId: -1,
1516
+ position: Object.freeze(add(ray.origin, scale(ray.direction, 1e3))),
1517
+ color: Object.freeze([0, 0, 0, 0]),
1518
+ emission: radiance,
1519
+ material: Object.freeze([1, 0, 1, 1])
1520
+ });
1521
+ }
1522
+ function traceWavefrontReferenceTriangles(config, ray, triangles, options = {}) {
1523
+ if (!config || typeof config !== "object") {
1524
+ throw new Error("config must be a wavefront path tracing config.");
1525
+ }
1526
+ const source = Array.isArray(triangles) ? triangles : [];
1527
+ let nearestHit = null;
1528
+ let nearestDistance = readOptionalMaxDistance(options.maxDistance);
1529
+ source.forEach((triangle, index) => {
1530
+ const hit = intersectWavefrontReferenceTriangle(ray, triangle, {
1531
+ maxDistance: Number.isFinite(nearestDistance) ? nearestDistance : void 0,
1532
+ triangleIndex: index
1533
+ });
1534
+ if (hit && hit.distance < nearestDistance) {
1535
+ nearestDistance = hit.distance;
1536
+ nearestHit = hit;
1537
+ }
1538
+ });
1539
+ return nearestHit ?? createWavefrontReferenceEnvironmentHit(config, ray);
1540
+ }
1136
1541
  function clampTileSizeForDevice(config, device) {
1137
1542
  const limit = Number(device?.limits?.maxStorageBufferBindingSize);
1138
1543
  if (!Number.isFinite(limit) || limit <= 0) {
@@ -1361,6 +1766,10 @@ struct FrameConfig {
1361
1766
  bvhBuildNodeCount: u32,
1362
1767
  bvhSortItemCount: u32,
1363
1768
  emissiveTriangleCount: u32,
1769
+ environmentPortalCount: u32,
1770
+ environmentPortalMode: u32,
1771
+ _portalPad0: u32,
1772
+ _portalPad1: u32,
1364
1773
  };
1365
1774
 
1366
1775
  struct Counters {
@@ -1384,6 +1793,18 @@ struct Candidate {
1384
1793
  mediumRefId: u32,
1385
1794
  };
1386
1795
 
1796
+ struct EnvironmentPortal {
1797
+ kind: u32,
1798
+ flags: u32,
1799
+ _pad0: u32,
1800
+ _pad1: u32,
1801
+ position: vec4<f32>,
1802
+ normal: vec4<f32>,
1803
+ tangent: vec4<f32>,
1804
+ bitangent: vec4<f32>,
1805
+ color: vec4<f32>,
1806
+ };
1807
+
1387
1808
  @group(0) @binding(0) var<storage, read_write> activeQueue: array<RayRecord>;
1388
1809
  @group(0) @binding(1) var<storage, read_write> nextQueue: array<RayRecord>;
1389
1810
  @group(0) @binding(2) var<storage, read_write> hits: array<HitRecord>;
@@ -1403,6 +1824,7 @@ struct Candidate {
1403
1824
  @group(0) @binding(16) var radianceImage: texture_storage_2d<rgba16float, write>;
1404
1825
  @group(0) @binding(17) var finalDenoiseInputRadiance: texture_2d<f32>;
1405
1826
  @group(0) @binding(18) var denoisedOutputImage: texture_storage_2d<rgba8unorm, write>;
1827
+ @group(0) @binding(19) var<storage, read> environmentPortals: array<EnvironmentPortal>;
1406
1828
 
1407
1829
  fn hash_u32(value: u32) -> u32 {
1408
1830
  var x = value;
@@ -1443,7 +1865,48 @@ fn saturate(value: f32) -> f32 {
1443
1865
  return clamp(value, 0.0, 1.0);
1444
1866
  }
1445
1867
 
1446
- fn environment_radiance(direction: vec3<f32>) -> vec3<f32> {
1868
+ fn max_component(value: vec3<f32>) -> f32 {
1869
+ return max(max(value.x, value.y), value.z);
1870
+ }
1871
+
1872
+ fn environment_portal_radiance_scale(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
1873
+ if (config.environmentPortalCount == 0u || config.environmentPortalMode == 0u) {
1874
+ return vec3<f32>(1.0);
1875
+ }
1876
+ var scale = vec3<f32>(0.0);
1877
+ for (var portalIndex = 0u; portalIndex < config.environmentPortalCount; portalIndex = portalIndex + 1u) {
1878
+ let portal = environmentPortals[portalIndex];
1879
+ if (portal.kind == 1u) {
1880
+ let portalNormal = safe_normalize(portal.normal.xyz, vec3<f32>(0.0, 0.0, 1.0));
1881
+ let denominator = dot(direction, portalNormal);
1882
+ let twoSided = (portal.flags & 1u) != 0u;
1883
+ var facing = abs(denominator) > 0.0001;
1884
+ if (!twoSided && denominator <= 0.0001) {
1885
+ facing = false;
1886
+ }
1887
+ if (facing) {
1888
+ let distance = dot(portal.position.xyz - origin, portalNormal) / denominator;
1889
+ if (distance > 0.001) {
1890
+ let hitPosition = origin + direction * distance;
1891
+ let local = hitPosition - portal.position.xyz;
1892
+ let tangent = safe_normalize(portal.tangent.xyz, vec3<f32>(1.0, 0.0, 0.0));
1893
+ let bitangent = safe_normalize(portal.bitangent.xyz, vec3<f32>(0.0, 1.0, 0.0));
1894
+ let u = dot(local, tangent);
1895
+ let v = dot(local, bitangent);
1896
+ if (abs(u) <= portal.tangent.w && abs(v) <= portal.bitangent.w) {
1897
+ let areaWeight = clamp(sqrt(max(portal.position.w, 0.0001)), 0.25, 4.0);
1898
+ let angleWeight = max(abs(denominator), 0.08);
1899
+ let portalScale = portal.color.rgb * portal.normal.w * portal.color.a * areaWeight * angleWeight;
1900
+ scale = max(scale, portalScale);
1901
+ }
1902
+ }
1903
+ }
1904
+ }
1905
+ }
1906
+ return scale;
1907
+ }
1908
+
1909
+ fn environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
1447
1910
  let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
1448
1911
  let upFactor = saturate(rayDirection.y * 0.5 + 0.5);
1449
1912
  let sunDirection = safe_normalize(
@@ -1454,10 +1917,26 @@ fn environment_radiance(direction: vec3<f32>) -> vec3<f32> {
1454
1917
  let gradient =
1455
1918
  config.environmentHorizonColor.xyz * (1.0 - upFactor) +
1456
1919
  config.environmentZenithColor.xyz * upFactor;
1920
+ let portalScale = environment_portal_radiance_scale(origin, rayDirection);
1921
+ let portalHit = max_component(portalScale) > 0.0001;
1457
1922
  return (
1458
1923
  gradient +
1459
1924
  config.environmentSunColor.xyz * sunGlow
1460
- ) * max(config.environmentSunDirectionIntensity.w, 0.0001);
1925
+ ) *
1926
+ max(config.environmentSunDirectionIntensity.w, 0.0001) *
1927
+ select(vec3<f32>(1.0), portalScale, portalHit);
1928
+ }
1929
+
1930
+ fn gated_environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
1931
+ let portalScale = environment_portal_radiance_scale(origin, safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0)));
1932
+ if (
1933
+ config.environmentPortalCount > 0u &&
1934
+ config.environmentPortalMode == 2u &&
1935
+ max_component(portalScale) <= 0.0001
1936
+ ) {
1937
+ return config.ambientColor.xyz * 0.65;
1938
+ }
1939
+ return environment_radiance(origin, direction);
1461
1940
  }
1462
1941
 
1463
1942
  fn default_mesh_range() -> MeshRange {
@@ -1728,7 +2207,7 @@ fn make_ray(pixelIndex: u32) -> RayRecord {
1728
2207
  }
1729
2208
 
1730
2209
  fn make_miss(ray: RayRecord) -> HitRecord {
1731
- let radiance = environment_radiance(ray.direction.xyz);
2210
+ let radiance = gated_environment_radiance(ray.origin.xyz, ray.direction.xyz);
1732
2211
  return HitRecord(
1733
2212
  ray.rayId,
1734
2213
  ray.sourcePixelId,
@@ -2214,6 +2693,21 @@ fn sample_emissive_triangle_direction(hit: HitRecord, seed: u32, fallback: vec3<
2214
2693
  return safe_normalize(lightPoint - hit.position.xyz, fallback);
2215
2694
  }
2216
2695
 
2696
+ fn sample_environment_portal_direction(hit: HitRecord, seed: u32, fallback: vec3<f32>) -> vec3<f32> {
2697
+ if (config.environmentPortalCount == 0u || config.environmentPortalMode == 0u) {
2698
+ return fallback;
2699
+ }
2700
+ let portalSlot = min(
2701
+ u32(random01(seed + 211u) * f32(config.environmentPortalCount)),
2702
+ config.environmentPortalCount - 1u
2703
+ );
2704
+ let portal = environmentPortals[portalSlot];
2705
+ let u = (random01(seed + 223u) * 2.0 - 1.0) * portal.tangent.w;
2706
+ let v = (random01(seed + 227u) * 2.0 - 1.0) * portal.bitangent.w;
2707
+ let portalTarget = portal.position.xyz + portal.tangent.xyz * u + portal.bitangent.xyz * v;
2708
+ return safe_normalize(portalTarget - hit.position.xyz, fallback);
2709
+ }
2710
+
2217
2711
  fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult {
2218
2712
  let roughness = clamp(hit.material.x, 0.0, 1.0);
2219
2713
  if (hit.materialKind == 1u) {
@@ -2253,8 +2747,17 @@ fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult
2253
2747
  let canSampleLight = dot(hit.shadingNormal.xyz, guidedLight) > -0.04;
2254
2748
  let guideProbability = select(0.38, 0.72, ray.bounce == 0u);
2255
2749
  let useGuidedLight = canSampleLight && random01(seed + 37u) < guideProbability;
2750
+ let guidedPortal = sample_environment_portal_direction(hit, seed, randomDiffuse);
2751
+ let canSamplePortal = dot(hit.shadingNormal.xyz, guidedPortal) > -0.04;
2752
+ let useGuidedPortal =
2753
+ !useGuidedLight &&
2754
+ canSamplePortal &&
2755
+ config.environmentPortalCount > 0u &&
2756
+ config.environmentPortalMode > 0u &&
2757
+ random01(seed + 89u) < 0.58;
2758
+ let guidedDirection = select(randomDiffuse, guidedPortal, useGuidedPortal);
2256
2759
  return ScatterResult(
2257
- vec4<f32>(select(randomDiffuse, guidedLight, useGuidedLight), 0.0),
2760
+ vec4<f32>(select(guidedDirection, guidedLight, useGuidedLight), 0.0),
2258
2761
  select(0u, RAY_FLAG_GUIDED_EMISSIVE, useGuidedLight),
2259
2762
  0u,
2260
2763
  0u,
@@ -2461,6 +2964,29 @@ fn fragmentMain(in: VertexOut) -> @location(0) vec4<f32> {
2461
2964
  return textureSample(renderTexture, renderSampler, in.uv);
2462
2965
  }
2463
2966
  `;
2967
+ function createWavefrontDeviceDescriptor(adapter, options = {}) {
2968
+ const requiredLimits = { ...options.requiredLimits ?? {} };
2969
+ const exposedStorageBufferLimit = Number(adapter?.limits?.maxStorageBuffersPerShaderStage);
2970
+ if (Number.isFinite(exposedStorageBufferLimit)) {
2971
+ if (exposedStorageBufferLimit < TRACE_STORAGE_BUFFER_BINDINGS) {
2972
+ throw new Error(
2973
+ `Wavefront mesh tracing requires maxStorageBuffersPerShaderStage>=${TRACE_STORAGE_BUFFER_BINDINGS}, but this adapter exposes ${exposedStorageBufferLimit}.`
2974
+ );
2975
+ }
2976
+ requiredLimits.maxStorageBuffersPerShaderStage = Math.max(
2977
+ Number(requiredLimits.maxStorageBuffersPerShaderStage ?? 0),
2978
+ TRACE_STORAGE_BUFFER_BINDINGS
2979
+ );
2980
+ }
2981
+ const descriptor = { ...options.deviceDescriptor ?? {} };
2982
+ if (Object.keys(requiredLimits).length > 0) {
2983
+ descriptor.requiredLimits = {
2984
+ ...descriptor.requiredLimits ?? {},
2985
+ ...requiredLimits
2986
+ };
2987
+ }
2988
+ return Object.keys(descriptor).length > 0 ? descriptor : void 0;
2989
+ }
2464
2990
  async function createWavefrontPathTracingComputeRenderer(options = {}) {
2465
2991
  assertAnalyticDisplayQualityPolicy(options);
2466
2992
  const constants = getGpuUsageConstants();
@@ -2479,7 +3005,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
2479
3005
  if (!adapter) {
2480
3006
  throw new Error("Unable to acquire a WebGPU adapter for wavefront path tracing.");
2481
3007
  }
2482
- const device = await adapter.requestDevice();
3008
+ const device = await adapter.requestDevice(createWavefrontDeviceDescriptor(adapter, options));
2483
3009
  const context = canvas.getContext("webgpu");
2484
3010
  if (!context || typeof context.configure !== "function") {
2485
3011
  throw new Error("Canvas WebGPU context does not support configure().");
@@ -2552,23 +3078,34 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
2552
3078
  Math.max(1, config.gpuMeshSource.meshes.count) * MESH_RANGE_RECORD_BYTES,
2553
3079
  "plasius.wavefront.meshRanges"
2554
3080
  );
3081
+ const environmentPortalBuffer = createBuffer(
3082
+ device,
3083
+ constants.buffer.STORAGE | constants.buffer.COPY_DST,
3084
+ Math.max(1, config.environmentPortalCapacity) * ENVIRONMENT_PORTAL_RECORD_BYTES,
3085
+ "plasius.wavefront.environmentPortals"
3086
+ );
2555
3087
  const bvhLeafRefBuffer = createBuffer(
2556
3088
  device,
2557
3089
  constants.buffer.STORAGE | constants.buffer.COPY_DST,
2558
3090
  Math.max(1, config.bvhLeafSortCapacity) * BVH_LEAF_REF_RECORD_BYTES,
2559
3091
  "plasius.wavefront.bvhLeafRefs"
2560
3092
  );
2561
- const configBuffer = createBuffer(
2562
- device,
2563
- constants.buffer.UNIFORM | constants.buffer.COPY_DST,
2564
- CONFIG_BUFFER_BYTES,
2565
- "plasius.wavefront.frameConfig"
2566
- );
3093
+ const tiles = createTiles(config.width, config.height, config.tileSize);
2567
3094
  const uniformOffsetAlignment = Number(device?.limits?.minUniformBufferOffsetAlignment);
2568
3095
  const configBufferStride = alignTo(
2569
3096
  CONFIG_BUFFER_BYTES,
2570
3097
  Number.isFinite(uniformOffsetAlignment) && uniformOffsetAlignment > 0 ? uniformOffsetAlignment : CONFIG_BUFFER_BYTES
2571
3098
  );
3099
+ const frameConfigSlotCount = Math.max(
3100
+ 1,
3101
+ tiles.length * config.samplesPerPixel + tiles.length + (config.denoise ? 1 : 0)
3102
+ );
3103
+ const configBuffer = createBuffer(
3104
+ device,
3105
+ constants.buffer.UNIFORM | constants.buffer.COPY_DST,
3106
+ frameConfigSlotCount * configBufferStride,
3107
+ "plasius.wavefront.frameConfig"
3108
+ );
2572
3109
  const bvhBuildConfigSlots = 1 + config.bvhSortStages.length + config.bvhBuildLevels.length;
2573
3110
  const bvhBuildConfigBuffer = createBuffer(
2574
3111
  device,
@@ -2602,6 +3139,11 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
2602
3139
  device.queue.writeBuffer(meshVertexBuffer, 0, config.gpuMeshSource.vertices.buffer);
2603
3140
  device.queue.writeBuffer(meshIndexBuffer, 0, config.gpuMeshSource.indices.buffer);
2604
3141
  device.queue.writeBuffer(meshRangeBuffer, 0, config.gpuMeshSource.meshes.buffer);
3142
+ const packedEnvironmentPortals = packEnvironmentPortals(
3143
+ config.environmentPortals,
3144
+ Math.max(1, config.environmentPortalCapacity)
3145
+ );
3146
+ device.queue.writeBuffer(environmentPortalBuffer, 0, packedEnvironmentPortals.buffer);
2605
3147
  const radianceTexture = device.createTexture({
2606
3148
  label: "plasius.wavefront.radiance",
2607
3149
  size: { width: config.width, height: config.height },
@@ -2653,7 +3195,8 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
2653
3195
  binding: 16,
2654
3196
  visibility: constants.shader.COMPUTE,
2655
3197
  storageTexture: { access: "write-only", format: "rgba16float" }
2656
- }
3198
+ },
3199
+ { binding: 19, visibility: constants.shader.COMPUTE, buffer: { type: "read-only-storage" } }
2657
3200
  ]
2658
3201
  });
2659
3202
  const accelerationBindGroupLayout = device.createBindGroupLayout({
@@ -2826,7 +3369,8 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
2826
3369
  { binding: 7, resource: outputView },
2827
3370
  { binding: 8, resource: { buffer: triangleBuffer } },
2828
3371
  { binding: 9, resource: { buffer: bvhNodeBuffer } },
2829
- { binding: 16, resource: radianceView }
3372
+ { binding: 16, resource: radianceView },
3373
+ { binding: 19, resource: { buffer: environmentPortalBuffer } }
2830
3374
  ]
2831
3375
  });
2832
3376
  }
@@ -2913,10 +3457,27 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
2913
3457
  ]
2914
3458
  });
2915
3459
  let frame = 0;
2916
- const tiles = createTiles(config.width, config.height, config.tileSize);
3460
+ let accelerationBuilt = !config.gpuAccelerationBuildRequired;
3461
+ let accelerationBuildCount = 0;
3462
+ function createFrameConfigWriter(frameIndex) {
3463
+ let slot = 0;
3464
+ return (tile, buildRange = {}) => {
3465
+ if (slot >= frameConfigSlotCount) {
3466
+ throw new Error("Wavefront frame config slot capacity exceeded.");
3467
+ }
3468
+ const offset = slot * configBufferStride;
3469
+ slot += 1;
3470
+ device.queue.writeBuffer(
3471
+ configBuffer,
3472
+ offset,
3473
+ createConfigPayload(config, tile, frameIndex, buildRange)
3474
+ );
3475
+ return offset;
3476
+ };
3477
+ }
2917
3478
  function dispatchGpuAccelerationBuild(frameIndex) {
2918
- if (!config.gpuAccelerationBuildRequired) {
2919
- return;
3479
+ if (!config.gpuAccelerationBuildRequired || accelerationBuilt) {
3480
+ return false;
2920
3481
  }
2921
3482
  const buildTile = tiles[0] ?? { x: 0, y: 0, width: 1, height: 1 };
2922
3483
  const encoder = device.createCommandEncoder({
@@ -2974,27 +3535,21 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
2974
3535
  }
2975
3536
  passEncoder.end();
2976
3537
  device.queue.submit([encoder.finish()]);
3538
+ accelerationBuilt = true;
3539
+ accelerationBuildCount += 1;
3540
+ return true;
2977
3541
  }
2978
- function dispatchTileSample(tile, frameIndex, sampleIndex) {
2979
- const sampleWeight = 1 / config.samplesPerPixel;
2980
- const configPayload = createConfigPayload(config, tile, frameIndex, {
2981
- sampleIndex,
2982
- sampleWeight
2983
- });
2984
- device.queue.writeBuffer(configBuffer, 0, configPayload);
2985
- const encoder = device.createCommandEncoder({
2986
- label: `plasius.wavefront.frame.${frameIndex}.tile.${tile.x}.${tile.y}.sample.${sampleIndex}`
2987
- });
3542
+ function encodeTileSample(encoder, tile, configOffset) {
2988
3543
  const passEncoder = encoder.beginComputePass({
2989
3544
  label: "plasius.wavefront.computePass"
2990
3545
  });
2991
3546
  const tileWorkgroups = Math.ceil(tile.width * tile.height / WORKGROUP_SIZE);
2992
3547
  const capacityWorkgroups = Math.ceil(config.tilePixelCapacity / WORKGROUP_SIZE);
2993
- passEncoder.setBindGroup(0, bindGroups[0], [0]);
3548
+ passEncoder.setBindGroup(0, bindGroups[0], [configOffset]);
2994
3549
  passEncoder.setPipeline(pipelines.generatePrimaryRays);
2995
3550
  passEncoder.dispatchWorkgroups(tileWorkgroups);
2996
3551
  for (let bounceIndex = 0; bounceIndex < config.maxDepth; bounceIndex += 1) {
2997
- passEncoder.setBindGroup(0, bindGroups[bounceIndex % 2], [0]);
3552
+ passEncoder.setBindGroup(0, bindGroups[bounceIndex % 2], [configOffset]);
2998
3553
  passEncoder.setPipeline(pipelines.intersectActiveQueue);
2999
3554
  passEncoder.dispatchWorkgroups(capacityWorkgroups);
3000
3555
  passEncoder.setPipeline(pipelines.resolveSurfaceRecords);
@@ -3003,71 +3558,38 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3003
3558
  passEncoder.dispatchWorkgroups(1);
3004
3559
  }
3005
3560
  passEncoder.end();
3006
- device.queue.submit([encoder.finish()]);
3007
3561
  }
3008
- function dispatchTileOutput(tile, frameIndex) {
3009
- const configPayload = createConfigPayload(config, tile, frameIndex, {
3010
- sampleIndex: 0,
3011
- sampleWeight: 1 / config.samplesPerPixel
3012
- });
3013
- device.queue.writeBuffer(configBuffer, 0, configPayload);
3014
- const encoder = device.createCommandEncoder({
3015
- label: `plasius.wavefront.frame.${frameIndex}.tile.${tile.x}.${tile.y}.output`
3016
- });
3562
+ function encodeTileOutput(encoder, tile, configOffset) {
3017
3563
  const passEncoder = encoder.beginComputePass({
3018
3564
  label: "plasius.wavefront.outputPass"
3019
3565
  });
3020
3566
  const tileWorkgroups = Math.ceil(tile.width * tile.height / WORKGROUP_SIZE);
3021
- passEncoder.setBindGroup(0, bindGroups[0], [0]);
3567
+ passEncoder.setBindGroup(0, bindGroups[0], [configOffset]);
3022
3568
  passEncoder.setPipeline(pipelines.accumulateTerminalRadiance);
3023
3569
  passEncoder.dispatchWorkgroups(tileWorkgroups);
3024
3570
  passEncoder.end();
3025
- device.queue.submit([encoder.finish()]);
3026
- }
3027
- function dispatchTile(tile, frameIndex) {
3028
- for (let sampleIndex = 0; sampleIndex < config.samplesPerPixel; sampleIndex += 1) {
3029
- dispatchTileSample(tile, frameIndex, sampleIndex);
3030
- }
3031
- dispatchTileOutput(tile, frameIndex);
3032
3571
  }
3033
- function dispatchDenoise(frameIndex) {
3572
+ function encodeDenoise(encoder, configOffset) {
3034
3573
  if (!config.denoise) {
3035
3574
  return;
3036
3575
  }
3037
- device.queue.writeBuffer(
3038
- configBuffer,
3039
- 0,
3040
- createConfigPayload(
3041
- config,
3042
- { x: 0, y: 0, width: config.width, height: config.height },
3043
- frameIndex,
3044
- { sampleIndex: 0, sampleWeight: 1 / config.samplesPerPixel }
3045
- )
3046
- );
3047
- const encoder = device.createCommandEncoder({
3048
- label: `plasius.wavefront.frame.${frameIndex}.denoise`
3049
- });
3050
3576
  const radiancePass = encoder.beginComputePass({
3051
3577
  label: "plasius.wavefront.denoiseRadiancePass"
3052
3578
  });
3053
- radiancePass.setBindGroup(0, denoiseRadianceBindGroup, [0]);
3579
+ radiancePass.setBindGroup(0, denoiseRadianceBindGroup, [configOffset]);
3054
3580
  radiancePass.setPipeline(pipelines.denoiseLinearRadiance);
3055
3581
  radiancePass.dispatchWorkgroups(Math.ceil(config.width / 8), Math.ceil(config.height / 8));
3056
3582
  radiancePass.end();
3057
3583
  const resolvePass = encoder.beginComputePass({
3058
3584
  label: "plasius.wavefront.denoiseResolvePass"
3059
3585
  });
3060
- resolvePass.setBindGroup(0, denoiseResolveBindGroup, [0]);
3586
+ resolvePass.setBindGroup(0, denoiseResolveBindGroup, [configOffset]);
3061
3587
  resolvePass.setPipeline(pipelines.resolveDenoisedOutputImage);
3062
3588
  resolvePass.dispatchWorkgroups(Math.ceil(config.width / 8), Math.ceil(config.height / 8));
3063
3589
  resolvePass.end();
3064
- device.queue.submit([encoder.finish()]);
3065
3590
  }
3066
- function present() {
3591
+ function encodePresent(encoder) {
3067
3592
  const texture = context.getCurrentTexture();
3068
- const encoder = device.createCommandEncoder({
3069
- label: `plasius.wavefront.present.${frame}`
3070
- });
3071
3593
  const passEncoder = encoder.beginRenderPass({
3072
3594
  label: "plasius.wavefront.presentPass",
3073
3595
  colorAttachments: [
@@ -3083,16 +3605,42 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3083
3605
  passEncoder.setBindGroup(0, presentBindGroup);
3084
3606
  passEncoder.draw(3);
3085
3607
  passEncoder.end();
3608
+ }
3609
+ function dispatchFrame(frameIndex) {
3610
+ const writeFrameConfig = createFrameConfigWriter(frameIndex);
3611
+ const encoder = device.createCommandEncoder({
3612
+ label: `plasius.wavefront.frame.${frameIndex}.batched`
3613
+ });
3614
+ for (const tile of tiles) {
3615
+ for (let sampleIndex = 0; sampleIndex < config.samplesPerPixel; sampleIndex += 1) {
3616
+ const configOffset = writeFrameConfig(tile, {
3617
+ sampleIndex,
3618
+ sampleWeight: 1 / config.samplesPerPixel
3619
+ });
3620
+ encodeTileSample(encoder, tile, configOffset);
3621
+ }
3622
+ const outputConfigOffset = writeFrameConfig(tile, {
3623
+ sampleIndex: 0,
3624
+ sampleWeight: 1 / config.samplesPerPixel
3625
+ });
3626
+ encodeTileOutput(encoder, tile, outputConfigOffset);
3627
+ }
3628
+ if (config.denoise) {
3629
+ const denoiseConfigOffset = writeFrameConfig(
3630
+ { x: 0, y: 0, width: config.width, height: config.height },
3631
+ { sampleIndex: 0, sampleWeight: 1 / config.samplesPerPixel }
3632
+ );
3633
+ encodeDenoise(encoder, denoiseConfigOffset);
3634
+ }
3635
+ encodePresent(encoder);
3086
3636
  device.queue.submit([encoder.finish()]);
3637
+ return 1;
3087
3638
  }
3088
3639
  function renderOnce() {
3089
3640
  frame += 1;
3090
- dispatchGpuAccelerationBuild(frame + config.frameIndex);
3091
- for (const tile of tiles) {
3092
- dispatchTile(tile, frame + config.frameIndex);
3093
- }
3094
- dispatchDenoise(frame + config.frameIndex);
3095
- present();
3641
+ const frameIndex = frame + config.frameIndex;
3642
+ const accelerationBuildSubmitted = dispatchGpuAccelerationBuild(frameIndex);
3643
+ const frameSubmissionCount = dispatchFrame(frameIndex);
3096
3644
  return Object.freeze({
3097
3645
  frame,
3098
3646
  width: config.width,
@@ -3106,10 +3654,17 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3106
3654
  sceneObjectCount: config.sceneObjectCount,
3107
3655
  triangleCount: config.triangleCount,
3108
3656
  emissiveTriangleCount: config.emissiveTriangleCount,
3657
+ environmentPortalCount: config.environmentPortalCount,
3658
+ environmentPortalMode: config.environmentPortalMode,
3109
3659
  bvhNodeCount: config.bvhNodeCount,
3110
3660
  displayQuality: config.displayQuality,
3111
3661
  accelerationBuildMode: config.accelerationBuildMode,
3112
3662
  gpuAccelerationBuildRequired: config.gpuAccelerationBuildRequired,
3663
+ accelerationBuildSubmitted,
3664
+ accelerationBuilt,
3665
+ accelerationBuildCount,
3666
+ commandSubmissions: frameSubmissionCount + (accelerationBuildSubmitted ? 1 : 0),
3667
+ frameConfigSlots: frameConfigSlotCount,
3113
3668
  memory: config.memory
3114
3669
  });
3115
3670
  }
@@ -3175,10 +3730,15 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3175
3730
  sceneObjectCount: config.sceneObjectCount,
3176
3731
  triangleCount: config.triangleCount,
3177
3732
  emissiveTriangleCount: config.emissiveTriangleCount,
3733
+ environmentPortalCount: config.environmentPortalCount,
3734
+ environmentPortalMode: config.environmentPortalMode,
3178
3735
  bvhNodeCount: config.bvhNodeCount,
3179
3736
  displayQuality: config.displayQuality,
3180
3737
  accelerationBuildMode: config.accelerationBuildMode,
3181
3738
  gpuAccelerationBuildRequired: config.gpuAccelerationBuildRequired,
3739
+ accelerationBuilt,
3740
+ accelerationBuildCount,
3741
+ frameConfigSlots: frameConfigSlotCount,
3182
3742
  memory: config.memory
3183
3743
  });
3184
3744
  }
@@ -3193,6 +3753,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3193
3753
  meshVertexBuffer.destroy?.();
3194
3754
  meshIndexBuffer.destroy?.();
3195
3755
  meshRangeBuffer.destroy?.();
3756
+ environmentPortalBuffer.destroy?.();
3196
3757
  bvhLeafRefBuffer.destroy?.();
3197
3758
  configBuffer.destroy?.();
3198
3759
  bvhBuildConfigBuffer.destroy?.();
@@ -4515,11 +5076,13 @@ export {
4515
5076
  createWavefrontPathTracingComputeConfig,
4516
5077
  createWavefrontPathTracingComputeRenderer,
4517
5078
  createWavefrontPathTracingPlan,
5079
+ createWavefrontReferenceRay,
4518
5080
  defaultRendererClearColor,
4519
5081
  defaultRendererWorkerProfile,
4520
5082
  estimateWavefrontPathTracingMemory,
4521
5083
  getRendererWorkerManifest,
4522
5084
  getRendererWorkerProfile,
5085
+ intersectWavefrontReferenceTriangle,
4523
5086
  normalizeWavefrontMesh,
4524
5087
  normalizeWavefrontSceneObject,
4525
5088
  packWavefrontBvhNodes,
@@ -4539,6 +5102,7 @@ export {
4539
5102
  rendererWorkerQueueClass,
4540
5103
  supportsWavefrontPathTracingCompute,
4541
5104
  supportsWebGpu,
5105
+ traceWavefrontReferenceTriangles,
4542
5106
  wavefrontMaterialKinds,
4543
5107
  wavefrontPathTracingComputeLimits,
4544
5108
  wavefrontSceneObjectKinds