@plasius/gpu-renderer 0.2.6 → 0.2.8

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
@@ -212,17 +212,19 @@ var DEFAULT_MAX_DEPTH = 6;
212
212
  var DEFAULT_TILE_SIZE = 128;
213
213
  var DEFAULT_SAMPLES_PER_PIXEL = 1;
214
214
  var MAX_SAMPLES_PER_PIXEL = 256;
215
- var DEFAULT_BRDF_LUT_SIZE = 256;
215
+ var DEFAULT_BRDF_LUT_SIZE = 128;
216
+ var DEFAULT_BRDF_LUT_SAMPLE_COUNT = 256;
216
217
  var DEFAULT_MAX_FRAME_PASSES_PER_SUBMISSION = 256;
217
218
  var DEFAULT_SCENE_OBJECT_CAPACITY = 128;
218
219
  var DEFAULT_ENVIRONMENT_PORTAL_CAPACITY = 32;
220
+ var DEFAULT_MEDIUM_PHASE_MODEL = 0;
219
221
  var WORKGROUP_SIZE = 64;
220
222
  var rendererWavefrontComputeMode = "webgpu-compute";
221
223
  var rendererWavefrontComputeWorkgroupSize = WORKGROUP_SIZE;
222
224
  var rendererWavefrontComputeStatsStride = 8;
223
225
  var RAY_RECORD_BYTES = 80;
224
226
  var HIT_RECORD_BYTES = 256;
225
- var SCENE_OBJECT_RECORD_BYTES = 144;
227
+ var SCENE_OBJECT_RECORD_BYTES = 160;
226
228
  var MESH_VERTEX_RECORD_BYTES = 48;
227
229
  var MESH_RANGE_RECORD_BYTES = 240;
228
230
  var TRIANGLE_RECORD_BYTES = 352;
@@ -231,11 +233,13 @@ var BVH_NODE_RECORD_BYTES = 48;
231
233
  var BVH_LEAF_REF_RECORD_BYTES = 16;
232
234
  var EMISSIVE_TRIANGLE_INDEX_BYTES = 4;
233
235
  var ENVIRONMENT_PORTAL_RECORD_BYTES = 96;
236
+ var MEDIUM_TABLE_ROWS = 2;
234
237
  var ACCUMULATION_RECORD_BYTES = 16;
235
238
  var PATH_VERTEX_RECORD_BYTES = 16;
236
239
  var GPU_SUBMITTED_WORK_TIMEOUT_MS = 5e3;
237
240
  var GPU_READBACK_COMPLETION_TIMEOUT_MS = 6e4;
238
241
  var GPU_MAX_SUBMITTED_WORK_TIMEOUT_MS = 6e4;
242
+ var GPU_MAX_SUBMITTED_WORK_DEADLINE_MS = 18e4;
239
243
  var CONFIG_BUFFER_BYTES = 320;
240
244
  var COUNTER_DISPATCH_ARGS_OFFSET = 16;
241
245
  var INDIRECT_DISPATCH_ARGS_BYTES = 12;
@@ -577,6 +581,156 @@ function deriveBounds(input) {
577
581
  }
578
582
  return null;
579
583
  }
584
+ function deriveBeerLambertAbsorptionFromAttenuationColor(attenuationColor, attenuationDistance, density = 1) {
585
+ const distance = Number(attenuationDistance);
586
+ const densityScale = Math.max(0, Number(density) || 0);
587
+ if (!Number.isFinite(distance) || distance <= 0 || densityScale <= 0) {
588
+ return [0, 0, 0];
589
+ }
590
+ return attenuationColor.slice(0, 3).map((channel) => {
591
+ const clamped = clamp(Number(channel) || 0, 1e-4, 1);
592
+ return Math.max(0, -Math.log(clamped) / distance * densityScale);
593
+ });
594
+ }
595
+ function readMediumPhaseModel(value) {
596
+ if (typeof value === "number" && Number.isFinite(value)) {
597
+ return Math.max(0, Math.trunc(value));
598
+ }
599
+ switch (String(value ?? "").trim().toLowerCase()) {
600
+ case "isotropic":
601
+ default:
602
+ return DEFAULT_MEDIUM_PHASE_MODEL;
603
+ }
604
+ }
605
+ function resolveWavefrontVolumeInput(input) {
606
+ return input?.volume ?? input?.material?.volume ?? null;
607
+ }
608
+ function normalizeWavefrontThickness(input, label) {
609
+ const volume = resolveWavefrontVolumeInput(input);
610
+ return Math.max(
611
+ 0,
612
+ readFiniteNumber(
613
+ label,
614
+ input?.thickness ?? volume?.thickness ?? input?.material?.thickness,
615
+ 0
616
+ )
617
+ );
618
+ }
619
+ function resolveWavefrontMediumId(input, fallbackId = 1) {
620
+ return input?.mediumRefId ?? input?.mediumId ?? input?.material?.mediumId ?? input?.materialRefId ?? input?.material?.id ?? input?.materialId ?? input?.id ?? fallbackId;
621
+ }
622
+ function deriveWavefrontTransportMedium(input, fallbackId = 1) {
623
+ const resolvedId = resolveWavefrontMediumId(input, fallbackId);
624
+ if (input?.medium) {
625
+ return normalizeWavefrontMedium(
626
+ {
627
+ ...input.medium,
628
+ id: input.medium.id ?? input.medium.mediumId ?? resolvedId
629
+ },
630
+ fallbackId
631
+ );
632
+ }
633
+ const volume = resolveWavefrontVolumeInput(input);
634
+ if (!volume) {
635
+ return null;
636
+ }
637
+ return normalizeWavefrontMedium(
638
+ {
639
+ id: resolvedId,
640
+ phaseModel: volume.phaseModel,
641
+ density: volume.density,
642
+ attenuationColor: volume.attenuationColor,
643
+ attenuationDistance: volume.attenuationDistance,
644
+ absorption: volume.absorption,
645
+ scattering: volume.scattering
646
+ },
647
+ fallbackId
648
+ );
649
+ }
650
+ function normalizeWavefrontMedium(input = {}, index = 0) {
651
+ const id = readNonNegativeInteger("medium id", input.id ?? input.mediumId, index);
652
+ const density = Math.max(0, readFiniteNumber("medium density", input.density, 1));
653
+ const attenuationColor = asColor(
654
+ input.attenuationColor ?? input.color ?? input.medium?.attenuationColor,
655
+ [1, 1, 1, 1]
656
+ );
657
+ const attenuationDistance = readFiniteNumber(
658
+ "medium attenuationDistance",
659
+ input.attenuationDistance ?? input.distance ?? input.medium?.attenuationDistance,
660
+ 0
661
+ );
662
+ const absorption = Array.isArray(input.absorption) || Array.isArray(input.medium?.absorption) ? asVec3(input.absorption ?? input.medium?.absorption, [0, 0, 0]).map(
663
+ (value) => Math.max(0, Number(value) || 0)
664
+ ) : deriveBeerLambertAbsorptionFromAttenuationColor(
665
+ attenuationColor,
666
+ attenuationDistance,
667
+ density
668
+ );
669
+ const scattering = asVec3(
670
+ input.scattering ?? input.medium?.scattering,
671
+ [0, 0, 0]
672
+ ).map((value) => Math.max(0, Number(value) || 0));
673
+ return Object.freeze({
674
+ id,
675
+ phaseModel: readMediumPhaseModel(input.phaseModel ?? input.medium?.phaseModel),
676
+ density,
677
+ attenuationColor: Object.freeze(attenuationColor),
678
+ attenuationDistance,
679
+ absorption: Object.freeze(absorption),
680
+ scattering: Object.freeze(scattering)
681
+ });
682
+ }
683
+ function collectWavefrontMediums(options, meshes, sceneObjects = []) {
684
+ const mediumsById = /* @__PURE__ */ new Map();
685
+ mediumsById.set(
686
+ 0,
687
+ Object.freeze({
688
+ id: 0,
689
+ phaseModel: DEFAULT_MEDIUM_PHASE_MODEL,
690
+ density: 0,
691
+ attenuationColor: Object.freeze([1, 1, 1, 1]),
692
+ attenuationDistance: 0,
693
+ absorption: Object.freeze([0, 0, 0]),
694
+ scattering: Object.freeze([0, 0, 0])
695
+ })
696
+ );
697
+ const register = (input, fallbackId = mediumsById.size) => {
698
+ if (!input) {
699
+ return;
700
+ }
701
+ const normalized = normalizeWavefrontMedium(
702
+ typeof input === "object" ? { id: fallbackId, ...input } : { id: fallbackId },
703
+ fallbackId
704
+ );
705
+ const existing = mediumsById.get(normalized.id);
706
+ if (existing && JSON.stringify(existing) !== JSON.stringify(normalized)) {
707
+ throw new Error(`Medium id ${normalized.id} is defined more than once with different values.`);
708
+ }
709
+ mediumsById.set(normalized.id, normalized);
710
+ };
711
+ for (const medium of options.mediums ?? []) {
712
+ register(medium);
713
+ }
714
+ for (const mesh of meshes) {
715
+ register(mesh.medium, mesh.mediumRefId ?? mesh.medium?.id ?? 0);
716
+ }
717
+ for (const mesh of meshes) {
718
+ if ((mesh.mediumRefId ?? 0) > 0 && !mediumsById.has(mesh.mediumRefId)) {
719
+ register({ id: mesh.mediumRefId });
720
+ }
721
+ }
722
+ for (const object of sceneObjects) {
723
+ register(object.medium, object.mediumRefId ?? object.medium?.id ?? 0);
724
+ }
725
+ for (const object of sceneObjects) {
726
+ if ((object.mediumRefId ?? 0) > 0 && !mediumsById.has(object.mediumRefId)) {
727
+ register({ id: object.mediumRefId });
728
+ }
729
+ }
730
+ return Object.freeze(
731
+ Array.from(mediumsById.values()).sort((left, right) => left.id - right.id)
732
+ );
733
+ }
580
734
  function normalizeWavefrontSceneObject(input = {}, index = 0) {
581
735
  const bounds = deriveBounds(input);
582
736
  const kind = readObjectKind(input.kind ?? input.type ?? (bounds ? "box" : "sphere"));
@@ -607,12 +761,19 @@ function normalizeWavefrontSceneObject(input = {}, index = 0) {
607
761
  input.specularColor ?? input.material?.specularColor,
608
762
  [1, 1, 1, 1]
609
763
  ).map((value, componentIndex) => componentIndex < 3 ? clamp(value, 0, 1) : 1);
764
+ const medium = deriveWavefrontTransportMedium(input, index + 1);
610
765
  const resolvedMaterialKind = emission[0] > 0 || emission[1] > 0 || emission[2] > 0 ? MATERIAL_EMISSIVE : materialKindInput === void 0 || materialKindInput === null ? transmission > 1e-3 || opacity < 0.999 ? MATERIAL_TRANSPARENT : materialKind : materialKind;
611
766
  return Object.freeze({
612
767
  id: readNonNegativeInteger("id", input.id, index + 1),
613
768
  kind,
614
769
  materialKind: resolvedMaterialKind,
615
770
  flags: readNonNegativeInteger("flags", input.flags, 0),
771
+ mediumRefId: readNonNegativeInteger(
772
+ "mediumRefId",
773
+ input.mediumRefId ?? medium?.id ?? input.medium?.id ?? input.mediumId,
774
+ 0
775
+ ),
776
+ medium,
616
777
  center: Object.freeze(center),
617
778
  halfExtent: Object.freeze(halfExtent),
618
779
  color: Object.freeze(color),
@@ -636,6 +797,7 @@ function normalizeWavefrontSceneObject(input = {}, index = 0) {
636
797
  ),
637
798
  specular: clamp(readFiniteNumber("specular", input.specular ?? input.material?.specular, 1), 0, 1),
638
799
  specularColor: Object.freeze(specularColor),
800
+ thickness: normalizeWavefrontThickness(input, "thickness"),
639
801
  transmission
640
802
  });
641
803
  }
