@plasius/gpu-renderer 0.2.2 → 0.2.4

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
@@ -4,6 +4,7 @@ var DEFAULT_HEIGHT = 720;
4
4
  var DEFAULT_MAX_DEPTH = 6;
5
5
  var DEFAULT_TILE_SIZE = 128;
6
6
  var DEFAULT_SAMPLES_PER_PIXEL = 1;
7
+ var DEFAULT_MAX_FRAME_PASSES_PER_SUBMISSION = 256;
7
8
  var DEFAULT_SCENE_OBJECT_CAPACITY = 128;
8
9
  var DEFAULT_ENVIRONMENT_PORTAL_CAPACITY = 32;
9
10
  var WORKGROUP_SIZE = 64;
@@ -21,11 +22,12 @@ var BVH_LEAF_REF_RECORD_BYTES = 16;
21
22
  var EMISSIVE_TRIANGLE_INDEX_BYTES = 4;
22
23
  var ENVIRONMENT_PORTAL_RECORD_BYTES = 96;
23
24
  var ACCUMULATION_RECORD_BYTES = 16;
24
- var CONFIG_BUFFER_BYTES = 272;
25
+ var PATH_VERTEX_RECORD_BYTES = 16;
26
+ var CONFIG_BUFFER_BYTES = 304;
25
27
  var COUNTER_DISPATCH_ARGS_OFFSET = 16;
26
28
  var INDIRECT_DISPATCH_ARGS_BYTES = 12;
27
29
  var COUNTER_BUFFER_BYTES = 32;
28
- var TRACE_STORAGE_BUFFER_BINDINGS = 9;
30
+ var TRACE_STORAGE_BUFFER_BINDINGS = 10;
29
31
  var MATERIAL_DIFFUSE = 0;
30
32
  var MATERIAL_METAL = 1;
31
33
  var MATERIAL_DIELECTRIC = 2;
@@ -48,7 +50,8 @@ var DEFAULT_ENVIRONMENT_LIGHTING = Object.freeze({
48
50
  sunColor: Object.freeze([2.8, 2.65, 2.35, 1]),
49
51
  intensity: 1,
50
52
  mode: 0,
51
- exposure: 1
53
+ exposure: 1,
54
+ sunlitBaseline: 0.16
52
55
  });
53
56
  var wavefrontPathTracingComputeLimits = Object.freeze({
54
57
  workgroupSize: WORKGROUP_SIZE,
@@ -65,6 +68,7 @@ var wavefrontPathTracingComputeLimits = Object.freeze({
65
68
  emissiveTriangleMetadataRecordBytes: BVH_NODE_RECORD_BYTES,
66
69
  environmentPortalRecordBytes: ENVIRONMENT_PORTAL_RECORD_BYTES,
67
70
  accumulationRecordBytes: ACCUMULATION_RECORD_BYTES,
71
+ pathVertexRecordBytes: PATH_VERTEX_RECORD_BYTES,
68
72
  counterRecordBytes: COUNTER_BUFFER_BYTES,
69
73
  indirectDispatchRecordBytes: INDIRECT_DISPATCH_ARGS_BYTES
70
74
  });
@@ -141,6 +145,33 @@ function asColor(value, fallback = [1, 1, 1, 1]) {
141
145
  clamp(readFiniteNumber("color[3]", value[3], fallback[3] ?? 1), 0, 1)
142
146
  ];
143
147
  }
148
+ function resolveEnvironmentMap(input = null) {
149
+ const source = input && typeof input === "object" ? input : null;
150
+ const hasTexture = Boolean(source?.view || source?.texture || source?.data);
151
+ const width = readPositiveInteger("environmentMap.width", source?.width, 1);
152
+ const height = readPositiveInteger("environmentMap.height", source?.height, 1);
153
+ return Object.freeze({
154
+ enabled: hasTexture && source?.enabled !== false,
155
+ width,
156
+ height,
157
+ format: typeof source?.format === "string" ? source.format : "rgba16float",
158
+ projection: typeof source?.projection === "string" ? source.projection : "equirectangular",
159
+ texture: source?.texture ?? null,
160
+ view: source?.view ?? null,
161
+ sampler: source?.sampler ?? null,
162
+ data: source?.data ?? null,
163
+ intensity: Math.max(0, readFiniteNumber("environmentMap.intensity", source?.intensity ?? source?.radianceScale, 1)),
164
+ rotationRadians: readFiniteNumber("environmentMap.rotationRadians", source?.rotationRadians ?? source?.rotation, 0),
165
+ ambientStrength: Math.max(
166
+ 0,
167
+ readFiniteNumber("environmentMap.ambientStrength", source?.ambientStrength, 0.32)
168
+ )
169
+ });
170
+ }
171
+ function resolveDeferredPathResolve(options = {}) {
172
+ const value = options.deferredPathResolve ?? options.deferredResolve ?? options.pathResolve?.deferred ?? true;
173
+ return value !== false;
174
+ }
144
175
  function emissionPower(emission) {
145
176
  return Math.max(0, emission?.[0] ?? 0) + Math.max(0, emission?.[1] ?? 0) + Math.max(0, emission?.[2] ?? 0);
146
177
  }
@@ -788,7 +819,15 @@ function resolveEnvironmentLighting(input, environmentColor, ambientColor) {
788
819
  sunColor: Object.freeze(asColor(source.sunColor, DEFAULT_ENVIRONMENT_LIGHTING.sunColor)),
789
820
  intensity: Math.max(1e-4, readFiniteNumber("environmentLighting.intensity", source.intensity, DEFAULT_ENVIRONMENT_LIGHTING.intensity)),
790
821
  mode: readNonNegativeInteger("environmentLighting.mode", source.mode, DEFAULT_ENVIRONMENT_LIGHTING.mode),
791
- exposure: Math.max(1e-4, readFiniteNumber("environmentLighting.exposure", source.exposure, DEFAULT_ENVIRONMENT_LIGHTING.exposure))
822
+ exposure: Math.max(1e-4, readFiniteNumber("environmentLighting.exposure", source.exposure, DEFAULT_ENVIRONMENT_LIGHTING.exposure)),
823
+ sunlitBaseline: Math.max(
824
+ 0,
825
+ readFiniteNumber(
826
+ "environmentLighting.sunlitBaseline",
827
+ source.sunlitBaseline ?? source.daylightBaseline,
828
+ DEFAULT_ENVIRONMENT_LIGHTING.sunlitBaseline
829
+ )
830
+ )
792
831
  });
793
832
  }
