@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.
@@ -4,6 +4,7 @@ const DEFAULT_MAX_DEPTH = 6;
4
4
  const DEFAULT_TILE_SIZE = 128;
5
5
  const DEFAULT_SAMPLES_PER_PIXEL = 1;
6
6
  const DEFAULT_SCENE_OBJECT_CAPACITY = 128;
7
+ const DEFAULT_ENVIRONMENT_PORTAL_CAPACITY = 32;
7
8
  const WORKGROUP_SIZE = 64;
8
9
  const RAY_RECORD_BYTES = 80;
9
10
  const HIT_RECORD_BYTES = 208;
@@ -14,9 +15,11 @@ const TRIANGLE_RECORD_BYTES = 208;
14
15
  const BVH_NODE_RECORD_BYTES = 48;
15
16
  const BVH_LEAF_REF_RECORD_BYTES = 16;
16
17
  const EMISSIVE_TRIANGLE_INDEX_BYTES = 4;
18
+ const ENVIRONMENT_PORTAL_RECORD_BYTES = 96;
17
19
  const ACCUMULATION_RECORD_BYTES = 16;
18
- const CONFIG_BUFFER_BYTES = 256;
20
+ const CONFIG_BUFFER_BYTES = 272;
19
21
  const COUNTER_BUFFER_BYTES = 16;
22
+ const TRACE_STORAGE_BUFFER_BINDINGS = 9;
20
23
  const HIT_TYPE_SURFACE = 0;
21
24
  const HIT_TYPE_EMISSIVE = 1;
22
25
  const MATERIAL_DIFFUSE = 0;
