@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.js CHANGED
@@ -5,6 +5,7 @@ var DEFAULT_MAX_DEPTH = 6;
5
5
  var DEFAULT_TILE_SIZE = 128;
6
6
  var DEFAULT_SAMPLES_PER_PIXEL = 1;
7
7
  var DEFAULT_SCENE_OBJECT_CAPACITY = 128;
8
+ var DEFAULT_ENVIRONMENT_PORTAL_CAPACITY = 32;
8
9
  var WORKGROUP_SIZE = 64;
9
10
  var RAY_RECORD_BYTES = 80;
10
11
  var HIT_RECORD_BYTES = 208;
@@ -15,9 +16,11 @@ var TRIANGLE_RECORD_BYTES = 208;
15
16
  var BVH_NODE_RECORD_BYTES = 48;
16
17
  var BVH_LEAF_REF_RECORD_BYTES = 16;
17
18
  var EMISSIVE_TRIANGLE_INDEX_BYTES = 4;
19
+ var ENVIRONMENT_PORTAL_RECORD_BYTES = 96;
18
20
  var ACCUMULATION_RECORD_BYTES = 16;
19
- var CONFIG_BUFFER_BYTES = 256;
21
+ var CONFIG_BUFFER_BYTES = 272;
20
22
  var COUNTER_BUFFER_BYTES = 16;
23
+ var TRACE_STORAGE_BUFFER_BINDINGS = 9;
21
24
  var MATERIAL_DIFFUSE = 0;
22
25
  var MATERIAL_METAL = 1;
23
26
  var MATERIAL_DIELECTRIC = 2;
@@ -44,6 +47,7 @@ var DEFAULT_ENVIRONMENT_LIGHTING = Object.freeze({
44
47
  });
