@plasius/gpu-renderer 0.1.14 → 0.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -69,6 +69,7 @@ var DEFAULT_MAX_DEPTH = 6;
69
69
  var DEFAULT_TILE_SIZE = 128;
70
70
  var DEFAULT_SAMPLES_PER_PIXEL = 1;
71
71
  var DEFAULT_SCENE_OBJECT_CAPACITY = 128;
72
+ var DEFAULT_ENVIRONMENT_PORTAL_CAPACITY = 32;
72
73
  var WORKGROUP_SIZE = 64;
73
74
  var RAY_RECORD_BYTES = 80;
74
75
  var HIT_RECORD_BYTES = 208;
@@ -79,9 +80,11 @@ var TRIANGLE_RECORD_BYTES = 208;
79
80
  var BVH_NODE_RECORD_BYTES = 48;
80
81
  var BVH_LEAF_REF_RECORD_BYTES = 16;
81
82
  var EMISSIVE_TRIANGLE_INDEX_BYTES = 4;
83
+ var ENVIRONMENT_PORTAL_RECORD_BYTES = 96;
82
84
  var ACCUMULATION_RECORD_BYTES = 16;
83
- var CONFIG_BUFFER_BYTES = 256;
85
+ var CONFIG_BUFFER_BYTES = 272;
84
86
  var COUNTER_BUFFER_BYTES = 16;
87
+ var TRACE_STORAGE_BUFFER_BINDINGS = 9;
85
88
  var MATERIAL_DIFFUSE = 0;
86
89
  var MATERIAL_METAL = 1;
87
90
  var MATERIAL_DIELECTRIC = 2;
@@ -108,6 +111,7 @@ var DEFAULT_ENVIRONMENT_LIGHTING = Object.freeze({
108
111
  });