@@ -48,6 +51,7 @@ const DEFAULT_ENVIRONMENT_LIGHTING = Object.freeze({
48
51
 
49
52
  export const wavefrontPathTracingComputeLimits = Object.freeze({
50
53
  workgroupSize: WORKGROUP_SIZE,
54
+ traceStorageBufferBindings: TRACE_STORAGE_BUFFER_BINDINGS,
51
55
  rayRecordBytes: RAY_RECORD_BYTES,
52
56
  hitRecordBytes: HIT_RECORD_BYTES,
53
57
  sceneObjectRecordBytes: SCENE_OBJECT_RECORD_BYTES,
@@ -58,6 +62,7 @@ export const wavefrontPathTracingComputeLimits = Object.freeze({
58
62
  bvhLeafReferenceRecordBytes: BVH_LEAF_REF_RECORD_BYTES,
59
63
  emissiveTriangleIndexBytes: EMISSIVE_TRIANGLE_INDEX_BYTES,
60
64
  emissiveTriangleMetadataRecordBytes: BVH_NODE_RECORD_BYTES,
65
+ environmentPortalRecordBytes: ENVIRONMENT_PORTAL_RECORD_BYTES,
61
66
  accumulationRecordBytes: ACCUMULATION_RECORD_BYTES,
62
67
  });
63
68
 
@@ -189,6 +194,33 @@ function normalize(value, fallback = [0, 0, 1]) {
189
194
  return [value[0] / length, value[1] / length, value[2] / length];
190
195
  }
191
196
 
197
+ function hashUint32(value) {
198
+ let x = value >>> 0;
199
+ x = ((((x >>> 16) ^ x) >>> 0) * 0x45d9f3b) >>> 0;
200
+ x = ((((x >>> 16) ^ x) >>> 0) * 0x45d9f3b) >>> 0;
201
+ return ((x >>> 16) ^ x) >>> 0;
202
+ }
203
+
204
+ function mixSeed(pixelId, sampleId, bounce, frameIndex, dimension) {
205
+ let x =
206
+ ((pixelId >>> 0) * 747796405) ^
207
+ ((sampleId >>> 0) * 2891336453) ^
208
+ ((bounce >>> 0) * 277803737) ^
209
+ ((frameIndex >>> 0) * 1442695041) ^
210
+ ((dimension >>> 0) * 1597334677);
211
+ x >>>= 0;
212
+ x ^= x >>> 16;
213
+ x = (x * 0x7feb352d) >>> 0;
214
+ x ^= x >>> 15;
215
+ x = (x * 0x846ca68b) >>> 0;
216
+ x ^= x >>> 16;
217
+ return x >>> 0;
218
+ }
219
+
220
+ function random01FromSeed(seed) {
221
+ return (hashUint32(seed) & 0x00ffffff) / 16777215;
222
+ }
223
+
192
224
  function getArrayLikeLength(value) {
193
225
  return Array.isArray(value) || ArrayBuffer.isView(value) ? value.length : 0;
194
226
  }
@@ -877,6 +909,165 @@ function resolveEnvironmentLighting(input, environmentColor, ambientColor) {
877
909
  });
878
910
  }
879
911
 
912
+ function evaluateReferenceEnvironmentRadiance(config, origin, direction) {
913
+ void origin;
914
+ const rayDirection = normalize(direction, [0, 1, 0]);
915
+ const upFactor = clamp(rayDirection[1] * 0.5 + 0.5, 0, 1);
916
+ const sunDirection = normalize(
917
+ config.environmentLighting?.sunDirection ?? DEFAULT_ENVIRONMENT_LIGHTING.sunDirection,
918
+ DEFAULT_ENVIRONMENT_LIGHTING.sunDirection
919
+ );
920
+ const sunGlow = Math.pow(clamp(dot(rayDirection, sunDirection), 0, 1), 192);
921
+ const horizonColor =
922
+ config.environmentLighting?.horizonColor ?? DEFAULT_ENVIRONMENT_LIGHTING.horizonColor;
923
+ const zenithColor =
924
+ config.environmentLighting?.zenithColor ?? DEFAULT_ENVIRONMENT_LIGHTING.zenithColor;
925
+ const sunColor = config.environmentLighting?.sunColor ?? DEFAULT_ENVIRONMENT_LIGHTING.sunColor;
926
+ const intensity = Math.max(
927
+ 0.0001,
928
+ Number(config.environmentLighting?.intensity ?? DEFAULT_ENVIRONMENT_LIGHTING.intensity)
929
+ );
930
+
931
+ return Object.freeze([
932
+ (horizonColor[0] * (1 - upFactor) + zenithColor[0] * upFactor + sunColor[0] * sunGlow) *
933
+ intensity,
934
+ (horizonColor[1] * (1 - upFactor) + zenithColor[1] * upFactor + sunColor[1] * sunGlow) *
935
+ intensity,
936
+ (horizonColor[2] * (1 - upFactor) + zenithColor[2] * upFactor + sunColor[2] * sunGlow) *
937
+ intensity,
938
+ 1,
939
+ ]);
940
+ }
941
+
942
+ function resolveEnvironmentPortalMode(value, hasPortals) {
943
+ if (value === undefined || value === null) {
944
+ return hasPortals ? 2 : 0;
945
+ }
946
+ if (Number.isInteger(value) && value >= 0 && value <= 2) {
947
+ return value;
948
+ }
949
+ if (value === "disabled") {
950
+ return 0;
951
+ }
952
+ if (value === "guide") {
953
+ return 1;
954
+ }
955
+ if (value === "guide-and-gate" || value === "gate") {
956
+ return 2;
957
+ }
958
+ throw new Error(
959
+ "environmentPortalMode must be disabled, guide, guide-and-gate, or an integer between 0 and 2."
960
+ );
961
+ }
962
+
963
+ function orthogonalPortalTangent(normal) {
964
+ if (Math.abs(normal[1]) < 0.92) {
965
+ return normalize(cross([0, 1, 0], normal), [1, 0, 0]);
966
+ }
967
+ return normalize(cross([1, 0, 0], normal), [0, 0, 1]);
968
+ }
969
+
970
+ function resolvePortalTangent(value, normal) {
971
+ const fallback = orthogonalPortalTangent(normal);
972
+ const tangent = asUnitVec3(value, fallback);
973
+ const projected = subtract(tangent, scale(normal, dot(tangent, normal)));
974
+ return normalize(projected, fallback);
975
+ }
976
+
977
+ function readPositiveFiniteNumber(name, value, fallback) {
978
+ const numeric = readFiniteNumber(name, value, fallback);
979
+ if (numeric <= 0) {
980
+ throw new Error(`${name} must be a positive finite number.`);
981
+ }
982
+ return numeric;
983
+ }
984
+
985
+ function readPortalExtent(name, value, halfName, halfValue) {
986
+ if (value !== undefined && value !== null) {
987
+ return readPositiveFiniteNumber(name, value, 1);
988
+ }
989
+ return readPositiveFiniteNumber(halfName, halfValue, 0.5) * 2;
990
+ }
991
+
992
+ function normalizeEnvironmentPortal(portal, index) {
993
+ if (!portal || typeof portal !== "object") {
994
+ throw new Error(`environmentPortals[${index}] must be an object.`);
995
+ }
996
+ const shape = portal.shape ?? portal.kind ?? "rectangle";
997
+ if (shape !== "rectangle") {
998
+ throw new Error(`environmentPortals[${index}].shape must be "rectangle".`);
999
+ }
1000
+ const position = asVec3(portal.position ?? portal.center, [0, 0, 0]);
1001
+ const normal = asUnitVec3(portal.normal, [0, 0, 1]);
1002
+ const tangent = resolvePortalTangent(portal.tangent, normal);
1003
+ const bitangent = normalize(cross(normal, tangent), [0, 1, 0]);
1004
+ const width = readPortalExtent(
1005
+ `environmentPortals[${index}].width`,
1006
+ portal.width,
1007
+ `environmentPortals[${index}].halfWidth`,
1008
+ portal.halfWidth
1009
+ );
1010
+ const height = readPortalExtent(
1011
+ `environmentPortals[${index}].height`,
1012
+ portal.height,
1013
+ `environmentPortals[${index}].halfHeight`,
1014
+ portal.halfHeight
1015
+ );
1016
+ const radianceScale = Math.max(
1017
+ 0,
1018
+ readFiniteNumber(
1019
+ `environmentPortals[${index}].radianceScale`,
1020
+ portal.radianceScale ?? portal.intensity,
1021
+ 1
1022
+ )
1023
+ );
1024
+ return Object.freeze({
1025
+ kind: 1,
1026
+ flags: portal.twoSided === false ? 0 : 1,
1027
+ position: Object.freeze([position[0], position[1], position[2], width * height]),
1028
+ normal: Object.freeze([normal[0], normal[1], normal[2], radianceScale]),
1029
+ tangent: Object.freeze([tangent[0], tangent[1], tangent[2], width * 0.5]),
1030
+ bitangent: Object.freeze([bitangent[0], bitangent[1], bitangent[2], height * 0.5]),
1031
+ color: Object.freeze(asColor(portal.color, [1, 1, 1, 1])),
1032
+ });
1033
+ }
1034
+
1035
+ function normalizeEnvironmentPortals(value) {
1036
+ if (value === undefined || value === null) {
1037
+ return Object.freeze([]);
1038
+ }
1039
+ if (!Array.isArray(value)) {
1040
+ throw new Error("environmentPortals must be an array when provided.");
1041
+ }
1042
+ return Object.freeze(value.map(normalizeEnvironmentPortal));
1043
+ }
1044
+
1045
+ function packEnvironmentPortals(portals, capacity) {
1046
+ const bytes = new ArrayBuffer(capacity * ENVIRONMENT_PORTAL_RECORD_BYTES);
1047
+ const data = new DataView(bytes);
1048
+ const floatView = new Float32Array(bytes);
1049
+ portals.forEach((portal, index) => {
1050
+ const byteOffset = index * ENVIRONMENT_PORTAL_RECORD_BYTES;
1051
+ const floatOffset = byteOffset / Float32Array.BYTES_PER_ELEMENT;
1052
+ data.setUint32(byteOffset, portal.kind, true);
1053
+ data.setUint32(byteOffset + 4, portal.flags, true);
1054
+ data.setUint32(byteOffset + 8, 0, true);
1055
+ data.setUint32(byteOffset + 12, 0, true);
1056
+ writeVec4(floatView, floatOffset + 4, portal.position);
1057
+ writeVec4(floatView, floatOffset + 8, portal.normal);
1058
+ writeVec4(floatView, floatOffset + 12, portal.tangent);
1059
+ writeVec4(floatView, floatOffset + 16, portal.bitangent);
1060
+ writeVec4(floatView, floatOffset + 20, portal.color);
1061
+ });
1062
+ return Object.freeze({
1063
+ buffer: bytes,
1064
+ portals,
1065
+ count: portals.length,
1066
+ capacity,
1067
+ recordBytes: ENVIRONMENT_PORTAL_RECORD_BYTES,
1068
+ });
1069
+ }
1070
+
880
1071
  function getCanvasDimension(canvas, key, fallback) {
881
1072
  const value = Number(canvas?.[key]);
882
1073
  if (Number.isFinite(value) && value > 0) {
@@ -935,6 +1126,11 @@ export function estimateWavefrontPathTracingMemory(options = {}) {
935
1126
  options.emissiveTriangleCapacity,
936
1127
  0
937
1128
  );
1129
+ const environmentPortalCapacity = readNonNegativeInteger(
1130
+ "environmentPortalCapacity",
1131
+ options.environmentPortalCapacity,
1132
+ 0
1133
+ );
938
1134
  const queueBytes = tilePixelCapacity * RAY_RECORD_BYTES;
939
1135
  const hitBytes = tilePixelCapacity * HIT_RECORD_BYTES;
940
1136
  const accumulationBytes = tilePixelCapacity * ACCUMULATION_RECORD_BYTES;
@@ -944,6 +1140,8 @@ export function estimateWavefrontPathTracingMemory(options = {}) {
944
1140
  const bvhLeafReferenceBytes = bvhLeafSortCapacity * BVH_LEAF_REF_RECORD_BYTES;
945
1141
  const emissiveTriangleMetadataBytes =
946
1142
  emissiveTriangleCapacity * BVH_NODE_RECORD_BYTES;
1143
+ const environmentPortalBytes =
1144
+ environmentPortalCapacity * ENVIRONMENT_PORTAL_RECORD_BYTES;
947
1145
 
948
1146
  return Object.freeze({
949
1147
  queueBytes,
@@ -955,6 +1153,7 @@ export function estimateWavefrontPathTracingMemory(options = {}) {
955
1153
  bvhNodeBytes,
956
1154
  bvhLeafReferenceBytes,
957
1155
  emissiveTriangleMetadataBytes,
1156
+ environmentPortalBytes,
958
1157
  configBytes: CONFIG_BUFFER_BYTES,
959
1158
  counterBytes: COUNTER_BUFFER_BYTES,
960
1159
  totalHotBufferBytes:
@@ -966,6 +1165,7 @@ export function estimateWavefrontPathTracingMemory(options = {}) {
966
1165
  bvhNodeBytes +
967
1166
  bvhLeafReferenceBytes +
968
1167
  emissiveTriangleMetadataBytes +
1168
+ environmentPortalBytes +
969
1169
  CONFIG_BUFFER_BYTES +
970
1170
  COUNTER_BUFFER_BYTES,
971
1171
  });
@@ -1048,6 +1248,25 @@ export function createWavefrontPathTracingComputeConfig(options = {}) {
1048
1248
  environmentColor,
1049
1249
  ambientColor
1050
1250
  );
1251
+ const environmentPortals = normalizeEnvironmentPortals(
1252
+ options.environmentPortals ??
1253
+ options.environmentLightPortals ??
1254
+ options.environmentLighting?.environmentPortals
1255
+ );
1256
+ const environmentPortalCapacity = Math.max(
1257
+ environmentPortals.length,
1258
+ readNonNegativeInteger(
1259
+ "environmentPortalCapacity",
1260
+ options.environmentPortalCapacity,
1261
+ DEFAULT_ENVIRONMENT_PORTAL_CAPACITY
1262
+ )
1263
+ );
1264
+ const environmentPortalMode = resolveEnvironmentPortalMode(
1265
+ options.environmentPortalMode ??
1266
+ options.portalMode ??
1267
+ options.environmentLighting?.environmentPortalMode,
1268
+ environmentPortals.length > 0
1269
+ );
1051
1270
 
1052
1271
  return Object.freeze({
1053
1272
  width,
@@ -1077,6 +1296,10 @@ export function createWavefrontPathTracingComputeConfig(options = {}) {
1077
1296
  environmentColor: environmentLighting.environmentColor,
1078
1297
  ambientColor: environmentLighting.ambientColor,
1079
1298
  environmentLighting,
1299
+ environmentPortals,
1300
+ environmentPortalCount: environmentPortals.length,
1301
+ environmentPortalCapacity,
1302
+ environmentPortalMode,
1080
1303
  displayQuality: options.displayQuality === true,
1081
1304
  requiresMeshBvhForDisplayQuality: true,
1082
1305
  denoise: options.denoise !== false,
@@ -1088,6 +1311,7 @@ export function createWavefrontPathTracingComputeConfig(options = {}) {
1088
1311
  bvhNodeCapacity,
1089
1312
  bvhLeafSortCapacity,
1090
1313
  emissiveTriangleCapacity: emissiveTriangleIndices.capacity,
1314
+ environmentPortalCapacity,
1091
1315
  }),
1092
1316
  });
1093
1317
  }
@@ -1298,6 +1522,10 @@ function createConfigPayload(config, tile, frameIndex, buildRange = {}) {
1298
1522
  data.setUint32(244, buildRange.count ?? 0, true);
1299
1523
  data.setUint32(248, buildRange.sortItemCount ?? 0, true);
1300
1524
  data.setUint32(252, config.emissiveTriangleCount ?? 0, true);
1525
+ data.setUint32(256, config.environmentPortalCount ?? 0, true);
1526
+ data.setUint32(260, config.environmentPortalMode ?? 0, true);
1527
+ data.setUint32(264, 0, true);
1528
+ data.setUint32(268, 0, true);
1301
1529
  return bytes;
1302
1530
  }
1303
1531
 
@@ -1318,6 +1546,229 @@ function createTiles(width, height, tileSize) {
1318
1546
  return Object.freeze(tiles);
1319
1547
  }
1320
1548
 
1549
+ function normalizeReferenceTile(config, tileInput = {}) {
1550
+ const tileX = clamp(
1551
+ readNonNegativeInteger("tile.x", tileInput.x, 0),
1552
+ 0,
1553
+ Math.max(0, config.width - 1)
1554
+ );
1555
+ const tileY = clamp(
1556
+ readNonNegativeInteger("tile.y", tileInput.y, 0),
1557
+ 0,
1558
+ Math.max(0, config.height - 1)
1559
+ );
1560
+ const tileWidth = clamp(
1561
+ readPositiveInteger("tile.width", tileInput.width, config.width - tileX),
1562
+ 1,
1563
+ config.width - tileX
1564
+ );
1565
+ const tileHeight = clamp(
1566
+ readPositiveInteger("tile.height", tileInput.height, config.height - tileY),
1567
+ 1,
1568
+ config.height - tileY
1569
+ );
1570
+
1571
+ return Object.freeze({
1572
+ x: tileX,
1573
+ y: tileY,
1574
+ width: tileWidth,
1575
+ height: tileHeight,
1576
+ });
1577
+ }
1578
+
1579
+ function repairReferenceShadingNormal(geometricNormal, shadingNormal) {
1580
+ const normal = normalize(shadingNormal, geometricNormal);
1581
+ return dot(normal, geometricNormal) < 0 ? scale(normal, -1) : normal;
1582
+ }
1583
+
1584
+ function readOptionalMaxDistance(value) {
1585
+ if (value === undefined || value === null) {
1586
+ return Number.POSITIVE_INFINITY;
1587
+ }
1588
+ const numeric = Number(value);
1589
+ if (!Number.isFinite(numeric) || numeric <= 0) {
1590
+ throw new Error("maxDistance must be a positive finite number when provided.");
1591
+ }
1592
+ return numeric;
1593
+ }
1594
+
1595
+ export function createWavefrontReferenceRay(config, options = {}) {
1596
+ if (!config || typeof config !== "object") {
1597
+ throw new Error("config must be a wavefront path tracing config.");
1598
+ }
1599
+
1600
+ const tile = normalizeReferenceTile(config, options.tile);
1601
+ const tilePixelCount = tile.width * tile.height;
1602
+ const pixelIndex = readNonNegativeInteger("pixelIndex", options.pixelIndex, 0);
1603
+ if (pixelIndex >= tilePixelCount) {
1604
+ throw new Error(`pixelIndex ${pixelIndex} exceeds tile capacity ${tilePixelCount}.`);
1605
+ }
1606
+
1607
+ const sampleIndex = readNonNegativeInteger("sampleIndex", options.sampleIndex, 0);
1608
+ const frameIndex = readNonNegativeInteger("frameIndex", options.frameIndex, config.frameIndex ?? 0);
1609
+ const jitterScale = clamp(readFiniteNumber("jitterScale", options.jitterScale, 0.35), 0, 1);
1610
+ const localX = pixelIndex % tile.width;
1611
+ const localY = Math.floor(pixelIndex / tile.width);
1612
+ const pixelX = tile.x + localX;
1613
+ const pixelY = tile.y + localY;
1614
+ const sourcePixelId = pixelY * config.width + pixelX;
1615
+ const jitterX = random01FromSeed(mixSeed(sourcePixelId, sampleIndex, 0, frameIndex, 1)) - 0.5;
1616
+ const jitterY = random01FromSeed(mixSeed(sourcePixelId, sampleIndex, 0, frameIndex, 2)) - 0.5;
1617
+ const ndcX = ((pixelX + 0.5 + jitterX * jitterScale) / config.width) * 2 - 1;
1618
+ const ndcY = 1 - ((pixelY + 0.5 + jitterY * jitterScale) / config.height) * 2;
1619
+ const viewX = ndcX * config.camera.tanHalfFovY * config.camera.aspect;
1620
+ const viewY = ndcY * config.camera.tanHalfFovY;
1621
+ const direction = normalize(
1622
+ add(
1623
+ add(config.camera.forward, scale(config.camera.right, viewX)),
1624
+ scale(config.camera.up, viewY)
1625
+ ),
1626
+ config.camera.forward
1627
+ );
1628
+
1629
+ return Object.freeze({
1630
+ rayId: pixelIndex,
1631
+ parentRayId: 0xffffffff,
1632
+ sourcePixelId,
1633
+ sampleId: sampleIndex,
1634
+ bounce: 0,
1635
+ mediumRefId: 0,
1636
+ flags: 0,
1637
+ origin: Object.freeze([...config.camera.position]),
1638
+ direction: Object.freeze(direction),
1639
+ throughput: Object.freeze([1, 1, 1, 1]),
1640
+ pixelX,
1641
+ pixelY,
1642
+ });
1643
+ }
1644
+
1645
+ export function intersectWavefrontReferenceTriangle(ray, triangle, options = {}) {
1646
+ if (!ray || typeof ray !== "object") {
1647
+ throw new Error("ray must be a wavefront reference ray.");
1648
+ }
1649
+ if (!triangle || typeof triangle !== "object") {
1650
+ throw new Error("triangle must be a wavefront triangle record.");
1651
+ }
1652
+
1653
+ const maxDistance = readOptionalMaxDistance(options.maxDistance);
1654
+ const triangleIndex = readNonNegativeInteger("triangleIndex", options.triangleIndex, 0);
1655
+ const edge1 = subtract(triangle.v1, triangle.v0);
1656
+ const edge2 = subtract(triangle.v2, triangle.v0);
1657
+ const pvec = cross(ray.direction, edge2);
1658
+ const determinant = dot(edge1, pvec);
1659
+ if (Math.abs(determinant) < 0.0000001) {
1660
+ return null;
1661
+ }
1662
+
1663
+ const invDet = 1 / determinant;
1664
+ const tvec = subtract(ray.origin, triangle.v0);
1665
+ const u = dot(tvec, pvec) * invDet;
1666
+ if (u < 0 || u > 1) {
1667
+ return null;
1668
+ }
1669
+
1670
+ const qvec = cross(tvec, edge1);
1671
+ const v = dot(ray.direction, qvec) * invDet;
1672
+ if (v < 0 || u + v > 1) {
1673
+ return null;
1674
+ }
1675
+
1676
+ const distance = dot(edge2, qvec) * invDet;
1677
+ if (distance <= 0.001 || distance > maxDistance) {
1678
+ return null;
1679
+ }
1680
+
1681
+ const geometric = normalize(cross(edge1, edge2), [0, 1, 0]);
1682
+ const frontFace = dot(ray.direction, geometric) < 0;
1683
+ const orientedGeometric = frontFace ? geometric : scale(geometric, -1);
1684
+ const w = 1 - u - v;
1685
+ const interpolated = [
1686
+ triangle.n0[0] * w + triangle.n1[0] * u + triangle.n2[0] * v,
1687
+ triangle.n0[1] * w + triangle.n1[1] * u + triangle.n2[1] * v,
1688
+ triangle.n0[2] * w + triangle.n1[2] * u + triangle.n2[2] * v,
1689
+ ];
1690
+ const shadingNormal = repairReferenceShadingNormal(orientedGeometric, interpolated);
1691
+ const uv = [
1692
+ triangle.uv0[0] * w + triangle.uv1[0] * u + triangle.uv2[0] * v,
1693
+ triangle.uv0[1] * w + triangle.uv1[1] * u + triangle.uv2[1] * v,
1694
+ ];
1695
+ const position = add(ray.origin, scale(ray.direction, distance));
1696
+
1697
+ return Object.freeze({
1698
+ hitType: "surface",
1699
+ rayId: ray.rayId,
1700
+ sourcePixelId: ray.sourcePixelId,
1701
+ distance,
1702
+ entityId: triangle.meshId,
1703
+ instanceId: 0,
1704
+ primitiveId: triangle.triangleId,
1705
+ materialId: triangle.materialKind,
1706
+ materialRefId: triangle.materialRefId,
1707
+ mediumRefId: triangle.mediumRefId,
1708
+ barycentrics: Object.freeze([w, u, v]),
1709
+ uv: Object.freeze(uv),
1710
+ geometricNormal: Object.freeze(orientedGeometric),
1711
+ shadingNormal: Object.freeze(shadingNormal),
1712
+ frontFace,
1713
+ triangleIndex,
1714
+ triangleId: triangle.triangleId,
1715
+ position: Object.freeze(position),
1716
+ color: triangle.color,
1717
+ emission: triangle.emission,
1718
+ material: triangle.material,
1719
+ });
1720
+ }
1721
+
1722
+ function createWavefrontReferenceEnvironmentHit(config, ray) {
1723
+ const radiance = evaluateReferenceEnvironmentRadiance(config, ray.origin, ray.direction);
1724
+ return Object.freeze({
1725
+ hitType: "environment",
1726
+ rayId: ray.rayId,
1727
+ sourcePixelId: ray.sourcePixelId,
1728
+ distance: -1,
1729
+ entityId: 0,
1730
+ instanceId: 0,
1731
+ primitiveId: 0,
1732
+ materialId: 0,
1733
+ materialRefId: 0,
1734
+ mediumRefId: 0,
1735
+ barycentrics: Object.freeze([0, 0, 0]),
1736
+ uv: Object.freeze([0, 0]),
1737
+ geometricNormal: Object.freeze(scale(ray.direction, -1)),
1738
+ shadingNormal: Object.freeze(scale(ray.direction, -1)),
1739
+ frontFace: true,
1740
+ triangleIndex: -1,
1741
+ triangleId: -1,
1742
+ position: Object.freeze(add(ray.origin, scale(ray.direction, 1000))),
1743
+ color: Object.freeze([0, 0, 0, 0]),
1744
+ emission: radiance,
1745
+ material: Object.freeze([1, 0, 1, 1]),
1746
+ });
1747
+ }
1748
+
1749
+ export function traceWavefrontReferenceTriangles(config, ray, triangles, options = {}) {
1750
+ if (!config || typeof config !== "object") {
1751
+ throw new Error("config must be a wavefront path tracing config.");
1752
+ }
1753
+
1754
+ const source = Array.isArray(triangles) ? triangles : [];
1755
+ let nearestHit = null;
1756
+ let nearestDistance = readOptionalMaxDistance(options.maxDistance);
1757
+
1758
+ source.forEach((triangle, index) => {
1759
+ const hit = intersectWavefrontReferenceTriangle(ray, triangle, {
1760
+ maxDistance: Number.isFinite(nearestDistance) ? nearestDistance : undefined,
1761
+ triangleIndex: index,
1762
+ });
1763
+ if (hit && hit.distance < nearestDistance) {
1764
+ nearestDistance = hit.distance;
1765
+ nearestHit = hit;
1766
+ }
1767
+ });
1768
+
1769
+ return nearestHit ?? createWavefrontReferenceEnvironmentHit(config, ray);
1770
+ }
1771
+
1321
1772
  function clampTileSizeForDevice(config, device) {
1322
1773
  const limit = Number(device?.limits?.maxStorageBufferBindingSize);
1323
1774
  if (!Number.isFinite(limit) || limit <= 0) {
@@ -1556,6 +2007,10 @@ struct FrameConfig {
1556
2007
  bvhBuildNodeCount: u32,
1557
2008
  bvhSortItemCount: u32,
1558
2009
  emissiveTriangleCount: u32,
2010
+ environmentPortalCount: u32,
2011
+ environmentPortalMode: u32,
2012
+ _portalPad0: u32,
2013
+ _portalPad1: u32,
1559
2014
  };
1560
2015
 
1561
2016
  struct Counters {
@@ -1579,6 +2034,18 @@ struct Candidate {
1579
2034
  mediumRefId: u32,
1580
2035
  };
1581
2036
 
2037
+ struct EnvironmentPortal {
2038
+ kind: u32,
2039
+ flags: u32,
2040
+ _pad0: u32,
2041
+ _pad1: u32,
2042
+ position: vec4<f32>,
2043
+ normal: vec4<f32>,
2044
+ tangent: vec4<f32>,
2045
+ bitangent: vec4<f32>,
2046
+ color: vec4<f32>,
2047
+ };
2048
+
1582
2049
  @group(0) @binding(0) var<storage, read_write> activeQueue: array<RayRecord>;
1583
2050
  @group(0) @binding(1) var<storage, read_write> nextQueue: array<RayRecord>;
1584
2051
  @group(0) @binding(2) var<storage, read_write> hits: array<HitRecord>;
@@ -1598,6 +2065,7 @@ struct Candidate {
1598
2065
  @group(0) @binding(16) var radianceImage: texture_storage_2d<rgba16float, write>;
1599
2066
  @group(0) @binding(17) var finalDenoiseInputRadiance: texture_2d<f32>;
1600
2067
  @group(0) @binding(18) var denoisedOutputImage: texture_storage_2d<rgba8unorm, write>;
2068
+ @group(0) @binding(19) var<storage, read> environmentPortals: array<EnvironmentPortal>;
1601
2069
 
1602
2070
  fn hash_u32(value: u32) -> u32 {
1603
2071
  var x = value;
@@ -1638,7 +2106,48 @@ fn saturate(value: f32) -> f32 {
1638
2106
  return clamp(value, 0.0, 1.0);
1639
2107
  }
1640
2108
 
1641
- fn environment_radiance(direction: vec3<f32>) -> vec3<f32> {
2109
+ fn max_component(value: vec3<f32>) -> f32 {
2110
+ return max(max(value.x, value.y), value.z);
2111
+ }
2112
+
2113
+ fn environment_portal_radiance_scale(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
2114
+ if (config.environmentPortalCount == 0u || config.environmentPortalMode == 0u) {
2115
+ return vec3<f32>(1.0);
2116
+ }
2117
+ var scale = vec3<f32>(0.0);
2118
+ for (var portalIndex = 0u; portalIndex < config.environmentPortalCount; portalIndex = portalIndex + 1u) {
2119
+ let portal = environmentPortals[portalIndex];
2120
+ if (portal.kind == 1u) {
2121
+ let portalNormal = safe_normalize(portal.normal.xyz, vec3<f32>(0.0, 0.0, 1.0));
2122
+ let denominator = dot(direction, portalNormal);
2123
+ let twoSided = (portal.flags & 1u) != 0u;
2124
+ var facing = abs(denominator) > 0.0001;
2125
+ if (!twoSided && denominator <= 0.0001) {
2126
+ facing = false;
2127
+ }
2128
+ if (facing) {
2129
+ let distance = dot(portal.position.xyz - origin, portalNormal) / denominator;
2130
+ if (distance > 0.001) {
2131
+ let hitPosition = origin + direction * distance;
2132
+ let local = hitPosition - portal.position.xyz;
2133
+ let tangent = safe_normalize(portal.tangent.xyz, vec3<f32>(1.0, 0.0, 0.0));
2134
+ let bitangent = safe_normalize(portal.bitangent.xyz, vec3<f32>(0.0, 1.0, 0.0));
2135
+ let u = dot(local, tangent);
2136
+ let v = dot(local, bitangent);
2137
+ if (abs(u) <= portal.tangent.w && abs(v) <= portal.bitangent.w) {
2138
+ let areaWeight = clamp(sqrt(max(portal.position.w, 0.0001)), 0.25, 4.0);
2139
+ let angleWeight = max(abs(denominator), 0.08);
2140
+ let portalScale = portal.color.rgb * portal.normal.w * portal.color.a * areaWeight * angleWeight;
2141
+ scale = max(scale, portalScale);
2142
+ }
2143
+ }
2144
+ }
2145
+ }
2146
+ }
2147
+ return scale;
2148
+ }
2149
+
2150
+ fn environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
1642
2151
  let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
1643
2152
  let upFactor = saturate(rayDirection.y * 0.5 + 0.5);
1644
2153
  let sunDirection = safe_normalize(
@@ -1649,10 +2158,26 @@ fn environment_radiance(direction: vec3<f32>) -> vec3<f32> {
1649
2158
  let gradient =
1650
2159
  config.environmentHorizonColor.xyz * (1.0 - upFactor) +
1651
2160
  config.environmentZenithColor.xyz * upFactor;
2161
+ let portalScale = environment_portal_radiance_scale(origin, rayDirection);
2162
+ let portalHit = max_component(portalScale) > 0.0001;
1652
2163
  return (
1653
2164
  gradient +
1654
2165
  config.environmentSunColor.xyz * sunGlow
1655
- ) * max(config.environmentSunDirectionIntensity.w, 0.0001);
2166
+ ) *
2167
+ max(config.environmentSunDirectionIntensity.w, 0.0001) *
2168
+ select(vec3<f32>(1.0), portalScale, portalHit);
2169
+ }
2170
+
2171
+ fn gated_environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
2172
+ let portalScale = environment_portal_radiance_scale(origin, safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0)));
2173
+ if (
2174
+ config.environmentPortalCount > 0u &&
2175
+ config.environmentPortalMode == 2u &&
2176
+ max_component(portalScale) <= 0.0001
2177
+ ) {
2178
+ return config.ambientColor.xyz * 0.65;
2179
+ }
2180
+ return environment_radiance(origin, direction);
1656
2181
  }
1657
2182
 
1658
2183
  fn default_mesh_range() -> MeshRange {
@@ -1923,7 +2448,7 @@ fn make_ray(pixelIndex: u32) -> RayRecord {
1923
2448
  }
1924
2449
 
1925
2450
  fn make_miss(ray: RayRecord) -> HitRecord {
1926
- let radiance = environment_radiance(ray.direction.xyz);
2451
+ let radiance = gated_environment_radiance(ray.origin.xyz, ray.direction.xyz);
1927
2452
  return HitRecord(
1928
2453
  ray.rayId,
1929
2454
  ray.sourcePixelId,
@@ -2409,6 +2934,21 @@ fn sample_emissive_triangle_direction(hit: HitRecord, seed: u32, fallback: vec3<
2409
2934
  return safe_normalize(lightPoint - hit.position.xyz, fallback);
2410
2935
  }
2411
2936
 
2937
+ fn sample_environment_portal_direction(hit: HitRecord, seed: u32, fallback: vec3<f32>) -> vec3<f32> {
2938
+ if (config.environmentPortalCount == 0u || config.environmentPortalMode == 0u) {
2939
+ return fallback;
2940
+ }
2941
+ let portalSlot = min(
2942
+ u32(random01(seed + 211u) * f32(config.environmentPortalCount)),
2943
+ config.environmentPortalCount - 1u
2944
+ );
2945
+ let portal = environmentPortals[portalSlot];
2946
+ let u = (random01(seed + 223u) * 2.0 - 1.0) * portal.tangent.w;
2947
+ let v = (random01(seed + 227u) * 2.0 - 1.0) * portal.bitangent.w;
2948
+ let portalTarget = portal.position.xyz + portal.tangent.xyz * u + portal.bitangent.xyz * v;
2949
+ return safe_normalize(portalTarget - hit.position.xyz, fallback);
2950
+ }
2951
+
2412
2952
  fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult {
2413
2953
  let roughness = clamp(hit.material.x, 0.0, 1.0);
2414
2954
  if (hit.materialKind == 1u) {
@@ -2448,8 +2988,17 @@ fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult
2448
2988
  let canSampleLight = dot(hit.shadingNormal.xyz, guidedLight) > -0.04;
2449
2989
  let guideProbability = select(0.38, 0.72, ray.bounce == 0u);
2450
2990
  let useGuidedLight = canSampleLight && random01(seed + 37u) < guideProbability;
2991
+ let guidedPortal = sample_environment_portal_direction(hit, seed, randomDiffuse);
2992
+ let canSamplePortal = dot(hit.shadingNormal.xyz, guidedPortal) > -0.04;
2993
+ let useGuidedPortal =
2994
+ !useGuidedLight &&
2995
+ canSamplePortal &&
2996
+ config.environmentPortalCount > 0u &&
2997
+ config.environmentPortalMode > 0u &&
2998
+ random01(seed + 89u) < 0.58;
2999
+ let guidedDirection = select(randomDiffuse, guidedPortal, useGuidedPortal);
2451
3000
  return ScatterResult(
2452
- vec4<f32>(select(randomDiffuse, guidedLight, useGuidedLight), 0.0),
3001
+ vec4<f32>(select(guidedDirection, guidedLight, useGuidedLight), 0.0),
2453
3002
  select(0u, RAY_FLAG_GUIDED_EMISSIVE, useGuidedLight),
2454
3003
  0u,
2455
3004
  0u,
@@ -2658,6 +3207,32 @@ fn fragmentMain(in: VertexOut) -> @location(0) vec4<f32> {
2658
3207
  }
2659
3208
  `;
2660
3209
 
3210
+ function createWavefrontDeviceDescriptor(adapter, options = {}) {
3211
+ const requiredLimits = { ...(options.requiredLimits ?? {}) };
3212
+ const exposedStorageBufferLimit = Number(adapter?.limits?.maxStorageBuffersPerShaderStage);
3213
+ if (Number.isFinite(exposedStorageBufferLimit)) {
3214
+ if (exposedStorageBufferLimit < TRACE_STORAGE_BUFFER_BINDINGS) {
3215
+ throw new Error(
3216
+ `Wavefront mesh tracing requires maxStorageBuffersPerShaderStage>=${TRACE_STORAGE_BUFFER_BINDINGS}, ` +
3217
+ `but this adapter exposes ${exposedStorageBufferLimit}.`
3218
+ );
3219
+ }
3220
+ requiredLimits.maxStorageBuffersPerShaderStage = Math.max(
3221
+ Number(requiredLimits.maxStorageBuffersPerShaderStage ?? 0),
3222
+ TRACE_STORAGE_BUFFER_BINDINGS
3223
+ );
3224
+ }
3225
+
3226
+ const descriptor = { ...(options.deviceDescriptor ?? {}) };
3227
+ if (Object.keys(requiredLimits).length > 0) {
3228
+ descriptor.requiredLimits = {
3229
+ ...(descriptor.requiredLimits ?? {}),
3230
+ ...requiredLimits,
3231
+ };
3232
+ }
3233
+ return Object.keys(descriptor).length > 0 ? descriptor : undefined;
3234
+ }
3235
+
2661
3236
  export async function createWavefrontPathTracingComputeRenderer(options = {}) {
2662
3237
  assertAnalyticDisplayQualityPolicy(options);
2663
3238
  const constants = getGpuUsageConstants();
@@ -2678,7 +3253,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
2678
3253
  throw new Error("Unable to acquire a WebGPU adapter for wavefront path tracing.");
2679
3254
  }
2680
3255
 
2681
- const device = await adapter.requestDevice();
3256
+ const device = await adapter.requestDevice(createWavefrontDeviceDescriptor(adapter, options));
2682
3257
  const context = canvas.getContext("webgpu");
2683
3258
  if (!context || typeof context.configure !== "function") {
2684
3259
  throw new Error("Canvas WebGPU context does not support configure().");
@@ -2758,18 +3333,19 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
2758
3333
  Math.max(1, config.gpuMeshSource.meshes.count) * MESH_RANGE_RECORD_BYTES,
2759
3334
  "plasius.wavefront.meshRanges"
2760
3335
  );
3336
+ const environmentPortalBuffer = createBuffer(
3337
+ device,
3338
+ constants.buffer.STORAGE | constants.buffer.COPY_DST,
3339
+ Math.max(1, config.environmentPortalCapacity) * ENVIRONMENT_PORTAL_RECORD_BYTES,
3340
+ "plasius.wavefront.environmentPortals"
3341
+ );
2761
3342
  const bvhLeafRefBuffer = createBuffer(
2762
3343
  device,
2763
3344
  constants.buffer.STORAGE | constants.buffer.COPY_DST,
2764
3345
  Math.max(1, config.bvhLeafSortCapacity) * BVH_LEAF_REF_RECORD_BYTES,
2765
3346
  "plasius.wavefront.bvhLeafRefs"
2766
3347
  );
2767
- const configBuffer = createBuffer(
2768
- device,
2769
- constants.buffer.UNIFORM | constants.buffer.COPY_DST,
2770
- CONFIG_BUFFER_BYTES,
2771
- "plasius.wavefront.frameConfig"
2772
- );
3348
+ const tiles = createTiles(config.width, config.height, config.tileSize);
2773
3349
  const uniformOffsetAlignment = Number(device?.limits?.minUniformBufferOffsetAlignment);
2774
3350
  const configBufferStride = alignTo(
2775
3351
  CONFIG_BUFFER_BYTES,
@@ -2777,6 +3353,16 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
2777
3353
  ? uniformOffsetAlignment
2778
3354
  : CONFIG_BUFFER_BYTES
2779
3355
  );
3356
+ const frameConfigSlotCount = Math.max(
3357
+ 1,
3358
+ tiles.length * config.samplesPerPixel + tiles.length + (config.denoise ? 1 : 0)
3359
+ );
3360
+ const configBuffer = createBuffer(
3361
+ device,
3362
+ constants.buffer.UNIFORM | constants.buffer.COPY_DST,
3363
+ frameConfigSlotCount * configBufferStride,
3364
+ "plasius.wavefront.frameConfig"
3365
+ );
2780
3366
  const bvhBuildConfigSlots =
2781
3367
  1 + config.bvhSortStages.length + config.bvhBuildLevels.length;
2782
3368
  const bvhBuildConfigBuffer = createBuffer(
@@ -2812,6 +3398,11 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
2812
3398
  device.queue.writeBuffer(meshVertexBuffer, 0, config.gpuMeshSource.vertices.buffer);
2813
3399
  device.queue.writeBuffer(meshIndexBuffer, 0, config.gpuMeshSource.indices.buffer);
2814
3400
  device.queue.writeBuffer(meshRangeBuffer, 0, config.gpuMeshSource.meshes.buffer);
3401
+ const packedEnvironmentPortals = packEnvironmentPortals(
3402
+ config.environmentPortals,
3403
+ Math.max(1, config.environmentPortalCapacity)
3404
+ );
3405
+ device.queue.writeBuffer(environmentPortalBuffer, 0, packedEnvironmentPortals.buffer);
2815
3406
 
2816
3407
  const radianceTexture = device.createTexture({
2817
3408
  label: "plasius.wavefront.radiance",
@@ -2873,6 +3464,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
2873
3464
  visibility: constants.shader.COMPUTE,
2874
3465
  storageTexture: { access: "write-only", format: "rgba16float" },
2875
3466
  },
3467
+ { binding: 19, visibility: constants.shader.COMPUTE, buffer: { type: "read-only-storage" } },
2876
3468
  ],
2877
3469
  });
2878
3470
  const accelerationBindGroupLayout = device.createBindGroupLayout({
@@ -3048,6 +3640,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3048
3640
  { binding: 8, resource: { buffer: triangleBuffer } },
3049
3641
  { binding: 9, resource: { buffer: bvhNodeBuffer } },
3050
3642
  { binding: 16, resource: radianceView },
3643
+ { binding: 19, resource: { buffer: environmentPortalBuffer } },
3051
3644
  ],
3052
3645
  });
3053
3646
  }
@@ -3139,11 +3732,29 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3139
3732
  });
3140
3733
 
3141
3734
  let frame = 0;
3142
- const tiles = createTiles(config.width, config.height, config.tileSize);
3735
+ let accelerationBuilt = !config.gpuAccelerationBuildRequired;
3736
+ let accelerationBuildCount = 0;
3737
+
3738
+ function createFrameConfigWriter(frameIndex) {
3739
+ let slot = 0;
3740
+ return (tile, buildRange = {}) => {
3741
+ if (slot >= frameConfigSlotCount) {
3742
+ throw new Error("Wavefront frame config slot capacity exceeded.");
3743
+ }
3744
+ const offset = slot * configBufferStride;
3745
+ slot += 1;
3746
+ device.queue.writeBuffer(
3747
+ configBuffer,
3748
+ offset,
3749
+ createConfigPayload(config, tile, frameIndex, buildRange)
3750
+ );
3751
+ return offset;
3752
+ };
3753
+ }
3143
3754
 
3144
3755
  function dispatchGpuAccelerationBuild(frameIndex) {
3145
- if (!config.gpuAccelerationBuildRequired) {
3146
- return;
3756
+ if (!config.gpuAccelerationBuildRequired || accelerationBuilt) {
3757
+ return false;
3147
3758
  }
3148
3759
  const buildTile = tiles[0] ?? { x: 0, y: 0, width: 1, height: 1 };
3149
3760
  const encoder = device.createCommandEncoder({
@@ -3201,30 +3812,24 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3201
3812
  }
3202
3813
  passEncoder.end();
3203
3814
  device.queue.submit([encoder.finish()]);
3815
+ accelerationBuilt = true;
3816
+ accelerationBuildCount += 1;
3817
+ return true;
3204
3818
  }
3205
3819
 
3206
- function dispatchTileSample(tile, frameIndex, sampleIndex) {
3207
- const sampleWeight = 1 / config.samplesPerPixel;
3208
- const configPayload = createConfigPayload(config, tile, frameIndex, {
3209
- sampleIndex,
3210
- sampleWeight,
3211
- });
3212
- device.queue.writeBuffer(configBuffer, 0, configPayload);
3213
- const encoder = device.createCommandEncoder({
3214
- label: `plasius.wavefront.frame.${frameIndex}.tile.${tile.x}.${tile.y}.sample.${sampleIndex}`,
3215
- });
3820
+ function encodeTileSample(encoder, tile, configOffset) {
3216
3821
  const passEncoder = encoder.beginComputePass({
3217
3822
  label: "plasius.wavefront.computePass",
3218
3823
  });
3219
3824
  const tileWorkgroups = Math.ceil((tile.width * tile.height) / WORKGROUP_SIZE);
3220
3825
  const capacityWorkgroups = Math.ceil(config.tilePixelCapacity / WORKGROUP_SIZE);
3221
3826
 
3222
- passEncoder.setBindGroup(0, bindGroups[0], [0]);
3827
+ passEncoder.setBindGroup(0, bindGroups[0], [configOffset]);
3223
3828
  passEncoder.setPipeline(pipelines.generatePrimaryRays);
3224
3829
  passEncoder.dispatchWorkgroups(tileWorkgroups);
3225
3830
 
3226
3831
  for (let bounceIndex = 0; bounceIndex < config.maxDepth; bounceIndex += 1) {
3227
- passEncoder.setBindGroup(0, bindGroups[bounceIndex % 2], [0]);
3832
+ passEncoder.setBindGroup(0, bindGroups[bounceIndex % 2], [configOffset]);
3228
3833
  passEncoder.setPipeline(pipelines.intersectActiveQueue);
3229
3834
  passEncoder.dispatchWorkgroups(capacityWorkgroups);
3230
3835
  passEncoder.setPipeline(pipelines.resolveSurfaceRecords);
@@ -3234,58 +3839,28 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3234
3839
  }
3235
3840
 
3236
3841
  passEncoder.end();
3237
- device.queue.submit([encoder.finish()]);
3238
3842
  }
3239
3843
 
3240
- function dispatchTileOutput(tile, frameIndex) {
3241
- const configPayload = createConfigPayload(config, tile, frameIndex, {
3242
- sampleIndex: 0,
3243
- sampleWeight: 1 / config.samplesPerPixel,
3244
- });
3245
- device.queue.writeBuffer(configBuffer, 0, configPayload);
3246
- const encoder = device.createCommandEncoder({
3247
- label: `plasius.wavefront.frame.${frameIndex}.tile.${tile.x}.${tile.y}.output`,
3248
- });
3844
+ function encodeTileOutput(encoder, tile, configOffset) {
3249
3845
  const passEncoder = encoder.beginComputePass({
3250
3846
  label: "plasius.wavefront.outputPass",
3251
3847
  });
3252
3848
  const tileWorkgroups = Math.ceil((tile.width * tile.height) / WORKGROUP_SIZE);
3253
3849
 
3254
- passEncoder.setBindGroup(0, bindGroups[0], [0]);
3850
+ passEncoder.setBindGroup(0, bindGroups[0], [configOffset]);
3255
3851
  passEncoder.setPipeline(pipelines.accumulateTerminalRadiance);
3256
3852
  passEncoder.dispatchWorkgroups(tileWorkgroups);
3257
3853
  passEncoder.end();
3258
- device.queue.submit([encoder.finish()]);
3259
- }
3260
-
3261
- function dispatchTile(tile, frameIndex) {
3262
- for (let sampleIndex = 0; sampleIndex < config.samplesPerPixel; sampleIndex += 1) {
3263
- dispatchTileSample(tile, frameIndex, sampleIndex);
3264
- }
3265
- dispatchTileOutput(tile, frameIndex);
3266
3854
  }
3267
3855
 
3268
- function dispatchDenoise(frameIndex) {
3856
+ function encodeDenoise(encoder, configOffset) {
3269
3857
  if (!config.denoise) {
3270
3858
  return;
3271
3859
  }
3272
- device.queue.writeBuffer(
3273
- configBuffer,
3274
- 0,
3275
- createConfigPayload(
3276
- config,
3277
- { x: 0, y: 0, width: config.width, height: config.height },
3278
- frameIndex,
3279
- { sampleIndex: 0, sampleWeight: 1 / config.samplesPerPixel }
3280
- )
3281
- );
3282
- const encoder = device.createCommandEncoder({
3283
- label: `plasius.wavefront.frame.${frameIndex}.denoise`,
3284
- });
3285
3860
  const radiancePass = encoder.beginComputePass({
3286
3861
  label: "plasius.wavefront.denoiseRadiancePass",
3287
3862
  });
3288
- radiancePass.setBindGroup(0, denoiseRadianceBindGroup, [0]);
3863
+ radiancePass.setBindGroup(0, denoiseRadianceBindGroup, [configOffset]);
3289
3864
  radiancePass.setPipeline(pipelines.denoiseLinearRadiance);
3290
3865
  radiancePass.dispatchWorkgroups(Math.ceil(config.width / 8), Math.ceil(config.height / 8));
3291
3866
  radiancePass.end();
@@ -3293,18 +3868,14 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3293
3868
  const resolvePass = encoder.beginComputePass({
3294
3869
  label: "plasius.wavefront.denoiseResolvePass",
3295
3870
  });
3296
- resolvePass.setBindGroup(0, denoiseResolveBindGroup, [0]);
3871
+ resolvePass.setBindGroup(0, denoiseResolveBindGroup, [configOffset]);
3297
3872
  resolvePass.setPipeline(pipelines.resolveDenoisedOutputImage);
3298
3873
  resolvePass.dispatchWorkgroups(Math.ceil(config.width / 8), Math.ceil(config.height / 8));
3299
3874
  resolvePass.end();
3300
- device.queue.submit([encoder.finish()]);
3301
3875
  }
3302
3876
 
3303
- function present() {
3877
+ function encodePresent(encoder) {
3304
3878
  const texture = context.getCurrentTexture();
3305
- const encoder = device.createCommandEncoder({
3306
- label: `plasius.wavefront.present.${frame}`,
3307
- });
3308
3879
  const passEncoder = encoder.beginRenderPass({
3309
3880
  label: "plasius.wavefront.presentPass",
3310
3881
  colorAttachments: [
@@ -3320,17 +3891,44 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3320
3891
  passEncoder.setBindGroup(0, presentBindGroup);
3321
3892
  passEncoder.draw(3);
3322
3893
  passEncoder.end();
3894
+ }
3895
+
3896
+ function dispatchFrame(frameIndex) {
3897
+ const writeFrameConfig = createFrameConfigWriter(frameIndex);
3898
+ const encoder = device.createCommandEncoder({
3899
+ label: `plasius.wavefront.frame.${frameIndex}.batched`,
3900
+ });
3901
+ for (const tile of tiles) {
3902
+ for (let sampleIndex = 0; sampleIndex < config.samplesPerPixel; sampleIndex += 1) {
3903
+ const configOffset = writeFrameConfig(tile, {
3904
+ sampleIndex,
3905
+ sampleWeight: 1 / config.samplesPerPixel,
3906
+ });
3907
+ encodeTileSample(encoder, tile, configOffset);
3908
+ }
3909
+ const outputConfigOffset = writeFrameConfig(tile, {
3910
+ sampleIndex: 0,
3911
+ sampleWeight: 1 / config.samplesPerPixel,
3912
+ });
3913
+ encodeTileOutput(encoder, tile, outputConfigOffset);
3914
+ }
3915
+ if (config.denoise) {
3916
+ const denoiseConfigOffset = writeFrameConfig(
3917
+ { x: 0, y: 0, width: config.width, height: config.height },
3918
+ { sampleIndex: 0, sampleWeight: 1 / config.samplesPerPixel }
3919
+ );
3920
+ encodeDenoise(encoder, denoiseConfigOffset);
3921
+ }
3922
+ encodePresent(encoder);
3323
3923
  device.queue.submit([encoder.finish()]);
3924
+ return 1;
3324
3925
  }
3325
3926
 
3326
3927
  function renderOnce() {
3327
3928
  frame += 1;
3328
- dispatchGpuAccelerationBuild(frame + config.frameIndex);
3329
- for (const tile of tiles) {
3330
- dispatchTile(tile, frame + config.frameIndex);
3331
- }
3332
- dispatchDenoise(frame + config.frameIndex);
3333
- present();
3929
+ const frameIndex = frame + config.frameIndex;
3930
+ const accelerationBuildSubmitted = dispatchGpuAccelerationBuild(frameIndex);
3931
+ const frameSubmissionCount = dispatchFrame(frameIndex);
3334
3932
  return Object.freeze({
3335
3933
  frame,
3336
3934
  width: config.width,
@@ -3344,10 +3942,17 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3344
3942
  sceneObjectCount: config.sceneObjectCount,
3345
3943
  triangleCount: config.triangleCount,
3346
3944
  emissiveTriangleCount: config.emissiveTriangleCount,
3945
+ environmentPortalCount: config.environmentPortalCount,
3946
+ environmentPortalMode: config.environmentPortalMode,
3347
3947
  bvhNodeCount: config.bvhNodeCount,
3348
3948
  displayQuality: config.displayQuality,
3349
3949
  accelerationBuildMode: config.accelerationBuildMode,
3350
3950
  gpuAccelerationBuildRequired: config.gpuAccelerationBuildRequired,
3951
+ accelerationBuildSubmitted,
3952
+ accelerationBuilt,
3953
+ accelerationBuildCount,
3954
+ commandSubmissions: frameSubmissionCount + (accelerationBuildSubmitted ? 1 : 0),
3955
+ frameConfigSlots: frameConfigSlotCount,
3351
3956
  memory: config.memory,
3352
3957
  });
3353
3958
  }
@@ -3416,10 +4021,15 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3416
4021
  sceneObjectCount: config.sceneObjectCount,
3417
4022
  triangleCount: config.triangleCount,
3418
4023
  emissiveTriangleCount: config.emissiveTriangleCount,
4024
+ environmentPortalCount: config.environmentPortalCount,
4025
+ environmentPortalMode: config.environmentPortalMode,
3419
4026
  bvhNodeCount: config.bvhNodeCount,
3420
4027
  displayQuality: config.displayQuality,
3421
4028
  accelerationBuildMode: config.accelerationBuildMode,
3422
4029
  gpuAccelerationBuildRequired: config.gpuAccelerationBuildRequired,
4030
+ accelerationBuilt,
4031
+ accelerationBuildCount,
4032
+ frameConfigSlots: frameConfigSlotCount,
3423
4033
  memory: config.memory,
3424
4034
  });
3425
4035
  }
@@ -3435,6 +4045,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3435
4045
  meshVertexBuffer.destroy?.();
3436
4046
  meshIndexBuffer.destroy?.();
3437
4047
  meshRangeBuffer.destroy?.();
4048
+ environmentPortalBuffer.destroy?.();
3438
4049
  bvhLeafRefBuffer.destroy?.();
3439
4050
  configBuffer.destroy?.();
3440
4051
  bvhBuildConfigBuffer.destroy?.();