45
48
  var wavefrontPathTracingComputeLimits = Object.freeze({
46
49
  workgroupSize: WORKGROUP_SIZE,
50
+ traceStorageBufferBindings: TRACE_STORAGE_BUFFER_BINDINGS,
47
51
  rayRecordBytes: RAY_RECORD_BYTES,
48
52
  hitRecordBytes: HIT_RECORD_BYTES,
49
53
  sceneObjectRecordBytes: SCENE_OBJECT_RECORD_BYTES,
@@ -54,6 +58,7 @@ var wavefrontPathTracingComputeLimits = Object.freeze({
54
58
  bvhLeafReferenceRecordBytes: BVH_LEAF_REF_RECORD_BYTES,
55
59
  emissiveTriangleIndexBytes: EMISSIVE_TRIANGLE_INDEX_BYTES,
56
60
  emissiveTriangleMetadataRecordBytes: BVH_NODE_RECORD_BYTES,
61
+ environmentPortalRecordBytes: ENVIRONMENT_PORTAL_RECORD_BYTES,
57
62
  accumulationRecordBytes: ACCUMULATION_RECORD_BYTES
58
63
  });
59
64
  var wavefrontSceneObjectKinds = Object.freeze({
@@ -145,6 +150,9 @@ function subtract(a, b) {
145
150
  function scale(a, scalar) {
146
151
  return [a[0] * scalar, a[1] * scalar, a[2] * scalar];
147
152
  }
153
+ function dot(a, b) {
154
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
155
+ }
148
156
  function cross(a, b) {
149
157
  return [
150
158
  a[1] * b[2] - a[2] * b[1],
@@ -757,6 +765,127 @@ function resolveEnvironmentLighting(input, environmentColor, ambientColor) {
757
765
  exposure: Math.max(1e-4, readFiniteNumber("environmentLighting.exposure", source.exposure, DEFAULT_ENVIRONMENT_LIGHTING.exposure))
758
766
  });
759
767
  }
768
+ function resolveEnvironmentPortalMode(value, hasPortals) {
769
+ if (value === void 0 || value === null) {
770
+ return hasPortals ? 2 : 0;
771
+ }
772
+ if (Number.isInteger(value) && value >= 0 && value <= 2) {
773
+ return value;
774
+ }
775
+ if (value === "disabled") {
776
+ return 0;
777
+ }
778
+ if (value === "guide") {
779
+ return 1;
780
+ }
781
+ if (value === "guide-and-gate" || value === "gate") {
782
+ return 2;
783
+ }
784
+ throw new Error(
785
+ "environmentPortalMode must be disabled, guide, guide-and-gate, or an integer between 0 and 2."
786
+ );
787
+ }
788
+ function orthogonalPortalTangent(normal) {
789
+ if (Math.abs(normal[1]) < 0.92) {
790
+ return normalize(cross([0, 1, 0], normal), [1, 0, 0]);
791
+ }
792
+ return normalize(cross([1, 0, 0], normal), [0, 0, 1]);
793
+ }
794
+ function resolvePortalTangent(value, normal) {
795
+ const fallback = orthogonalPortalTangent(normal);
796
+ const tangent = asUnitVec3(value, fallback);
797
+ const projected = subtract(tangent, scale(normal, dot(tangent, normal)));
798
+ return normalize(projected, fallback);
799
+ }
800
+ function readPositiveFiniteNumber(name, value, fallback) {
801
+ const numeric = readFiniteNumber(name, value, fallback);
802
+ if (numeric <= 0) {
803
+ throw new Error(`${name} must be a positive finite number.`);
804
+ }
805
+ return numeric;
806
+ }
807
+ function readPortalExtent(name, value, halfName, halfValue) {
808
+ if (value !== void 0 && value !== null) {
809
+ return readPositiveFiniteNumber(name, value, 1);
810
+ }
811
+ return readPositiveFiniteNumber(halfName, halfValue, 0.5) * 2;
812
+ }
813
+ function normalizeEnvironmentPortal(portal, index) {
814
+ if (!portal || typeof portal !== "object") {
815
+ throw new Error(`environmentPortals[${index}] must be an object.`);
816
+ }
817
+ const shape = portal.shape ?? portal.kind ?? "rectangle";
818
+ if (shape !== "rectangle") {
819
+ throw new Error(`environmentPortals[${index}].shape must be "rectangle".`);
820
+ }
821
+ const position = asVec3(portal.position ?? portal.center, [0, 0, 0]);
822
+ const normal = asUnitVec3(portal.normal, [0, 0, 1]);
823
+ const tangent = resolvePortalTangent(portal.tangent, normal);
824
+ const bitangent = normalize(cross(normal, tangent), [0, 1, 0]);
825
+ const width = readPortalExtent(
826
+ `environmentPortals[${index}].width`,
827
+ portal.width,
828
+ `environmentPortals[${index}].halfWidth`,
829
+ portal.halfWidth
830
+ );
831
+ const height = readPortalExtent(
832
+ `environmentPortals[${index}].height`,
833
+ portal.height,
834
+ `environmentPortals[${index}].halfHeight`,
835
+ portal.halfHeight
836
+ );
837
+ const radianceScale = Math.max(
838
+ 0,
839
+ readFiniteNumber(
840
+ `environmentPortals[${index}].radianceScale`,
841
+ portal.radianceScale ?? portal.intensity,
842
+ 1
843
+ )
844
+ );
845
+ return Object.freeze({
846
+ kind: 1,
847
+ flags: portal.twoSided === false ? 0 : 1,
848
+ position: Object.freeze([position[0], position[1], position[2], width * height]),
849
+ normal: Object.freeze([normal[0], normal[1], normal[2], radianceScale]),
850
+ tangent: Object.freeze([tangent[0], tangent[1], tangent[2], width * 0.5]),
851
+ bitangent: Object.freeze([bitangent[0], bitangent[1], bitangent[2], height * 0.5]),
852
+ color: Object.freeze(asColor(portal.color, [1, 1, 1, 1]))
853
+ });
854
+ }
855
+ function normalizeEnvironmentPortals(value) {
856
+ if (value === void 0 || value === null) {
857
+ return Object.freeze([]);
858
+ }
859
+ if (!Array.isArray(value)) {
860
+ throw new Error("environmentPortals must be an array when provided.");
861
+ }
862
+ return Object.freeze(value.map(normalizeEnvironmentPortal));
863
+ }
864
+ function packEnvironmentPortals(portals, capacity) {
865
+ const bytes = new ArrayBuffer(capacity * ENVIRONMENT_PORTAL_RECORD_BYTES);
866
+ const data = new DataView(bytes);
867
+ const floatView = new Float32Array(bytes);
868
+ portals.forEach((portal, index) => {
869
+ const byteOffset = index * ENVIRONMENT_PORTAL_RECORD_BYTES;
870
+ const floatOffset = byteOffset / Float32Array.BYTES_PER_ELEMENT;
871
+ data.setUint32(byteOffset, portal.kind, true);
872
+ data.setUint32(byteOffset + 4, portal.flags, true);
873
+ data.setUint32(byteOffset + 8, 0, true);
874
+ data.setUint32(byteOffset + 12, 0, true);
875
+ writeVec4(floatView, floatOffset + 4, portal.position);
876
+ writeVec4(floatView, floatOffset + 8, portal.normal);
877
+ writeVec4(floatView, floatOffset + 12, portal.tangent);
878
+ writeVec4(floatView, floatOffset + 16, portal.bitangent);
879
+ writeVec4(floatView, floatOffset + 20, portal.color);
880
+ });
881
+ return Object.freeze({
882
+ buffer: bytes,
883
+ portals,
884
+ count: portals.length,
885
+ capacity,
886
+ recordBytes: ENVIRONMENT_PORTAL_RECORD_BYTES
887
+ });
888
+ }
760
889
  function getCanvasDimension(canvas, key, fallback) {
761
890
  const value = Number(canvas?.[key]);
762
891
  if (Number.isFinite(value) && value > 0) {
@@ -812,6 +941,11 @@ function estimateWavefrontPathTracingMemory(options = {}) {
812
941
  options.emissiveTriangleCapacity,
813
942
  0
814
943
  );
944
+ const environmentPortalCapacity = readNonNegativeInteger(
945
+ "environmentPortalCapacity",
946
+ options.environmentPortalCapacity,
947
+ 0
948
+ );
815
949
  const queueBytes = tilePixelCapacity * RAY_RECORD_BYTES;
816
950
  const hitBytes = tilePixelCapacity * HIT_RECORD_BYTES;
817
951
  const accumulationBytes = tilePixelCapacity * ACCUMULATION_RECORD_BYTES;
@@ -820,6 +954,7 @@ function estimateWavefrontPathTracingMemory(options = {}) {
820
954
  const bvhNodeBytes = bvhNodeCapacity * BVH_NODE_RECORD_BYTES;
821
955
  const bvhLeafReferenceBytes = bvhLeafSortCapacity * BVH_LEAF_REF_RECORD_BYTES;
822
956
  const emissiveTriangleMetadataBytes = emissiveTriangleCapacity * BVH_NODE_RECORD_BYTES;
957
+ const environmentPortalBytes = environmentPortalCapacity * ENVIRONMENT_PORTAL_RECORD_BYTES;
823
958
  return Object.freeze({
824
959
  queueBytes,
825
960
  queuePairBytes: queueBytes * 2,
@@ -830,9 +965,10 @@ function estimateWavefrontPathTracingMemory(options = {}) {
830
965
  bvhNodeBytes,
831
966
  bvhLeafReferenceBytes,
832
967
  emissiveTriangleMetadataBytes,
968
+ environmentPortalBytes,
833
969
  configBytes: CONFIG_BUFFER_BYTES,
834
970
  counterBytes: COUNTER_BUFFER_BYTES,
835
- totalHotBufferBytes: queueBytes * 2 + hitBytes + accumulationBytes + sceneObjectBytes + triangleBytes + bvhNodeBytes + bvhLeafReferenceBytes + emissiveTriangleMetadataBytes + CONFIG_BUFFER_BYTES + COUNTER_BUFFER_BYTES
971
+ totalHotBufferBytes: queueBytes * 2 + hitBytes + accumulationBytes + sceneObjectBytes + triangleBytes + bvhNodeBytes + bvhLeafReferenceBytes + emissiveTriangleMetadataBytes + environmentPortalBytes + CONFIG_BUFFER_BYTES + COUNTER_BUFFER_BYTES
836
972
  });
837
973
  }
838
974
  function createWavefrontPathTracingComputeConfig(options = {}) {
@@ -893,6 +1029,21 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
893
1029
  environmentColor,
894
1030
  ambientColor
895
1031
  );
1032
+ const environmentPortals = normalizeEnvironmentPortals(
1033
+ options.environmentPortals ?? options.environmentLightPortals ?? options.environmentLighting?.environmentPortals
1034
+ );
1035
+ const environmentPortalCapacity = Math.max(
1036
+ environmentPortals.length,
1037
+ readNonNegativeInteger(
1038
+ "environmentPortalCapacity",
1039
+ options.environmentPortalCapacity,
1040
+ DEFAULT_ENVIRONMENT_PORTAL_CAPACITY
1041
+ )
1042
+ );
1043
+ const environmentPortalMode = resolveEnvironmentPortalMode(
1044
+ options.environmentPortalMode ?? options.portalMode ?? options.environmentLighting?.environmentPortalMode,
1045
+ environmentPortals.length > 0
1046
+ );
896
1047
  return Object.freeze({
897
1048
  width,
898
1049
  height,
@@ -921,6 +1072,10 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
921
1072
  environmentColor: environmentLighting.environmentColor,
922
1073
  ambientColor: environmentLighting.ambientColor,
923
1074
  environmentLighting,
1075
+ environmentPortals,
1076
+ environmentPortalCount: environmentPortals.length,
1077
+ environmentPortalCapacity,
1078
+ environmentPortalMode,
924
1079
  displayQuality: options.displayQuality === true,
925
1080
  requiresMeshBvhForDisplayQuality: true,
926
1081
  denoise: options.denoise !== false,
@@ -931,7 +1086,8 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
931
1086
  triangleCapacity,
932
1087
  bvhNodeCapacity,
933
1088
  bvhLeafSortCapacity,
934
- emissiveTriangleCapacity: emissiveTriangleIndices.capacity
1089
+ emissiveTriangleCapacity: emissiveTriangleIndices.capacity,
1090
+ environmentPortalCapacity
935
1091
  })
936
1092
  });
937
1093
  }
@@ -1115,6 +1271,10 @@ function createConfigPayload(config, tile, frameIndex, buildRange = {}) {
1115
1271
  data.setUint32(244, buildRange.count ?? 0, true);
1116
1272
  data.setUint32(248, buildRange.sortItemCount ?? 0, true);
1117
1273
  data.setUint32(252, config.emissiveTriangleCount ?? 0, true);
1274
+ data.setUint32(256, config.environmentPortalCount ?? 0, true);
1275
+ data.setUint32(260, config.environmentPortalMode ?? 0, true);
1276
+ data.setUint32(264, 0, true);
1277
+ data.setUint32(268, 0, true);
1118
1278
  return bytes;
1119
1279
  }
1120
1280
  function createTiles(width, height, tileSize) {
@@ -1361,6 +1521,10 @@ struct FrameConfig {
1361
1521
  bvhBuildNodeCount: u32,
1362
1522
  bvhSortItemCount: u32,
1363
1523
  emissiveTriangleCount: u32,
1524
+ environmentPortalCount: u32,
1525
+ environmentPortalMode: u32,
1526
+ _portalPad0: u32,
1527
+ _portalPad1: u32,
1364
1528
  };
1365
1529
 
1366
1530
  struct Counters {
@@ -1384,6 +1548,18 @@ struct Candidate {
1384
1548
  mediumRefId: u32,
1385
1549
  };
1386
1550
 
1551
+ struct EnvironmentPortal {
1552
+ kind: u32,
1553
+ flags: u32,
1554
+ _pad0: u32,
1555
+ _pad1: u32,
1556
+ position: vec4<f32>,
1557
+ normal: vec4<f32>,
1558
+ tangent: vec4<f32>,
1559
+ bitangent: vec4<f32>,
1560
+ color: vec4<f32>,
1561
+ };
1562
+
1387
1563
  @group(0) @binding(0) var<storage, read_write> activeQueue: array<RayRecord>;
1388
1564
  @group(0) @binding(1) var<storage, read_write> nextQueue: array<RayRecord>;
1389
1565
  @group(0) @binding(2) var<storage, read_write> hits: array<HitRecord>;
@@ -1403,6 +1579,7 @@ struct Candidate {
1403
1579
  @group(0) @binding(16) var radianceImage: texture_storage_2d<rgba16float, write>;
1404
1580
  @group(0) @binding(17) var finalDenoiseInputRadiance: texture_2d<f32>;
1405
1581
  @group(0) @binding(18) var denoisedOutputImage: texture_storage_2d<rgba8unorm, write>;
1582
+ @group(0) @binding(19) var<storage, read> environmentPortals: array<EnvironmentPortal>;
1406
1583
 
1407
1584
  fn hash_u32(value: u32) -> u32 {
1408
1585
  var x = value;
@@ -1443,7 +1620,48 @@ fn saturate(value: f32) -> f32 {
1443
1620
  return clamp(value, 0.0, 1.0);
1444
1621
  }
1445
1622
 
1446
- fn environment_radiance(direction: vec3<f32>) -> vec3<f32> {
1623
+ fn max_component(value: vec3<f32>) -> f32 {
1624
+ return max(max(value.x, value.y), value.z);
1625
+ }
1626
+
1627
+ fn environment_portal_radiance_scale(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
1628
+ if (config.environmentPortalCount == 0u || config.environmentPortalMode == 0u) {
1629
+ return vec3<f32>(1.0);
1630
+ }
1631
+ var scale = vec3<f32>(0.0);
1632
+ for (var portalIndex = 0u; portalIndex < config.environmentPortalCount; portalIndex = portalIndex + 1u) {
1633
+ let portal = environmentPortals[portalIndex];
1634
+ if (portal.kind == 1u) {
1635
+ let portalNormal = safe_normalize(portal.normal.xyz, vec3<f32>(0.0, 0.0, 1.0));
1636
+ let denominator = dot(direction, portalNormal);
1637
+ let twoSided = (portal.flags & 1u) != 0u;
1638
+ var facing = abs(denominator) > 0.0001;
1639
+ if (!twoSided && denominator <= 0.0001) {
1640
+ facing = false;
1641
+ }
1642
+ if (facing) {
1643
+ let distance = dot(portal.position.xyz - origin, portalNormal) / denominator;
1644
+ if (distance > 0.001) {
1645
+ let hitPosition = origin + direction * distance;
1646
+ let local = hitPosition - portal.position.xyz;
1647
+ let tangent = safe_normalize(portal.tangent.xyz, vec3<f32>(1.0, 0.0, 0.0));
1648
+ let bitangent = safe_normalize(portal.bitangent.xyz, vec3<f32>(0.0, 1.0, 0.0));
1649
+ let u = dot(local, tangent);
1650
+ let v = dot(local, bitangent);
1651
+ if (abs(u) <= portal.tangent.w && abs(v) <= portal.bitangent.w) {
1652
+ let areaWeight = clamp(sqrt(max(portal.position.w, 0.0001)), 0.25, 4.0);
1653
+ let angleWeight = max(abs(denominator), 0.08);
1654
+ let portalScale = portal.color.rgb * portal.normal.w * portal.color.a * areaWeight * angleWeight;
1655
+ scale = max(scale, portalScale);
1656
+ }
1657
+ }
1658
+ }
1659
+ }
1660
+ }
1661
+ return scale;
1662
+ }
1663
+
1664
+ fn environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
1447
1665
  let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
1448
1666
  let upFactor = saturate(rayDirection.y * 0.5 + 0.5);
1449
1667
  let sunDirection = safe_normalize(
@@ -1454,10 +1672,26 @@ fn environment_radiance(direction: vec3<f32>) -> vec3<f32> {
1454
1672
  let gradient =
1455
1673
  config.environmentHorizonColor.xyz * (1.0 - upFactor) +
1456
1674
  config.environmentZenithColor.xyz * upFactor;
1675
+ let portalScale = environment_portal_radiance_scale(origin, rayDirection);
1676
+ let portalHit = max_component(portalScale) > 0.0001;
1457
1677
  return (
1458
1678
  gradient +
1459
1679
  config.environmentSunColor.xyz * sunGlow
1460
- ) * max(config.environmentSunDirectionIntensity.w, 0.0001);
1680
+ ) *
1681
+ max(config.environmentSunDirectionIntensity.w, 0.0001) *
1682
+ select(vec3<f32>(1.0), portalScale, portalHit);
1683
+ }
1684
+
1685
+ fn gated_environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
1686
+ let portalScale = environment_portal_radiance_scale(origin, safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0)));
1687
+ if (
1688
+ config.environmentPortalCount > 0u &&
1689
+ config.environmentPortalMode == 2u &&
1690
+ max_component(portalScale) <= 0.0001
1691
+ ) {
1692
+ return config.ambientColor.xyz * 0.65;
1693
+ }
1694
+ return environment_radiance(origin, direction);
1461
1695
  }
1462
1696
 
1463
1697
  fn default_mesh_range() -> MeshRange {
@@ -1728,7 +1962,7 @@ fn make_ray(pixelIndex: u32) -> RayRecord {
1728
1962
  }
1729
1963
 
1730
1964
  fn make_miss(ray: RayRecord) -> HitRecord {
1731
- let radiance = environment_radiance(ray.direction.xyz);
1965
+ let radiance = gated_environment_radiance(ray.origin.xyz, ray.direction.xyz);
1732
1966
  return HitRecord(
1733
1967
  ray.rayId,
1734
1968
  ray.sourcePixelId,
@@ -2214,6 +2448,21 @@ fn sample_emissive_triangle_direction(hit: HitRecord, seed: u32, fallback: vec3<
2214
2448
  return safe_normalize(lightPoint - hit.position.xyz, fallback);
2215
2449
  }
2216
2450
 
2451
+ fn sample_environment_portal_direction(hit: HitRecord, seed: u32, fallback: vec3<f32>) -> vec3<f32> {
2452
+ if (config.environmentPortalCount == 0u || config.environmentPortalMode == 0u) {
2453
+ return fallback;
2454
+ }
2455
+ let portalSlot = min(
2456
+ u32(random01(seed + 211u) * f32(config.environmentPortalCount)),
2457
+ config.environmentPortalCount - 1u
2458
+ );
2459
+ let portal = environmentPortals[portalSlot];
2460
+ let u = (random01(seed + 223u) * 2.0 - 1.0) * portal.tangent.w;
2461
+ let v = (random01(seed + 227u) * 2.0 - 1.0) * portal.bitangent.w;
2462
+ let portalTarget = portal.position.xyz + portal.tangent.xyz * u + portal.bitangent.xyz * v;
2463
+ return safe_normalize(portalTarget - hit.position.xyz, fallback);
2464
+ }
2465
+
2217
2466
  fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult {
2218
2467
  let roughness = clamp(hit.material.x, 0.0, 1.0);
2219
2468
  if (hit.materialKind == 1u) {
@@ -2253,8 +2502,17 @@ fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult
2253
2502
  let canSampleLight = dot(hit.shadingNormal.xyz, guidedLight) > -0.04;
2254
2503
  let guideProbability = select(0.38, 0.72, ray.bounce == 0u);
2255
2504
  let useGuidedLight = canSampleLight && random01(seed + 37u) < guideProbability;
2505
+ let guidedPortal = sample_environment_portal_direction(hit, seed, randomDiffuse);
2506
+ let canSamplePortal = dot(hit.shadingNormal.xyz, guidedPortal) > -0.04;
2507
+ let useGuidedPortal =
2508
+ !useGuidedLight &&
2509
+ canSamplePortal &&
2510
+ config.environmentPortalCount > 0u &&
2511
+ config.environmentPortalMode > 0u &&
2512
+ random01(seed + 89u) < 0.58;
2513
+ let guidedDirection = select(randomDiffuse, guidedPortal, useGuidedPortal);
2256
2514
  return ScatterResult(
2257
- vec4<f32>(select(randomDiffuse, guidedLight, useGuidedLight), 0.0),
2515
+ vec4<f32>(select(guidedDirection, guidedLight, useGuidedLight), 0.0),
2258
2516
  select(0u, RAY_FLAG_GUIDED_EMISSIVE, useGuidedLight),
2259
2517
  0u,
2260
2518
  0u,
@@ -2461,6 +2719,29 @@ fn fragmentMain(in: VertexOut) -> @location(0) vec4<f32> {
2461
2719
  return textureSample(renderTexture, renderSampler, in.uv);
2462
2720
  }
2463
2721
  `;
2722
+ function createWavefrontDeviceDescriptor(adapter, options = {}) {
2723
+ const requiredLimits = { ...options.requiredLimits ?? {} };
2724
+ const exposedStorageBufferLimit = Number(adapter?.limits?.maxStorageBuffersPerShaderStage);
2725
+ if (Number.isFinite(exposedStorageBufferLimit)) {
2726
+ if (exposedStorageBufferLimit < TRACE_STORAGE_BUFFER_BINDINGS) {
2727
+ throw new Error(
2728
+ `Wavefront mesh tracing requires maxStorageBuffersPerShaderStage>=${TRACE_STORAGE_BUFFER_BINDINGS}, but this adapter exposes ${exposedStorageBufferLimit}.`
2729
+ );
2730
+ }
2731
+ requiredLimits.maxStorageBuffersPerShaderStage = Math.max(
2732
+ Number(requiredLimits.maxStorageBuffersPerShaderStage ?? 0),
2733
+ TRACE_STORAGE_BUFFER_BINDINGS
2734
+ );
2735
+ }
2736
+ const descriptor = { ...options.deviceDescriptor ?? {} };
2737
+ if (Object.keys(requiredLimits).length > 0) {
2738
+ descriptor.requiredLimits = {
2739
+ ...descriptor.requiredLimits ?? {},
2740
+ ...requiredLimits
2741
+ };
2742
+ }
2743
+ return Object.keys(descriptor).length > 0 ? descriptor : void 0;
2744
+ }
2464
2745
  async function createWavefrontPathTracingComputeRenderer(options = {}) {
2465
2746
  assertAnalyticDisplayQualityPolicy(options);
2466
2747
  const constants = getGpuUsageConstants();
@@ -2479,7 +2760,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
2479
2760
  if (!adapter) {
2480
2761
  throw new Error("Unable to acquire a WebGPU adapter for wavefront path tracing.");
2481
2762
  }
2482
- const device = await adapter.requestDevice();
2763
+ const device = await adapter.requestDevice(createWavefrontDeviceDescriptor(adapter, options));
2483
2764
  const context = canvas.getContext("webgpu");
2484
2765
  if (!context || typeof context.configure !== "function") {
2485
2766
  throw new Error("Canvas WebGPU context does not support configure().");
@@ -2552,23 +2833,34 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
2552
2833
  Math.max(1, config.gpuMeshSource.meshes.count) * MESH_RANGE_RECORD_BYTES,
2553
2834
  "plasius.wavefront.meshRanges"
2554
2835
  );
2836
+ const environmentPortalBuffer = createBuffer(
2837
+ device,
2838
+ constants.buffer.STORAGE | constants.buffer.COPY_DST,
2839
+ Math.max(1, config.environmentPortalCapacity) * ENVIRONMENT_PORTAL_RECORD_BYTES,
2840
+ "plasius.wavefront.environmentPortals"
2841
+ );
2555
2842
  const bvhLeafRefBuffer = createBuffer(
2556
2843
  device,
2557
2844
  constants.buffer.STORAGE | constants.buffer.COPY_DST,
2558
2845
  Math.max(1, config.bvhLeafSortCapacity) * BVH_LEAF_REF_RECORD_BYTES,
2559
2846
  "plasius.wavefront.bvhLeafRefs"
2560
2847
  );
2561
- const configBuffer = createBuffer(
2562
- device,
2563
- constants.buffer.UNIFORM | constants.buffer.COPY_DST,
2564
- CONFIG_BUFFER_BYTES,
2565
- "plasius.wavefront.frameConfig"
2566
- );
2848
+ const tiles = createTiles(config.width, config.height, config.tileSize);
2567
2849
  const uniformOffsetAlignment = Number(device?.limits?.minUniformBufferOffsetAlignment);
2568
2850
  const configBufferStride = alignTo(
2569
2851
  CONFIG_BUFFER_BYTES,
2570
2852
  Number.isFinite(uniformOffsetAlignment) && uniformOffsetAlignment > 0 ? uniformOffsetAlignment : CONFIG_BUFFER_BYTES
2571
2853
  );
2854
+ const frameConfigSlotCount = Math.max(
2855
+ 1,
2856
+ tiles.length * config.samplesPerPixel + tiles.length + (config.denoise ? 1 : 0)
2857
+ );
2858
+ const configBuffer = createBuffer(
2859
+ device,
2860
+ constants.buffer.UNIFORM | constants.buffer.COPY_DST,
2861
+ frameConfigSlotCount * configBufferStride,
2862
+ "plasius.wavefront.frameConfig"
2863
+ );
2572
2864
  const bvhBuildConfigSlots = 1 + config.bvhSortStages.length + config.bvhBuildLevels.length;
2573
2865
  const bvhBuildConfigBuffer = createBuffer(
2574
2866
  device,
@@ -2602,6 +2894,11 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
2602
2894
  device.queue.writeBuffer(meshVertexBuffer, 0, config.gpuMeshSource.vertices.buffer);
2603
2895
  device.queue.writeBuffer(meshIndexBuffer, 0, config.gpuMeshSource.indices.buffer);
2604
2896
  device.queue.writeBuffer(meshRangeBuffer, 0, config.gpuMeshSource.meshes.buffer);
2897
+ const packedEnvironmentPortals = packEnvironmentPortals(
2898
+ config.environmentPortals,
2899
+ Math.max(1, config.environmentPortalCapacity)
2900
+ );
2901
+ device.queue.writeBuffer(environmentPortalBuffer, 0, packedEnvironmentPortals.buffer);
2605
2902
  const radianceTexture = device.createTexture({
2606
2903
  label: "plasius.wavefront.radiance",
2607
2904
  size: { width: config.width, height: config.height },
@@ -2653,7 +2950,8 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
2653
2950
  binding: 16,
2654
2951
  visibility: constants.shader.COMPUTE,
2655
2952
  storageTexture: { access: "write-only", format: "rgba16float" }
2656
- }
2953
+ },
2954
+ { binding: 19, visibility: constants.shader.COMPUTE, buffer: { type: "read-only-storage" } }
2657
2955
  ]
2658
2956
  });
2659
2957
  const accelerationBindGroupLayout = device.createBindGroupLayout({
@@ -2826,7 +3124,8 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
2826
3124
  { binding: 7, resource: outputView },
2827
3125
  { binding: 8, resource: { buffer: triangleBuffer } },
2828
3126
  { binding: 9, resource: { buffer: bvhNodeBuffer } },
2829
- { binding: 16, resource: radianceView }
3127
+ { binding: 16, resource: radianceView },
3128
+ { binding: 19, resource: { buffer: environmentPortalBuffer } }
2830
3129
  ]
2831
3130
  });
2832
3131
  }
@@ -2913,10 +3212,27 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
2913
3212
  ]
2914
3213
  });
2915
3214
  let frame = 0;
2916
- const tiles = createTiles(config.width, config.height, config.tileSize);
3215
+ let accelerationBuilt = !config.gpuAccelerationBuildRequired;
3216
+ let accelerationBuildCount = 0;
3217
+ function createFrameConfigWriter(frameIndex) {
3218
+ let slot = 0;
3219
+ return (tile, buildRange = {}) => {
3220
+ if (slot >= frameConfigSlotCount) {
3221
+ throw new Error("Wavefront frame config slot capacity exceeded.");
3222
+ }
3223
+ const offset = slot * configBufferStride;
3224
+ slot += 1;
3225
+ device.queue.writeBuffer(
3226
+ configBuffer,
3227
+ offset,
3228
+ createConfigPayload(config, tile, frameIndex, buildRange)
3229
+ );
3230
+ return offset;
3231
+ };
3232
+ }
2917
3233
  function dispatchGpuAccelerationBuild(frameIndex) {
2918
- if (!config.gpuAccelerationBuildRequired) {
2919
- return;
3234
+ if (!config.gpuAccelerationBuildRequired || accelerationBuilt) {
3235
+ return false;
2920
3236
  }
2921
3237
  const buildTile = tiles[0] ?? { x: 0, y: 0, width: 1, height: 1 };
2922
3238
  const encoder = device.createCommandEncoder({
@@ -2974,27 +3290,21 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
2974
3290
  }
2975
3291
  passEncoder.end();
2976
3292
  device.queue.submit([encoder.finish()]);
3293
+ accelerationBuilt = true;
3294
+ accelerationBuildCount += 1;
3295
+ return true;
2977
3296
  }
2978
- function dispatchTileSample(tile, frameIndex, sampleIndex) {
2979
- const sampleWeight = 1 / config.samplesPerPixel;
2980
- const configPayload = createConfigPayload(config, tile, frameIndex, {
2981
- sampleIndex,
2982
- sampleWeight
2983
- });
2984
- device.queue.writeBuffer(configBuffer, 0, configPayload);
2985
- const encoder = device.createCommandEncoder({
2986
- label: `plasius.wavefront.frame.${frameIndex}.tile.${tile.x}.${tile.y}.sample.${sampleIndex}`
2987
- });
3297
+ function encodeTileSample(encoder, tile, configOffset) {
2988
3298
  const passEncoder = encoder.beginComputePass({
2989
3299
  label: "plasius.wavefront.computePass"
2990
3300
  });
2991
3301
  const tileWorkgroups = Math.ceil(tile.width * tile.height / WORKGROUP_SIZE);
2992
3302
  const capacityWorkgroups = Math.ceil(config.tilePixelCapacity / WORKGROUP_SIZE);
2993
- passEncoder.setBindGroup(0, bindGroups[0], [0]);
3303
+ passEncoder.setBindGroup(0, bindGroups[0], [configOffset]);
2994
3304
  passEncoder.setPipeline(pipelines.generatePrimaryRays);
2995
3305
  passEncoder.dispatchWorkgroups(tileWorkgroups);
2996
3306
  for (let bounceIndex = 0; bounceIndex < config.maxDepth; bounceIndex += 1) {
2997
- passEncoder.setBindGroup(0, bindGroups[bounceIndex % 2], [0]);
3307
+ passEncoder.setBindGroup(0, bindGroups[bounceIndex % 2], [configOffset]);
2998
3308
  passEncoder.setPipeline(pipelines.intersectActiveQueue);
2999
3309
  passEncoder.dispatchWorkgroups(capacityWorkgroups);
3000
3310
  passEncoder.setPipeline(pipelines.resolveSurfaceRecords);
@@ -3003,71 +3313,38 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3003
3313
  passEncoder.dispatchWorkgroups(1);
3004
3314
  }
3005
3315
  passEncoder.end();
3006
- device.queue.submit([encoder.finish()]);
3007
3316
  }
3008
- function dispatchTileOutput(tile, frameIndex) {
3009
- const configPayload = createConfigPayload(config, tile, frameIndex, {
3010
- sampleIndex: 0,
3011
- sampleWeight: 1 / config.samplesPerPixel
3012
- });
3013
- device.queue.writeBuffer(configBuffer, 0, configPayload);
3014
- const encoder = device.createCommandEncoder({
3015
- label: `plasius.wavefront.frame.${frameIndex}.tile.${tile.x}.${tile.y}.output`
3016
- });
3317
+ function encodeTileOutput(encoder, tile, configOffset) {
3017
3318
  const passEncoder = encoder.beginComputePass({
3018
3319
  label: "plasius.wavefront.outputPass"
3019
3320
  });
3020
3321
  const tileWorkgroups = Math.ceil(tile.width * tile.height / WORKGROUP_SIZE);
3021
- passEncoder.setBindGroup(0, bindGroups[0], [0]);
3322
+ passEncoder.setBindGroup(0, bindGroups[0], [configOffset]);
3022
3323
  passEncoder.setPipeline(pipelines.accumulateTerminalRadiance);
3023
3324
  passEncoder.dispatchWorkgroups(tileWorkgroups);
3024
3325
  passEncoder.end();
3025
- device.queue.submit([encoder.finish()]);
3026
- }
3027
- function dispatchTile(tile, frameIndex) {
3028
- for (let sampleIndex = 0; sampleIndex < config.samplesPerPixel; sampleIndex += 1) {
3029
- dispatchTileSample(tile, frameIndex, sampleIndex);
3030
- }
3031
- dispatchTileOutput(tile, frameIndex);
3032
3326
  }
3033
- function dispatchDenoise(frameIndex) {
3327
+ function encodeDenoise(encoder, configOffset) {
3034
3328
  if (!config.denoise) {
3035
3329
  return;
3036
3330
  }
3037
- device.queue.writeBuffer(
3038
- configBuffer,
3039
- 0,
3040
- createConfigPayload(
3041
- config,
3042
- { x: 0, y: 0, width: config.width, height: config.height },
3043
- frameIndex,
3044
- { sampleIndex: 0, sampleWeight: 1 / config.samplesPerPixel }
3045
- )
3046
- );
3047
- const encoder = device.createCommandEncoder({
3048
- label: `plasius.wavefront.frame.${frameIndex}.denoise`
3049
- });
3050
3331
  const radiancePass = encoder.beginComputePass({
3051
3332
  label: "plasius.wavefront.denoiseRadiancePass"
3052
3333
  });
3053
- radiancePass.setBindGroup(0, denoiseRadianceBindGroup, [0]);
3334
+ radiancePass.setBindGroup(0, denoiseRadianceBindGroup, [configOffset]);
3054
3335
  radiancePass.setPipeline(pipelines.denoiseLinearRadiance);
3055
3336
  radiancePass.dispatchWorkgroups(Math.ceil(config.width / 8), Math.ceil(config.height / 8));
3056
3337
  radiancePass.end();
3057
3338
  const resolvePass = encoder.beginComputePass({
3058
3339
  label: "plasius.wavefront.denoiseResolvePass"
3059
3340
  });
3060
- resolvePass.setBindGroup(0, denoiseResolveBindGroup, [0]);
3341
+ resolvePass.setBindGroup(0, denoiseResolveBindGroup, [configOffset]);
3061
3342
  resolvePass.setPipeline(pipelines.resolveDenoisedOutputImage);
3062
3343
  resolvePass.dispatchWorkgroups(Math.ceil(config.width / 8), Math.ceil(config.height / 8));
3063
3344
  resolvePass.end();
3064
- device.queue.submit([encoder.finish()]);
3065
3345
  }
3066
- function present() {
3346
+ function encodePresent(encoder) {
3067
3347
  const texture = context.getCurrentTexture();
3068
- const encoder = device.createCommandEncoder({
3069
- label: `plasius.wavefront.present.${frame}`
3070
- });
3071
3348
  const passEncoder = encoder.beginRenderPass({
3072
3349
  label: "plasius.wavefront.presentPass",
3073
3350
  colorAttachments: [
@@ -3083,16 +3360,42 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3083
3360
  passEncoder.setBindGroup(0, presentBindGroup);
3084
3361
  passEncoder.draw(3);
3085
3362
  passEncoder.end();
3363
+ }
3364
+ function dispatchFrame(frameIndex) {
3365
+ const writeFrameConfig = createFrameConfigWriter(frameIndex);
3366
+ const encoder = device.createCommandEncoder({
3367
+ label: `plasius.wavefront.frame.${frameIndex}.batched`
3368
+ });
3369
+ for (const tile of tiles) {
3370
+ for (let sampleIndex = 0; sampleIndex < config.samplesPerPixel; sampleIndex += 1) {
3371
+ const configOffset = writeFrameConfig(tile, {
3372
+ sampleIndex,
3373
+ sampleWeight: 1 / config.samplesPerPixel
3374
+ });
3375
+ encodeTileSample(encoder, tile, configOffset);
3376
+ }
3377
+ const outputConfigOffset = writeFrameConfig(tile, {
3378
+ sampleIndex: 0,
3379
+ sampleWeight: 1 / config.samplesPerPixel
3380
+ });
3381
+ encodeTileOutput(encoder, tile, outputConfigOffset);
3382
+ }
3383
+ if (config.denoise) {
3384
+ const denoiseConfigOffset = writeFrameConfig(
3385
+ { x: 0, y: 0, width: config.width, height: config.height },
3386
+ { sampleIndex: 0, sampleWeight: 1 / config.samplesPerPixel }
3387
+ );
3388
+ encodeDenoise(encoder, denoiseConfigOffset);
3389
+ }
3390
+ encodePresent(encoder);
3086
3391
  device.queue.submit([encoder.finish()]);
3392
+ return 1;
3087
3393
  }
3088
3394
  function renderOnce() {
3089
3395
  frame += 1;
3090
- dispatchGpuAccelerationBuild(frame + config.frameIndex);
3091
- for (const tile of tiles) {
3092
- dispatchTile(tile, frame + config.frameIndex);
3093
- }
3094
- dispatchDenoise(frame + config.frameIndex);
3095
- present();
3396
+ const frameIndex = frame + config.frameIndex;
3397
+ const accelerationBuildSubmitted = dispatchGpuAccelerationBuild(frameIndex);
3398
+ const frameSubmissionCount = dispatchFrame(frameIndex);
3096
3399
  return Object.freeze({
3097
3400
  frame,
3098
3401
  width: config.width,
@@ -3106,10 +3409,17 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3106
3409
  sceneObjectCount: config.sceneObjectCount,
3107
3410
  triangleCount: config.triangleCount,
3108
3411
  emissiveTriangleCount: config.emissiveTriangleCount,
3412
+ environmentPortalCount: config.environmentPortalCount,
3413
+ environmentPortalMode: config.environmentPortalMode,
3109
3414
  bvhNodeCount: config.bvhNodeCount,
3110
3415
  displayQuality: config.displayQuality,
3111
3416
  accelerationBuildMode: config.accelerationBuildMode,
3112
3417
  gpuAccelerationBuildRequired: config.gpuAccelerationBuildRequired,
3418
+ accelerationBuildSubmitted,
3419
+ accelerationBuilt,
3420
+ accelerationBuildCount,
3421
+ commandSubmissions: frameSubmissionCount + (accelerationBuildSubmitted ? 1 : 0),
3422
+ frameConfigSlots: frameConfigSlotCount,
3113
3423
  memory: config.memory
3114
3424
  });
3115
3425
  }
@@ -3175,10 +3485,15 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3175
3485
  sceneObjectCount: config.sceneObjectCount,
3176
3486
  triangleCount: config.triangleCount,
3177
3487
  emissiveTriangleCount: config.emissiveTriangleCount,
3488
+ environmentPortalCount: config.environmentPortalCount,
3489
+ environmentPortalMode: config.environmentPortalMode,
3178
3490
  bvhNodeCount: config.bvhNodeCount,
3179
3491
  displayQuality: config.displayQuality,
3180
3492
  accelerationBuildMode: config.accelerationBuildMode,
3181
3493
  gpuAccelerationBuildRequired: config.gpuAccelerationBuildRequired,
3494
+ accelerationBuilt,
3495
+ accelerationBuildCount,
3496
+ frameConfigSlots: frameConfigSlotCount,
3182
3497
  memory: config.memory
3183
3498
  });
3184
3499
  }
@@ -3193,6 +3508,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3193
3508
  meshVertexBuffer.destroy?.();
3194
3509
  meshIndexBuffer.destroy?.();
3195
3510
  meshRangeBuffer.destroy?.();
3511
+ environmentPortalBuffer.destroy?.();
3196
3512
  bvhLeafRefBuffer.destroy?.();
3197
3513
  configBuffer.destroy?.();
3198
3514
  bvhBuildConfigBuffer.destroy?.();