109
112
  var wavefrontPathTracingComputeLimits = Object.freeze({
110
113
  workgroupSize: WORKGROUP_SIZE,
114
+ traceStorageBufferBindings: TRACE_STORAGE_BUFFER_BINDINGS,
111
115
  rayRecordBytes: RAY_RECORD_BYTES,
112
116
  hitRecordBytes: HIT_RECORD_BYTES,
113
117
  sceneObjectRecordBytes: SCENE_OBJECT_RECORD_BYTES,
@@ -118,6 +122,7 @@ var wavefrontPathTracingComputeLimits = Object.freeze({
118
122
  bvhLeafReferenceRecordBytes: BVH_LEAF_REF_RECORD_BYTES,
119
123
  emissiveTriangleIndexBytes: EMISSIVE_TRIANGLE_INDEX_BYTES,
120
124
  emissiveTriangleMetadataRecordBytes: BVH_NODE_RECORD_BYTES,
125
+ environmentPortalRecordBytes: ENVIRONMENT_PORTAL_RECORD_BYTES,
121
126
  accumulationRecordBytes: ACCUMULATION_RECORD_BYTES
122
127
  });
123
128
  var wavefrontSceneObjectKinds = Object.freeze({
@@ -209,6 +214,9 @@ function subtract(a, b) {
209
214
  function scale(a, scalar) {
210
215
  return [a[0] * scalar, a[1] * scalar, a[2] * scalar];
211
216
  }
217
+ function dot(a, b) {
218
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
219
+ }
212
220
  function cross(a, b) {
213
221
  return [
214
222
  a[1] * b[2] - a[2] * b[1],
@@ -821,6 +829,127 @@ function resolveEnvironmentLighting(input, environmentColor, ambientColor) {
821
829
  exposure: Math.max(1e-4, readFiniteNumber("environmentLighting.exposure", source.exposure, DEFAULT_ENVIRONMENT_LIGHTING.exposure))
822
830
  });
823
831
  }
832
+ function resolveEnvironmentPortalMode(value, hasPortals) {
833
+ if (value === void 0 || value === null) {
834
+ return hasPortals ? 2 : 0;
835
+ }
836
+ if (Number.isInteger(value) && value >= 0 && value <= 2) {
837
+ return value;
838
+ }
839
+ if (value === "disabled") {
840
+ return 0;
841
+ }
842
+ if (value === "guide") {
843
+ return 1;
844
+ }
845
+ if (value === "guide-and-gate" || value === "gate") {
846
+ return 2;
847
+ }
848
+ throw new Error(
849
+ "environmentPortalMode must be disabled, guide, guide-and-gate, or an integer between 0 and 2."
850
+ );
851
+ }
852
+ function orthogonalPortalTangent(normal) {
853
+ if (Math.abs(normal[1]) < 0.92) {
854
+ return normalize(cross([0, 1, 0], normal), [1, 0, 0]);
855
+ }
856
+ return normalize(cross([1, 0, 0], normal), [0, 0, 1]);
857
+ }
858
+ function resolvePortalTangent(value, normal) {
859
+ const fallback = orthogonalPortalTangent(normal);
860
+ const tangent = asUnitVec3(value, fallback);
861
+ const projected = subtract(tangent, scale(normal, dot(tangent, normal)));
862
+ return normalize(projected, fallback);
863
+ }
864
+ function readPositiveFiniteNumber(name, value, fallback) {
865
+ const numeric = readFiniteNumber(name, value, fallback);
866
+ if (numeric <= 0) {
867
+ throw new Error(`${name} must be a positive finite number.`);
868
+ }
869
+ return numeric;
870
+ }
871
+ function readPortalExtent(name, value, halfName, halfValue) {
872
+ if (value !== void 0 && value !== null) {
873
+ return readPositiveFiniteNumber(name, value, 1);
874
+ }
875
+ return readPositiveFiniteNumber(halfName, halfValue, 0.5) * 2;
876
+ }
877
+ function normalizeEnvironmentPortal(portal, index) {
878
+ if (!portal || typeof portal !== "object") {
879
+ throw new Error(`environmentPortals[${index}] must be an object.`);
880
+ }
881
+ const shape = portal.shape ?? portal.kind ?? "rectangle";
882
+ if (shape !== "rectangle") {
883
+ throw new Error(`environmentPortals[${index}].shape must be "rectangle".`);
884
+ }
885
+ const position = asVec3(portal.position ?? portal.center, [0, 0, 0]);
886
+ const normal = asUnitVec3(portal.normal, [0, 0, 1]);
887
+ const tangent = resolvePortalTangent(portal.tangent, normal);
888
+ const bitangent = normalize(cross(normal, tangent), [0, 1, 0]);
889
+ const width = readPortalExtent(
890
+ `environmentPortals[${index}].width`,
891
+ portal.width,
892
+ `environmentPortals[${index}].halfWidth`,
893
+ portal.halfWidth
894
+ );
895
+ const height = readPortalExtent(
896
+ `environmentPortals[${index}].height`,
897
+ portal.height,
898
+ `environmentPortals[${index}].halfHeight`,
899
+ portal.halfHeight
900
+ );
901
+ const radianceScale = Math.max(
902
+ 0,
903
+ readFiniteNumber(
904
+ `environmentPortals[${index}].radianceScale`,
905
+ portal.radianceScale ?? portal.intensity,
906
+ 1
907
+ )
908
+ );
909
+ return Object.freeze({
910
+ kind: 1,
911
+ flags: portal.twoSided === false ? 0 : 1,
912
+ position: Object.freeze([position[0], position[1], position[2], width * height]),
913
+ normal: Object.freeze([normal[0], normal[1], normal[2], radianceScale]),
914
+ tangent: Object.freeze([tangent[0], tangent[1], tangent[2], width * 0.5]),
915
+ bitangent: Object.freeze([bitangent[0], bitangent[1], bitangent[2], height * 0.5]),
916
+ color: Object.freeze(asColor(portal.color, [1, 1, 1, 1]))
917
+ });
918
+ }
919
+ function normalizeEnvironmentPortals(value) {
920
+ if (value === void 0 || value === null) {
921
+ return Object.freeze([]);
922
+ }
923
+ if (!Array.isArray(value)) {
924
+ throw new Error("environmentPortals must be an array when provided.");
925
+ }
926
+ return Object.freeze(value.map(normalizeEnvironmentPortal));
927
+ }
928
+ function packEnvironmentPortals(portals, capacity) {
929
+ const bytes = new ArrayBuffer(capacity * ENVIRONMENT_PORTAL_RECORD_BYTES);
930
+ const data = new DataView(bytes);
931
+ const floatView = new Float32Array(bytes);
932
+ portals.forEach((portal, index) => {
933
+ const byteOffset = index * ENVIRONMENT_PORTAL_RECORD_BYTES;
934
+ const floatOffset = byteOffset / Float32Array.BYTES_PER_ELEMENT;
935
+ data.setUint32(byteOffset, portal.kind, true);
936
+ data.setUint32(byteOffset + 4, portal.flags, true);
937
+ data.setUint32(byteOffset + 8, 0, true);
938
+ data.setUint32(byteOffset + 12, 0, true);
939
+ writeVec4(floatView, floatOffset + 4, portal.position);
940
+ writeVec4(floatView, floatOffset + 8, portal.normal);
941
+ writeVec4(floatView, floatOffset + 12, portal.tangent);
942
+ writeVec4(floatView, floatOffset + 16, portal.bitangent);
943
+ writeVec4(floatView, floatOffset + 20, portal.color);
944
+ });
945
+ return Object.freeze({
946
+ buffer: bytes,
947
+ portals,
948
+ count: portals.length,
949
+ capacity,
950
+ recordBytes: ENVIRONMENT_PORTAL_RECORD_BYTES
951
+ });
952
+ }
824
953
  function getCanvasDimension(canvas, key, fallback) {
825
954
  const value = Number(canvas?.[key]);
826
955
  if (Number.isFinite(value) && value > 0) {
@@ -876,6 +1005,11 @@ function estimateWavefrontPathTracingMemory(options = {}) {
876
1005
  options.emissiveTriangleCapacity,
877
1006
  0
878
1007
  );
1008
+ const environmentPortalCapacity = readNonNegativeInteger(
1009
+ "environmentPortalCapacity",
1010
+ options.environmentPortalCapacity,
1011
+ 0
1012
+ );
879
1013
  const queueBytes = tilePixelCapacity * RAY_RECORD_BYTES;
880
1014
  const hitBytes = tilePixelCapacity * HIT_RECORD_BYTES;
881
1015
  const accumulationBytes = tilePixelCapacity * ACCUMULATION_RECORD_BYTES;
@@ -884,6 +1018,7 @@ function estimateWavefrontPathTracingMemory(options = {}) {
884
1018
  const bvhNodeBytes = bvhNodeCapacity * BVH_NODE_RECORD_BYTES;
885
1019
  const bvhLeafReferenceBytes = bvhLeafSortCapacity * BVH_LEAF_REF_RECORD_BYTES;
886
1020
  const emissiveTriangleMetadataBytes = emissiveTriangleCapacity * BVH_NODE_RECORD_BYTES;
1021
+ const environmentPortalBytes = environmentPortalCapacity * ENVIRONMENT_PORTAL_RECORD_BYTES;
887
1022
  return Object.freeze({
888
1023
  queueBytes,
889
1024
  queuePairBytes: queueBytes * 2,
@@ -894,9 +1029,10 @@ function estimateWavefrontPathTracingMemory(options = {}) {
894
1029
  bvhNodeBytes,
895
1030
  bvhLeafReferenceBytes,
896
1031
  emissiveTriangleMetadataBytes,
1032
+ environmentPortalBytes,
897
1033
  configBytes: CONFIG_BUFFER_BYTES,
898
1034
  counterBytes: COUNTER_BUFFER_BYTES,
899
- totalHotBufferBytes: queueBytes * 2 + hitBytes + accumulationBytes + sceneObjectBytes + triangleBytes + bvhNodeBytes + bvhLeafReferenceBytes + emissiveTriangleMetadataBytes + CONFIG_BUFFER_BYTES + COUNTER_BUFFER_BYTES
1035
+ totalHotBufferBytes: queueBytes * 2 + hitBytes + accumulationBytes + sceneObjectBytes + triangleBytes + bvhNodeBytes + bvhLeafReferenceBytes + emissiveTriangleMetadataBytes + environmentPortalBytes + CONFIG_BUFFER_BYTES + COUNTER_BUFFER_BYTES
900
1036
  });
901
1037
  }
902
1038
  function createWavefrontPathTracingComputeConfig(options = {}) {
@@ -957,6 +1093,21 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
957
1093
  environmentColor,
958
1094
  ambientColor
959
1095
  );
1096
+ const environmentPortals = normalizeEnvironmentPortals(
1097
+ options.environmentPortals ?? options.environmentLightPortals ?? options.environmentLighting?.environmentPortals
1098
+ );
1099
+ const environmentPortalCapacity = Math.max(
1100
+ environmentPortals.length,
1101
+ readNonNegativeInteger(
1102
+ "environmentPortalCapacity",
1103
+ options.environmentPortalCapacity,
1104
+ DEFAULT_ENVIRONMENT_PORTAL_CAPACITY
1105
+ )
1106
+ );
1107
+ const environmentPortalMode = resolveEnvironmentPortalMode(
1108
+ options.environmentPortalMode ?? options.portalMode ?? options.environmentLighting?.environmentPortalMode,
1109
+ environmentPortals.length > 0
1110
+ );
960
1111
  return Object.freeze({
961
1112
  width,
962
1113
  height,
@@ -985,6 +1136,10 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
985
1136
  environmentColor: environmentLighting.environmentColor,
986
1137
  ambientColor: environmentLighting.ambientColor,
987
1138
  environmentLighting,
1139
+ environmentPortals,
1140
+ environmentPortalCount: environmentPortals.length,
1141
+ environmentPortalCapacity,
1142
+ environmentPortalMode,
988
1143
  displayQuality: options.displayQuality === true,
989
1144
  requiresMeshBvhForDisplayQuality: true,
990
1145
  denoise: options.denoise !== false,
@@ -995,7 +1150,8 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
995
1150
  triangleCapacity,
996
1151
  bvhNodeCapacity,
997
1152
  bvhLeafSortCapacity,
998
- emissiveTriangleCapacity: emissiveTriangleIndices.capacity
1153
+ emissiveTriangleCapacity: emissiveTriangleIndices.capacity,
1154
+ environmentPortalCapacity
999
1155
  })
1000
1156
  });
1001
1157
  }
@@ -1179,6 +1335,10 @@ function createConfigPayload(config, tile, frameIndex, buildRange = {}) {
1179
1335
  data.setUint32(244, buildRange.count ?? 0, true);
1180
1336
  data.setUint32(248, buildRange.sortItemCount ?? 0, true);
1181
1337
  data.setUint32(252, config.emissiveTriangleCount ?? 0, true);
1338
+ data.setUint32(256, config.environmentPortalCount ?? 0, true);
1339
+ data.setUint32(260, config.environmentPortalMode ?? 0, true);
1340
+ data.setUint32(264, 0, true);
1341
+ data.setUint32(268, 0, true);
1182
1342
  return bytes;
1183
1343
  }
1184
1344
  function createTiles(width, height, tileSize) {
@@ -1425,6 +1585,10 @@ struct FrameConfig {
1425
1585
  bvhBuildNodeCount: u32,
1426
1586
  bvhSortItemCount: u32,
1427
1587
  emissiveTriangleCount: u32,
1588
+ environmentPortalCount: u32,
1589
+ environmentPortalMode: u32,
1590
+ _portalPad0: u32,
1591
+ _portalPad1: u32,
1428
1592
  };
1429
1593
 
1430
1594
  struct Counters {
@@ -1448,6 +1612,18 @@ struct Candidate {
1448
1612
  mediumRefId: u32,
1449
1613
  };
1450
1614
 
1615
+ struct EnvironmentPortal {
1616
+ kind: u32,
1617
+ flags: u32,
1618
+ _pad0: u32,
1619
+ _pad1: u32,
1620
+ position: vec4<f32>,
1621
+ normal: vec4<f32>,
1622
+ tangent: vec4<f32>,
1623
+ bitangent: vec4<f32>,
1624
+ color: vec4<f32>,
1625
+ };
1626
+
1451
1627
  @group(0) @binding(0) var<storage, read_write> activeQueue: array<RayRecord>;
1452
1628
  @group(0) @binding(1) var<storage, read_write> nextQueue: array<RayRecord>;
1453
1629
  @group(0) @binding(2) var<storage, read_write> hits: array<HitRecord>;
@@ -1467,6 +1643,7 @@ struct Candidate {
1467
1643
  @group(0) @binding(16) var radianceImage: texture_storage_2d<rgba16float, write>;
1468
1644
  @group(0) @binding(17) var finalDenoiseInputRadiance: texture_2d<f32>;
1469
1645
  @group(0) @binding(18) var denoisedOutputImage: texture_storage_2d<rgba8unorm, write>;
1646
+ @group(0) @binding(19) var<storage, read> environmentPortals: array<EnvironmentPortal>;
1470
1647
 
1471
1648
  fn hash_u32(value: u32) -> u32 {
1472
1649
  var x = value;
@@ -1507,7 +1684,48 @@ fn saturate(value: f32) -> f32 {
1507
1684
  return clamp(value, 0.0, 1.0);
1508
1685
  }
1509
1686
 
1510
- fn environment_radiance(direction: vec3<f32>) -> vec3<f32> {
1687
+ fn max_component(value: vec3<f32>) -> f32 {
1688
+ return max(max(value.x, value.y), value.z);
1689
+ }
1690
+
1691
+ fn environment_portal_radiance_scale(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
1692
+ if (config.environmentPortalCount == 0u || config.environmentPortalMode == 0u) {
1693
+ return vec3<f32>(1.0);
1694
+ }
1695
+ var scale = vec3<f32>(0.0);
1696
+ for (var portalIndex = 0u; portalIndex < config.environmentPortalCount; portalIndex = portalIndex + 1u) {
1697
+ let portal = environmentPortals[portalIndex];
1698
+ if (portal.kind == 1u) {
1699
+ let portalNormal = safe_normalize(portal.normal.xyz, vec3<f32>(0.0, 0.0, 1.0));
1700
+ let denominator = dot(direction, portalNormal);
1701
+ let twoSided = (portal.flags & 1u) != 0u;
1702
+ var facing = abs(denominator) > 0.0001;
1703
+ if (!twoSided && denominator <= 0.0001) {
1704
+ facing = false;
1705
+ }
1706
+ if (facing) {
1707
+ let distance = dot(portal.position.xyz - origin, portalNormal) / denominator;
1708
+ if (distance > 0.001) {
1709
+ let hitPosition = origin + direction * distance;
1710
+ let local = hitPosition - portal.position.xyz;
1711
+ let tangent = safe_normalize(portal.tangent.xyz, vec3<f32>(1.0, 0.0, 0.0));
1712
+ let bitangent = safe_normalize(portal.bitangent.xyz, vec3<f32>(0.0, 1.0, 0.0));
1713
+ let u = dot(local, tangent);
1714
+ let v = dot(local, bitangent);
1715
+ if (abs(u) <= portal.tangent.w && abs(v) <= portal.bitangent.w) {
1716
+ let areaWeight = clamp(sqrt(max(portal.position.w, 0.0001)), 0.25, 4.0);
1717
+ let angleWeight = max(abs(denominator), 0.08);
1718
+ let portalScale = portal.color.rgb * portal.normal.w * portal.color.a * areaWeight * angleWeight;
1719
+ scale = max(scale, portalScale);
1720
+ }
1721
+ }
1722
+ }
1723
+ }
1724
+ }
1725
+ return scale;
1726
+ }
1727
+
1728
+ fn environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
1511
1729
  let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
1512
1730
  let upFactor = saturate(rayDirection.y * 0.5 + 0.5);
1513
1731
  let sunDirection = safe_normalize(
@@ -1518,10 +1736,26 @@ fn environment_radiance(direction: vec3<f32>) -> vec3<f32> {
1518
1736
  let gradient =
1519
1737
  config.environmentHorizonColor.xyz * (1.0 - upFactor) +
1520
1738
  config.environmentZenithColor.xyz * upFactor;
1739
+ let portalScale = environment_portal_radiance_scale(origin, rayDirection);
1740
+ let portalHit = max_component(portalScale) > 0.0001;
1521
1741
  return (
1522
1742
  gradient +
1523
1743
  config.environmentSunColor.xyz * sunGlow
1524
- ) * max(config.environmentSunDirectionIntensity.w, 0.0001);
1744
+ ) *
1745
+ max(config.environmentSunDirectionIntensity.w, 0.0001) *
1746
+ select(vec3<f32>(1.0), portalScale, portalHit);
1747
+ }
1748
+
1749
+ fn gated_environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
1750
+ let portalScale = environment_portal_radiance_scale(origin, safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0)));
1751
+ if (
1752
+ config.environmentPortalCount > 0u &&
1753
+ config.environmentPortalMode == 2u &&
1754
+ max_component(portalScale) <= 0.0001
1755
+ ) {
1756
+ return config.ambientColor.xyz * 0.65;
1757
+ }
1758
+ return environment_radiance(origin, direction);
1525
1759
  }
1526
1760
 
1527
1761
  fn default_mesh_range() -> MeshRange {
@@ -1792,7 +2026,7 @@ fn make_ray(pixelIndex: u32) -> RayRecord {
1792
2026
  }
1793
2027
 
1794
2028
  fn make_miss(ray: RayRecord) -> HitRecord {
1795
- let radiance = environment_radiance(ray.direction.xyz);
2029
+ let radiance = gated_environment_radiance(ray.origin.xyz, ray.direction.xyz);
1796
2030
  return HitRecord(
1797
2031
  ray.rayId,
1798
2032
  ray.sourcePixelId,
@@ -2278,6 +2512,21 @@ fn sample_emissive_triangle_direction(hit: HitRecord, seed: u32, fallback: vec3<
2278
2512
  return safe_normalize(lightPoint - hit.position.xyz, fallback);
2279
2513
  }
2280
2514
 
2515
+ fn sample_environment_portal_direction(hit: HitRecord, seed: u32, fallback: vec3<f32>) -> vec3<f32> {
2516
+ if (config.environmentPortalCount == 0u || config.environmentPortalMode == 0u) {
2517
+ return fallback;
2518
+ }
2519
+ let portalSlot = min(
2520
+ u32(random01(seed + 211u) * f32(config.environmentPortalCount)),
2521
+ config.environmentPortalCount - 1u
2522
+ );
2523
+ let portal = environmentPortals[portalSlot];
2524
+ let u = (random01(seed + 223u) * 2.0 - 1.0) * portal.tangent.w;
2525
+ let v = (random01(seed + 227u) * 2.0 - 1.0) * portal.bitangent.w;
2526
+ let portalTarget = portal.position.xyz + portal.tangent.xyz * u + portal.bitangent.xyz * v;
2527
+ return safe_normalize(portalTarget - hit.position.xyz, fallback);
2528
+ }
2529
+
2281
2530
  fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult {
2282
2531
  let roughness = clamp(hit.material.x, 0.0, 1.0);
2283
2532
  if (hit.materialKind == 1u) {
@@ -2317,8 +2566,17 @@ fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult
2317
2566
  let canSampleLight = dot(hit.shadingNormal.xyz, guidedLight) > -0.04;
2318
2567
  let guideProbability = select(0.38, 0.72, ray.bounce == 0u);
2319
2568
  let useGuidedLight = canSampleLight && random01(seed + 37u) < guideProbability;
2569
+ let guidedPortal = sample_environment_portal_direction(hit, seed, randomDiffuse);
2570
+ let canSamplePortal = dot(hit.shadingNormal.xyz, guidedPortal) > -0.04;
2571
+ let useGuidedPortal =
2572
+ !useGuidedLight &&
2573
+ canSamplePortal &&
2574
+ config.environmentPortalCount > 0u &&
2575
+ config.environmentPortalMode > 0u &&
2576
+ random01(seed + 89u) < 0.58;
2577
+ let guidedDirection = select(randomDiffuse, guidedPortal, useGuidedPortal);
2320
2578
  return ScatterResult(
2321
- vec4<f32>(select(randomDiffuse, guidedLight, useGuidedLight), 0.0),
2579
+ vec4<f32>(select(guidedDirection, guidedLight, useGuidedLight), 0.0),
2322
2580
  select(0u, RAY_FLAG_GUIDED_EMISSIVE, useGuidedLight),
2323
2581
  0u,
2324
2582
  0u,
@@ -2525,6 +2783,29 @@ fn fragmentMain(in: VertexOut) -> @location(0) vec4<f32> {
2525
2783
  return textureSample(renderTexture, renderSampler, in.uv);
2526
2784
  }
2527
2785
  `;
2786
+ function createWavefrontDeviceDescriptor(adapter, options = {}) {
2787
+ const requiredLimits = { ...options.requiredLimits ?? {} };
2788
+ const exposedStorageBufferLimit = Number(adapter?.limits?.maxStorageBuffersPerShaderStage);
2789
+ if (Number.isFinite(exposedStorageBufferLimit)) {
2790
+ if (exposedStorageBufferLimit < TRACE_STORAGE_BUFFER_BINDINGS) {
2791
+ throw new Error(
2792
+ `Wavefront mesh tracing requires maxStorageBuffersPerShaderStage>=${TRACE_STORAGE_BUFFER_BINDINGS}, but this adapter exposes ${exposedStorageBufferLimit}.`
2793
+ );
2794
+ }
2795
+ requiredLimits.maxStorageBuffersPerShaderStage = Math.max(
2796
+ Number(requiredLimits.maxStorageBuffersPerShaderStage ?? 0),
2797
+ TRACE_STORAGE_BUFFER_BINDINGS
2798
+ );
2799
+ }
2800
+ const descriptor = { ...options.deviceDescriptor ?? {} };
2801
+ if (Object.keys(requiredLimits).length > 0) {
2802
+ descriptor.requiredLimits = {
2803
+ ...descriptor.requiredLimits ?? {},
2804
+ ...requiredLimits
2805
+ };
2806
+ }
2807
+ return Object.keys(descriptor).length > 0 ? descriptor : void 0;
2808
+ }
2528
2809
  async function createWavefrontPathTracingComputeRenderer(options = {}) {
2529
2810
  assertAnalyticDisplayQualityPolicy(options);
2530
2811
  const constants = getGpuUsageConstants();
@@ -2543,7 +2824,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
2543
2824
  if (!adapter) {
2544
2825
  throw new Error("Unable to acquire a WebGPU adapter for wavefront path tracing.");
2545
2826
  }
2546
- const device = await adapter.requestDevice();
2827
+ const device = await adapter.requestDevice(createWavefrontDeviceDescriptor(adapter, options));
2547
2828
  const context = canvas.getContext("webgpu");
2548
2829
  if (!context || typeof context.configure !== "function") {
2549
2830
  throw new Error("Canvas WebGPU context does not support configure().");
@@ -2616,23 +2897,34 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
2616
2897
  Math.max(1, config.gpuMeshSource.meshes.count) * MESH_RANGE_RECORD_BYTES,
2617
2898
  "plasius.wavefront.meshRanges"
2618
2899
  );
2900
+ const environmentPortalBuffer = createBuffer(
2901
+ device,
2902
+ constants.buffer.STORAGE | constants.buffer.COPY_DST,
2903
+ Math.max(1, config.environmentPortalCapacity) * ENVIRONMENT_PORTAL_RECORD_BYTES,
2904
+ "plasius.wavefront.environmentPortals"
2905
+ );
2619
2906
  const bvhLeafRefBuffer = createBuffer(
2620
2907
  device,
2621
2908
  constants.buffer.STORAGE | constants.buffer.COPY_DST,
2622
2909
  Math.max(1, config.bvhLeafSortCapacity) * BVH_LEAF_REF_RECORD_BYTES,
2623
2910
  "plasius.wavefront.bvhLeafRefs"
2624
2911
  );
2625
- const configBuffer = createBuffer(
2626
- device,
2627
- constants.buffer.UNIFORM | constants.buffer.COPY_DST,
2628
- CONFIG_BUFFER_BYTES,
2629
- "plasius.wavefront.frameConfig"
2630
- );
2912
+ const tiles = createTiles(config.width, config.height, config.tileSize);
2631
2913
  const uniformOffsetAlignment = Number(device?.limits?.minUniformBufferOffsetAlignment);
2632
2914
  const configBufferStride = alignTo(
2633
2915
  CONFIG_BUFFER_BYTES,
2634
2916
  Number.isFinite(uniformOffsetAlignment) && uniformOffsetAlignment > 0 ? uniformOffsetAlignment : CONFIG_BUFFER_BYTES
2635
2917
  );
2918
+ const frameConfigSlotCount = Math.max(
2919
+ 1,
2920
+ tiles.length * config.samplesPerPixel + tiles.length + (config.denoise ? 1 : 0)
2921
+ );
2922
+ const configBuffer = createBuffer(
2923
+ device,
2924
+ constants.buffer.UNIFORM | constants.buffer.COPY_DST,
2925
+ frameConfigSlotCount * configBufferStride,
2926
+ "plasius.wavefront.frameConfig"
2927
+ );
2636
2928
  const bvhBuildConfigSlots = 1 + config.bvhSortStages.length + config.bvhBuildLevels.length;
2637
2929
  const bvhBuildConfigBuffer = createBuffer(
2638
2930
  device,
@@ -2666,6 +2958,11 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
2666
2958
  device.queue.writeBuffer(meshVertexBuffer, 0, config.gpuMeshSource.vertices.buffer);
2667
2959
  device.queue.writeBuffer(meshIndexBuffer, 0, config.gpuMeshSource.indices.buffer);
2668
2960
  device.queue.writeBuffer(meshRangeBuffer, 0, config.gpuMeshSource.meshes.buffer);
2961
+ const packedEnvironmentPortals = packEnvironmentPortals(
2962
+ config.environmentPortals,
2963
+ Math.max(1, config.environmentPortalCapacity)
2964
+ );
2965
+ device.queue.writeBuffer(environmentPortalBuffer, 0, packedEnvironmentPortals.buffer);
2669
2966
  const radianceTexture = device.createTexture({
2670
2967
  label: "plasius.wavefront.radiance",
2671
2968
  size: { width: config.width, height: config.height },
@@ -2717,7 +3014,8 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
2717
3014
  binding: 16,
2718
3015
  visibility: constants.shader.COMPUTE,
2719
3016
  storageTexture: { access: "write-only", format: "rgba16float" }
2720
- }
3017
+ },
3018
+ { binding: 19, visibility: constants.shader.COMPUTE, buffer: { type: "read-only-storage" } }
2721
3019
  ]
2722
3020
  });
2723
3021
  const accelerationBindGroupLayout = device.createBindGroupLayout({
@@ -2890,7 +3188,8 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
2890
3188
  { binding: 7, resource: outputView },
2891
3189
  { binding: 8, resource: { buffer: triangleBuffer } },
2892
3190
  { binding: 9, resource: { buffer: bvhNodeBuffer } },
2893
- { binding: 16, resource: radianceView }
3191
+ { binding: 16, resource: radianceView },
3192
+ { binding: 19, resource: { buffer: environmentPortalBuffer } }
2894
3193
  ]
2895
3194
  });
2896
3195
  }
@@ -2977,10 +3276,27 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
2977
3276
  ]
2978
3277
  });
2979
3278
  let frame = 0;
2980
- const tiles = createTiles(config.width, config.height, config.tileSize);
3279
+ let accelerationBuilt = !config.gpuAccelerationBuildRequired;
3280
+ let accelerationBuildCount = 0;
3281
+ function createFrameConfigWriter(frameIndex) {
3282
+ let slot = 0;
3283
+ return (tile, buildRange = {}) => {
3284
+ if (slot >= frameConfigSlotCount) {
3285
+ throw new Error("Wavefront frame config slot capacity exceeded.");
3286
+ }
3287
+ const offset = slot * configBufferStride;
3288
+ slot += 1;
3289
+ device.queue.writeBuffer(
3290
+ configBuffer,
3291
+ offset,
3292
+ createConfigPayload(config, tile, frameIndex, buildRange)
3293
+ );
3294
+ return offset;
3295
+ };
3296
+ }
2981
3297
  function dispatchGpuAccelerationBuild(frameIndex) {
2982
- if (!config.gpuAccelerationBuildRequired) {
2983
- return;
3298
+ if (!config.gpuAccelerationBuildRequired || accelerationBuilt) {
3299
+ return false;
2984
3300
  }
2985
3301
  const buildTile = tiles[0] ?? { x: 0, y: 0, width: 1, height: 1 };
2986
3302
  const encoder = device.createCommandEncoder({
@@ -3038,27 +3354,21 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3038
3354
  }
3039
3355
  passEncoder.end();
3040
3356
  device.queue.submit([encoder.finish()]);
3357
+ accelerationBuilt = true;
3358
+ accelerationBuildCount += 1;
3359
+ return true;
3041
3360
  }
3042
- function dispatchTileSample(tile, frameIndex, sampleIndex) {
3043
- const sampleWeight = 1 / config.samplesPerPixel;
3044
- const configPayload = createConfigPayload(config, tile, frameIndex, {
3045
- sampleIndex,
3046
- sampleWeight
3047
- });
3048
- device.queue.writeBuffer(configBuffer, 0, configPayload);
3049
- const encoder = device.createCommandEncoder({
3050
- label: `plasius.wavefront.frame.${frameIndex}.tile.${tile.x}.${tile.y}.sample.${sampleIndex}`
3051
- });
3361
+ function encodeTileSample(encoder, tile, configOffset) {
3052
3362
  const passEncoder = encoder.beginComputePass({
3053
3363
  label: "plasius.wavefront.computePass"
3054
3364
  });
3055
3365
  const tileWorkgroups = Math.ceil(tile.width * tile.height / WORKGROUP_SIZE);
3056
3366
  const capacityWorkgroups = Math.ceil(config.tilePixelCapacity / WORKGROUP_SIZE);
3057
- passEncoder.setBindGroup(0, bindGroups[0], [0]);
3367
+ passEncoder.setBindGroup(0, bindGroups[0], [configOffset]);
3058
3368
  passEncoder.setPipeline(pipelines.generatePrimaryRays);
3059
3369
  passEncoder.dispatchWorkgroups(tileWorkgroups);
3060
3370
  for (let bounceIndex = 0; bounceIndex < config.maxDepth; bounceIndex += 1) {
3061
- passEncoder.setBindGroup(0, bindGroups[bounceIndex % 2], [0]);
3371
+ passEncoder.setBindGroup(0, bindGroups[bounceIndex % 2], [configOffset]);
3062
3372
  passEncoder.setPipeline(pipelines.intersectActiveQueue);
3063
3373
  passEncoder.dispatchWorkgroups(capacityWorkgroups);
3064
3374
  passEncoder.setPipeline(pipelines.resolveSurfaceRecords);
@@ -3067,71 +3377,38 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3067
3377
  passEncoder.dispatchWorkgroups(1);
3068
3378
  }
3069
3379
  passEncoder.end();
3070
- device.queue.submit([encoder.finish()]);
3071
3380
  }
3072
- function dispatchTileOutput(tile, frameIndex) {
3073
- const configPayload = createConfigPayload(config, tile, frameIndex, {
3074
- sampleIndex: 0,
3075
- sampleWeight: 1 / config.samplesPerPixel
3076
- });
3077
- device.queue.writeBuffer(configBuffer, 0, configPayload);
3078
- const encoder = device.createCommandEncoder({
3079
- label: `plasius.wavefront.frame.${frameIndex}.tile.${tile.x}.${tile.y}.output`
3080
- });
3381
+ function encodeTileOutput(encoder, tile, configOffset) {
3081
3382
  const passEncoder = encoder.beginComputePass({
3082
3383
  label: "plasius.wavefront.outputPass"
3083
3384
  });
3084
3385
  const tileWorkgroups = Math.ceil(tile.width * tile.height / WORKGROUP_SIZE);
3085
- passEncoder.setBindGroup(0, bindGroups[0], [0]);
3386
+ passEncoder.setBindGroup(0, bindGroups[0], [configOffset]);
3086
3387
  passEncoder.setPipeline(pipelines.accumulateTerminalRadiance);
3087
3388
  passEncoder.dispatchWorkgroups(tileWorkgroups);
3088
3389
  passEncoder.end();
3089
- device.queue.submit([encoder.finish()]);
3090
- }
3091
- function dispatchTile(tile, frameIndex) {
3092
- for (let sampleIndex = 0; sampleIndex < config.samplesPerPixel; sampleIndex += 1) {
3093
- dispatchTileSample(tile, frameIndex, sampleIndex);
3094
- }
3095
- dispatchTileOutput(tile, frameIndex);
3096
3390
  }
3097
- function dispatchDenoise(frameIndex) {
3391
+ function encodeDenoise(encoder, configOffset) {
3098
3392
  if (!config.denoise) {
3099
3393
  return;
3100
3394
  }
3101
- device.queue.writeBuffer(
3102
- configBuffer,
3103
- 0,
3104
- createConfigPayload(
3105
- config,
3106
- { x: 0, y: 0, width: config.width, height: config.height },
3107
- frameIndex,
3108
- { sampleIndex: 0, sampleWeight: 1 / config.samplesPerPixel }
3109
- )
3110
- );
3111
- const encoder = device.createCommandEncoder({
3112
- label: `plasius.wavefront.frame.${frameIndex}.denoise`
3113
- });
3114
3395
  const radiancePass = encoder.beginComputePass({
3115
3396
  label: "plasius.wavefront.denoiseRadiancePass"
3116
3397
  });
3117
- radiancePass.setBindGroup(0, denoiseRadianceBindGroup, [0]);
3398
+ radiancePass.setBindGroup(0, denoiseRadianceBindGroup, [configOffset]);
3118
3399
  radiancePass.setPipeline(pipelines.denoiseLinearRadiance);
3119
3400
  radiancePass.dispatchWorkgroups(Math.ceil(config.width / 8), Math.ceil(config.height / 8));
3120
3401
  radiancePass.end();
3121
3402
  const resolvePass = encoder.beginComputePass({
3122
3403
  label: "plasius.wavefront.denoiseResolvePass"
3123
3404
  });
3124
- resolvePass.setBindGroup(0, denoiseResolveBindGroup, [0]);
3405
+ resolvePass.setBindGroup(0, denoiseResolveBindGroup, [configOffset]);
3125
3406
  resolvePass.setPipeline(pipelines.resolveDenoisedOutputImage);
3126
3407
  resolvePass.dispatchWorkgroups(Math.ceil(config.width / 8), Math.ceil(config.height / 8));
3127
3408
  resolvePass.end();
3128
- device.queue.submit([encoder.finish()]);
3129
3409
  }
3130
- function present() {
3410
+ function encodePresent(encoder) {
3131
3411
  const texture = context.getCurrentTexture();
3132
- const encoder = device.createCommandEncoder({
3133
- label: `plasius.wavefront.present.${frame}`
3134
- });
3135
3412
  const passEncoder = encoder.beginRenderPass({
3136
3413
  label: "plasius.wavefront.presentPass",
3137
3414
  colorAttachments: [
@@ -3147,16 +3424,42 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3147
3424
  passEncoder.setBindGroup(0, presentBindGroup);
3148
3425
  passEncoder.draw(3);
3149
3426
  passEncoder.end();
3427
+ }
3428
+ function dispatchFrame(frameIndex) {
3429
+ const writeFrameConfig = createFrameConfigWriter(frameIndex);
3430
+ const encoder = device.createCommandEncoder({
3431
+ label: `plasius.wavefront.frame.${frameIndex}.batched`
3432
+ });
3433
+ for (const tile of tiles) {
3434
+ for (let sampleIndex = 0; sampleIndex < config.samplesPerPixel; sampleIndex += 1) {
3435
+ const configOffset = writeFrameConfig(tile, {
3436
+ sampleIndex,
3437
+ sampleWeight: 1 / config.samplesPerPixel
3438
+ });
3439
+ encodeTileSample(encoder, tile, configOffset);
3440
+ }
3441
+ const outputConfigOffset = writeFrameConfig(tile, {
3442
+ sampleIndex: 0,
3443
+ sampleWeight: 1 / config.samplesPerPixel
3444
+ });
3445
+ encodeTileOutput(encoder, tile, outputConfigOffset);
3446
+ }
3447
+ if (config.denoise) {
3448
+ const denoiseConfigOffset = writeFrameConfig(
3449
+ { x: 0, y: 0, width: config.width, height: config.height },
3450
+ { sampleIndex: 0, sampleWeight: 1 / config.samplesPerPixel }
3451
+ );
3452
+ encodeDenoise(encoder, denoiseConfigOffset);
3453
+ }
3454
+ encodePresent(encoder);
3150
3455
  device.queue.submit([encoder.finish()]);
3456
+ return 1;
3151
3457
  }
3152
3458
  function renderOnce() {
3153
3459
  frame += 1;
3154
- dispatchGpuAccelerationBuild(frame + config.frameIndex);
3155
- for (const tile of tiles) {
3156
- dispatchTile(tile, frame + config.frameIndex);
3157
- }
3158
- dispatchDenoise(frame + config.frameIndex);
3159
- present();
3460
+ const frameIndex = frame + config.frameIndex;
3461
+ const accelerationBuildSubmitted = dispatchGpuAccelerationBuild(frameIndex);
3462
+ const frameSubmissionCount = dispatchFrame(frameIndex);
3160
3463
  return Object.freeze({
3161
3464
  frame,
3162
3465
  width: config.width,
@@ -3170,10 +3473,17 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3170
3473
  sceneObjectCount: config.sceneObjectCount,
3171
3474
  triangleCount: config.triangleCount,
3172
3475
  emissiveTriangleCount: config.emissiveTriangleCount,
3476
+ environmentPortalCount: config.environmentPortalCount,
3477
+ environmentPortalMode: config.environmentPortalMode,
3173
3478
  bvhNodeCount: config.bvhNodeCount,
3174
3479
  displayQuality: config.displayQuality,
3175
3480
  accelerationBuildMode: config.accelerationBuildMode,
3176
3481
  gpuAccelerationBuildRequired: config.gpuAccelerationBuildRequired,
3482
+ accelerationBuildSubmitted,
3483
+ accelerationBuilt,
3484
+ accelerationBuildCount,
3485
+ commandSubmissions: frameSubmissionCount + (accelerationBuildSubmitted ? 1 : 0),
3486
+ frameConfigSlots: frameConfigSlotCount,
3177
3487
  memory: config.memory
3178
3488
  });
3179
3489
  }
@@ -3239,10 +3549,15 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3239
3549
  sceneObjectCount: config.sceneObjectCount,
3240
3550
  triangleCount: config.triangleCount,
3241
3551
  emissiveTriangleCount: config.emissiveTriangleCount,
3552
+ environmentPortalCount: config.environmentPortalCount,
3553
+ environmentPortalMode: config.environmentPortalMode,
3242
3554
  bvhNodeCount: config.bvhNodeCount,
3243
3555
  displayQuality: config.displayQuality,
3244
3556
  accelerationBuildMode: config.accelerationBuildMode,
3245
3557
  gpuAccelerationBuildRequired: config.gpuAccelerationBuildRequired,
3558
+ accelerationBuilt,
3559
+ accelerationBuildCount,
3560
+ frameConfigSlots: frameConfigSlotCount,
3246
3561
  memory: config.memory
3247
3562
  });
3248
3563
  }
@@ -3257,6 +3572,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3257
3572
  meshVertexBuffer.destroy?.();
3258
3573
  meshIndexBuffer.destroy?.();
3259
3574
  meshRangeBuffer.destroy?.();
3575
+ environmentPortalBuffer.destroy?.();
3260
3576
  bvhLeafRefBuffer.destroy?.();
3261
3577
  configBuffer.destroy?.();
3262
3578
  bvhBuildConfigBuffer.destroy?.();