794
833
  function evaluateReferenceEnvironmentRadiance(config, origin, direction) {
@@ -973,6 +1012,11 @@ function estimateWavefrontPathTracingMemory(options = {}) {
973
1012
  options.tilePixelCapacity,
974
1013
  DEFAULT_TILE_SIZE * DEFAULT_TILE_SIZE
975
1014
  );
1015
+ const maxDepth = clamp(
1016
+ readPositiveInteger("maxDepth", options.maxDepth, DEFAULT_MAX_DEPTH),
1017
+ 1,
1018
+ 16
1019
+ );
976
1020
  const sceneObjectCapacity = readPositiveInteger(
977
1021
  "sceneObjectCapacity",
978
1022
  options.sceneObjectCapacity,
@@ -998,6 +1042,7 @@ function estimateWavefrontPathTracingMemory(options = {}) {
998
1042
  const queueBytes = tilePixelCapacity * RAY_RECORD_BYTES;
999
1043
  const hitBytes = tilePixelCapacity * HIT_RECORD_BYTES;
1000
1044
  const accumulationBytes = tilePixelCapacity * ACCUMULATION_RECORD_BYTES;
1045
+ const pathVertexBytes = tilePixelCapacity * (maxDepth + 1) * PATH_VERTEX_RECORD_BYTES;
1001
1046
  const sceneObjectBytes = sceneObjectCapacity * SCENE_OBJECT_RECORD_BYTES;
1002
1047
  const triangleBytes = triangleCapacity * TRIANGLE_RECORD_BYTES;
1003
1048
  const bvhNodeBytes = bvhNodeCapacity * BVH_NODE_RECORD_BYTES;
@@ -1009,6 +1054,7 @@ function estimateWavefrontPathTracingMemory(options = {}) {
1009
1054
  queuePairBytes: queueBytes * 2,
1010
1055
  hitBytes,
1011
1056
  accumulationBytes,
1057
+ pathVertexBytes,
1012
1058
  sceneObjectBytes,
1013
1059
  triangleBytes,
1014
1060
  bvhNodeBytes,
@@ -1018,7 +1064,7 @@ function estimateWavefrontPathTracingMemory(options = {}) {
1018
1064
  configBytes: CONFIG_BUFFER_BYTES,
1019
1065
  counterBytes: COUNTER_BUFFER_BYTES,
1020
1066
  indirectDispatchBytes: INDIRECT_DISPATCH_ARGS_BYTES,
1021
- totalHotBufferBytes: queueBytes * 2 + hitBytes + accumulationBytes + sceneObjectBytes + triangleBytes + bvhNodeBytes + bvhLeafReferenceBytes + emissiveTriangleMetadataBytes + environmentPortalBytes + CONFIG_BUFFER_BYTES + COUNTER_BUFFER_BYTES + INDIRECT_DISPATCH_ARGS_BYTES
1067
+ totalHotBufferBytes: queueBytes * 2 + hitBytes + accumulationBytes + pathVertexBytes + sceneObjectBytes + triangleBytes + bvhNodeBytes + bvhLeafReferenceBytes + emissiveTriangleMetadataBytes + environmentPortalBytes + CONFIG_BUFFER_BYTES + COUNTER_BUFFER_BYTES + INDIRECT_DISPATCH_ARGS_BYTES
1022
1068
  });
1023
1069
  }
1024
1070
  function createWavefrontPathTracingComputeConfig(options = {}) {
@@ -1034,6 +1080,15 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
1034
1080
  1,
1035
1081
  64
1036
1082
  );
1083
+ const maxFramePassesPerSubmission = clamp(
1084
+ readPositiveInteger(
1085
+ "maxFramePassesPerSubmission",
1086
+ options.maxFramePassesPerSubmission,
1087
+ DEFAULT_MAX_FRAME_PASSES_PER_SUBMISSION
1088
+ ),
1089
+ 1,
1090
+ 4096
1091
+ );
1037
1092
  const tilePixelCapacity = readPositiveInteger(
1038
1093
  "tilePixelCapacity",
1039
1094
  options.tilePixelCapacity,
@@ -1094,6 +1149,10 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
1094
1149
  options.environmentPortalMode ?? options.portalMode ?? options.environmentLighting?.environmentPortalMode,
1095
1150
  environmentPortals.length > 0
1096
1151
  );
1152
+ const environmentMap = resolveEnvironmentMap(
1153
+ options.environmentMap ?? options.environmentTexture ?? options.environmentLighting?.environmentMap
1154
+ );
1155
+ const deferredPathResolve = resolveDeferredPathResolve(options);
1097
1156
  return Object.freeze({
1098
1157
  mode: rendererWavefrontComputeMode,
1099
1158
  width,
@@ -1101,6 +1160,7 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
1101
1160
  maxDepth,
1102
1161
  tileSize,
1103
1162
  samplesPerPixel,
1163
+ maxFramePassesPerSubmission,
1104
1164
  tilePixelCapacity,
1105
1165
  sceneObjects,
1106
1166
  sceneObjectCount: sceneObjects.length,
@@ -1127,12 +1187,15 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
1127
1187
  environmentPortalCount: environmentPortals.length,
1128
1188
  environmentPortalCapacity,
1129
1189
  environmentPortalMode,
1190
+ environmentMap,
1191
+ deferredPathResolve,
1130
1192
  displayQuality: options.displayQuality === true,
1131
1193
  requiresMeshBvhForDisplayQuality: true,
1132
1194
  denoise: options.denoise !== false,
1133
1195
  frameIndex: readNonNegativeInteger("frameIndex", options.frameIndex, 0),
1134
1196
  memory: estimateWavefrontPathTracingMemory({
1135
1197
  tilePixelCapacity,
1198
+ maxDepth,
1136
1199
  sceneObjectCapacity,
1137
1200
  triangleCapacity,
1138
1201
  bvhNodeCapacity,
@@ -1329,6 +1392,18 @@ function createConfigPayload(config, tile, frameIndex, buildRange = {}) {
1329
1392
  data.setUint32(260, config.environmentPortalMode ?? 0, true);
1330
1393
  data.setUint32(264, 0, true);
1331
1394
  data.setUint32(268, 0, true);
1395
+ writeVec4(floatView, 272, [
1396
+ config.environmentMap.enabled ? 1 : 0,
1397
+ config.environmentMap.intensity,
1398
+ config.environmentMap.rotationRadians,
1399
+ config.environmentMap.ambientStrength
1400
+ ]);
1401
+ writeVec4(floatView, 288, [
1402
+ config.deferredPathResolve ? 1 : 0,
1403
+ config.environmentLighting.sunlitBaseline,
1404
+ 0,
1405
+ 0
1406
+ ]);
1332
1407
  return bytes;
1333
1408
  }
1334
1409
  function createTiles(width, height, tileSize) {
@@ -1574,6 +1649,136 @@ function alignTo(value, alignment) {
1574
1649
  const resolvedAlignment = Math.max(1, alignment);
1575
1650
  return Math.ceil(value / resolvedAlignment) * resolvedAlignment;
1576
1651
  }
1652
+ function float32ToFloat16Bits(value) {
1653
+ const floatView = new Float32Array(1);
1654
+ const intView = new Uint32Array(floatView.buffer);
1655
+ floatView[0] = Number.isFinite(value) ? value : 0;
1656
+ const x = intView[0];
1657
+ const sign = x >> 16 & 32768;
1658
+ let mantissa = x & 8388607;
1659
+ let exponent = x >> 23 & 255;
1660
+ if (exponent === 255) {
1661
+ return sign | (mantissa ? 32256 : 31744);
1662
+ }
1663
+ exponent = exponent - 127 + 15;
1664
+ if (exponent >= 31) {
1665
+ return sign | 31744;
1666
+ }
1667
+ if (exponent <= 0) {
1668
+ if (exponent < -10) {
1669
+ return sign;
1670
+ }
1671
+ mantissa = (mantissa | 8388608) >> 1 - exponent;
1672
+ return sign | mantissa + 4096 >> 13;
1673
+ }
1674
+ return sign | exponent << 10 | mantissa + 4096 >> 13;
1675
+ }
1676
+ function environmentMapIntegerScale(data) {
1677
+ if (data instanceof Uint8Array) {
1678
+ return 1 / 255;
1679
+ }
1680
+ if (data instanceof Uint16Array) {
1681
+ return 1 / 65535;
1682
+ }
1683
+ return 1;
1684
+ }
1685
+ function readEnvironmentMapComponent(data, index, fallback, integerScale = 1) {
1686
+ if (!data || index >= data.length) {
1687
+ return fallback;
1688
+ }
1689
+ const value = Number(data[index]);
1690
+ return Number.isFinite(value) ? Math.max(0, value) * integerScale : fallback;
1691
+ }
1692
+ function createEnvironmentMapUploadBytes(environmentMap, fallbackColor) {
1693
+ const width = Math.max(1, environmentMap.width);
1694
+ const height = Math.max(1, environmentMap.height);
1695
+ const rowBytes = width * 8;
1696
+ const bytesPerRow = alignTo(rowBytes, 256);
1697
+ const bytes = new Uint8Array(bytesPerRow * height);
1698
+ const data = environmentMap.data;
1699
+ const integerScale = environmentMapIntegerScale(data);
1700
+ const view = new DataView(bytes.buffer);
1701
+ const writeComponent = (targetOffset, sourceOffset, fallback) => {
1702
+ view.setUint16(
1703
+ targetOffset,
1704
+ float32ToFloat16Bits(
1705
+ readEnvironmentMapComponent(data, sourceOffset, fallback, integerScale)
1706
+ ),
1707
+ true
1708
+ );
1709
+ };
1710
+ for (let y = 0; y < height; y += 1) {
1711
+ for (let x = 0; x < width; x += 1) {
1712
+ const sourceOffset = (y * width + x) * 4;
1713
+ const targetOffset = y * bytesPerRow + x * 8;
1714
+ writeComponent(targetOffset, sourceOffset, fallbackColor[0]);
1715
+ writeComponent(targetOffset + 2, sourceOffset + 1, fallbackColor[1]);
1716
+ writeComponent(targetOffset + 4, sourceOffset + 2, fallbackColor[2]);
1717
+ writeComponent(targetOffset + 6, sourceOffset + 3, fallbackColor[3] ?? 1);
1718
+ }
1719
+ }
1720
+ return Object.freeze({
1721
+ bytes,
1722
+ bytesPerRow,
1723
+ width,
1724
+ height
1725
+ });
1726
+ }
1727
+ function createEnvironmentMapResource(device, constants, environmentMap, fallbackColor) {
1728
+ if (environmentMap.view) {
1729
+ return Object.freeze({
1730
+ view: environmentMap.view,
1731
+ sampler: environmentMap.sampler ?? device.createSampler({
1732
+ label: "plasius.wavefront.environmentMapSampler",
1733
+ addressModeU: "repeat",
1734
+ addressModeV: "clamp-to-edge",
1735
+ magFilter: "linear",
1736
+ minFilter: "linear"
1737
+ }),
1738
+ texture: null,
1739
+ ownsTexture: false
1740
+ });
1741
+ }
1742
+ if (environmentMap.texture && typeof environmentMap.texture.createView === "function") {
1743
+ return Object.freeze({
1744
+ view: environmentMap.texture.createView(),
1745
+ sampler: environmentMap.sampler ?? device.createSampler({
1746
+ label: "plasius.wavefront.environmentMapSampler",
1747
+ addressModeU: "repeat",
1748
+ addressModeV: "clamp-to-edge",
1749
+ magFilter: "linear",
1750
+ minFilter: "linear"
1751
+ }),
1752
+ texture: environmentMap.texture,
1753
+ ownsTexture: false
1754
+ });
1755
+ }
1756
+ const upload = createEnvironmentMapUploadBytes(environmentMap, fallbackColor);
1757
+ const texture = device.createTexture({
1758
+ label: environmentMap.enabled ? "plasius.wavefront.environmentMap" : "plasius.wavefront.environmentMapFallback",
1759
+ size: { width: upload.width, height: upload.height },
1760
+ format: "rgba16float",
1761
+ usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
1762
+ });
1763
+ device.queue.writeTexture(
1764
+ { texture },
1765
+ upload.bytes,
1766
+ { bytesPerRow: upload.bytesPerRow, rowsPerImage: upload.height },
1767
+ { width: upload.width, height: upload.height, depthOrArrayLayers: 1 }
1768
+ );
1769
+ return Object.freeze({
1770
+ view: texture.createView(),
1771
+ sampler: environmentMap.sampler ?? device.createSampler({
1772
+ label: "plasius.wavefront.environmentMapSampler",
1773
+ addressModeU: "repeat",
1774
+ addressModeV: "clamp-to-edge",
1775
+ magFilter: "linear",
1776
+ minFilter: "linear"
1777
+ }),
1778
+ texture,
1779
+ ownsTexture: true
1780
+ });
1781
+ }
1577
1782
  async function getPipelineDiagnostics(shaderModule) {
1578
1783
  if (typeof shaderModule?.compilationInfo !== "function") {
1579
1784
  return "";
@@ -1782,6 +1987,8 @@ struct FrameConfig {
1782
1987
  environmentPortalMode: u32,
1783
1988
  _portalPad0: u32,
1784
1989
  _portalPad1: u32,
1990
+ environmentMapSettings: vec4<f32>,
1991
+ pathResolveSettings: vec4<f32>,
1785
1992
  };
1786
1993
 
1787
1994
  struct Counters {
@@ -1841,6 +2048,9 @@ struct EnvironmentPortal {
1841
2048
  @group(0) @binding(17) var finalDenoiseInputRadiance: texture_2d<f32>;
1842
2049
  @group(0) @binding(18) var denoisedOutputImage: texture_storage_2d<rgba8unorm, write>;
1843
2050
  @group(0) @binding(19) var<storage, read> environmentPortals: array<EnvironmentPortal>;
2051
+ @group(0) @binding(20) var environmentMapTexture: texture_2d<f32>;
2052
+ @group(0) @binding(21) var environmentMapSampler: sampler;
2053
+ @group(0) @binding(22) var<storage, read_write> pathVertices: array<vec4<f32>>;
1844
2054
 
1845
2055
  fn hash_u32(value: u32) -> u32 {
1846
2056
  var x = value;
@@ -1885,6 +2095,89 @@ fn max_component(value: vec3<f32>) -> f32 {
1885
2095
  return max(max(value.x, value.y), value.z);
1886
2096
  }
1887
2097
 
2098
+ fn environment_map_enabled() -> bool {
2099
+ return config.environmentMapSettings.x > 0.5;
2100
+ }
2101
+
2102
+ fn deferred_path_resolve_enabled() -> bool {
2103
+ return config.pathResolveSettings.x > 0.5;
2104
+ }
2105
+
2106
+ fn path_vertex_count_per_ray() -> u32 {
2107
+ return config.maxDepth + 1u;
2108
+ }
2109
+
2110
+ fn path_vertex_index(rayId: u32, depth: u32) -> u32 {
2111
+ return rayId * path_vertex_count_per_ray() + min(depth, config.maxDepth);
2112
+ }
2113
+
2114
+ fn clear_deferred_path(rayId: u32) {
2115
+ if (!deferred_path_resolve_enabled()) {
2116
+ return;
2117
+ }
2118
+
2119
+ for (var depth = 0u; depth <= config.maxDepth; depth = depth + 1u) {
2120
+ pathVertices[path_vertex_index(rayId, depth)] = vec4<f32>(0.0);
2121
+ if (depth == config.maxDepth) {
2122
+ break;
2123
+ }
2124
+ }
2125
+ }
2126
+
2127
+ fn record_deferred_path_response(ray: RayRecord, response: vec3<f32>) {
2128
+ if (!deferred_path_resolve_enabled() || ray.rayId >= config.tilePixelCount || ray.bounce >= config.maxDepth) {
2129
+ return;
2130
+ }
2131
+ pathVertices[path_vertex_index(ray.rayId, ray.bounce)] =
2132
+ vec4<f32>(max(response, vec3<f32>(0.0)), 1.0);
2133
+ }
2134
+
2135
+ fn record_deferred_terminal_source(ray: RayRecord, sourceRadiance: vec3<f32>) {
2136
+ if (!deferred_path_resolve_enabled() || ray.rayId >= config.tilePixelCount) {
2137
+ return;
2138
+ }
2139
+ pathVertices[path_vertex_index(ray.rayId, config.maxDepth)] =
2140
+ vec4<f32>(clamp_sample_radiance(sourceRadiance), 1.0);
2141
+ }
2142
+
2143
+ fn environment_map_uv(direction: vec3<f32>) -> vec2<f32> {
2144
+ let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
2145
+ let rotationTurns = config.environmentMapSettings.z / 6.28318530718;
2146
+ let u = fract(atan2(rayDirection.z, rayDirection.x) / 6.28318530718 + 0.5 + rotationTurns);
2147
+ let v = acos(clamp(rayDirection.y, -1.0, 1.0)) / 3.14159265359;
2148
+ return vec2<f32>(u, clamp(v, 0.0, 1.0));
2149
+ }
2150
+
2151
+ fn environment_map_radiance(direction: vec3<f32>) -> vec3<f32> {
2152
+ let uv = environment_map_uv(direction);
2153
+ let texel = max(textureSampleLevel(environmentMapTexture, environmentMapSampler, uv, 0.0).rgb, vec3<f32>(0.0));
2154
+ return texel * max(config.environmentMapSettings.y, 0.0);
2155
+ }
2156
+
2157
+ fn procedural_environment_radiance(direction: vec3<f32>) -> vec3<f32> {
2158
+ let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
2159
+ let upFactor = saturate(rayDirection.y * 0.5 + 0.5);
2160
+ let sunDirection = safe_normalize(
2161
+ config.environmentSunDirectionIntensity.xyz,
2162
+ vec3<f32>(0.0, 1.0, 0.0)
2163
+ );
2164
+ let sunGlow = pow(saturate(dot(rayDirection, sunDirection)), 192.0);
2165
+ let gradient =
2166
+ config.environmentHorizonColor.xyz * (1.0 - upFactor) +
2167
+ config.environmentZenithColor.xyz * upFactor;
2168
+ return (
2169
+ gradient +
2170
+ config.environmentSunColor.xyz * sunGlow
2171
+ ) * max(config.environmentSunDirectionIntensity.w, 0.0001);
2172
+ }
2173
+
2174
+ fn base_environment_radiance(direction: vec3<f32>) -> vec3<f32> {
2175
+ if (environment_map_enabled()) {
2176
+ return environment_map_radiance(direction);
2177
+ }
2178
+ return procedural_environment_radiance(direction);
2179
+ }
2180
+
1888
2181
  fn environment_portal_radiance_scale(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
1889
2182
  if (config.environmentPortalCount == 0u || config.environmentPortalMode == 0u) {
1890
2183
  return vec3<f32>(1.0);
@@ -1924,22 +2217,9 @@ fn environment_portal_radiance_scale(origin: vec3<f32>, direction: vec3<f32>) ->
1924
2217
 
1925
2218
  fn environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
1926
2219
  let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
1927
- let upFactor = saturate(rayDirection.y * 0.5 + 0.5);
1928
- let sunDirection = safe_normalize(
1929
- config.environmentSunDirectionIntensity.xyz,
1930
- vec3<f32>(0.0, 1.0, 0.0)
1931
- );
1932
- let sunGlow = pow(saturate(dot(rayDirection, sunDirection)), 192.0);
1933
- let gradient =
1934
- config.environmentHorizonColor.xyz * (1.0 - upFactor) +
1935
- config.environmentZenithColor.xyz * upFactor;
1936
2220
  let portalScale = environment_portal_radiance_scale(origin, rayDirection);
1937
2221
  let portalHit = max_component(portalScale) > 0.0001;
1938
- return (
1939
- gradient +
1940
- config.environmentSunColor.xyz * sunGlow
1941
- ) *
1942
- max(config.environmentSunDirectionIntensity.w, 0.0001) *
2222
+ return base_environment_radiance(rayDirection) *
1943
2223
  select(vec3<f32>(1.0), portalScale, portalHit);
1944
2224
  }
1945
2225
 
@@ -1955,16 +2235,143 @@ fn gated_environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f
1955
2235
  return environment_radiance(origin, direction);
1956
2236
  }
1957
2237
 
1958
- fn terminal_surface_environment_contribution(ray: RayRecord, hit: HitRecord) -> vec3<f32> {
2238
+ fn surface_path_response(hit: HitRecord) -> vec3<f32> {
2239
+ let color = clamp(hit.color.xyz, vec3<f32>(0.0), vec3<f32>(1.0));
2240
+ let opacity = clamp(hit.material.z, 0.0, 1.0);
2241
+ let materialEnergy = select(0.68, 0.92, hit.materialKind == 1u || hit.materialKind == 2u);
2242
+ let transparentEnergy = select(materialEnergy, 0.9, hit.hitType == 3u);
2243
+ return mix(vec3<f32>(1.0), color, max(opacity, 0.18)) * transparentEnergy;
2244
+ }
2245
+
2246
+ fn sunlit_baseline_radiance(normal: vec3<f32>) -> vec3<f32> {
2247
+ let baseline = max(config.pathResolveSettings.y, 0.0);
2248
+ if (baseline <= 0.000001) {
2249
+ return vec3<f32>(0.0);
2250
+ }
2251
+ let sunDirection = safe_normalize(
2252
+ config.environmentSunDirectionIntensity.xyz,
2253
+ vec3<f32>(0.0, 1.0, 0.0)
2254
+ );
2255
+ let sunFacing = saturate(dot(normal, sunDirection));
2256
+ let skyFacing = 0.35 + saturate(normal.y * 0.5 + 0.5) * 0.65;
2257
+ let directionalWeight = 0.38 + sunFacing * 0.62;
2258
+ let sunTint = max(config.environmentSunColor.xyz, vec3<f32>(0.0));
2259
+ return clamp_sample_radiance(sunTint * baseline * skyFacing * directionalWeight * 0.04);
2260
+ }
2261
+
2262
+ fn terminal_surface_environment_source(hit: HitRecord) -> vec3<f32> {
1959
2263
  let normal = safe_normalize(hit.shadingNormal.xyz, vec3<f32>(0.0, 1.0, 0.0));
1960
- let surfaceColor = max(hit.color.xyz, config.ambientColor.xyz);
1961
2264
  let normalEnvironment = gated_environment_radiance(
1962
2265
  hit.position.xyz + normal * 0.003,
1963
2266
  normal
1964
2267
  );
1965
- let environmentFloor = max(config.ambientColor.xyz, normalEnvironment * 0.12);
2268
+ let sunlitFloor = sunlit_baseline_radiance(normal);
2269
+ let ambientFloor = select(
2270
+ max(config.ambientColor.xyz, sunlitFloor * 0.82),
2271
+ max(config.ambientColor.xyz * 0.35, sunlitFloor * 0.58),
2272
+ environment_map_enabled()
2273
+ );
2274
+ let environmentInfluence = select(
2275
+ max(0.12, config.pathResolveSettings.y * 0.42),
2276
+ max(config.environmentMapSettings.w, max(0.12, config.pathResolveSettings.y * 0.42)),
2277
+ environment_map_enabled()
2278
+ );
2279
+ let environmentFloor = max(ambientFloor, max(sunlitFloor, normalEnvironment * environmentInfluence));
1966
2280
  let materialFloor = select(0.7, 1.0, hit.materialKind == 0u || hit.materialKind == 3u);
1967
- return clamp_sample_radiance(ray.throughput.xyz * surfaceColor * environmentFloor * materialFloor);
2281
+ return clamp_sample_radiance(environmentFloor * materialFloor);
2282
+ }
2283
+
2284
+ fn terminal_surface_environment_contribution(ray: RayRecord, hit: HitRecord) -> vec3<f32> {
2285
+ let surfaceColor = max(hit.color.xyz, config.ambientColor.xyz);
2286
+ return clamp_sample_radiance(
2287
+ ray.throughput.xyz *
2288
+ surfaceColor *
2289
+ terminal_surface_environment_source(hit)
2290
+ );
2291
+ }
2292
+
2293
+ fn direct_environment_portal_irradiance(origin: vec3<f32>, normal: vec3<f32>) -> vec3<f32> {
2294
+ if (config.environmentPortalCount == 0u || config.environmentPortalMode == 0u) {
2295
+ return vec3<f32>(0.0);
2296
+ }
2297
+
2298
+ var irradiance = vec3<f32>(0.0);
2299
+ for (var portalIndex = 0u; portalIndex < config.environmentPortalCount; portalIndex = portalIndex + 1u) {
2300
+ let portal = environmentPortals[portalIndex];
2301
+ if (portal.kind != 1u) {
2302
+ continue;
2303
+ }
2304
+
2305
+ let toPortal = portal.position.xyz - origin;
2306
+ let distanceSquared = max(dot(toPortal, toPortal), 0.01);
2307
+ let direction = safe_normalize(toPortal, normal);
2308
+ let surfaceFacing = saturate(dot(normal, direction));
2309
+ if (surfaceFacing <= 0.0001) {
2310
+ continue;
2311
+ }
2312
+
2313
+ let portalNormal = safe_normalize(portal.normal.xyz, vec3<f32>(0.0, 0.0, 1.0));
2314
+ let twoSided = (portal.flags & 1u) != 0u;
2315
+ let portalFacing = select(
2316
+ saturate(dot(-direction, portalNormal)),
2317
+ max(abs(dot(direction, portalNormal)), 0.15),
2318
+ twoSided
2319
+ );
2320
+ let area = max(portal.position.w, 0.0001);
2321
+ let distanceFalloff = clamp(area / max(distanceSquared, area * 0.25), 0.0, 2.5);
2322
+ irradiance = irradiance +
2323
+ portal.color.rgb *
2324
+ portal.normal.w *
2325
+ portal.color.a *
2326
+ surfaceFacing *
2327
+ portalFacing *
2328
+ distanceFalloff;
2329
+ }
2330
+ return irradiance;
2331
+ }
2332
+
2333
+ fn surface_direct_environment_contribution(ray: RayRecord, hit: HitRecord) -> vec3<f32> {
2334
+ let normal = safe_normalize(hit.shadingNormal.xyz, vec3<f32>(0.0, 1.0, 0.0));
2335
+ let origin = hit.position.xyz + normal * 0.003;
2336
+ let viewDirection = safe_normalize(-ray.direction.xyz, normal);
2337
+ let surfaceColor = clamp(max(hit.color.xyz, config.ambientColor.xyz * 0.35), vec3<f32>(0.0), vec3<f32>(1.0));
2338
+ let roughness = clamp(hit.material.x, 0.0, 1.0);
2339
+ let metallic = clamp(hit.material.y, 0.0, 1.0);
2340
+
2341
+ let normalEnvironment = gated_environment_radiance(origin, normal);
2342
+ let skyVisibility = 0.35 + saturate(normal.y * 0.5 + 0.5) * 0.45;
2343
+ let sunlitFloor = sunlit_baseline_radiance(normal);
2344
+ let ambientIrradiance = max(
2345
+ select(config.ambientColor.xyz * 0.72, config.ambientColor.xyz * 0.28, environment_map_enabled()),
2346
+ sunlitFloor * select(0.72, 0.45, environment_map_enabled())
2347
+ );
2348
+ let environmentIrradianceScale = select(
2349
+ max(0.16, config.pathResolveSettings.y * 0.45),
2350
+ max(config.environmentMapSettings.w, max(0.16, config.pathResolveSettings.y * 0.45)),
2351
+ environment_map_enabled()
2352
+ );
2353
+ let skyIrradiance = max(ambientIrradiance, normalEnvironment * skyVisibility * environmentIrradianceScale);
2354
+
2355
+ let sunDirection = safe_normalize(
2356
+ config.environmentSunDirectionIntensity.xyz,
2357
+ vec3<f32>(0.0, 1.0, 0.0)
2358
+ );
2359
+ let sunFacing = saturate(dot(normal, sunDirection));
2360
+ let sunRadiance = gated_environment_radiance(origin, sunDirection);
2361
+ let sunIrradiance = sunRadiance * sunFacing * 0.2;
2362
+ let portalIrradiance = direct_environment_portal_irradiance(origin, normal);
2363
+
2364
+ let diffuseWeight = select(1.0 - metallic * 0.65, 0.22, hit.materialKind == 1u);
2365
+ let diffuse = surfaceColor * (skyIrradiance + sunIrradiance + portalIrradiance) * diffuseWeight;
2366
+
2367
+ let halfVector = safe_normalize(sunDirection + viewDirection, normal);
2368
+ let specularPower = 8.0 + (1.0 - roughness) * 96.0;
2369
+ let specularFacing = pow(saturate(dot(normal, halfVector)), specularPower) * sunFacing;
2370
+ let specularTint = mix(vec3<f32>(0.04), surfaceColor, metallic);
2371
+ let specular = specularTint * sunRadiance * specularFacing * select(0.16, 0.48, hit.materialKind == 1u || hit.materialKind == 2u);
2372
+
2373
+ let bounceWeight = select(1.0, 0.38, ray.bounce > 0u);
2374
+ return clamp_sample_radiance(ray.throughput.xyz * (diffuse + specular) * bounceWeight);
1968
2375
  }
1969
2376
 
1970
2377
  fn default_mesh_range() -> MeshRange {
@@ -2567,6 +2974,7 @@ fn generatePrimaryRays(@builtin(global_invocation_id) globalId: vec3<u32>) {
2567
2974
  return;
2568
2975
  }
2569
2976
  activeQueue[index] = make_ray(index);
2977
+ clear_deferred_path(index);
2570
2978
  if (u32(config.projectionAndSampling.w) == 0u) {
2571
2979
  accumulation[index] = vec4<f32>(0.0);
2572
2980
  }
@@ -2819,27 +3227,51 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
2819
3227
 
2820
3228
  if (hit.hitType == 1u) {
2821
3229
  let guidedLightWeight = select(1.0, 0.24, (ray.flags & RAY_FLAG_GUIDED_EMISSIVE) != 0u);
2822
- contribution = clamp_sample_radiance(
2823
- ray.throughput.xyz * max(hit.emission.xyz, hit.color.xyz) * guidedLightWeight
2824
- );
2825
- accumulation[ray.rayId] =
2826
- accumulation[ray.rayId] + vec4<f32>(contribution * sample_weight(), 1.0);
3230
+ let sourceRadiance = max(hit.emission.xyz, hit.color.xyz) * guidedLightWeight;
3231
+ if (deferred_path_resolve_enabled()) {
3232
+ record_deferred_terminal_source(ray, sourceRadiance);
3233
+ } else {
3234
+ contribution = clamp_sample_radiance(ray.throughput.xyz * sourceRadiance);
3235
+ accumulation[ray.rayId] =
3236
+ accumulation[ray.rayId] + vec4<f32>(contribution * sample_weight(), 1.0);
3237
+ }
2827
3238
  atomicAdd(&counters.terminatedCount, 1u);
2828
3239
  return;
2829
3240
  }
2830
3241
 
2831
3242
  if (hit.hitType == 2u) {
2832
- contribution = clamp_sample_radiance(ray.throughput.xyz * max(hit.color.xyz, config.ambientColor.xyz));
2833
- accumulation[ray.rayId] =
2834
- accumulation[ray.rayId] + vec4<f32>(contribution * sample_weight(), 1.0);
3243
+ if (deferred_path_resolve_enabled()) {
3244
+ record_deferred_terminal_source(ray, hit.color.xyz);
3245
+ } else {
3246
+ contribution = clamp_sample_radiance(ray.throughput.xyz * max(hit.color.xyz, config.ambientColor.xyz));
3247
+ accumulation[ray.rayId] =
3248
+ accumulation[ray.rayId] + vec4<f32>(contribution * sample_weight(), 1.0);
3249
+ }
2835
3250
  atomicAdd(&counters.terminatedCount, 1u);
2836
3251
  return;
2837
3252
  }
2838
3253
 
2839
- if (ray.bounce + 1u >= config.maxDepth) {
2840
- let terminalEnvironment = terminal_surface_environment_contribution(ray, hit);
3254
+ let response = surface_path_response(hit);
3255
+ record_deferred_path_response(ray, response);
3256
+
3257
+ let shouldEstimateDirectEnvironment =
3258
+ !deferred_path_resolve_enabled() &&
3259
+ (hit.materialKind == 0u || hit.materialKind == 1u) &&
3260
+ hit.material.z >= 0.95;
3261
+ if (shouldEstimateDirectEnvironment) {
3262
+ let directEnvironment = surface_direct_environment_contribution(ray, hit);
2841
3263
  accumulation[ray.rayId] =
2842
- accumulation[ray.rayId] + vec4<f32>(terminalEnvironment * sample_weight(), 1.0);
3264
+ accumulation[ray.rayId] + vec4<f32>(directEnvironment * sample_weight(), 0.0);
3265
+ }
3266
+
3267
+ if (ray.bounce + 1u >= config.maxDepth) {
3268
+ if (deferred_path_resolve_enabled()) {
3269
+ record_deferred_terminal_source(ray, terminal_surface_environment_source(hit));
3270
+ } else {
3271
+ let terminalEnvironment = terminal_surface_environment_contribution(ray, hit);
3272
+ accumulation[ray.rayId] =
3273
+ accumulation[ray.rayId] + vec4<f32>(terminalEnvironment * sample_weight(), 1.0);
3274
+ }
2843
3275
  atomicAdd(&counters.terminatedCount, 1u);
2844
3276
  return;
2845
3277
  }
@@ -2848,17 +3280,17 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
2848
3280
  let scatter = scatter_direction(ray, hit, seed);
2849
3281
  let nextIndex = atomicAdd(&counters.nextCount, 1u);
2850
3282
  if (nextIndex >= config.tilePixelCount) {
2851
- let overflowEnvironment = terminal_surface_environment_contribution(ray, hit);
2852
- accumulation[ray.rayId] =
2853
- accumulation[ray.rayId] + vec4<f32>(overflowEnvironment * sample_weight(), 1.0);
3283
+ if (deferred_path_resolve_enabled()) {
3284
+ record_deferred_terminal_source(ray, terminal_surface_environment_source(hit));
3285
+ } else {
3286
+ let overflowEnvironment = terminal_surface_environment_contribution(ray, hit);
3287
+ accumulation[ray.rayId] =
3288
+ accumulation[ray.rayId] + vec4<f32>(overflowEnvironment * sample_weight(), 1.0);
3289
+ }
2854
3290
  atomicAdd(&counters.terminatedCount, 1u);
2855
3291
  return;
2856
3292
  }
2857
- let color = clamp(hit.color.xyz, vec3<f32>(0.0), vec3<f32>(1.0));
2858
- let opacity = clamp(hit.material.z, 0.0, 1.0);
2859
- let materialEnergy = select(0.68, 0.92, hit.materialKind == 1u || hit.materialKind == 2u);
2860
- let transparentEnergy = select(materialEnergy, 0.9, hit.hitType == 3u);
2861
- let throughput = ray.throughput.xyz * mix(vec3<f32>(1.0), color, max(opacity, 0.18)) * transparentEnergy;
3293
+ let throughput = ray.throughput.xyz * response;
2862
3294
  nextQueue[nextIndex] = RayRecord(
2863
3295
  ray.rayId,
2864
3296
  ray.rayId,
@@ -2886,6 +3318,27 @@ fn compactAndSwapQueues(@builtin(global_invocation_id) globalId: vec3<u32>) {
2886
3318
  write_active_dispatch_args(activeCount);
2887
3319
  }
2888
3320
 
3321
+ fn resolve_deferred_path_radiance(rayId: u32) -> vec3<f32> {
3322
+ let terminal = pathVertices[path_vertex_index(rayId, config.maxDepth)];
3323
+ if (terminal.w <= 0.0) {
3324
+ return vec3<f32>(0.0);
3325
+ }
3326
+
3327
+ var radiance = terminal.xyz;
3328
+ var depth = config.maxDepth;
3329
+ loop {
3330
+ if (depth == 0u) {
3331
+ break;
3332
+ }
3333
+ depth = depth - 1u;
3334
+ let response = pathVertices[path_vertex_index(rayId, depth)];
3335
+ if (response.w > 0.0) {
3336
+ radiance = radiance * response.xyz;
3337
+ }
3338
+ }
3339
+ return clamp_sample_radiance(radiance);
3340
+ }
3341
+
2889
3342
  @compute @workgroup_size(64)
2890
3343
  fn accumulateTerminalRadiance(@builtin(global_invocation_id) globalId: vec3<u32>) {
2891
3344
  let index = globalId.x;
@@ -2895,7 +3348,12 @@ fn accumulateTerminalRadiance(@builtin(global_invocation_id) globalId: vec3<u32>
2895
3348
  let localX = index % config.tileWidth;
2896
3349
  let localY = index / config.tileWidth;
2897
3350
  let pixel = vec2<i32>(i32(config.tileX + localX), i32(config.tileY + localY));
2898
- let radiance = max(accumulation[index].xyz, vec3<f32>(0.0));
3351
+ var radiance = max(accumulation[index].xyz, vec3<f32>(0.0));
3352
+ if (deferred_path_resolve_enabled()) {
3353
+ let resolved = resolve_deferred_path_radiance(index) * sample_weight();
3354
+ radiance = clamp_sample_radiance(radiance + resolved);
3355
+ accumulation[index] = vec4<f32>(radiance, 1.0);
3356
+ }
2899
3357
 
2900
3358
  textureStore(radianceImage, pixel, vec4<f32>(radiance, 1.0));
2901
3359
  if (config.denoise == 0u) {
@@ -3033,6 +3491,125 @@ function createWavefrontDeviceDescriptor(adapter, options = {}) {
3033
3491
  }
3034
3492
  return Object.keys(descriptor).length > 0 ? descriptor : void 0;
3035
3493
  }
3494
+ function readGpuLimit(adapter, device, name) {
3495
+ const adapterValue = Number(adapter?.limits?.[name]);
3496
+ if (Number.isFinite(adapterValue)) {
3497
+ return adapterValue;
3498
+ }
3499
+ const deviceValue = Number(device?.limits?.[name]);
3500
+ return Number.isFinite(deviceValue) ? deviceValue : null;
3501
+ }
3502
+ function createAdapterInfoSnapshot(adapter) {
3503
+ const info = adapter?.info;
3504
+ if (!info || typeof info !== "object") {
3505
+ return null;
3506
+ }
3507
+ return Object.freeze({
3508
+ vendor: typeof info.vendor === "string" ? info.vendor : "",
3509
+ architecture: typeof info.architecture === "string" ? info.architecture : "",
3510
+ device: typeof info.device === "string" ? info.device : "",
3511
+ description: typeof info.description === "string" ? info.description : ""
3512
+ });
3513
+ }
3514
+ function createGpuAdapterParallelismDiagnostics(adapter, device) {
3515
+ return Object.freeze({
3516
+ physicalCoreCount: null,
3517
+ physicalCoreCountAvailable: false,
3518
+ physicalCoreCountUnavailableReason: "WebGPU does not expose physical GPU core counts.",
3519
+ adapterInfo: createAdapterInfoSnapshot(adapter),
3520
+ adapterLimits: Object.freeze({
3521
+ maxComputeInvocationsPerWorkgroup: readGpuLimit(adapter, device, "maxComputeInvocationsPerWorkgroup"),
3522
+ maxComputeWorkgroupSizeX: readGpuLimit(adapter, device, "maxComputeWorkgroupSizeX"),
3523
+ maxComputeWorkgroupSizeY: readGpuLimit(adapter, device, "maxComputeWorkgroupSizeY"),
3524
+ maxComputeWorkgroupSizeZ: readGpuLimit(adapter, device, "maxComputeWorkgroupSizeZ"),
3525
+ maxComputeWorkgroupsPerDimension: readGpuLimit(adapter, device, "maxComputeWorkgroupsPerDimension"),
3526
+ maxStorageBuffersPerShaderStage: readGpuLimit(adapter, device, "maxStorageBuffersPerShaderStage"),
3527
+ maxStorageBufferBindingSize: readGpuLimit(adapter, device, "maxStorageBufferBindingSize")
3528
+ }),
3529
+ configuredWorkgroupSize: WORKGROUP_SIZE
3530
+ });
3531
+ }
3532
+ function createGpuParallelismCounters() {
3533
+ return {
3534
+ directDispatches: 0,
3535
+ directWorkgroups: 0,
3536
+ directShaderInvocations: 0,
3537
+ multiWorkgroupDispatches: 0,
3538
+ largestDirectWorkgroupsPerDispatch: 0,
3539
+ indirectDispatches: 0,
3540
+ estimatedIndirectWorkgroupsUpperBound: 0,
3541
+ estimatedIndirectShaderInvocationsUpperBound: 0,
3542
+ indirectDispatchesWithMultiWorkgroupCapacity: 0,
3543
+ largestEstimatedIndirectWorkgroupsPerDispatch: 0
3544
+ };
3545
+ }
3546
+ function countDispatchWorkgroups(groups) {
3547
+ return groups.reduce((product, value) => {
3548
+ const numeric = Number(value ?? 1);
3549
+ const count = Number.isFinite(numeric) ? Math.max(1, Math.trunc(numeric)) : 1;
3550
+ return product * count;
3551
+ }, 1);
3552
+ }
3553
+ function recordDirectDispatch(parallelism, groups, invocationsPerWorkgroup = WORKGROUP_SIZE) {
3554
+ const workgroups = countDispatchWorkgroups(groups);
3555
+ parallelism.directDispatches += 1;
3556
+ parallelism.directWorkgroups += workgroups;
3557
+ parallelism.directShaderInvocations += workgroups * invocationsPerWorkgroup;
3558
+ parallelism.largestDirectWorkgroupsPerDispatch = Math.max(
3559
+ parallelism.largestDirectWorkgroupsPerDispatch,
3560
+ workgroups
3561
+ );
3562
+ if (workgroups > 1) {
3563
+ parallelism.multiWorkgroupDispatches += 1;
3564
+ }
3565
+ }
3566
+ function recordIndirectDispatch(parallelism, estimatedWorkgroupsUpperBound, invocationsPerWorkgroup = WORKGROUP_SIZE) {
3567
+ const workgroups = Math.max(1, Math.trunc(Number(estimatedWorkgroupsUpperBound) || 1));
3568
+ parallelism.indirectDispatches += 1;
3569
+ parallelism.estimatedIndirectWorkgroupsUpperBound += workgroups;
3570
+ parallelism.estimatedIndirectShaderInvocationsUpperBound += workgroups * invocationsPerWorkgroup;
3571
+ parallelism.largestEstimatedIndirectWorkgroupsPerDispatch = Math.max(
3572
+ parallelism.largestEstimatedIndirectWorkgroupsPerDispatch,
3573
+ workgroups
3574
+ );
3575
+ if (workgroups > 1) {
3576
+ parallelism.indirectDispatchesWithMultiWorkgroupCapacity += 1;
3577
+ }
3578
+ }
3579
+ function createGpuParallelismDiagnostics(adapterDiagnostics, counters) {
3580
+ const totalEstimatedWorkgroupsUpperBound = counters.directWorkgroups + counters.estimatedIndirectWorkgroupsUpperBound;
3581
+ const totalEstimatedShaderInvocationsUpperBound = counters.directShaderInvocations + counters.estimatedIndirectShaderInvocationsUpperBound;
3582
+ const exposesMultiWorkgroupParallelism = counters.multiWorkgroupDispatches > 0 || counters.indirectDispatchesWithMultiWorkgroupCapacity > 0;
3583
+ return Object.freeze({
3584
+ ...adapterDiagnostics,
3585
+ directDispatches: counters.directDispatches,
3586
+ directWorkgroups: counters.directWorkgroups,
3587
+ directShaderInvocations: counters.directShaderInvocations,
3588
+ multiWorkgroupDispatches: counters.multiWorkgroupDispatches,
3589
+ largestDirectWorkgroupsPerDispatch: counters.largestDirectWorkgroupsPerDispatch,
3590
+ indirectDispatches: counters.indirectDispatches,
3591
+ estimatedIndirectWorkgroupsUpperBound: counters.estimatedIndirectWorkgroupsUpperBound,
3592
+ estimatedIndirectShaderInvocationsUpperBound: counters.estimatedIndirectShaderInvocationsUpperBound,
3593
+ indirectDispatchesWithMultiWorkgroupCapacity: counters.indirectDispatchesWithMultiWorkgroupCapacity,
3594
+ largestEstimatedIndirectWorkgroupsPerDispatch: counters.largestEstimatedIndirectWorkgroupsPerDispatch,
3595
+ totalEstimatedWorkgroupsUpperBound,
3596
+ totalEstimatedShaderInvocationsUpperBound,
3597
+ exposesMultiWorkgroupParallelism,
3598
+ likelyUsesMoreThanOnePhysicalGpuCore: null,
3599
+ coreUtilizationStatus: "not-exposed-by-webgpu"
3600
+ });
3601
+ }
3602
+ function createEnvironmentMapSnapshot(environmentMap) {
3603
+ return Object.freeze({
3604
+ enabled: environmentMap.enabled,
3605
+ width: environmentMap.width,
3606
+ height: environmentMap.height,
3607
+ projection: environmentMap.projection,
3608
+ intensity: environmentMap.intensity,
3609
+ rotationRadians: environmentMap.rotationRadians,
3610
+ ambientStrength: environmentMap.ambientStrength
3611
+ });
3612
+ }
3036
3613
  async function createWavefrontPathTracingComputeRenderer(options = {}) {
3037
3614
  assertAnalyticDisplayQualityPolicy(options);
3038
3615
  const constants = getGpuUsageConstants();
@@ -3052,6 +3629,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3052
3629
  throw new Error("Unable to acquire a WebGPU adapter for wavefront path tracing.");
3053
3630
  }
3054
3631
  const device = await adapter.requestDevice(createWavefrontDeviceDescriptor(adapter, options));
3632
+ const gpuAdapterParallelism = createGpuAdapterParallelismDiagnostics(adapter, device);
3055
3633
  const context = canvas.getContext("webgpu");
3056
3634
  if (!context || typeof context.configure !== "function") {
3057
3635
  throw new Error("Canvas WebGPU context does not support configure().");
@@ -3079,6 +3657,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3079
3657
  const rayQueueBytes = config.tilePixelCapacity * RAY_RECORD_BYTES;
3080
3658
  const hitBytes = config.tilePixelCapacity * HIT_RECORD_BYTES;
3081
3659
  const accumulationBytes = config.tilePixelCapacity * ACCUMULATION_RECORD_BYTES;
3660
+ const pathVertexBytes = config.tilePixelCapacity * (config.maxDepth + 1) * PATH_VERTEX_RECORD_BYTES;
3082
3661
  const activeQueue = createBuffer(device, bufferUsage, rayQueueBytes, "plasius.wavefront.activeQueue");
3083
3662
  const nextQueue = createBuffer(device, bufferUsage, rayQueueBytes, "plasius.wavefront.nextQueue");
3084
3663
  const hitBuffer = createBuffer(device, bufferUsage, hitBytes, "plasius.wavefront.hitBuffer");
@@ -3088,6 +3667,12 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3088
3667
  accumulationBytes,
3089
3668
  "plasius.wavefront.accumulation"
3090
3669
  );
3670
+ const pathVertexBuffer = createBuffer(
3671
+ device,
3672
+ bufferUsage,
3673
+ pathVertexBytes,
3674
+ "plasius.wavefront.pathVertices"
3675
+ );
3091
3676
  const sceneObjectBuffer = createBuffer(
3092
3677
  device,
3093
3678
  constants.buffer.STORAGE | constants.buffer.COPY_DST,
@@ -3142,9 +3727,10 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3142
3727
  CONFIG_BUFFER_BYTES,
3143
3728
  Number.isFinite(uniformOffsetAlignment) && uniformOffsetAlignment > 0 ? uniformOffsetAlignment : CONFIG_BUFFER_BYTES
3144
3729
  );
3730
+ const outputConfigSlotCount = config.deferredPathResolve ? 0 : tiles.length;
3145
3731
  const frameConfigSlotCount = Math.max(
3146
3732
  1,
3147
- tiles.length * config.samplesPerPixel + tiles.length + (config.denoise ? 1 : 0)
3733
+ tiles.length * config.samplesPerPixel + outputConfigSlotCount + (config.denoise ? 1 : 0)
3148
3734
  );
3149
3735
  const configBuffer = createBuffer(
3150
3736
  device,
@@ -3222,6 +3808,12 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3222
3808
  magFilter: "nearest",
3223
3809
  minFilter: "nearest"
3224
3810
  });
3811
+ const environmentMapResource = createEnvironmentMapResource(
3812
+ device,
3813
+ constants,
3814
+ config.environmentMap,
3815
+ config.environmentColor
3816
+ );
3225
3817
  const traceBindGroupLayout = device.createBindGroupLayout({
3226
3818
  label: "plasius.wavefront.traceBindGroupLayout",
3227
3819
  entries: [
@@ -3248,7 +3840,10 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3248
3840
  visibility: constants.shader.COMPUTE,
3249
3841
  storageTexture: { access: "write-only", format: "rgba16float" }
3250
3842
  },
3251
- { binding: 19, visibility: constants.shader.COMPUTE, buffer: { type: "read-only-storage" } }
3843
+ { binding: 19, visibility: constants.shader.COMPUTE, buffer: { type: "read-only-storage" } },
3844
+ { binding: 20, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
3845
+ { binding: 21, visibility: constants.shader.COMPUTE, sampler: { type: "filtering" } },
3846
+ { binding: 22, visibility: constants.shader.COMPUTE, buffer: { type: "storage" } }
3252
3847
  ]
3253
3848
  });
3254
3849
  const accelerationBindGroupLayout = device.createBindGroupLayout({
@@ -3422,7 +4017,10 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3422
4017
  { binding: 8, resource: { buffer: triangleBuffer } },
3423
4018
  { binding: 9, resource: { buffer: bvhNodeBuffer } },
3424
4019
  { binding: 16, resource: radianceView },
3425
- { binding: 19, resource: { buffer: environmentPortalBuffer } }
4020
+ { binding: 19, resource: { buffer: environmentPortalBuffer } },
4021
+ { binding: 20, resource: environmentMapResource.view },
4022
+ { binding: 21, resource: environmentMapResource.sampler },
4023
+ { binding: 22, resource: { buffer: pathVertexBuffer } }
3426
4024
  ]
3427
4025
  });
3428
4026
  }
@@ -3511,6 +4109,11 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3511
4109
  let frame = 0;
3512
4110
  let accelerationBuilt = !config.gpuAccelerationBuildRequired;
3513
4111
  let accelerationBuildCount = 0;
4112
+ let activeCameraOptions = options.camera ?? DEFAULT_CAMERA;
4113
+ let lastGpuParallelism = createGpuParallelismDiagnostics(
4114
+ gpuAdapterParallelism,
4115
+ createGpuParallelismCounters()
4116
+ );
3514
4117
  function createFrameConfigWriter(frameIndex) {
3515
4118
  let slot = 0;
3516
4119
  return (tile, buildRange = {}) => {
@@ -3527,7 +4130,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3527
4130
  return offset;
3528
4131
  };
3529
4132
  }
3530
- function dispatchGpuAccelerationBuild(frameIndex) {
4133
+ function dispatchGpuAccelerationBuild(frameIndex, parallelism) {
3531
4134
  if (!config.gpuAccelerationBuildRequired || accelerationBuilt) {
3532
4135
  return false;
3533
4136
  }
@@ -3566,24 +4169,32 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3566
4169
  });
3567
4170
  passEncoder.setBindGroup(0, bvhBuildBindGroup, [0]);
3568
4171
  passEncoder.setPipeline(pipelines.prepareMeshTrianglesAndLeaves);
3569
- passEncoder.dispatchWorkgroups(Math.ceil(config.bvhLeafSortCapacity / WORKGROUP_SIZE));
4172
+ const prepareWorkgroups = Math.ceil(config.bvhLeafSortCapacity / WORKGROUP_SIZE);
4173
+ passEncoder.dispatchWorkgroups(prepareWorkgroups);
4174
+ recordDirectDispatch(parallelism, [prepareWorkgroups]);
3570
4175
  passEncoder.setPipeline(pipelines.sortBvhLeafRefs);
3571
4176
  for (let stageIndex = 0; stageIndex < config.bvhSortStages.length; stageIndex += 1) {
3572
4177
  passEncoder.setBindGroup(0, bvhBuildBindGroup, [
3573
4178
  (stageIndex + 1) * configBufferStride
3574
4179
  ]);
3575
- passEncoder.dispatchWorkgroups(Math.ceil(config.bvhLeafSortCapacity / WORKGROUP_SIZE));
4180
+ const sortWorkgroups = Math.ceil(config.bvhLeafSortCapacity / WORKGROUP_SIZE);
4181
+ passEncoder.dispatchWorkgroups(sortWorkgroups);
4182
+ recordDirectDispatch(parallelism, [sortWorkgroups]);
3576
4183
  }
3577
4184
  passEncoder.setBindGroup(0, bvhBuildBindGroup, [0]);
3578
4185
  passEncoder.setPipeline(pipelines.writeSortedBvhLeaves);
3579
- passEncoder.dispatchWorkgroups(Math.ceil(config.triangleCount / WORKGROUP_SIZE));
4186
+ const leafWriteWorkgroups = Math.ceil(config.triangleCount / WORKGROUP_SIZE);
4187
+ passEncoder.dispatchWorkgroups(leafWriteWorkgroups);
4188
+ recordDirectDispatch(parallelism, [leafWriteWorkgroups]);
3580
4189
  passEncoder.setPipeline(pipelines.buildBvhInternalLevel);
3581
4190
  for (let levelIndex = 0; levelIndex < config.bvhBuildLevels.length; levelIndex += 1) {
3582
4191
  const buildLevel = config.bvhBuildLevels[levelIndex];
3583
4192
  passEncoder.setBindGroup(0, bvhBuildBindGroup, [
3584
4193
  (buildLevelConfigStart + levelIndex) * configBufferStride
3585
4194
  ]);
3586
- passEncoder.dispatchWorkgroups(Math.ceil(buildLevel.count / WORKGROUP_SIZE));
4195
+ const levelWorkgroups = Math.ceil(buildLevel.count / WORKGROUP_SIZE);
4196
+ passEncoder.dispatchWorkgroups(levelWorkgroups);
4197
+ recordDirectDispatch(parallelism, [levelWorkgroups]);
3587
4198
  }
3588
4199
  passEncoder.end();
3589
4200
  device.queue.submit([encoder.finish()]);
@@ -3591,7 +4202,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3591
4202
  accelerationBuildCount += 1;
3592
4203
  return true;
3593
4204
  }
3594
- function encodeTileSample(encoder, tile, configOffset) {
4205
+ function encodeTileSample(encoder, tile, configOffset, parallelism) {
3595
4206
  const generatePass = encoder.beginComputePass({
3596
4207
  label: "plasius.wavefront.generatePrimaryRaysPass"
3597
4208
  });
@@ -3599,6 +4210,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3599
4210
  generatePass.setBindGroup(0, bindGroups[0], [configOffset]);
3600
4211
  generatePass.setPipeline(pipelines.generatePrimaryRays);
3601
4212
  generatePass.dispatchWorkgroups(tileWorkgroups);
4213
+ recordDirectDispatch(parallelism, [tileWorkgroups]);
3602
4214
  generatePass.end();
3603
4215
  for (let bounceIndex = 0; bounceIndex < config.maxDepth; bounceIndex += 1) {
3604
4216
  encoder.copyBufferToBuffer(
@@ -3614,14 +4226,17 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3614
4226
  passEncoder.setBindGroup(0, bindGroups[bounceIndex % 2], [configOffset]);
3615
4227
  passEncoder.setPipeline(pipelines.intersectActiveQueue);
3616
4228
  passEncoder.dispatchWorkgroupsIndirect(activeDispatchBuffer, 0);
4229
+ recordIndirectDispatch(parallelism, tileWorkgroups);
3617
4230
  passEncoder.setPipeline(pipelines.resolveSurfaceRecords);
3618
4231
  passEncoder.dispatchWorkgroupsIndirect(activeDispatchBuffer, 0);
4232
+ recordIndirectDispatch(parallelism, tileWorkgroups);
3619
4233
  passEncoder.setPipeline(pipelines.compactAndSwapQueues);
3620
4234
  passEncoder.dispatchWorkgroups(1);
4235
+ recordDirectDispatch(parallelism, [1], 1);
3621
4236
  passEncoder.end();
3622
4237
  }
3623
4238
  }
3624
- function encodeTileOutput(encoder, tile, configOffset) {
4239
+ function encodeTileOutput(encoder, tile, configOffset, parallelism) {
3625
4240
  const passEncoder = encoder.beginComputePass({
3626
4241
  label: "plasius.wavefront.outputPass"
3627
4242
  });
@@ -3629,25 +4244,30 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3629
4244
  passEncoder.setBindGroup(0, bindGroups[0], [configOffset]);
3630
4245
  passEncoder.setPipeline(pipelines.accumulateTerminalRadiance);
3631
4246
  passEncoder.dispatchWorkgroups(tileWorkgroups);
4247
+ recordDirectDispatch(parallelism, [tileWorkgroups]);
3632
4248
  passEncoder.end();
3633
4249
  }
3634
- function encodeDenoise(encoder, configOffset) {
4250
+ function encodeDenoise(encoder, configOffset, parallelism) {
3635
4251
  if (!config.denoise) {
3636
4252
  return;
3637
4253
  }
4254
+ const denoiseWorkgroupsX = Math.ceil(config.width / 8);
4255
+ const denoiseWorkgroupsY = Math.ceil(config.height / 8);
3638
4256
  const radiancePass = encoder.beginComputePass({
3639
4257
  label: "plasius.wavefront.denoiseRadiancePass"
3640
4258
  });
3641
4259
  radiancePass.setBindGroup(0, denoiseRadianceBindGroup, [configOffset]);
3642
4260
  radiancePass.setPipeline(pipelines.denoiseLinearRadiance);
3643
- radiancePass.dispatchWorkgroups(Math.ceil(config.width / 8), Math.ceil(config.height / 8));
4261
+ radiancePass.dispatchWorkgroups(denoiseWorkgroupsX, denoiseWorkgroupsY);
4262
+ recordDirectDispatch(parallelism, [denoiseWorkgroupsX, denoiseWorkgroupsY]);
3644
4263
  radiancePass.end();
3645
4264
  const resolvePass = encoder.beginComputePass({
3646
4265
  label: "plasius.wavefront.denoiseResolvePass"
3647
4266
  });
3648
4267
  resolvePass.setBindGroup(0, denoiseResolveBindGroup, [configOffset]);
3649
4268
  resolvePass.setPipeline(pipelines.resolveDenoisedOutputImage);
3650
- resolvePass.dispatchWorkgroups(Math.ceil(config.width / 8), Math.ceil(config.height / 8));
4269
+ resolvePass.dispatchWorkgroups(denoiseWorkgroupsX, denoiseWorkgroupsY);
4270
+ recordDirectDispatch(parallelism, [denoiseWorkgroupsX, denoiseWorkgroupsY]);
3651
4271
  resolvePass.end();
3652
4272
  }
3653
4273
  function encodePresent(encoder) {
@@ -3668,41 +4288,68 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3668
4288
  passEncoder.draw(3);
3669
4289
  passEncoder.end();
3670
4290
  }
3671
- function dispatchFrame(frameIndex) {
4291
+ function dispatchFrame(frameIndex, parallelism) {
3672
4292
  const writeFrameConfig = createFrameConfigWriter(frameIndex);
3673
- const encoder = device.createCommandEncoder({
3674
- label: `plasius.wavefront.frame.${frameIndex}.batched`
4293
+ let submissionCount = 0;
4294
+ let encodedFramePasses = 0;
4295
+ let encoder = device.createCommandEncoder({
4296
+ label: `plasius.wavefront.frame.${frameIndex}.batched.${submissionCount + 1}`
3675
4297
  });
4298
+ function submitCurrentEncoder() {
4299
+ if (encodedFramePasses <= 0) {
4300
+ return;
4301
+ }
4302
+ device.queue.submit([encoder.finish()]);
4303
+ submissionCount += 1;
4304
+ encodedFramePasses = 0;
4305
+ encoder = device.createCommandEncoder({
4306
+ label: `plasius.wavefront.frame.${frameIndex}.batched.${submissionCount + 1}`
4307
+ });
4308
+ }
4309
+ function reserveEncoder(passCount = 1) {
4310
+ if (encodedFramePasses > 0 && encodedFramePasses + passCount > config.maxFramePassesPerSubmission) {
4311
+ submitCurrentEncoder();
4312
+ }
4313
+ encodedFramePasses += passCount;
4314
+ return encoder;
4315
+ }
3676
4316
  for (const tile of tiles) {
3677
4317
  for (let sampleIndex = 0; sampleIndex < config.samplesPerPixel; sampleIndex += 1) {
3678
4318
  const configOffset = writeFrameConfig(tile, {
3679
4319
  sampleIndex,
3680
4320
  sampleWeight: 1 / config.samplesPerPixel
3681
4321
  });
3682
- encodeTileSample(encoder, tile, configOffset);
4322
+ encodeTileSample(reserveEncoder(), tile, configOffset, parallelism);
4323
+ if (config.deferredPathResolve) {
4324
+ encodeTileOutput(reserveEncoder(), tile, configOffset, parallelism);
4325
+ }
4326
+ }
4327
+ if (!config.deferredPathResolve) {
4328
+ const outputConfigOffset = writeFrameConfig(tile, {
4329
+ sampleIndex: 0,
4330
+ sampleWeight: 1 / config.samplesPerPixel
4331
+ });
4332
+ encodeTileOutput(reserveEncoder(), tile, outputConfigOffset, parallelism);
3683
4333
  }
3684
- const outputConfigOffset = writeFrameConfig(tile, {
3685
- sampleIndex: 0,
3686
- sampleWeight: 1 / config.samplesPerPixel
3687
- });
3688
- encodeTileOutput(encoder, tile, outputConfigOffset);
3689
4334
  }
3690
4335
  if (config.denoise) {
3691
4336
  const denoiseConfigOffset = writeFrameConfig(
3692
4337
  { x: 0, y: 0, width: config.width, height: config.height },
3693
4338
  { sampleIndex: 0, sampleWeight: 1 / config.samplesPerPixel }
3694
4339
  );
3695
- encodeDenoise(encoder, denoiseConfigOffset);
4340
+ encodeDenoise(reserveEncoder(), denoiseConfigOffset, parallelism);
3696
4341
  }
3697
- encodePresent(encoder);
3698
- device.queue.submit([encoder.finish()]);
3699
- return 1;
4342
+ encodePresent(reserveEncoder());
4343
+ submitCurrentEncoder();
4344
+ return submissionCount;
3700
4345
  }
3701
4346
  function renderOnce() {
3702
4347
  frame += 1;
3703
4348
  const frameIndex = frame + config.frameIndex;
3704
- const accelerationBuildSubmitted = dispatchGpuAccelerationBuild(frameIndex);
3705
- const frameSubmissionCount = dispatchFrame(frameIndex);
4349
+ const parallelismCounters = createGpuParallelismCounters();
4350
+ const accelerationBuildSubmitted = dispatchGpuAccelerationBuild(frameIndex, parallelismCounters);
4351
+ const frameSubmissionCount = dispatchFrame(frameIndex, parallelismCounters);
4352
+ lastGpuParallelism = createGpuParallelismDiagnostics(gpuAdapterParallelism, parallelismCounters);
3706
4353
  return Object.freeze({
3707
4354
  frame,
3708
4355
  width: config.width,
@@ -3711,6 +4358,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3711
4358
  tiles: tiles.length,
3712
4359
  tileSize: config.tileSize,
3713
4360
  samplesPerPixel: config.samplesPerPixel,
4361
+ maxFramePassesPerSubmission: config.maxFramePassesPerSubmission,
3714
4362
  screenRays: config.width * config.height,
3715
4363
  primaryRays: config.width * config.height * config.samplesPerPixel,
3716
4364
  sceneObjectCount: config.sceneObjectCount,
@@ -3718,6 +4366,8 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3718
4366
  emissiveTriangleCount: config.emissiveTriangleCount,
3719
4367
  environmentPortalCount: config.environmentPortalCount,
3720
4368
  environmentPortalMode: config.environmentPortalMode,
4369
+ environmentMap: createEnvironmentMapSnapshot(config.environmentMap),
4370
+ deferredPathResolve: config.deferredPathResolve,
3721
4371
  bvhNodeCount: config.bvhNodeCount,
3722
4372
  displayQuality: config.displayQuality,
3723
4373
  accelerationBuildMode: config.accelerationBuildMode,
@@ -3727,6 +4377,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3727
4377
  accelerationBuildCount,
3728
4378
  commandSubmissions: frameSubmissionCount + (accelerationBuildSubmitted ? 1 : 0),
3729
4379
  frameConfigSlots: frameConfigSlotCount,
4380
+ gpuParallelism: lastGpuParallelism,
3730
4381
  memory: config.memory
3731
4382
  });
3732
4383
  }
@@ -3797,11 +4448,29 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3797
4448
  samplesPerPixel: config.samplesPerPixel,
3798
4449
  sceneObjectCapacity: config.sceneObjectCapacity,
3799
4450
  sceneObjects: packedScene.objects,
4451
+ camera: activeCameraOptions,
3800
4452
  frameIndex: config.frameIndex
3801
4453
  });
3802
4454
  device.queue.writeBuffer(sceneObjectBuffer, 0, packedScene.buffer);
3803
4455
  return config;
3804
4456
  }
4457
+ function updateCamera(cameraOptions = {}) {
4458
+ activeCameraOptions = cameraOptions;
4459
+ config = createWavefrontPathTracingComputeConfig({
4460
+ ...options,
4461
+ canvas,
4462
+ width: config.width,
4463
+ height: config.height,
4464
+ maxDepth: config.maxDepth,
4465
+ tileSize: config.tileSize,
4466
+ samplesPerPixel: config.samplesPerPixel,
4467
+ sceneObjectCapacity: config.sceneObjectCapacity,
4468
+ sceneObjects: packedScene.objects,
4469
+ camera: activeCameraOptions,
4470
+ frameIndex: config.frameIndex
4471
+ });
4472
+ return config;
4473
+ }
3805
4474
  function getSnapshot() {
3806
4475
  return Object.freeze({
3807
4476
  frame,
@@ -3811,11 +4480,14 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3811
4480
  tiles: tiles.length,
3812
4481
  tileSize: config.tileSize,
3813
4482
  samplesPerPixel: config.samplesPerPixel,
4483
+ maxFramePassesPerSubmission: config.maxFramePassesPerSubmission,
3814
4484
  sceneObjectCount: config.sceneObjectCount,
3815
4485
  triangleCount: config.triangleCount,
3816
4486
  emissiveTriangleCount: config.emissiveTriangleCount,
3817
4487
  environmentPortalCount: config.environmentPortalCount,
3818
4488
  environmentPortalMode: config.environmentPortalMode,
4489
+ environmentMap: createEnvironmentMapSnapshot(config.environmentMap),
4490
+ deferredPathResolve: config.deferredPathResolve,
3819
4491
  bvhNodeCount: config.bvhNodeCount,
3820
4492
  displayQuality: config.displayQuality,
3821
4493
  accelerationBuildMode: config.accelerationBuildMode,
@@ -3823,6 +4495,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3823
4495
  accelerationBuilt,
3824
4496
  accelerationBuildCount,
3825
4497
  frameConfigSlots: frameConfigSlotCount,
4498
+ gpuParallelism: lastGpuParallelism,
3826
4499
  memory: config.memory
3827
4500
  });
3828
4501
  }
@@ -3831,6 +4504,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3831
4504
  nextQueue.destroy?.();
3832
4505
  hitBuffer.destroy?.();
3833
4506
  accumulationBuffer.destroy?.();
4507
+ pathVertexBuffer.destroy?.();
3834
4508
  sceneObjectBuffer.destroy?.();
3835
4509
  triangleBuffer.destroy?.();
3836
4510
  bvhNodeBuffer.destroy?.();
@@ -3846,6 +4520,9 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3846
4520
  radianceTexture.destroy?.();
3847
4521
  denoiseScratchTexture.destroy?.();
3848
4522
  outputTexture.destroy?.();
4523
+ if (environmentMapResource.ownsTexture) {
4524
+ environmentMapResource.texture?.destroy?.();
4525
+ }
3849
4526
  context.unconfigure?.();
3850
4527
  }
3851
4528
  return Object.freeze({
@@ -3858,6 +4535,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3858
4535
  renderFrame,
3859
4536
  readOutputProbe,
3860
4537
  updateSceneObjects,
4538
+ updateCamera,
3861
4539
  getSnapshot,
3862
4540
  destroy
3863
4541
  });