@@ -729,6 +891,7 @@ function normalizeWavefrontMesh(input = {}, meshIndex = 0) {
729
891
  input.specularColor ?? input.material?.specularColor,
730
892
  [1, 1, 1, 1]
731
893
  ).map((value, componentIndex) => componentIndex < 3 ? clamp(value, 0, 1) : 1);
894
+ const medium = deriveWavefrontTransportMedium(input, meshIndex + 1);
732
895
  const resolvedMaterialKind = emission[0] > 0 || emission[1] > 0 || emission[2] > 0 ? MATERIAL_EMISSIVE : materialKindInput === void 0 || materialKindInput === null ? transmission > 1e-3 || opacity < 0.999 ? MATERIAL_TRANSPARENT : materialKind : materialKind;
733
896
  return Object.freeze({
734
897
  id: readNonNegativeInteger("mesh id", input.id, meshIndex + 1),
@@ -745,9 +908,10 @@ function normalizeWavefrontMesh(input = {}, meshIndex = 0) {
745
908
  ),
746
909
  mediumRefId: readNonNegativeInteger(
747
910
  "mesh mediumRefId",
748
- input.mediumRefId ?? input.medium?.id ?? input.mediumId,
911
+ input.mediumRefId ?? medium?.id ?? input.medium?.id ?? input.mediumId ?? input.material?.mediumId,
749
912
  0
750
913
  ),
914
+ medium,
751
915
  color: Object.freeze(color),
752
916
  emission: Object.freeze(emission),
753
917
  roughness: clamp(readFiniteNumber("roughness", input.roughness ?? input.material?.roughness, 0.72), 0, 1),
@@ -769,6 +933,7 @@ function normalizeWavefrontMesh(input = {}, meshIndex = 0) {
769
933
  ),
770
934
  specular: clamp(readFiniteNumber("specular", input.specular ?? input.material?.specular, 1), 0, 1),
771
935
  specularColor: Object.freeze(specularColor),
936
+ thickness: normalizeWavefrontThickness(input, "mesh thickness"),
772
937
  transmission,
773
938
  baseColorTexture: input.baseColorTexture ?? input.material?.baseColorTexture ?? null,
774
939
  metallicRoughnessTexture: input.metallicRoughnessTexture ?? input.material?.metallicRoughnessTexture ?? null,
@@ -780,141 +945,9 @@ function normalizeWavefrontMesh(input = {}, meshIndex = 0) {
780
945
  function clampUnit(value) {
781
946
  return clamp(Number(value) || 0, 0, 1);
782
947
  }
783
- function srgbToLinear(value) {
784
- const channel = clampUnit(value);
785
- if (channel <= 0.04045) {
786
- return channel / 12.92;
787
- }
788
- return ((channel + 0.055) / 1.055) ** 2.4;
789
- }
790
- function sampleTextureRgba(texture, uv = [0, 0], colorSpace = "linear") {
791
- if (!texture || !Number.isFinite(texture.width) || !Number.isFinite(texture.height) || !texture.data || texture.width <= 0 || texture.height <= 0) {
792
- return [1, 1, 1, 1];
793
- }
794
- const u = (uv[0] % 1 + 1) % 1;
795
- const v = (uv[1] % 1 + 1) % 1;
796
- const x = Math.min(texture.width - 1, Math.max(0, Math.round(u * (texture.width - 1))));
797
- const y = Math.min(texture.height - 1, Math.max(0, Math.round((1 - v) * (texture.height - 1))));
798
- const offset = (y * texture.width + x) * 4;
799
- const data = texture.data;
800
- const scale2 = resolveTextureSampleScale(data);
801
- const defaultChannel = scale2 === 1 ? 1 : Math.round(1 / scale2);
802
- const color = [
803
- (data[offset] ?? defaultChannel) * scale2,
804
- (data[offset + 1] ?? defaultChannel) * scale2,
805
- (data[offset + 2] ?? defaultChannel) * scale2,
806
- (data[offset + 3] ?? defaultChannel) * scale2
807
- ];
808
- if (colorSpace === "srgb") {
809
- return [srgbToLinear(color[0]), srgbToLinear(color[1]), srgbToLinear(color[2]), color[3]];
810
- }
811
- return color;
812
- }
813
- function resolveTextureSampleScale(data) {
814
- if (data instanceof Uint8Array || data instanceof Uint8ClampedArray) {
815
- return 1 / 255;
816
- }
817
- if (data instanceof Uint16Array) {
818
- return 1 / 65535;
819
- }
820
- if (Array.isArray(data) && data.some((value) => Number(value) > 1)) {
821
- return 1 / 255;
822
- }
823
- return 1;
824
- }
825
- function normalizeVectorOrFallback(vector, fallback) {
826
- return normalize(Array.isArray(vector) ? vector : fallback, fallback);
827
- }
828
- function buildTriangleTangentBasis(v0, v1, v2, uv0, uv1, uv2, fallbackNormal) {
829
- const edge1 = subtract(v1, v0);
830
- const edge2 = subtract(v2, v0);
831
- const deltaUv1 = [uv1[0] - uv0[0], uv1[1] - uv0[1]];
832
- const deltaUv2 = [uv2[0] - uv0[0], uv2[1] - uv0[1]];
833
- const determinant = deltaUv1[0] * deltaUv2[1] - deltaUv1[1] * deltaUv2[0];
834
- if (Math.abs(determinant) < 1e-6) {
835
- const tangentFallback = Math.abs(fallbackNormal[1]) < 0.999 ? [0, 1, 0] : [1, 0, 0];
836
- const tangent2 = normalize(cross(tangentFallback, fallbackNormal), [1, 0, 0]);
837
- const bitangent2 = normalize(cross(fallbackNormal, tangent2), [0, 0, 1]);
838
- return { tangent: tangent2, bitangent: bitangent2 };
839
- }
840
- const inverse = 1 / determinant;
841
- const tangent = normalize(
842
- [
843
- inverse * (edge1[0] * deltaUv2[1] - edge2[0] * deltaUv1[1]),
844
- inverse * (edge1[1] * deltaUv2[1] - edge2[1] * deltaUv1[1]),
845
- inverse * (edge1[2] * deltaUv2[1] - edge2[2] * deltaUv1[1])
846
- ],
847
- [1, 0, 0]
848
- );
849
- const bitangent = normalize(
850
- [
851
- inverse * (-edge1[0] * deltaUv2[0] + edge2[0] * deltaUv1[0]),
852
- inverse * (-edge1[1] * deltaUv2[0] + edge2[1] * deltaUv1[0]),
853
- inverse * (-edge1[2] * deltaUv2[0] + edge2[2] * deltaUv1[0])
854
- ],
855
- [0, 0, 1]
856
- );
857
- return { tangent, bitangent };
858
- }
859
- function applyNormalMap(normal, tangent, bitangent, normalTexture, uv) {
860
- if (!normalTexture) {
861
- return normalizeVectorOrFallback(normal, [0, 1, 0]);
862
- }
863
- const sample = sampleTextureRgba(normalTexture, uv, "linear");
864
- const strength = clampUnit(normalTexture.scale ?? 1);
865
- const tangentNormal = normalize(
866
- [
867
- (sample[0] * 2 - 1) * strength,
868
- (sample[1] * 2 - 1) * strength,
869
- 1 + (sample[2] * 2 - 1 - 1) * strength
870
- ],
871
- [0, 0, 1]
872
- );
873
- return normalize(
874
- [
875
- tangent[0] * tangentNormal[0] + bitangent[0] * tangentNormal[1] + normal[0] * tangentNormal[2],
876
- tangent[1] * tangentNormal[0] + bitangent[1] * tangentNormal[1] + normal[1] * tangentNormal[2],
877
- tangent[2] * tangentNormal[0] + bitangent[2] * tangentNormal[1] + normal[2] * tangentNormal[2]
878
- ],
879
- normal
880
- );
881
- }
882
- function sampleBaseColor(mesh, uv) {
883
- const sample = mesh.baseColorTexture ? sampleTextureRgba(mesh.baseColorTexture, uv, "srgb") : [1, 1, 1, 1];
884
- return [
885
- clampUnit(mesh.color[0] * sample[0]),
886
- clampUnit(mesh.color[1] * sample[1]),
887
- clampUnit(mesh.color[2] * sample[2]),
888
- clampUnit((mesh.color[3] ?? 1) * sample[3])
889
- ];
890
- }
891
- function sampleSurfaceMaterial(mesh, uv) {
892
- const textureSample = mesh.metallicRoughnessTexture ? sampleTextureRgba(mesh.metallicRoughnessTexture, uv, "linear") : [1, 1, 1, 1];
893
- return {
894
- roughness: clamp(mesh.roughness * textureSample[1], 0, 1),
895
- metallic: clamp(mesh.metallic * textureSample[2], 0, 1)
896
- };
897
- }
898
- function averageColors(colors) {
899
- const count = Math.max(colors.length, 1);
900
- return colors.reduce(
901
- (accumulator, color) => [
902
- accumulator[0] + color[0] / count,
903
- accumulator[1] + color[1] / count,
904
- accumulator[2] + color[2] / count,
905
- accumulator[3] + color[3] / count
906
- ],
907
- [0, 0, 0, 0]
908
- );
909
- }
910
- function averageNumbers(values, fallback = 0) {
911
- if (!Array.isArray(values) || values.length === 0) {
912
- return fallback;
913
- }
914
- return values.reduce((sum, value) => sum + value, 0) / values.length;
915
- }
916
- function createMeshTriangleRecords(meshes) {
948
+ function createMeshTriangleRecords(meshes, gpuMaterialSource = null) {
917
949
  const source = Array.isArray(meshes) ? meshes : [];
950
+ const resolvedMaterialSource = gpuMaterialSource ?? createWavefrontGpuMaterialSource(source);
918
951
  let nextTriangleId = 0;
919
952
  return source.flatMap((meshInput, meshIndex) => {
920
953
  const mesh = normalizeWavefrontMesh(meshInput, meshIndex);
@@ -933,16 +966,6 @@ function createMeshTriangleRecords(meshes) {
933
966
  const uv0 = mesh.uvs ? readVector2(mesh.uvs, a) : [0, 0];
934
967
  const uv1 = mesh.uvs ? readVector2(mesh.uvs, b) : [0, 0];
935
968
  const uv2 = mesh.uvs ? readVector2(mesh.uvs, c) : [0, 0];
936
- const tangentBasis = buildTriangleTangentBasis(v0, v1, v2, uv0, uv1, uv2, faceNormal);
937
- const shadedN0 = applyNormalMap(n0, tangentBasis.tangent, tangentBasis.bitangent, mesh.normalTexture, uv0);
938
- const shadedN1 = applyNormalMap(n1, tangentBasis.tangent, tangentBasis.bitangent, mesh.normalTexture, uv1);
939
- const shadedN2 = applyNormalMap(n2, tangentBasis.tangent, tangentBasis.bitangent, mesh.normalTexture, uv2);
940
- const sampledColors = [sampleBaseColor(mesh, uv0), sampleBaseColor(mesh, uv1), sampleBaseColor(mesh, uv2)];
941
- const sampledMaterials = [
942
- sampleSurfaceMaterial(mesh, uv0),
943
- sampleSurfaceMaterial(mesh, uv1),
944
- sampleSurfaceMaterial(mesh, uv2)
945
- ];
946
969
  const bounds = triangleBounds(v0, v1, v2);
947
970
  triangles.push(
948
971
  Object.freeze({
@@ -956,17 +979,17 @@ function createMeshTriangleRecords(meshes) {
956
979
  v0: Object.freeze(v0),
957
980
  v1: Object.freeze(v1),
958
981
  v2: Object.freeze(v2),
959
- n0: Object.freeze(shadedN0),
960
- n1: Object.freeze(shadedN1),
961
- n2: Object.freeze(shadedN2),
982
+ n0: Object.freeze(n0),
983
+ n1: Object.freeze(n1),
984
+ n2: Object.freeze(n2),
962
985
  uv0: Object.freeze(uv0),
963
986
  uv1: Object.freeze(uv1),
964
987
  uv2: Object.freeze(uv2),
965
- color: Object.freeze(averageColors(sampledColors)),
988
+ color: mesh.color,
966
989
  emission: mesh.emission,
967
990
  material: Object.freeze([
968
- averageNumbers(sampledMaterials.map((sample) => sample.roughness), mesh.roughness),
969
- averageNumbers(sampledMaterials.map((sample) => sample.metallic), mesh.metallic),
991
+ mesh.roughness,
992
+ mesh.metallic,
970
993
  mesh.opacity,
971
994
  mesh.ior
972
995
  ]),
@@ -980,7 +1003,7 @@ function createMeshTriangleRecords(meshes) {
980
1003
  mesh.clearcoatRoughness,
981
1004
  mesh.specular,
982
1005
  mesh.transmission,
983
- 0
1006
+ mesh.thickness
984
1007
  ]),
985
1008
  specularColor: Object.freeze([
986
1009
  mesh.specularColor[0] ?? 1,
@@ -988,6 +1011,27 @@ function createMeshTriangleRecords(meshes) {
988
1011
  mesh.specularColor[2] ?? 1,
989
1012
  1
990
1013
  ]),
1014
+ baseColorAtlas: Object.freeze(
1015
+ resolvedMaterialSource.baseColorAtlas.resolveRect(mesh.baseColorTexture)
1016
+ ),
1017
+ metallicRoughnessAtlas: Object.freeze(
1018
+ resolvedMaterialSource.metallicRoughnessAtlas.resolveRect(mesh.metallicRoughnessTexture)
1019
+ ),
1020
+ normalAtlas: Object.freeze(
1021
+ resolvedMaterialSource.normalAtlas.resolveRect(mesh.normalTexture)
1022
+ ),
1023
+ occlusionAtlas: Object.freeze(
1024
+ resolvedMaterialSource.occlusionAtlas.resolveRect(mesh.occlusionTexture)
1025
+ ),
1026
+ emissiveAtlas: Object.freeze(
1027
+ resolvedMaterialSource.emissiveAtlas.resolveRect(mesh.emissiveTexture)
1028
+ ),
1029
+ textureSettings: Object.freeze([
1030
+ clampUnit(mesh.normalTexture?.scale ?? mesh.normalTexture?.strength ?? 1),
1031
+ clampUnit(mesh.occlusionTexture?.strength ?? 1),
1032
+ clampUnit(mesh.emissiveTexture?.strength ?? 1),
1033
+ 0
1034
+ ]),
991
1035
  bounds: Object.freeze({
992
1036
  min: Object.freeze(bounds.min),
993
1037
  max: Object.freeze(bounds.max)
@@ -1062,9 +1106,10 @@ function buildBvh(triangles, maxLeafTriangles = 4) {
1062
1106
  triangles: Object.freeze(orderedTriangles)
1063
1107
  });
1064
1108
  }
1065
- function createWavefrontMeshAcceleration(meshes = []) {
1109
+ function createWavefrontMeshAcceleration(meshes = [], gpuMaterialSource = null) {
1066
1110
  const source = Array.isArray(meshes) ? meshes : [meshes];
1067
- const triangles = createMeshTriangleRecords(source);
1111
+ const resolvedMaterialSource = gpuMaterialSource ?? createWavefrontGpuMaterialSource(source);
1112
+ const triangles = createMeshTriangleRecords(source, resolvedMaterialSource);
1068
1113
  return buildBvh(triangles);
1069
1114
  }
1070
1115
  function estimateMeshSourceShape(meshes) {
@@ -1274,7 +1319,7 @@ function createWavefrontGpuMaterialSource(meshes = []) {
1274
1319
  mesh.clearcoatRoughness,
1275
1320
  mesh.specular,
1276
1321
  mesh.transmission,
1277
- 0
1322
+ mesh.thickness
1278
1323
  ]);
1279
1324
  writeVec4(floatView, byteOffset + 80, [
1280
1325
  mesh.specularColor[0] ?? 1,
@@ -1358,12 +1403,12 @@ function createWavefrontBvhBuildLevels(triangleCountInput) {
1358
1403
  return Object.freeze(levels);
1359
1404
  }
1360
1405
  function resolveAccelerationBuildMode(options = {}) {
1361
- const mode = options.accelerationBuildMode ?? (options.displayQuality === true ? "gpu" : "cpu-debug");
1362
- if (mode !== "gpu" && mode !== "cpu-debug") {
1363
- throw new Error('accelerationBuildMode must be either "gpu" or "cpu-debug".');
1364
- }
1365
- if (options.displayQuality === true && mode !== "gpu") {
1366
- throw new Error("Display-quality path tracing requires GPU-built mesh acceleration.");
1406
+ const requestedMode = options.accelerationBuildMode ?? (options.displayQuality === true ? "cpu-upload" : "cpu-debug");
1407
+ const mode = requestedMode === "cpu-debug" ? "cpu-upload" : requestedMode;
1408
+ if (mode !== "gpu" && mode !== "cpu-upload") {
1409
+ throw new Error(
1410
+ 'accelerationBuildMode must be either "gpu", "cpu-upload", or the legacy alias "cpu-debug".'
1411
+ );
1367
1412
  }
1368
1413
  return mode;
1369
1414
  }
@@ -1442,7 +1487,7 @@ function createWavefrontGpuMeshSource(meshes = [], gpuMaterialSourceInput = null
1442
1487
  mesh.clearcoatRoughness,
1443
1488
  mesh.specular,
1444
1489
  mesh.transmission,
1445
- 0
1490
+ mesh.thickness
1446
1491
  ]);
1447
1492
  writeVec4(meshFloats, floatOffset * 4 + 128, [
1448
1493
  mesh.specularColor[0] ?? 1,
@@ -1543,12 +1588,16 @@ function normalizeSceneObjects(sceneObjects, useDefaultScene = true) {
1543
1588
  const source = Array.isArray(sceneObjects) && sceneObjects.length > 0 ? sceneObjects : useDefaultScene ? createDefaultWavefrontSceneObjects() : [];
1544
1589
  return source.map((object, index) => normalizeWavefrontSceneObject(object, index));
1545
1590
  }
1591
+ function normalizeWavefrontMeshes(meshes) {
1592
+ const source = Array.isArray(meshes) ? meshes : [];
1593
+ return source.map((mesh, index) => normalizeWavefrontMesh(mesh, index));
1594
+ }
1546
1595
  function normalizeMeshes(options = {}) {
1547
1596
  if (Array.isArray(options.meshes)) {
1548
- return options.meshes;
1597
+ return normalizeWavefrontMeshes(options.meshes);
1549
1598
  }
1550
1599
  if (options.mesh) {
1551
- return [options.mesh];
1600
+ return normalizeWavefrontMeshes([options.mesh]);
1552
1601
  }
1553
1602
  return [];
1554
1603
  }
@@ -1845,7 +1894,7 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
1845
1894
  const meshSourceShape = estimateMeshSourceShape(meshes);
1846
1895
  const gpuMaterialSource = meshes.length > 0 ? createWavefrontGpuMaterialSource(meshes) : createWavefrontGpuMaterialSource([]);
1847
1896
  const gpuMeshSource = meshes.length > 0 ? createWavefrontGpuMeshSource(meshes, gpuMaterialSource) : createWavefrontGpuMeshSource([]);
1848
- const meshAcceleration = accelerationBuildMode === "cpu-debug" ? createWavefrontMeshAcceleration(meshes) : Object.freeze({ nodes: Object.freeze([]), triangles: Object.freeze([]) });
1897
+ const meshAcceleration = accelerationBuildMode === "cpu-upload" ? createWavefrontMeshAcceleration(meshes, gpuMaterialSource) : Object.freeze({ nodes: Object.freeze([]), triangles: Object.freeze([]) });
1849
1898
  const emissiveTriangleIndices = createWavefrontEmissiveTriangleIndexSource(
1850
1899
  meshes,
1851
1900
  options.emissiveTriangleCapacity
@@ -1855,6 +1904,7 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
1855
1904
  const sceneObjects = Object.freeze(
1856
1905
  normalizeSceneObjects(options.sceneObjects, meshes.length === 0)
1857
1906
  );
1907
+ const mediums = collectWavefrontMediums(options, meshes, sceneObjects);
1858
1908
  const sceneObjectCapacity = Math.max(
1859
1909
  sceneObjects.length,
1860
1910
  readPositiveInteger("sceneObjectCapacity", options.sceneObjectCapacity, DEFAULT_SCENE_OBJECT_CAPACITY)
@@ -1913,6 +1963,8 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
1913
1963
  sceneObjects,
1914
1964
  sceneObjectCount: sceneObjects.length,
1915
1965
  sceneObjectCapacity,
1966
+ mediums,
1967
+ mediumCount: mediums.length,
1916
1968
  accelerationBuildMode,
1917
1969
  gpuAccelerationBuildRequired: accelerationBuildMode === "gpu" && triangleCount > 0,
1918
1970
  gpuMeshSource,
@@ -2014,29 +2066,30 @@ function packWavefrontSceneObjects(sceneObjects, capacity = sceneObjects.length)
2014
2066
  uintView[u32 + 1] = object.id;
2015
2067
  uintView[u32 + 2] = object.materialKind;
2016
2068
  uintView[u32 + 3] = object.flags;
2017
- writeVec4(floatView, byteOffset + 16, [...object.center, 0]);
2018
- writeVec4(floatView, byteOffset + 32, [...object.halfExtent, 0]);
2019
- writeVec4(floatView, byteOffset + 48, object.color);
2020
- writeVec4(floatView, byteOffset + 64, object.emission);
2021
- writeVec4(floatView, byteOffset + 80, [
2069
+ uintView[u32 + 4] = object.mediumRefId;
2070
+ writeVec4(floatView, byteOffset + 32, [...object.center, 0]);
2071
+ writeVec4(floatView, byteOffset + 48, [...object.halfExtent, 0]);
2072
+ writeVec4(floatView, byteOffset + 64, object.color);
2073
+ writeVec4(floatView, byteOffset + 80, object.emission);
2074
+ writeVec4(floatView, byteOffset + 96, [
2022
2075
  object.roughness,
2023
2076
  object.metallic,
2024
2077
  object.opacity,
2025
2078
  object.ior
2026
2079
  ]);
2027
- writeVec4(floatView, byteOffset + 96, [
2080
+ writeVec4(floatView, byteOffset + 112, [
2028
2081
  object.sheenColor[0] ?? 0,
2029
2082
  object.sheenColor[1] ?? 0,
2030
2083
  object.sheenColor[2] ?? 0,
2031
2084
  object.clearcoat
2032
2085
  ]);
2033
- writeVec4(floatView, byteOffset + 112, [
2086
+ writeVec4(floatView, byteOffset + 128, [
2034
2087
  object.clearcoatRoughness,
2035
2088
  object.specular,
2036
2089
  object.transmission,
2037
- 0
2090
+ object.thickness
2038
2091
  ]);
2039
- writeVec4(floatView, byteOffset + 128, [
2092
+ writeVec4(floatView, byteOffset + 144, [
2040
2093
  object.specularColor[0] ?? 1,
2041
2094
  object.specularColor[1] ?? 1,
2042
2095
  object.specularColor[2] ?? 1,
@@ -2572,7 +2625,7 @@ function integrateBrdfSample(nDotV, roughness, sampleCount) {
2572
2625
  }
2573
2626
  return [scaleTerm / sampleCount, biasTerm / sampleCount];
2574
2627
  }
2575
- function createBrdfLutUploadBytes(size = DEFAULT_BRDF_LUT_SIZE, sampleCount = 1024) {
2628
+ function createBrdfLutUploadBytes(size = DEFAULT_BRDF_LUT_SIZE, sampleCount = DEFAULT_BRDF_LUT_SAMPLE_COUNT) {
2576
2629
  const cacheKey = `${Math.max(1, Math.trunc(size))}:${Math.max(1, Math.trunc(sampleCount))}`;
2577
2630
  const cached = BRDF_LUT_UPLOAD_CACHE.get(cacheKey);
2578
2631
  if (cached) {
@@ -2944,6 +2997,80 @@ function createBrdfLutResource(device, constants, size = DEFAULT_BRDF_LUT_SIZE)
2944
2997
  height: upload.height
2945
2998
  });
2946
2999
  }
3000
+ function createMediumTextureResource(device, constants, mediums) {
3001
+ const normalized = Array.isArray(mediums) && mediums.length > 0 ? mediums : [{ id: 0 }];
3002
+ const width = Math.max(
3003
+ 1,
3004
+ normalized.reduce((maximum, medium) => Math.max(maximum, medium.id ?? 0), 0) + 1
3005
+ );
3006
+ const level = {
3007
+ width,
3008
+ height: MEDIUM_TABLE_ROWS,
3009
+ data: new Float32Array(width * MEDIUM_TABLE_ROWS * 4)
3010
+ };
3011
+ for (const medium of normalized) {
3012
+ const mediumId = Math.max(0, Math.trunc(Number(medium.id) || 0));
3013
+ const absorptionOffset = mediumId * 4;
3014
+ level.data[absorptionOffset] = Math.max(0, medium.absorption?.[0] ?? 0);
3015
+ level.data[absorptionOffset + 1] = Math.max(0, medium.absorption?.[1] ?? 0);
3016
+ level.data[absorptionOffset + 2] = Math.max(0, medium.absorption?.[2] ?? 0);
3017
+ level.data[absorptionOffset + 3] = Math.max(0, medium.phaseModel ?? 0);
3018
+ const scatteringOffset = (width + mediumId) * 4;
3019
+ level.data[scatteringOffset] = Math.max(0, medium.scattering?.[0] ?? 0);
3020
+ level.data[scatteringOffset + 1] = Math.max(0, medium.scattering?.[1] ?? 0);
3021
+ level.data[scatteringOffset + 2] = Math.max(0, medium.scattering?.[2] ?? 0);
3022
+ level.data[scatteringOffset + 3] = Math.max(0, medium.density ?? 0);
3023
+ }
3024
+ const upload = createFloat16RgbaUploadFromLevels([level])[0];
3025
+ const texture = device.createTexture({
3026
+ label: "plasius.wavefront.mediumTable",
3027
+ size: { width, height: MEDIUM_TABLE_ROWS },
3028
+ format: "rgba16float",
3029
+ usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
3030
+ });
3031
+ device.queue.writeTexture(
3032
+ { texture },
3033
+ upload.bytes,
3034
+ { bytesPerRow: upload.bytesPerRow, rowsPerImage: upload.height },
3035
+ { width, height: MEDIUM_TABLE_ROWS, depthOrArrayLayers: 1 }
3036
+ );
3037
+ return Object.freeze({
3038
+ texture,
3039
+ view: texture.createView(),
3040
+ ownsTexture: true,
3041
+ count: normalized.length,
3042
+ width
3043
+ });
3044
+ }
3045
+ function mediumTablesEqual(left, right) {
3046
+ const leftMediums = Array.isArray(left) ? left : [];
3047
+ const rightMediums = Array.isArray(right) ? right : [];
3048
+ if (leftMediums.length !== rightMediums.length) {
3049
+ return false;
3050
+ }
3051
+ for (let index = 0; index < leftMediums.length; index += 1) {
3052
+ const leftMedium = leftMediums[index];
3053
+ const rightMedium = rightMediums[index];
3054
+ if ((leftMedium?.id ?? 0) !== (rightMedium?.id ?? 0)) {
3055
+ return false;
3056
+ }
3057
+ if ((leftMedium?.phaseModel ?? 0) !== (rightMedium?.phaseModel ?? 0)) {
3058
+ return false;
3059
+ }
3060
+ if ((leftMedium?.density ?? 0) !== (rightMedium?.density ?? 0)) {
3061
+ return false;
3062
+ }
3063
+ for (let component = 0; component < 3; component += 1) {
3064
+ if ((leftMedium?.absorption?.[component] ?? 0) !== (rightMedium?.absorption?.[component] ?? 0)) {
3065
+ return false;
3066
+ }
3067
+ if ((leftMedium?.scattering?.[component] ?? 0) !== (rightMedium?.scattering?.[component] ?? 0)) {
3068
+ return false;
3069
+ }
3070
+ }
3071
+ }
3072
+ return true;
3073
+ }
2947
3074
  function createAtlasTextureResource(device, constants, atlas, label) {
2948
3075
  const upload = createRgba8TextureUpload(atlas);
2949
3076
  const texture = device.createTexture({
@@ -3082,6 +3209,10 @@ struct SceneObject {
3082
3209
  objectId: u32,
3083
3210
  materialKind: u32,
3084
3211
  flags: u32,
3212
+ mediumRefId: u32,
3213
+ pad0: u32,
3214
+ pad1: u32,
3215
+ pad2: u32,
3085
3216
  center: vec4<f32>,
3086
3217
  halfExtent: vec4<f32>,
3087
3218
  color: vec4<f32>,
@@ -3142,9 +3273,9 @@ struct BvhLeafRef {
3142
3273
  struct ScatterResult {
3143
3274
  direction: vec4<f32>,
3144
3275
  pdf: f32,
3276
+ mediumRefId: u32,
3145
3277
  flags: u32,
3146
3278
  pad0: u32,
3147
- pad1: u32,
3148
3279
  };
3149
3280
 
3150
3281
  struct MeshVertex {
@@ -3290,6 +3421,7 @@ struct EnvironmentPortal {
3290
3421
  @group(0) @binding(29) var brdfLutTexture: texture_2d<f32>;
3291
3422
  @group(0) @binding(30) var brdfLutSampler: sampler;
3292
3423
  @group(0) @binding(31) var environmentSamplingTexture: texture_2d<f32>;
3424
+ @group(0) @binding(32) var mediumTableTexture: texture_2d<f32>;
3293
3425
 
3294
3426
  fn hash_u32(value: u32) -> u32 {
3295
3427
  var x = value;
@@ -3955,6 +4087,60 @@ fn gated_environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f
3955
4087
  return environment_radiance(origin, direction);
3956
4088
  }
3957
4089
 
4090
+ fn medium_dimensions() -> vec2<u32> {
4091
+ return textureDimensions(mediumTableTexture);
4092
+ }
4093
+
4094
+ fn medium_valid(mediumRefId: u32) -> bool {
4095
+ let dimensions = medium_dimensions();
4096
+ return mediumRefId > 0u && mediumRefId < dimensions.x;
4097
+ }
4098
+
4099
+ fn medium_absorption(mediumRefId: u32) -> vec3<f32> {
4100
+ if (!medium_valid(mediumRefId)) {
4101
+ return vec3<f32>(0.0);
4102
+ }
4103
+ return max(
4104
+ textureLoad(mediumTableTexture, vec2<i32>(i32(mediumRefId), 0), 0).xyz,
4105
+ vec3<f32>(0.0)
4106
+ );
4107
+ }
4108
+
4109
+ fn medium_scattering(mediumRefId: u32) -> vec3<f32> {
4110
+ if (!medium_valid(mediumRefId)) {
4111
+ return vec3<f32>(0.0);
4112
+ }
4113
+ return max(
4114
+ textureLoad(mediumTableTexture, vec2<i32>(i32(mediumRefId), 1), 0).xyz,
4115
+ vec3<f32>(0.0)
4116
+ );
4117
+ }
4118
+
4119
+ fn medium_transmittance(mediumRefId: u32, distance: f32) -> vec3<f32> {
4120
+ if (!medium_valid(mediumRefId) || distance <= 0.000001) {
4121
+ return vec3<f32>(1.0);
4122
+ }
4123
+ let extinction = medium_absorption(mediumRefId) + medium_scattering(mediumRefId);
4124
+ return vec3<f32>(
4125
+ exp(-extinction.x * distance),
4126
+ exp(-extinction.y * distance),
4127
+ exp(-extinction.z * distance)
4128
+ );
4129
+ }
4130
+
4131
+ fn transmitted_medium_ref_id(ray: RayRecord, hit: HitRecord) -> u32 {
4132
+ if (hit.mediumRefId == 0u) {
4133
+ return ray.mediumRefId;
4134
+ }
4135
+ if (hit.frontFace == 1u) {
4136
+ return hit.mediumRefId;
4137
+ }
4138
+ if (ray.mediumRefId == hit.mediumRefId) {
4139
+ return 0u;
4140
+ }
4141
+ return ray.mediumRefId;
4142
+ }
4143
+
3958
4144
  fn surface_path_response(hit: HitRecord) -> vec3<f32> {
3959
4145
  let color = clamp(hit.color.xyz, vec3<f32>(0.0), vec3<f32>(1.0));
3960
4146
  let opacity = clamp(hit.material.z, 0.0, 1.0);
@@ -4053,11 +4239,15 @@ fn terminal_surface_environment_source(ray: RayRecord, hit: HitRecord) -> vec3<f
4053
4239
  return clamp_sample_radiance(environmentFloor * materialFloor);
4054
4240
  }
4055
4241
 
4056
- fn terminal_surface_environment_contribution(ray: RayRecord, hit: HitRecord) -> vec3<f32> {
4242
+ fn terminal_surface_environment_contribution(
4243
+ ray: RayRecord,
4244
+ throughput: vec3<f32>,
4245
+ hit: HitRecord
4246
+ ) -> vec3<f32> {
4057
4247
  let surfaceColor = max(hit.color.xyz, config.ambientColor.xyz);
4058
4248
  let occlusion = mix(0.75, 1.0, clamp(hit.occlusion, 0.0, 1.0));
4059
4249
  return clamp_sample_radiance(
4060
- ray.throughput.xyz *
4250
+ throughput *
4061
4251
  surfaceColor *
4062
4252
  terminal_surface_environment_source(ray, hit) *
4063
4253
  occlusion
@@ -4531,7 +4721,7 @@ fn intersect_sphere(ray: RayRecord, object: SceneObject) -> Candidate {
4531
4721
  0xffffffffu,
4532
4722
  object.objectId,
4533
4723
  object.objectId,
4534
- 0u
4724
+ object.mediumRefId
4535
4725
  );
4536
4726
  }
4537
4727
 
@@ -4583,7 +4773,7 @@ fn intersect_box(ray: RayRecord, object: SceneObject) -> Candidate {
4583
4773
  0xffffffffu,
4584
4774
  object.objectId,
4585
4775
  object.objectId,
4586
- 0u
4776
+ object.mediumRefId
4587
4777
  );
4588
4778
  }
4589
4779
 
@@ -4834,6 +5024,10 @@ fn intersectActiveQueue(@builtin(global_invocation_id) globalId: vec3<u32>) {
4834
5024
  let ray = activeQueue[index];
4835
5025
  var nearest = 1000000.0;
4836
5026
  var hitObject = SceneObject(
5027
+ 0u,
5028
+ 0u,
5029
+ 0u,
5030
+ 0u,
4837
5031
  0u,
4838
5032
  0u,
4839
5033
  0u,
@@ -5037,9 +5231,9 @@ fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult
5037
5231
  return ScatterResult(
5038
5232
  vec4<f32>(reflect(ray.direction.xyz, normal), 0.0),
5039
5233
  1.0,
5234
+ ray.mediumRefId,
5040
5235
  RAY_FLAG_DELTA_SAMPLE,
5041
5236
  0u,
5042
- 0u
5043
5237
  );
5044
5238
  }
5045
5239
 
@@ -5059,17 +5253,17 @@ fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult
5059
5253
  return ScatterResult(
5060
5254
  vec4<f32>(reflect(ray.direction.xyz, normal), 0.0),
5061
5255
  1.0,
5256
+ ray.mediumRefId,
5062
5257
  RAY_FLAG_DELTA_SAMPLE,
5063
5258
  0u,
5064
- 0u
5065
5259
  );
5066
5260
  }
5067
5261
  return ScatterResult(
5068
5262
  vec4<f32>(refract_direction(ray.direction.xyz, normal, etaRatio), 0.0),
5069
5263
  1.0,
5264
+ transmitted_medium_ref_id(ray, hit),
5070
5265
  RAY_FLAG_DELTA_SAMPLE,
5071
5266
  0u,
5072
- 0u
5073
5267
  );
5074
5268
  }
5075
5269
 
@@ -5084,9 +5278,9 @@ fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult
5084
5278
  return ScatterResult(
5085
5279
  vec4<f32>(guidedDirection, 0.0),
5086
5280
  guidedPdf,
5281
+ ray.mediumRefId,
5087
5282
  RAY_FLAG_GUIDED_EMISSIVE,
5088
5283
  0u,
5089
- 0u
5090
5284
  );
5091
5285
  }
5092
5286
  }
@@ -5094,7 +5288,7 @@ fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult
5094
5288
  let guidedDirection = sample_environment_portal_direction(hit, seed + 131u, normal);
5095
5289
  if (dot(normal, guidedDirection) > 0.000001) {
5096
5290
  let guidedPdf = max(evaluate_surface_bsdf_pdf(hit, viewDirection, guidedDirection), 0.000001);
5097
- return ScatterResult(vec4<f32>(guidedDirection, 0.0), guidedPdf, 0u, 0u, 0u);
5291
+ return ScatterResult(vec4<f32>(guidedDirection, 0.0), guidedPdf, ray.mediumRefId, 0u, 0u);
5098
5292
  }
5099
5293
  }
5100
5294
 
@@ -5128,7 +5322,7 @@ fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult
5128
5322
  );
5129
5323
  }
5130
5324
  let pdf = max(evaluate_surface_bsdf_pdf(hit, viewDirection, lightDirection), 0.000001);
5131
- return ScatterResult(vec4<f32>(lightDirection, 0.0), pdf, 0u, 0u, 0u);
5325
+ return ScatterResult(vec4<f32>(lightDirection, 0.0), pdf, ray.mediumRefId, 0u, 0u);
5132
5326
  }
5133
5327
 
5134
5328
  @compute @workgroup_size(64)
@@ -5141,15 +5335,17 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
5141
5335
 
5142
5336
  let ray = activeQueue[index];
5143
5337
  let hit = hits[index];
5338
+ let segmentTransmittance = medium_transmittance(ray.mediumRefId, hit.distance);
5339
+ let arrivingThroughput = ray.throughput.xyz * segmentTransmittance;
5144
5340
  var contribution = vec3<f32>(0.0);
5145
5341
 
5146
5342
  if (hit.hitType == 1u) {
5147
5343
  let guidedLightWeight = select(1.0, 0.24, (ray.flags & RAY_FLAG_GUIDED_EMISSIVE) != 0u);
5148
5344
  let sourceRadiance = max(hit.emission.xyz, hit.color.xyz) * guidedLightWeight;
5149
5345
  if (deferred_path_resolve_enabled()) {
5150
- record_deferred_terminal_source(ray, sourceRadiance);
5346
+ record_deferred_terminal_source(ray, sourceRadiance * segmentTransmittance);
5151
5347
  } else {
5152
- contribution = clamp_sample_radiance(ray.throughput.xyz * sourceRadiance);
5348
+ contribution = clamp_sample_radiance(arrivingThroughput * sourceRadiance);
5153
5349
  accumulation[ray.rayId] =
5154
5350
  accumulation[ray.rayId] + vec4<f32>(contribution * sample_weight(), 1.0);
5155
5351
  }
@@ -5166,9 +5362,9 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
5166
5362
  sourceRadiance = sourceRadiance * misWeight;
5167
5363
  }
5168
5364
  if (deferred_path_resolve_enabled()) {
5169
- record_deferred_terminal_source(ray, sourceRadiance);
5365
+ record_deferred_terminal_source(ray, sourceRadiance * segmentTransmittance);
5170
5366
  } else {
5171
- contribution = clamp_sample_radiance(ray.throughput.xyz * sourceRadiance);
5367
+ contribution = clamp_sample_radiance(arrivingThroughput * sourceRadiance);
5172
5368
  accumulation[ray.rayId] =
5173
5369
  accumulation[ray.rayId] + vec4<f32>(contribution * sample_weight(), 1.0);
5174
5370
  }
@@ -5176,7 +5372,11 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
5176
5372
  return;
5177
5373
  }
5178
5374
 
5179
- let response = stabilize_surface_path_response(ray, hit, surface_path_response(hit));
5375
+ let response = stabilize_surface_path_response(
5376
+ ray,
5377
+ hit,
5378
+ surface_path_response(hit) * segmentTransmittance
5379
+ );
5180
5380
  record_deferred_path_response(ray, response);
5181
5381
 
5182
5382
  let shouldEstimateDirectEnvironment =
@@ -5184,7 +5384,22 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
5184
5384
  hit.material.z >= 0.95 &&
5185
5385
  ray.bounce < 2u;
5186
5386
  if (shouldEstimateDirectEnvironment) {
5187
- let directEnvironment = surface_direct_environment_contribution(ray, hit);
5387
+ let directEnvironment = surface_direct_environment_contribution(
5388
+ RayRecord(
5389
+ ray.rayId,
5390
+ ray.parentRayId,
5391
+ ray.sourcePixelId,
5392
+ ray.sampleId,
5393
+ ray.bounce,
5394
+ ray.mediumRefId,
5395
+ ray.flags,
5396
+ 0u,
5397
+ ray.origin,
5398
+ ray.direction,
5399
+ vec4<f32>(arrivingThroughput, ray.throughput.w)
5400
+ ),
5401
+ hit
5402
+ );
5188
5403
  accumulation[ray.rayId] =
5189
5404
  accumulation[ray.rayId] + vec4<f32>(directEnvironment * sample_weight(), 0.0);
5190
5405
  }
@@ -5193,7 +5408,11 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
5193
5408
  if (deferred_path_resolve_enabled()) {
5194
5409
  record_deferred_terminal_source(ray, terminal_surface_environment_source(ray, hit));
5195
5410
  } else {
5196
- let terminalEnvironment = terminal_surface_environment_contribution(ray, hit);
5411
+ let terminalEnvironment = terminal_surface_environment_contribution(
5412
+ ray,
5413
+ arrivingThroughput,
5414
+ hit
5415
+ );
5197
5416
  accumulation[ray.rayId] =
5198
5417
  accumulation[ray.rayId] + vec4<f32>(terminalEnvironment * sample_weight(), 1.0);
5199
5418
  }
@@ -5208,7 +5427,11 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
5208
5427
  if (deferred_path_resolve_enabled()) {
5209
5428
  record_deferred_terminal_source(ray, terminal_surface_environment_source(ray, hit));
5210
5429
  } else {
5211
- let overflowEnvironment = terminal_surface_environment_contribution(ray, hit);
5430
+ let overflowEnvironment = terminal_surface_environment_contribution(
5431
+ ray,
5432
+ arrivingThroughput,
5433
+ hit
5434
+ );
5212
5435
  accumulation[ray.rayId] =
5213
5436
  accumulation[ray.rayId] + vec4<f32>(overflowEnvironment * sample_weight(), 1.0);
5214
5437
  }
@@ -5222,7 +5445,7 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
5222
5445
  ray.sourcePixelId,
5223
5446
  ray.sampleId,
5224
5447
  ray.bounce + 1u,
5225
- ray.mediumRefId,
5448
+ scatter.mediumRefId,
5226
5449
  scatter.flags,
5227
5450
  0u,
5228
5451
  vec4<f32>(offset_origin(hit.position.xyz, hit.shadingNormal.xyz), 1.0),
@@ -5483,9 +5706,22 @@ function nowMs() {
5483
5706
  }
5484
5707
  return Date.now();
5485
5708
  }
5486
- function estimateSubmittedGpuWorkTimeoutMs(config, tileCount, overrideTimeoutMs = null) {
5709
+ function estimateAccelerationBuildWaitFactor(config) {
5710
+ if (config?.gpuAccelerationBuildRequired !== true) {
5711
+ return 1;
5712
+ }
5713
+ const bvhSortStageCount = Array.isArray(config?.bvhSortStages) ? config.bvhSortStages.length : 0;
5714
+ const bvhBuildLevelCount = Array.isArray(config?.bvhBuildLevels) ? config.bvhBuildLevels.length : 0;
5715
+ const accelerationStageCount = 2 + bvhSortStageCount + bvhBuildLevelCount;
5716
+ return Math.max(1, 1 + accelerationStageCount / 96);
5717
+ }
5718
+ function estimateSubmittedGpuWorkTiming(config, tileCount, overrideTimeoutMs = null, options = {}) {
5487
5719
  if (Number.isFinite(overrideTimeoutMs)) {
5488
- return Math.max(1, Math.trunc(Number(overrideTimeoutMs)));
5720
+ const overrideMs = Math.max(1, Math.trunc(Number(overrideTimeoutMs)));
5721
+ return Object.freeze({
5722
+ timeoutMs: overrideMs,
5723
+ maxWaitMs: overrideMs
5724
+ });
5489
5725
  }
5490
5726
  const samplesPerPixel = Math.max(
5491
5727
  1,
@@ -5496,10 +5732,26 @@ function estimateSubmittedGpuWorkTimeoutMs(config, tileCount, overrideTimeoutMs
5496
5732
  const denoisePasses = config?.denoise ? samplesPerPixel < 4 ? 2 : 1 : 0;
5497
5733
  const tiles = Math.max(1, Number(tileCount ?? 1));
5498
5734
  const estimatedPasses = tiles * (samplesPerPixel * (maxDepth + 1 + deferredResolvePasses) + denoisePasses + 1);
5499
- return Math.min(
5735
+ const triangleCount = Math.max(0, Number(config?.triangleCount ?? 0));
5736
+ const geometryFactor = Math.max(1, triangleCount / 131072);
5737
+ const includeAccelerationBuild = options.includeAccelerationBuild === true;
5738
+ const accelerationFactor = includeAccelerationBuild ? estimateAccelerationBuildWaitFactor(config) : 1;
5739
+ const estimatedWindowMs = Math.round(
5740
+ (GPU_SUBMITTED_WORK_TIMEOUT_MS + estimatedPasses * 5) * geometryFactor * accelerationFactor
5741
+ );
5742
+ const timeoutMs = Math.min(
5500
5743
  GPU_MAX_SUBMITTED_WORK_TIMEOUT_MS,
5501
- GPU_SUBMITTED_WORK_TIMEOUT_MS + estimatedPasses * 5
5744
+ Math.max(GPU_SUBMITTED_WORK_TIMEOUT_MS, estimatedWindowMs)
5745
+ );
5746
+ const maxWaitMultiplier = includeAccelerationBuild ? 3 : 2;
5747
+ const maxWaitMs = Math.min(
5748
+ GPU_MAX_SUBMITTED_WORK_DEADLINE_MS,
5749
+ Math.max(timeoutMs, estimatedWindowMs * maxWaitMultiplier)
5502
5750
  );
5751
+ return Object.freeze({
5752
+ timeoutMs,
5753
+ maxWaitMs
5754
+ });
5503
5755
  }
5504
5756
  async function createWavefrontPathTracingComputeRenderer(options = {}) {
5505
5757
  assertAnalyticDisplayQualityPolicy(options);
@@ -5711,6 +5963,11 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
5711
5963
  config.environmentMap,
5712
5964
  config.environmentColor
5713
5965
  );
5966
+ let mediumTextureResource = createMediumTextureResource(
5967
+ device,
5968
+ constants,
5969
+ config.mediums
5970
+ );
5714
5971
  config = Object.freeze({
5715
5972
  ...config,
5716
5973
  environmentMap: Object.freeze({
@@ -5797,7 +6054,8 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
5797
6054
  { binding: 28, visibility: constants.shader.COMPUTE, sampler: { type: "filtering" } },
5798
6055
  { binding: 29, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
5799
6056
  { binding: 30, visibility: constants.shader.COMPUTE, sampler: { type: "filtering" } },
5800
- { binding: 31, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } }
6057
+ { binding: 31, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
6058
+ { binding: 32, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } }
5801
6059
  ]
5802
6060
  });
5803
6061
  const accelerationBindGroupLayout = device.createBindGroupLayout({
@@ -5984,14 +6242,18 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
5984
6242
  { binding: 28, resource: materialAtlasSampler },
5985
6243
  { binding: 29, resource: brdfLutResource.view },
5986
6244
  { binding: 30, resource: brdfLutResource.sampler },
5987
- { binding: 31, resource: environmentSamplingResource.view }
6245
+ { binding: 31, resource: environmentSamplingResource.view },
6246
+ { binding: 32, resource: mediumTextureResource.view }
5988
6247
  ]
5989
6248
  });
5990
6249
  }
5991
- const bindGroups = [
5992
- createTraceBindGroup(activeQueue, nextQueue, "plasius.wavefront.bind.activeNext"),
5993
- createTraceBindGroup(nextQueue, activeQueue, "plasius.wavefront.bind.nextActive")
5994
- ];
6250
+ function createTraceBindGroups() {
6251
+ return [
6252
+ createTraceBindGroup(activeQueue, nextQueue, "plasius.wavefront.bind.activeNext"),
6253
+ createTraceBindGroup(nextQueue, activeQueue, "plasius.wavefront.bind.nextActive")
6254
+ ];
6255
+ }
6256
+ let bindGroups = createTraceBindGroups();
5995
6257
  const bvhBuildBindGroup = device.createBindGroup({
5996
6258
  label: "plasius.wavefront.bind.bvhBuild",
5997
6259
  layout: accelerationBindGroupLayout,
@@ -6169,6 +6431,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
6169
6431
  emissiveTriangleCount: config.emissiveTriangleCount,
6170
6432
  environmentPortalCount: config.environmentPortalCount,
6171
6433
  environmentPortalMode: config.environmentPortalMode,
6434
+ mediumCount: config.mediumCount,
6172
6435
  environmentMap: createEnvironmentMapSnapshot(config.environmentMap),
6173
6436
  deferredPathResolve: config.deferredPathResolve,
6174
6437
  bvhNodeCount: config.bvhNodeCount,
@@ -6464,6 +6727,10 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
6464
6727
  1,
6465
6728
  Number.isFinite(options2.timeoutMs) ? Number(options2.timeoutMs) : GPU_SUBMITTED_WORK_TIMEOUT_MS
6466
6729
  );
6730
+ const maxWaitMs = Math.max(
6731
+ timeoutMs,
6732
+ Number.isFinite(options2.maxWaitMs) ? Number(options2.maxWaitMs) : timeoutMs
6733
+ );
6467
6734
  const allowTimeout = options2.allowTimeout !== false;
6468
6735
  const completionPromise = device.queue.onSubmittedWorkDone().then(
6469
6736
  () => ({ status: "done" }),
@@ -6476,43 +6743,57 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
6476
6743
  `WebGPU device lost while waiting for submitted work (${info?.reason ?? "unknown"}).`
6477
6744
  );
6478
6745
  }) : null;
6479
- let timeoutHandle = null;
6480
- let resolveTimeoutPromise = null;
6481
- let timeoutSettled = false;
6482
- const settleTimeoutPromise = (value) => {
6483
- if (timeoutSettled) {
6484
- return;
6746
+ const startedAtMs = nowMs();
6747
+ while (true) {
6748
+ const elapsedMs = Math.max(0, nowMs() - startedAtMs);
6749
+ const remainingMs = Math.max(0, maxWaitMs - elapsedMs);
6750
+ if (remainingMs <= 0) {
6751
+ if (!allowTimeout) {
6752
+ throw new Error(`Timed out after ${Math.round(maxWaitMs)} ms waiting for submitted GPU work.`);
6753
+ }
6754
+ console.warn(
6755
+ `[plasius.wavefront] Submitted GPU work did not report completion within ${Math.round(maxWaitMs)} ms; continuing.`
6756
+ );
6757
+ return false;
6485
6758
  }
6486
- timeoutSettled = true;
6487
- resolveTimeoutPromise?.(value);
6488
- };
6489
- const timeoutPromise = new Promise((resolve) => {
6490
- resolveTimeoutPromise = resolve;
6491
- timeoutHandle = setTimeout(() => settleTimeoutPromise({ status: "timeout" }), timeoutMs);
6492
- });
6493
- let result;
6494
- try {
6495
- result = await Promise.race(
6496
- [completionPromise, timeoutPromise, lossPromise].filter(Boolean)
6497
- );
6498
- } finally {
6499
- if (timeoutHandle !== null) {
6500
- clearTimeout(timeoutHandle);
6501
- settleTimeoutPromise({ status: "cancelled" });
6759
+ const waitWindowMs = Math.max(1, Math.min(timeoutMs, remainingMs));
6760
+ let timeoutHandle = null;
6761
+ let resolveTimeoutPromise = null;
6762
+ let timeoutSettled = false;
6763
+ const settleTimeoutPromise = (value) => {
6764
+ if (timeoutSettled) {
6765
+ return;
6766
+ }
6767
+ timeoutSettled = true;
6768
+ resolveTimeoutPromise?.(value);
6769
+ };
6770
+ const timeoutPromise = new Promise((resolve) => {
6771
+ resolveTimeoutPromise = resolve;
6772
+ timeoutHandle = setTimeout(
6773
+ () => settleTimeoutPromise({ status: "timeout" }),
6774
+ waitWindowMs
6775
+ );
6776
+ });
6777
+ let result;
6778
+ try {
6779
+ result = await Promise.race(
6780
+ [completionPromise, timeoutPromise, lossPromise].filter(Boolean)
6781
+ );
6782
+ } finally {
6783
+ if (timeoutHandle !== null) {
6784
+ clearTimeout(timeoutHandle);
6785
+ settleTimeoutPromise({ status: "cancelled" });
6786
+ }
6502
6787
  }
6503
- }
6504
- if (result?.status === "timeout") {
6505
- if (!allowTimeout) {
6506
- throw new Error(`Timed out after ${timeoutMs} ms waiting for submitted GPU work.`);
6788
+ if (result?.status === "done") {
6789
+ return true;
6790
+ }
6791
+ if (result?.status !== "timeout") {
6792
+ return true;
6507
6793
  }
6508
- console.warn(
6509
- `[plasius.wavefront] Submitted GPU work did not report completion within ${timeoutMs} ms; continuing.`
6510
- );
6511
- return false;
6512
6794
  }
6513
- return true;
6514
6795
  }
6515
- function dispatchFrameAwaitingGpu(frameIndex, parallelism, renderedSamplesPerPixel = config.samplesPerPixel) {
6796
+ function dispatchFrameAwaitingGpu(frameIndex, parallelism, renderedSamplesPerPixel = config.samplesPerPixel, optionsForFrame = {}) {
6516
6797
  const samplePassesPerSample = config.maxDepth + 1 + (config.deferredPathResolve ? 1 : 0);
6517
6798
  const denoisePassCount = config.denoise ? renderedSamplesPerPixel < 4 ? 2 : 1 : 0;
6518
6799
  const tailPassCount = denoisePassCount + 1;
@@ -6522,17 +6803,42 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
6522
6803
  Math.max(config.maxFramePassesPerSubmission - tailPassCount, 1) / Math.max(samplePassesPerSample, 1)
6523
6804
  )
6524
6805
  );
6525
- let submissionCount = 0;
6526
- for (const tile of tiles) {
6527
- for (let sampleStart = 0; sampleStart < renderedSamplesPerPixel; sampleStart += sampleBatchSize) {
6528
- const sampleEnd = Math.min(renderedSamplesPerPixel, sampleStart + sampleBatchSize);
6806
+ const sampleRangeStart = clamp(
6807
+ readNonNegativeInteger("sampleRangeStart", optionsForFrame.sampleRangeStart, 0),
6808
+ 0,
6809
+ renderedSamplesPerPixel
6810
+ );
6811
+ const sampleRangeEnd = clamp(
6812
+ readPositiveInteger("sampleRangeEnd", optionsForFrame.sampleRangeEnd, renderedSamplesPerPixel),
6813
+ sampleRangeStart,
6814
+ renderedSamplesPerPixel
6815
+ );
6816
+ const includeDenoise = optionsForFrame.includeDenoise === true;
6817
+ const includePresent = optionsForFrame.includePresent === true;
6818
+ const tileStartIndex = clamp(
6819
+ readNonNegativeInteger("tileStartIndex", optionsForFrame.tileStartIndex, 0),
6820
+ 0,
6821
+ tiles.length
6822
+ );
6823
+ const tileEndIndex = clamp(
6824
+ readPositiveInteger("tileEndIndex", optionsForFrame.tileEndIndex, tiles.length),
6825
+ tileStartIndex,
6826
+ tiles.length
6827
+ );
6828
+ let submissionCount = Math.max(
6829
+ 0,
6830
+ readNonNegativeInteger("startingSubmissionCount", optionsForFrame.startingSubmissionCount, 0)
6831
+ );
6832
+ let slot = Math.max(0, readNonNegativeInteger("startingSlot", optionsForFrame.startingSlot, 0));
6833
+ for (const tile of tiles.slice(tileStartIndex, tileEndIndex)) {
6834
+ for (let sampleStart = sampleRangeStart; sampleStart < sampleRangeEnd; sampleStart += sampleBatchSize) {
6835
+ const sampleEnd = Math.min(sampleRangeEnd, sampleStart + sampleBatchSize);
6529
6836
  const batch = createGpuSubmissionBatcher({
6530
6837
  device,
6531
6838
  frameIndex,
6532
6839
  maxFramePassesPerSubmission: config.maxFramePassesPerSubmission,
6533
6840
  startingSubmissionCount: submissionCount
6534
6841
  });
6535
- let slot = 0;
6536
6842
  for (let sampleIndex = sampleStart; sampleIndex < sampleEnd; sampleIndex += 1) {
6537
6843
  const configOffset = writeFrameConfigSlot(slot, tile, frameIndex, {
6538
6844
  sampleIndex,
@@ -6549,41 +6855,50 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
6549
6855
  encodeTileOutput(batch.reserve(1), tile, configOffset, parallelism);
6550
6856
  }
6551
6857
  }
6552
- if (!config.deferredPathResolve && sampleEnd >= renderedSamplesPerPixel) {
6858
+ if (!config.deferredPathResolve && sampleRangeEnd >= renderedSamplesPerPixel) {
6553
6859
  const outputConfigOffset = writeFrameConfigSlot(slot, tile, frameIndex, {
6554
6860
  sampleIndex: 0,
6555
6861
  sampleWeight: 1 / renderedSamplesPerPixel
6556
6862
  });
6863
+ slot += 1;
6557
6864
  encodeTileOutput(batch.reserve(1), tile, outputConfigOffset, parallelism);
6558
6865
  }
6559
6866
  batch.flush();
6560
6867
  submissionCount += batch.getSubmissionCount();
6561
6868
  }
6562
6869
  }
6563
- const tail = createGpuSubmissionBatcher({
6564
- device,
6565
- frameIndex,
6566
- maxFramePassesPerSubmission: config.maxFramePassesPerSubmission,
6567
- startingSubmissionCount: submissionCount
6568
- });
6569
- if (config.denoise) {
6570
- const denoiseConfigOffset = writeFrameConfigSlot(
6571
- 0,
6572
- { x: 0, y: 0, width: config.width, height: config.height },
6870
+ if (includeDenoise || includePresent) {
6871
+ const tail = createGpuSubmissionBatcher({
6872
+ device,
6573
6873
  frameIndex,
6574
- { sampleIndex: 0, sampleWeight: 1 / renderedSamplesPerPixel }
6575
- );
6576
- encodeDenoise(
6577
- tail.reserve(denoisePassCount),
6578
- denoiseConfigOffset,
6579
- parallelism,
6580
- renderedSamplesPerPixel
6581
- );
6874
+ maxFramePassesPerSubmission: config.maxFramePassesPerSubmission,
6875
+ startingSubmissionCount: submissionCount
6876
+ });
6877
+ if (includeDenoise && config.denoise) {
6878
+ const denoiseConfigOffset = writeFrameConfigSlot(
6879
+ slot,
6880
+ { x: 0, y: 0, width: config.width, height: config.height },
6881
+ frameIndex,
6882
+ { sampleIndex: 0, sampleWeight: 1 / renderedSamplesPerPixel }
6883
+ );
6884
+ slot += 1;
6885
+ encodeDenoise(
6886
+ tail.reserve(denoisePassCount),
6887
+ denoiseConfigOffset,
6888
+ parallelism,
6889
+ renderedSamplesPerPixel
6890
+ );
6891
+ }
6892
+ if (includePresent) {
6893
+ encodePresent(tail.reserve(1));
6894
+ }
6895
+ tail.flush();
6896
+ submissionCount += tail.getSubmissionCount();
6582
6897
  }
6583
- encodePresent(tail.reserve(1));
6584
- tail.flush();
6585
- submissionCount += tail.getSubmissionCount();
6586
- return submissionCount;
6898
+ return Object.freeze({
6899
+ submissionCount,
6900
+ slot
6901
+ });
6587
6902
  }
6588
6903
  async function readOutputProbe(optionsForProbe = {}) {
6589
6904
  const mapMode = constants.map;
@@ -6629,24 +6944,59 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
6629
6944
  const awaitGPUCompletion = renderOptions.awaitGPUCompletion !== false;
6630
6945
  const samplingPlan = resolveRenderedSamplesPerPixel(renderOptions, awaitGPUCompletion);
6631
6946
  const useThrottledHighSamplePath = awaitGPUCompletion && samplingPlan.renderedSamplesPerPixel >= 8;
6632
- const submittedWorkTimeoutMs = estimateSubmittedGpuWorkTimeoutMs(
6633
- { ...config, renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel },
6634
- tiles.length,
6635
- renderOptions.submittedWorkTimeoutMs
6636
- );
6637
6947
  const frameStartTimeMs = nowMs();
6638
- const submissionWaitOptions = awaitGPUCompletion ? { timeoutMs: submittedWorkTimeoutMs, allowTimeout: false } : { timeoutMs: submittedWorkTimeoutMs };
6639
6948
  let frameStats;
6640
6949
  if (useThrottledHighSamplePath) {
6641
6950
  frame += 1;
6642
6951
  const frameIndex = frame + config.frameIndex;
6643
6952
  const parallelismCounters = createGpuParallelismCounters();
6644
6953
  const accelerationBuildSubmitted = dispatchGpuAccelerationBuild(frameIndex, parallelismCounters);
6645
- const frameSubmissionCount = dispatchFrameAwaitingGpu(
6646
- frameIndex,
6647
- parallelismCounters,
6648
- samplingPlan.renderedSamplesPerPixel
6649
- );
6954
+ let frameSubmissionCount = 0;
6955
+ let frameConfigSlot = 0;
6956
+ if (accelerationBuildSubmitted) {
6957
+ const accelerationWaitOptions = {
6958
+ ...estimateSubmittedGpuWorkTiming(
6959
+ { ...config, renderedSamplesPerPixel: 1 },
6960
+ 1,
6961
+ renderOptions.submittedWorkTimeoutMs,
6962
+ { includeAccelerationBuild: true }
6963
+ ),
6964
+ allowTimeout: false
6965
+ };
6966
+ await waitForSubmittedGpuWork(accelerationWaitOptions);
6967
+ }
6968
+ for (let tileIndex = 0; tileIndex < tiles.length; tileIndex += 1) {
6969
+ const tileRangeDispatch = dispatchFrameAwaitingGpu(
6970
+ frameIndex,
6971
+ parallelismCounters,
6972
+ samplingPlan.renderedSamplesPerPixel,
6973
+ {
6974
+ sampleRangeStart: 0,
6975
+ sampleRangeEnd: samplingPlan.renderedSamplesPerPixel,
6976
+ tileStartIndex: tileIndex,
6977
+ tileEndIndex: tileIndex + 1,
6978
+ startingSubmissionCount: frameSubmissionCount,
6979
+ startingSlot: frameConfigSlot,
6980
+ includeDenoise: tileIndex + 1 >= tiles.length,
6981
+ includePresent: tileIndex + 1 >= tiles.length
6982
+ }
6983
+ );
6984
+ frameSubmissionCount = tileRangeDispatch.submissionCount;
6985
+ frameConfigSlot = tileRangeDispatch.slot;
6986
+ const tileWaitOptions = {
6987
+ ...estimateSubmittedGpuWorkTiming(
6988
+ { ...config, renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel },
6989
+ 1,
6990
+ renderOptions.submittedWorkTimeoutMs,
6991
+ {
6992
+ includeDenoise: tileIndex + 1 >= tiles.length && config.denoise,
6993
+ includePresent: tileIndex + 1 >= tiles.length
6994
+ }
6995
+ ),
6996
+ allowTimeout: false
6997
+ };
6998
+ await waitForSubmittedGpuWork(tileWaitOptions);
6999
+ }
6650
7000
  frameStats = createFrameStats({
6651
7001
  frameIndex,
6652
7002
  accelerationBuildSubmitted,
@@ -6658,10 +7008,24 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
6658
7008
  budgetConstrained: samplingPlan.budgetConstrained
6659
7009
  });
6660
7010
  } else {
7011
+ const submittedWorkTiming = estimateSubmittedGpuWorkTiming(
7012
+ { ...config, renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel },
7013
+ tiles.length,
7014
+ renderOptions.submittedWorkTimeoutMs,
7015
+ { includeAccelerationBuild: config.gpuAccelerationBuildRequired && !accelerationBuilt }
7016
+ );
7017
+ const submissionWaitOptions = awaitGPUCompletion ? {
7018
+ timeoutMs: submittedWorkTiming.timeoutMs,
7019
+ maxWaitMs: submittedWorkTiming.maxWaitMs,
7020
+ allowTimeout: false
7021
+ } : {
7022
+ timeoutMs: submittedWorkTiming.timeoutMs,
7023
+ maxWaitMs: submittedWorkTiming.maxWaitMs
7024
+ };
6661
7025
  frameStats = renderOnce(renderOptions, samplingPlan);
6662
- }
6663
- if (awaitGPUCompletion) {
6664
- await waitForSubmittedGpuWork(submissionWaitOptions);
7026
+ if (awaitGPUCompletion) {
7027
+ await waitForSubmittedGpuWork(submissionWaitOptions);
7028
+ }
6665
7029
  }
6666
7030
  const frameTimeMs = Math.max(0, nowMs() - frameStartTimeMs);
6667
7031
  if (awaitGPUCompletion) {
@@ -6716,10 +7080,22 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
6716
7080
  ...overrides
6717
7081
  });
6718
7082
  }
7083
+ function rebuildMediumResources(nextConfig) {
7084
+ const previousMediumTextureResource = mediumTextureResource;
7085
+ mediumTextureResource = createMediumTextureResource(device, constants, nextConfig.mediums);
7086
+ bindGroups = createTraceBindGroups();
7087
+ if (previousMediumTextureResource?.ownsTexture) {
7088
+ previousMediumTextureResource.texture?.destroy?.();
7089
+ }
7090
+ }
6719
7091
  function updateSceneObjects(sceneObjects) {
6720
7092
  const nextPackedScene = packWavefrontSceneObjects(sceneObjects, config.sceneObjectCapacity);
6721
7093
  packedScene = nextPackedScene;
6722
- config = rebuildLiveConfig();
7094
+ const nextConfig = rebuildLiveConfig();
7095
+ if (!mediumTablesEqual(config.mediums, nextConfig.mediums)) {
7096
+ rebuildMediumResources(nextConfig);
7097
+ }
7098
+ config = nextConfig;
6723
7099
  device.queue.writeBuffer(sceneObjectBuffer, 0, packedScene.buffer);
6724
7100
  return config;
6725
7101
  }
@@ -6743,6 +7119,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
6743
7119
  emissiveTriangleCount: config.emissiveTriangleCount,
6744
7120
  environmentPortalCount: config.environmentPortalCount,
6745
7121
  environmentPortalMode: config.environmentPortalMode,
7122
+ mediumCount: config.mediumCount,
6746
7123
  environmentMap: createEnvironmentMapSnapshot(config.environmentMap),
6747
7124
  deferredPathResolve: config.deferredPathResolve,
6748
7125
  bvhNodeCount: config.bvhNodeCount,
@@ -6783,6 +7160,9 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
6783
7160
  if (environmentSamplingResource.ownsTexture) {
6784
7161
  environmentSamplingResource.texture?.destroy?.();
6785
7162
  }
7163
+ if (mediumTextureResource.ownsTexture) {
7164
+ mediumTextureResource.texture?.destroy?.();
7165
+ }
6786
7166
  brdfLutResource.texture?.destroy?.();
6787
7167
  if (baseColorAtlasResource.ownsTexture) {
6788
7168
  baseColorAtlasResource.texture?.destroy?.();