@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.js CHANGED
@@ -138,17 +138,19 @@ var DEFAULT_MAX_DEPTH = 6;
138
138
  var DEFAULT_TILE_SIZE = 128;
139
139
  var DEFAULT_SAMPLES_PER_PIXEL = 1;
140
140
  var MAX_SAMPLES_PER_PIXEL = 256;
141
- var DEFAULT_BRDF_LUT_SIZE = 256;
141
+ var DEFAULT_BRDF_LUT_SIZE = 128;
142
+ var DEFAULT_BRDF_LUT_SAMPLE_COUNT = 256;
142
143
  var DEFAULT_MAX_FRAME_PASSES_PER_SUBMISSION = 256;
143
144
  var DEFAULT_SCENE_OBJECT_CAPACITY = 128;
144
145
  var DEFAULT_ENVIRONMENT_PORTAL_CAPACITY = 32;
146
+ var DEFAULT_MEDIUM_PHASE_MODEL = 0;
145
147
  var WORKGROUP_SIZE = 64;
146
148
  var rendererWavefrontComputeMode = "webgpu-compute";
147
149
  var rendererWavefrontComputeWorkgroupSize = WORKGROUP_SIZE;
148
150
  var rendererWavefrontComputeStatsStride = 8;
149
151
  var RAY_RECORD_BYTES = 80;
150
152
  var HIT_RECORD_BYTES = 256;
151
- var SCENE_OBJECT_RECORD_BYTES = 144;
153
+ var SCENE_OBJECT_RECORD_BYTES = 160;
152
154
  var MESH_VERTEX_RECORD_BYTES = 48;
153
155
  var MESH_RANGE_RECORD_BYTES = 240;
154
156
  var TRIANGLE_RECORD_BYTES = 352;
@@ -157,11 +159,13 @@ var BVH_NODE_RECORD_BYTES = 48;
157
159
  var BVH_LEAF_REF_RECORD_BYTES = 16;
158
160
  var EMISSIVE_TRIANGLE_INDEX_BYTES = 4;
159
161
  var ENVIRONMENT_PORTAL_RECORD_BYTES = 96;
162
+ var MEDIUM_TABLE_ROWS = 2;
160
163
  var ACCUMULATION_RECORD_BYTES = 16;
161
164
  var PATH_VERTEX_RECORD_BYTES = 16;
162
165
  var GPU_SUBMITTED_WORK_TIMEOUT_MS = 5e3;
163
166
  var GPU_READBACK_COMPLETION_TIMEOUT_MS = 6e4;
164
167
  var GPU_MAX_SUBMITTED_WORK_TIMEOUT_MS = 6e4;
168
+ var GPU_MAX_SUBMITTED_WORK_DEADLINE_MS = 18e4;
165
169
  var CONFIG_BUFFER_BYTES = 320;
166
170
  var COUNTER_DISPATCH_ARGS_OFFSET = 16;
167
171
  var INDIRECT_DISPATCH_ARGS_BYTES = 12;
@@ -503,6 +507,156 @@ function deriveBounds(input) {
503
507
  }
504
508
  return null;
505
509
  }
510
+ function deriveBeerLambertAbsorptionFromAttenuationColor(attenuationColor, attenuationDistance, density = 1) {
511
+ const distance = Number(attenuationDistance);
512
+ const densityScale = Math.max(0, Number(density) || 0);
513
+ if (!Number.isFinite(distance) || distance <= 0 || densityScale <= 0) {
514
+ return [0, 0, 0];
515
+ }
516
+ return attenuationColor.slice(0, 3).map((channel) => {
517
+ const clamped = clamp(Number(channel) || 0, 1e-4, 1);
518
+ return Math.max(0, -Math.log(clamped) / distance * densityScale);
519
+ });
520
+ }
521
+ function readMediumPhaseModel(value) {
522
+ if (typeof value === "number" && Number.isFinite(value)) {
523
+ return Math.max(0, Math.trunc(value));
524
+ }
525
+ switch (String(value ?? "").trim().toLowerCase()) {
526
+ case "isotropic":
527
+ default:
528
+ return DEFAULT_MEDIUM_PHASE_MODEL;
529
+ }
530
+ }
531
+ function resolveWavefrontVolumeInput(input) {
532
+ return input?.volume ?? input?.material?.volume ?? null;
533
+ }
534
+ function normalizeWavefrontThickness(input, label) {
535
+ const volume = resolveWavefrontVolumeInput(input);
536
+ return Math.max(
537
+ 0,
538
+ readFiniteNumber(
539
+ label,
540
+ input?.thickness ?? volume?.thickness ?? input?.material?.thickness,
541
+ 0
542
+ )
543
+ );
544
+ }
545
+ function resolveWavefrontMediumId(input, fallbackId = 1) {
546
+ return input?.mediumRefId ?? input?.mediumId ?? input?.material?.mediumId ?? input?.materialRefId ?? input?.material?.id ?? input?.materialId ?? input?.id ?? fallbackId;
547
+ }
548
+ function deriveWavefrontTransportMedium(input, fallbackId = 1) {
549
+ const resolvedId = resolveWavefrontMediumId(input, fallbackId);
550
+ if (input?.medium) {
551
+ return normalizeWavefrontMedium(
552
+ {
553
+ ...input.medium,
554
+ id: input.medium.id ?? input.medium.mediumId ?? resolvedId
555
+ },
556
+ fallbackId
557
+ );
558
+ }
559
+ const volume = resolveWavefrontVolumeInput(input);
560
+ if (!volume) {
561
+ return null;
562
+ }
563
+ return normalizeWavefrontMedium(
564
+ {
565
+ id: resolvedId,
566
+ phaseModel: volume.phaseModel,
567
+ density: volume.density,
568
+ attenuationColor: volume.attenuationColor,
569
+ attenuationDistance: volume.attenuationDistance,
570
+ absorption: volume.absorption,
571
+ scattering: volume.scattering
572
+ },
573
+ fallbackId
574
+ );
575
+ }
576
+ function normalizeWavefrontMedium(input = {}, index = 0) {
577
+ const id = readNonNegativeInteger("medium id", input.id ?? input.mediumId, index);
578
+ const density = Math.max(0, readFiniteNumber("medium density", input.density, 1));
579
+ const attenuationColor = asColor(
580
+ input.attenuationColor ?? input.color ?? input.medium?.attenuationColor,
581
+ [1, 1, 1, 1]
582
+ );
583
+ const attenuationDistance = readFiniteNumber(
584
+ "medium attenuationDistance",
585
+ input.attenuationDistance ?? input.distance ?? input.medium?.attenuationDistance,
586
+ 0
587
+ );
588
+ const absorption = Array.isArray(input.absorption) || Array.isArray(input.medium?.absorption) ? asVec3(input.absorption ?? input.medium?.absorption, [0, 0, 0]).map(
589
+ (value) => Math.max(0, Number(value) || 0)
590
+ ) : deriveBeerLambertAbsorptionFromAttenuationColor(
591
+ attenuationColor,
592
+ attenuationDistance,
593
+ density
594
+ );
595
+ const scattering = asVec3(
596
+ input.scattering ?? input.medium?.scattering,
597
+ [0, 0, 0]
598
+ ).map((value) => Math.max(0, Number(value) || 0));
599
+ return Object.freeze({
600
+ id,
601
+ phaseModel: readMediumPhaseModel(input.phaseModel ?? input.medium?.phaseModel),
602
+ density,
603
+ attenuationColor: Object.freeze(attenuationColor),
604
+ attenuationDistance,
605
+ absorption: Object.freeze(absorption),
606
+ scattering: Object.freeze(scattering)
607
+ });
608
+ }
609
+ function collectWavefrontMediums(options, meshes, sceneObjects = []) {
610
+ const mediumsById = /* @__PURE__ */ new Map();
611
+ mediumsById.set(
612
+ 0,
613
+ Object.freeze({
614
+ id: 0,
615
+ phaseModel: DEFAULT_MEDIUM_PHASE_MODEL,
616
+ density: 0,
617
+ attenuationColor: Object.freeze([1, 1, 1, 1]),
618
+ attenuationDistance: 0,
619
+ absorption: Object.freeze([0, 0, 0]),
620
+ scattering: Object.freeze([0, 0, 0])
621
+ })
622
+ );
623
+ const register = (input, fallbackId = mediumsById.size) => {
624
+ if (!input) {
625
+ return;
626
+ }
627
+ const normalized = normalizeWavefrontMedium(
628
+ typeof input === "object" ? { id: fallbackId, ...input } : { id: fallbackId },
629
+ fallbackId
630
+ );
631
+ const existing = mediumsById.get(normalized.id);
632
+ if (existing && JSON.stringify(existing) !== JSON.stringify(normalized)) {
633
+ throw new Error(`Medium id ${normalized.id} is defined more than once with different values.`);
634
+ }
635
+ mediumsById.set(normalized.id, normalized);
636
+ };
637
+ for (const medium of options.mediums ?? []) {
638
+ register(medium);
639
+ }
640
+ for (const mesh of meshes) {
641
+ register(mesh.medium, mesh.mediumRefId ?? mesh.medium?.id ?? 0);
642
+ }
643
+ for (const mesh of meshes) {
644
+ if ((mesh.mediumRefId ?? 0) > 0 && !mediumsById.has(mesh.mediumRefId)) {
645
+ register({ id: mesh.mediumRefId });
646
+ }
647
+ }
648
+ for (const object of sceneObjects) {
649
+ register(object.medium, object.mediumRefId ?? object.medium?.id ?? 0);
650
+ }
651
+ for (const object of sceneObjects) {
652
+ if ((object.mediumRefId ?? 0) > 0 && !mediumsById.has(object.mediumRefId)) {
653
+ register({ id: object.mediumRefId });
654
+ }
655
+ }
656
+ return Object.freeze(
657
+ Array.from(mediumsById.values()).sort((left, right) => left.id - right.id)
658
+ );
659
+ }
506
660
  function normalizeWavefrontSceneObject(input = {}, index = 0) {
507
661
  const bounds = deriveBounds(input);
508
662
  const kind = readObjectKind(input.kind ?? input.type ?? (bounds ? "box" : "sphere"));
@@ -533,12 +687,19 @@ function normalizeWavefrontSceneObject(input = {}, index = 0) {
533
687
  input.specularColor ?? input.material?.specularColor,
534
688
  [1, 1, 1, 1]
535
689
  ).map((value, componentIndex) => componentIndex < 3 ? clamp(value, 0, 1) : 1);
690
+ const medium = deriveWavefrontTransportMedium(input, index + 1);
536
691
  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;
537
692
  return Object.freeze({
538
693
  id: readNonNegativeInteger("id", input.id, index + 1),
539
694
  kind,
540
695
  materialKind: resolvedMaterialKind,
541
696
  flags: readNonNegativeInteger("flags", input.flags, 0),
697
+ mediumRefId: readNonNegativeInteger(
698
+ "mediumRefId",
699
+ input.mediumRefId ?? medium?.id ?? input.medium?.id ?? input.mediumId,
700
+ 0
701
+ ),
702
+ medium,
542
703
  center: Object.freeze(center),
543
704
  halfExtent: Object.freeze(halfExtent),
544
705
  color: Object.freeze(color),
@@ -562,6 +723,7 @@ function normalizeWavefrontSceneObject(input = {}, index = 0) {
562
723
  ),
563
724
  specular: clamp(readFiniteNumber("specular", input.specular ?? input.material?.specular, 1), 0, 1),
564
725
  specularColor: Object.freeze(specularColor),
726
+ thickness: normalizeWavefrontThickness(input, "thickness"),
565
727
  transmission
566
728
  });
567
729
  }
@@ -655,6 +817,7 @@ function normalizeWavefrontMesh(input = {}, meshIndex = 0) {
655
817
  input.specularColor ?? input.material?.specularColor,
656
818
  [1, 1, 1, 1]
657
819
  ).map((value, componentIndex) => componentIndex < 3 ? clamp(value, 0, 1) : 1);
820
+ const medium = deriveWavefrontTransportMedium(input, meshIndex + 1);
658
821
  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;
659
822
  return Object.freeze({
660
823
  id: readNonNegativeInteger("mesh id", input.id, meshIndex + 1),
@@ -671,9 +834,10 @@ function normalizeWavefrontMesh(input = {}, meshIndex = 0) {
671
834
  ),
672
835
  mediumRefId: readNonNegativeInteger(
673
836
  "mesh mediumRefId",
674
- input.mediumRefId ?? input.medium?.id ?? input.mediumId,
837
+ input.mediumRefId ?? medium?.id ?? input.medium?.id ?? input.mediumId ?? input.material?.mediumId,
675
838
  0
676
839
  ),
840
+ medium,
677
841
  color: Object.freeze(color),
678
842
  emission: Object.freeze(emission),
679
843
  roughness: clamp(readFiniteNumber("roughness", input.roughness ?? input.material?.roughness, 0.72), 0, 1),
@@ -695,6 +859,7 @@ function normalizeWavefrontMesh(input = {}, meshIndex = 0) {
695
859
  ),
696
860
  specular: clamp(readFiniteNumber("specular", input.specular ?? input.material?.specular, 1), 0, 1),
697
861
  specularColor: Object.freeze(specularColor),
862
+ thickness: normalizeWavefrontThickness(input, "mesh thickness"),
698
863
  transmission,
699
864
  baseColorTexture: input.baseColorTexture ?? input.material?.baseColorTexture ?? null,
700
865
  metallicRoughnessTexture: input.metallicRoughnessTexture ?? input.material?.metallicRoughnessTexture ?? null,
@@ -706,141 +871,9 @@ function normalizeWavefrontMesh(input = {}, meshIndex = 0) {
706
871
  function clampUnit(value) {
707
872
  return clamp(Number(value) || 0, 0, 1);
708
873
  }
709
- function srgbToLinear(value) {
710
- const channel = clampUnit(value);
711
- if (channel <= 0.04045) {
712
- return channel / 12.92;
713
- }
714
- return ((channel + 0.055) / 1.055) ** 2.4;
715
- }
716
- function sampleTextureRgba(texture, uv = [0, 0], colorSpace = "linear") {
717
- if (!texture || !Number.isFinite(texture.width) || !Number.isFinite(texture.height) || !texture.data || texture.width <= 0 || texture.height <= 0) {
718
- return [1, 1, 1, 1];
719
- }
720
- const u = (uv[0] % 1 + 1) % 1;
721
- const v = (uv[1] % 1 + 1) % 1;
722
- const x = Math.min(texture.width - 1, Math.max(0, Math.round(u * (texture.width - 1))));
723
- const y = Math.min(texture.height - 1, Math.max(0, Math.round((1 - v) * (texture.height - 1))));
724
- const offset = (y * texture.width + x) * 4;
725
- const data = texture.data;
726
- const scale2 = resolveTextureSampleScale(data);
727
- const defaultChannel = scale2 === 1 ? 1 : Math.round(1 / scale2);
728
- const color = [
729
- (data[offset] ?? defaultChannel) * scale2,
730
- (data[offset + 1] ?? defaultChannel) * scale2,
731
- (data[offset + 2] ?? defaultChannel) * scale2,
732
- (data[offset + 3] ?? defaultChannel) * scale2
733
- ];
734
- if (colorSpace === "srgb") {
735
- return [srgbToLinear(color[0]), srgbToLinear(color[1]), srgbToLinear(color[2]), color[3]];
736
- }
737
- return color;
738
- }
739
- function resolveTextureSampleScale(data) {
740
- if (data instanceof Uint8Array || data instanceof Uint8ClampedArray) {
741
- return 1 / 255;
742
- }
743
- if (data instanceof Uint16Array) {
744
- return 1 / 65535;
745
- }
746
- if (Array.isArray(data) && data.some((value) => Number(value) > 1)) {
747
- return 1 / 255;
748
- }
749
- return 1;
750
- }
751
- function normalizeVectorOrFallback(vector, fallback) {
752
- return normalize(Array.isArray(vector) ? vector : fallback, fallback);
753
- }
754
- function buildTriangleTangentBasis(v0, v1, v2, uv0, uv1, uv2, fallbackNormal) {
755
- const edge1 = subtract(v1, v0);
756
- const edge2 = subtract(v2, v0);
757
- const deltaUv1 = [uv1[0] - uv0[0], uv1[1] - uv0[1]];
758
- const deltaUv2 = [uv2[0] - uv0[0], uv2[1] - uv0[1]];
759
- const determinant = deltaUv1[0] * deltaUv2[1] - deltaUv1[1] * deltaUv2[0];
760
- if (Math.abs(determinant) < 1e-6) {
761
- const tangentFallback = Math.abs(fallbackNormal[1]) < 0.999 ? [0, 1, 0] : [1, 0, 0];
762
- const tangent2 = normalize(cross(tangentFallback, fallbackNormal), [1, 0, 0]);
763
- const bitangent2 = normalize(cross(fallbackNormal, tangent2), [0, 0, 1]);
764
- return { tangent: tangent2, bitangent: bitangent2 };
765
- }
766
- const inverse = 1 / determinant;
767
- const tangent = normalize(
768
- [
769
- inverse * (edge1[0] * deltaUv2[1] - edge2[0] * deltaUv1[1]),
770
- inverse * (edge1[1] * deltaUv2[1] - edge2[1] * deltaUv1[1]),
771
- inverse * (edge1[2] * deltaUv2[1] - edge2[2] * deltaUv1[1])
772
- ],
773
- [1, 0, 0]
774
- );
775
- const bitangent = normalize(
776
- [
777
- inverse * (-edge1[0] * deltaUv2[0] + edge2[0] * deltaUv1[0]),
778
- inverse * (-edge1[1] * deltaUv2[0] + edge2[1] * deltaUv1[0]),
779
- inverse * (-edge1[2] * deltaUv2[0] + edge2[2] * deltaUv1[0])
780
- ],
781
- [0, 0, 1]
782
- );
783
- return { tangent, bitangent };
784
- }
785
- function applyNormalMap(normal, tangent, bitangent, normalTexture, uv) {
786
- if (!normalTexture) {
787
- return normalizeVectorOrFallback(normal, [0, 1, 0]);
788
- }
789
- const sample = sampleTextureRgba(normalTexture, uv, "linear");
790
- const strength = clampUnit(normalTexture.scale ?? 1);
791
- const tangentNormal = normalize(
792
- [
793
- (sample[0] * 2 - 1) * strength,
794
- (sample[1] * 2 - 1) * strength,
795
- 1 + (sample[2] * 2 - 1 - 1) * strength
796
- ],
797
- [0, 0, 1]
798
- );
799
- return normalize(
800
- [
801
- tangent[0] * tangentNormal[0] + bitangent[0] * tangentNormal[1] + normal[0] * tangentNormal[2],
802
- tangent[1] * tangentNormal[0] + bitangent[1] * tangentNormal[1] + normal[1] * tangentNormal[2],
803
- tangent[2] * tangentNormal[0] + bitangent[2] * tangentNormal[1] + normal[2] * tangentNormal[2]
804
- ],
805
- normal
806
- );
807
- }
808
- function sampleBaseColor(mesh, uv) {
809
- const sample = mesh.baseColorTexture ? sampleTextureRgba(mesh.baseColorTexture, uv, "srgb") : [1, 1, 1, 1];
810
- return [
811
- clampUnit(mesh.color[0] * sample[0]),
812
- clampUnit(mesh.color[1] * sample[1]),
813
- clampUnit(mesh.color[2] * sample[2]),
814
- clampUnit((mesh.color[3] ?? 1) * sample[3])
815
- ];
816
- }
817
- function sampleSurfaceMaterial(mesh, uv) {
818
- const textureSample = mesh.metallicRoughnessTexture ? sampleTextureRgba(mesh.metallicRoughnessTexture, uv, "linear") : [1, 1, 1, 1];
819
- return {
820
- roughness: clamp(mesh.roughness * textureSample[1], 0, 1),
821
- metallic: clamp(mesh.metallic * textureSample[2], 0, 1)
822
- };
823
- }
824
- function averageColors(colors) {
825
- const count = Math.max(colors.length, 1);
826
- return colors.reduce(
827
- (accumulator, color) => [
828
- accumulator[0] + color[0] / count,
829
- accumulator[1] + color[1] / count,
830
- accumulator[2] + color[2] / count,
831
- accumulator[3] + color[3] / count
832
- ],
833
- [0, 0, 0, 0]
834
- );
835
- }
836
- function averageNumbers(values, fallback = 0) {
837
- if (!Array.isArray(values) || values.length === 0) {
838
- return fallback;
839
- }
840
- return values.reduce((sum, value) => sum + value, 0) / values.length;
841
- }
842
- function createMeshTriangleRecords(meshes) {
874
+ function createMeshTriangleRecords(meshes, gpuMaterialSource = null) {
843
875
  const source = Array.isArray(meshes) ? meshes : [];
876
+ const resolvedMaterialSource = gpuMaterialSource ?? createWavefrontGpuMaterialSource(source);
844
877
  let nextTriangleId = 0;
845
878
  return source.flatMap((meshInput, meshIndex) => {
846
879
  const mesh = normalizeWavefrontMesh(meshInput, meshIndex);
@@ -859,16 +892,6 @@ function createMeshTriangleRecords(meshes) {
859
892
  const uv0 = mesh.uvs ? readVector2(mesh.uvs, a) : [0, 0];
860
893
  const uv1 = mesh.uvs ? readVector2(mesh.uvs, b) : [0, 0];
861
894
  const uv2 = mesh.uvs ? readVector2(mesh.uvs, c) : [0, 0];
862
- const tangentBasis = buildTriangleTangentBasis(v0, v1, v2, uv0, uv1, uv2, faceNormal);
863
- const shadedN0 = applyNormalMap(n0, tangentBasis.tangent, tangentBasis.bitangent, mesh.normalTexture, uv0);
864
- const shadedN1 = applyNormalMap(n1, tangentBasis.tangent, tangentBasis.bitangent, mesh.normalTexture, uv1);
865
- const shadedN2 = applyNormalMap(n2, tangentBasis.tangent, tangentBasis.bitangent, mesh.normalTexture, uv2);
866
- const sampledColors = [sampleBaseColor(mesh, uv0), sampleBaseColor(mesh, uv1), sampleBaseColor(mesh, uv2)];
867
- const sampledMaterials = [
868
- sampleSurfaceMaterial(mesh, uv0),
869
- sampleSurfaceMaterial(mesh, uv1),
870
- sampleSurfaceMaterial(mesh, uv2)
871
- ];
872
895
  const bounds = triangleBounds(v0, v1, v2);
873
896
  triangles.push(
874
897
  Object.freeze({
@@ -882,17 +905,17 @@ function createMeshTriangleRecords(meshes) {
882
905
  v0: Object.freeze(v0),
883
906
  v1: Object.freeze(v1),
884
907
  v2: Object.freeze(v2),
885
- n0: Object.freeze(shadedN0),
886
- n1: Object.freeze(shadedN1),
887
- n2: Object.freeze(shadedN2),
908
+ n0: Object.freeze(n0),
909
+ n1: Object.freeze(n1),
910
+ n2: Object.freeze(n2),
888
911
  uv0: Object.freeze(uv0),
889
912
  uv1: Object.freeze(uv1),
890
913
  uv2: Object.freeze(uv2),
891
- color: Object.freeze(averageColors(sampledColors)),
914
+ color: mesh.color,
892
915
  emission: mesh.emission,
893
916
  material: Object.freeze([
894
- averageNumbers(sampledMaterials.map((sample) => sample.roughness), mesh.roughness),
895
- averageNumbers(sampledMaterials.map((sample) => sample.metallic), mesh.metallic),
917
+ mesh.roughness,
918
+ mesh.metallic,
896
919
  mesh.opacity,
897
920
  mesh.ior
898
921
  ]),
@@ -906,7 +929,7 @@ function createMeshTriangleRecords(meshes) {
906
929
  mesh.clearcoatRoughness,
907
930
  mesh.specular,
908
931
  mesh.transmission,
909
- 0
932
+ mesh.thickness
910
933
  ]),
911
934
  specularColor: Object.freeze([
912
935
  mesh.specularColor[0] ?? 1,
@@ -914,6 +937,27 @@ function createMeshTriangleRecords(meshes) {
914
937
  mesh.specularColor[2] ?? 1,
915
938
  1
916
939
  ]),
940
+ baseColorAtlas: Object.freeze(
941
+ resolvedMaterialSource.baseColorAtlas.resolveRect(mesh.baseColorTexture)
942
+ ),
943
+ metallicRoughnessAtlas: Object.freeze(
944
+ resolvedMaterialSource.metallicRoughnessAtlas.resolveRect(mesh.metallicRoughnessTexture)
945
+ ),
946
+ normalAtlas: Object.freeze(
947
+ resolvedMaterialSource.normalAtlas.resolveRect(mesh.normalTexture)
948
+ ),
949
+ occlusionAtlas: Object.freeze(
950
+ resolvedMaterialSource.occlusionAtlas.resolveRect(mesh.occlusionTexture)
951
+ ),
952
+ emissiveAtlas: Object.freeze(
953
+ resolvedMaterialSource.emissiveAtlas.resolveRect(mesh.emissiveTexture)
954
+ ),
955
+ textureSettings: Object.freeze([
956
+ clampUnit(mesh.normalTexture?.scale ?? mesh.normalTexture?.strength ?? 1),
957
+ clampUnit(mesh.occlusionTexture?.strength ?? 1),
958
+ clampUnit(mesh.emissiveTexture?.strength ?? 1),
959
+ 0
960
+ ]),
917
961
  bounds: Object.freeze({
918
962
  min: Object.freeze(bounds.min),
919
963
  max: Object.freeze(bounds.max)
@@ -988,9 +1032,10 @@ function buildBvh(triangles, maxLeafTriangles = 4) {
988
1032
  triangles: Object.freeze(orderedTriangles)
989
1033
  });
990
1034
  }
991
- function createWavefrontMeshAcceleration(meshes = []) {
1035
+ function createWavefrontMeshAcceleration(meshes = [], gpuMaterialSource = null) {
992
1036
  const source = Array.isArray(meshes) ? meshes : [meshes];
993
- const triangles = createMeshTriangleRecords(source);
1037
+ const resolvedMaterialSource = gpuMaterialSource ?? createWavefrontGpuMaterialSource(source);
1038
+ const triangles = createMeshTriangleRecords(source, resolvedMaterialSource);
994
1039
  return buildBvh(triangles);
995
1040
  }
996
1041
  function estimateMeshSourceShape(meshes) {
@@ -1200,7 +1245,7 @@ function createWavefrontGpuMaterialSource(meshes = []) {
1200
1245
  mesh.clearcoatRoughness,
1201
1246
  mesh.specular,
1202
1247
  mesh.transmission,
1203
- 0
1248
+ mesh.thickness
1204
1249
  ]);
1205
1250
  writeVec4(floatView, byteOffset + 80, [
1206
1251
  mesh.specularColor[0] ?? 1,
@@ -1284,12 +1329,12 @@ function createWavefrontBvhBuildLevels(triangleCountInput) {
1284
1329
  return Object.freeze(levels);
1285
1330
  }
1286
1331
  function resolveAccelerationBuildMode(options = {}) {
1287
- const mode = options.accelerationBuildMode ?? (options.displayQuality === true ? "gpu" : "cpu-debug");
1288
- if (mode !== "gpu" && mode !== "cpu-debug") {
1289
- throw new Error('accelerationBuildMode must be either "gpu" or "cpu-debug".');
1290
- }
1291
- if (options.displayQuality === true && mode !== "gpu") {
1292
- throw new Error("Display-quality path tracing requires GPU-built mesh acceleration.");
1332
+ const requestedMode = options.accelerationBuildMode ?? (options.displayQuality === true ? "cpu-upload" : "cpu-debug");
1333
+ const mode = requestedMode === "cpu-debug" ? "cpu-upload" : requestedMode;
1334
+ if (mode !== "gpu" && mode !== "cpu-upload") {
1335
+ throw new Error(
1336
+ 'accelerationBuildMode must be either "gpu", "cpu-upload", or the legacy alias "cpu-debug".'
1337
+ );
1293
1338
  }
1294
1339
  return mode;
1295
1340
  }
@@ -1368,7 +1413,7 @@ function createWavefrontGpuMeshSource(meshes = [], gpuMaterialSourceInput = null
1368
1413
  mesh.clearcoatRoughness,
1369
1414
  mesh.specular,
1370
1415
  mesh.transmission,
1371
- 0
1416
+ mesh.thickness
1372
1417
  ]);
1373
1418
  writeVec4(meshFloats, floatOffset * 4 + 128, [
1374
1419
  mesh.specularColor[0] ?? 1,
@@ -1469,12 +1514,16 @@ function normalizeSceneObjects(sceneObjects, useDefaultScene = true) {
1469
1514
  const source = Array.isArray(sceneObjects) && sceneObjects.length > 0 ? sceneObjects : useDefaultScene ? createDefaultWavefrontSceneObjects() : [];
1470
1515
  return source.map((object, index) => normalizeWavefrontSceneObject(object, index));
1471
1516
  }
1517
+ function normalizeWavefrontMeshes(meshes) {
1518
+ const source = Array.isArray(meshes) ? meshes : [];
1519
+ return source.map((mesh, index) => normalizeWavefrontMesh(mesh, index));
1520
+ }
1472
1521
  function normalizeMeshes(options = {}) {
1473
1522
  if (Array.isArray(options.meshes)) {
1474
- return options.meshes;
1523
+ return normalizeWavefrontMeshes(options.meshes);
1475
1524
  }
1476
1525
  if (options.mesh) {
1477
- return [options.mesh];
1526
+ return normalizeWavefrontMeshes([options.mesh]);
1478
1527
  }
1479
1528
  return [];
1480
1529
  }
@@ -1771,7 +1820,7 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
1771
1820
  const meshSourceShape = estimateMeshSourceShape(meshes);
1772
1821
  const gpuMaterialSource = meshes.length > 0 ? createWavefrontGpuMaterialSource(meshes) : createWavefrontGpuMaterialSource([]);
1773
1822
  const gpuMeshSource = meshes.length > 0 ? createWavefrontGpuMeshSource(meshes, gpuMaterialSource) : createWavefrontGpuMeshSource([]);
1774
- const meshAcceleration = accelerationBuildMode === "cpu-debug" ? createWavefrontMeshAcceleration(meshes) : Object.freeze({ nodes: Object.freeze([]), triangles: Object.freeze([]) });
1823
+ const meshAcceleration = accelerationBuildMode === "cpu-upload" ? createWavefrontMeshAcceleration(meshes, gpuMaterialSource) : Object.freeze({ nodes: Object.freeze([]), triangles: Object.freeze([]) });
1775
1824
  const emissiveTriangleIndices = createWavefrontEmissiveTriangleIndexSource(
1776
1825
  meshes,
1777
1826
  options.emissiveTriangleCapacity
@@ -1781,6 +1830,7 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
1781
1830
  const sceneObjects = Object.freeze(
1782
1831
  normalizeSceneObjects(options.sceneObjects, meshes.length === 0)
1783
1832
  );
1833
+ const mediums = collectWavefrontMediums(options, meshes, sceneObjects);
1784
1834
  const sceneObjectCapacity = Math.max(
1785
1835
  sceneObjects.length,
1786
1836
  readPositiveInteger("sceneObjectCapacity", options.sceneObjectCapacity, DEFAULT_SCENE_OBJECT_CAPACITY)
@@ -1839,6 +1889,8 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
1839
1889
  sceneObjects,
1840
1890
  sceneObjectCount: sceneObjects.length,
1841
1891
  sceneObjectCapacity,
1892
+ mediums,
1893
+ mediumCount: mediums.length,
1842
1894
  accelerationBuildMode,
1843
1895
  gpuAccelerationBuildRequired: accelerationBuildMode === "gpu" && triangleCount > 0,
1844
1896
  gpuMeshSource,
@@ -1940,29 +1992,30 @@ function packWavefrontSceneObjects(sceneObjects, capacity = sceneObjects.length)
1940
1992
  uintView[u32 + 1] = object.id;
1941
1993
  uintView[u32 + 2] = object.materialKind;
1942
1994
  uintView[u32 + 3] = object.flags;
1943
- writeVec4(floatView, byteOffset + 16, [...object.center, 0]);
1944
- writeVec4(floatView, byteOffset + 32, [...object.halfExtent, 0]);
1945
- writeVec4(floatView, byteOffset + 48, object.color);
1946
- writeVec4(floatView, byteOffset + 64, object.emission);
1947
- writeVec4(floatView, byteOffset + 80, [
1995
+ uintView[u32 + 4] = object.mediumRefId;
1996
+ writeVec4(floatView, byteOffset + 32, [...object.center, 0]);
1997
+ writeVec4(floatView, byteOffset + 48, [...object.halfExtent, 0]);
1998
+ writeVec4(floatView, byteOffset + 64, object.color);
1999
+ writeVec4(floatView, byteOffset + 80, object.emission);
2000
+ writeVec4(floatView, byteOffset + 96, [
1948
2001
  object.roughness,
1949
2002
  object.metallic,
1950
2003
  object.opacity,
1951
2004
  object.ior
1952
2005
  ]);
1953
- writeVec4(floatView, byteOffset + 96, [
2006
+ writeVec4(floatView, byteOffset + 112, [
1954
2007
  object.sheenColor[0] ?? 0,
1955
2008
  object.sheenColor[1] ?? 0,
1956
2009
  object.sheenColor[2] ?? 0,
1957
2010
  object.clearcoat
1958
2011
  ]);
1959
- writeVec4(floatView, byteOffset + 112, [
2012
+ writeVec4(floatView, byteOffset + 128, [
1960
2013
  object.clearcoatRoughness,
1961
2014
  object.specular,
1962
2015
  object.transmission,
1963
- 0
2016
+ object.thickness
1964
2017
  ]);
1965
- writeVec4(floatView, byteOffset + 128, [
2018
+ writeVec4(floatView, byteOffset + 144, [
1966
2019
  object.specularColor[0] ?? 1,
1967
2020
  object.specularColor[1] ?? 1,
1968
2021
  object.specularColor[2] ?? 1,
@@ -2498,7 +2551,7 @@ function integrateBrdfSample(nDotV, roughness, sampleCount) {
2498
2551
  }
2499
2552
  return [scaleTerm / sampleCount, biasTerm / sampleCount];
2500
2553
  }
2501
- function createBrdfLutUploadBytes(size = DEFAULT_BRDF_LUT_SIZE, sampleCount = 1024) {
2554
+ function createBrdfLutUploadBytes(size = DEFAULT_BRDF_LUT_SIZE, sampleCount = DEFAULT_BRDF_LUT_SAMPLE_COUNT) {
2502
2555
  const cacheKey = `${Math.max(1, Math.trunc(size))}:${Math.max(1, Math.trunc(sampleCount))}`;
2503
2556
  const cached = BRDF_LUT_UPLOAD_CACHE.get(cacheKey);
2504
2557
  if (cached) {
@@ -2870,6 +2923,80 @@ function createBrdfLutResource(device, constants, size = DEFAULT_BRDF_LUT_SIZE)
2870
2923
  height: upload.height
2871
2924
  });
2872
2925
  }
2926
+ function createMediumTextureResource(device, constants, mediums) {
2927
+ const normalized = Array.isArray(mediums) && mediums.length > 0 ? mediums : [{ id: 0 }];
2928
+ const width = Math.max(
2929
+ 1,
2930
+ normalized.reduce((maximum, medium) => Math.max(maximum, medium.id ?? 0), 0) + 1
2931
+ );
2932
+ const level = {
2933
+ width,
2934
+ height: MEDIUM_TABLE_ROWS,
2935
+ data: new Float32Array(width * MEDIUM_TABLE_ROWS * 4)
2936
+ };
2937
+ for (const medium of normalized) {
2938
+ const mediumId = Math.max(0, Math.trunc(Number(medium.id) || 0));
2939
+ const absorptionOffset = mediumId * 4;
2940
+ level.data[absorptionOffset] = Math.max(0, medium.absorption?.[0] ?? 0);
2941
+ level.data[absorptionOffset + 1] = Math.max(0, medium.absorption?.[1] ?? 0);
2942
+ level.data[absorptionOffset + 2] = Math.max(0, medium.absorption?.[2] ?? 0);
2943
+ level.data[absorptionOffset + 3] = Math.max(0, medium.phaseModel ?? 0);
2944
+ const scatteringOffset = (width + mediumId) * 4;
2945
+ level.data[scatteringOffset] = Math.max(0, medium.scattering?.[0] ?? 0);
2946
+ level.data[scatteringOffset + 1] = Math.max(0, medium.scattering?.[1] ?? 0);
2947
+ level.data[scatteringOffset + 2] = Math.max(0, medium.scattering?.[2] ?? 0);
2948
+ level.data[scatteringOffset + 3] = Math.max(0, medium.density ?? 0);
2949
+ }
2950
+ const upload = createFloat16RgbaUploadFromLevels([level])[0];
2951
+ const texture = device.createTexture({
2952
+ label: "plasius.wavefront.mediumTable",
2953
+ size: { width, height: MEDIUM_TABLE_ROWS },
2954
+ format: "rgba16float",
2955
+ usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
2956
+ });
2957
+ device.queue.writeTexture(
2958
+ { texture },
2959
+ upload.bytes,
2960
+ { bytesPerRow: upload.bytesPerRow, rowsPerImage: upload.height },
2961
+ { width, height: MEDIUM_TABLE_ROWS, depthOrArrayLayers: 1 }
2962
+ );
2963
+ return Object.freeze({
2964
+ texture,
2965
+ view: texture.createView(),
2966
+ ownsTexture: true,
2967
+ count: normalized.length,
2968
+ width
2969
+ });
2970
+ }
2971
+ function mediumTablesEqual(left, right) {
2972
+ const leftMediums = Array.isArray(left) ? left : [];
2973
+ const rightMediums = Array.isArray(right) ? right : [];
2974
+ if (leftMediums.length !== rightMediums.length) {
2975
+ return false;
2976
+ }
2977
+ for (let index = 0; index < leftMediums.length; index += 1) {
2978
+ const leftMedium = leftMediums[index];
2979
+ const rightMedium = rightMediums[index];
2980
+ if ((leftMedium?.id ?? 0) !== (rightMedium?.id ?? 0)) {
2981
+ return false;
2982
+ }
2983
+ if ((leftMedium?.phaseModel ?? 0) !== (rightMedium?.phaseModel ?? 0)) {
2984
+ return false;
2985
+ }
2986
+ if ((leftMedium?.density ?? 0) !== (rightMedium?.density ?? 0)) {
2987
+ return false;
2988
+ }
2989
+ for (let component = 0; component < 3; component += 1) {
2990
+ if ((leftMedium?.absorption?.[component] ?? 0) !== (rightMedium?.absorption?.[component] ?? 0)) {
2991
+ return false;
2992
+ }
2993
+ if ((leftMedium?.scattering?.[component] ?? 0) !== (rightMedium?.scattering?.[component] ?? 0)) {
2994
+ return false;
2995
+ }
2996
+ }
2997
+ }
2998
+ return true;
2999
+ }
2873
3000
  function createAtlasTextureResource(device, constants, atlas, label) {
2874
3001
  const upload = createRgba8TextureUpload(atlas);
2875
3002
  const texture = device.createTexture({
@@ -3008,6 +3135,10 @@ struct SceneObject {
3008
3135
  objectId: u32,
3009
3136
  materialKind: u32,
3010
3137
  flags: u32,
3138
+ mediumRefId: u32,
3139
+ pad0: u32,
3140
+ pad1: u32,
3141
+ pad2: u32,
3011
3142
  center: vec4<f32>,
3012
3143
  halfExtent: vec4<f32>,
3013
3144
  color: vec4<f32>,
@@ -3068,9 +3199,9 @@ struct BvhLeafRef {
3068
3199
  struct ScatterResult {
3069
3200
  direction: vec4<f32>,
3070
3201
  pdf: f32,
3202
+ mediumRefId: u32,
3071
3203
  flags: u32,
3072
3204
  pad0: u32,
3073
- pad1: u32,
3074
3205
  };
3075
3206
 
3076
3207
  struct MeshVertex {
@@ -3216,6 +3347,7 @@ struct EnvironmentPortal {
3216
3347
  @group(0) @binding(29) var brdfLutTexture: texture_2d<f32>;
3217
3348
  @group(0) @binding(30) var brdfLutSampler: sampler;
3218
3349
  @group(0) @binding(31) var environmentSamplingTexture: texture_2d<f32>;
3350
+ @group(0) @binding(32) var mediumTableTexture: texture_2d<f32>;
3219
3351
 
3220
3352
  fn hash_u32(value: u32) -> u32 {
3221
3353
  var x = value;
@@ -3881,6 +4013,60 @@ fn gated_environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f
3881
4013
  return environment_radiance(origin, direction);
3882
4014
  }
3883
4015
 
4016
+ fn medium_dimensions() -> vec2<u32> {
4017
+ return textureDimensions(mediumTableTexture);
4018
+ }
4019
+
4020
+ fn medium_valid(mediumRefId: u32) -> bool {
4021
+ let dimensions = medium_dimensions();
4022
+ return mediumRefId > 0u && mediumRefId < dimensions.x;
4023
+ }
4024
+
4025
+ fn medium_absorption(mediumRefId: u32) -> vec3<f32> {
4026
+ if (!medium_valid(mediumRefId)) {
4027
+ return vec3<f32>(0.0);
4028
+ }
4029
+ return max(
4030
+ textureLoad(mediumTableTexture, vec2<i32>(i32(mediumRefId), 0), 0).xyz,
4031
+ vec3<f32>(0.0)
4032
+ );
4033
+ }
4034
+
4035
+ fn medium_scattering(mediumRefId: u32) -> vec3<f32> {
4036
+ if (!medium_valid(mediumRefId)) {
4037
+ return vec3<f32>(0.0);
4038
+ }
4039
+ return max(
4040
+ textureLoad(mediumTableTexture, vec2<i32>(i32(mediumRefId), 1), 0).xyz,
4041
+ vec3<f32>(0.0)
4042
+ );
4043
+ }
4044
+
4045
+ fn medium_transmittance(mediumRefId: u32, distance: f32) -> vec3<f32> {
4046
+ if (!medium_valid(mediumRefId) || distance <= 0.000001) {
4047
+ return vec3<f32>(1.0);
4048
+ }
4049
+ let extinction = medium_absorption(mediumRefId) + medium_scattering(mediumRefId);
4050
+ return vec3<f32>(
4051
+ exp(-extinction.x * distance),
4052
+ exp(-extinction.y * distance),
4053
+ exp(-extinction.z * distance)
4054
+ );
4055
+ }
4056
+
4057
+ fn transmitted_medium_ref_id(ray: RayRecord, hit: HitRecord) -> u32 {
4058
+ if (hit.mediumRefId == 0u) {
4059
+ return ray.mediumRefId;
4060
+ }
4061
+ if (hit.frontFace == 1u) {
4062
+ return hit.mediumRefId;
4063
+ }
4064
+ if (ray.mediumRefId == hit.mediumRefId) {
4065
+ return 0u;
4066
+ }
4067
+ return ray.mediumRefId;
4068
+ }
4069
+
3884
4070
  fn surface_path_response(hit: HitRecord) -> vec3<f32> {
3885
4071
  let color = clamp(hit.color.xyz, vec3<f32>(0.0), vec3<f32>(1.0));
3886
4072
  let opacity = clamp(hit.material.z, 0.0, 1.0);
@@ -3979,11 +4165,15 @@ fn terminal_surface_environment_source(ray: RayRecord, hit: HitRecord) -> vec3<f
3979
4165
  return clamp_sample_radiance(environmentFloor * materialFloor);
3980
4166
  }
3981
4167
 
3982
- fn terminal_surface_environment_contribution(ray: RayRecord, hit: HitRecord) -> vec3<f32> {
4168
+ fn terminal_surface_environment_contribution(
4169
+ ray: RayRecord,
4170
+ throughput: vec3<f32>,
4171
+ hit: HitRecord
4172
+ ) -> vec3<f32> {
3983
4173
  let surfaceColor = max(hit.color.xyz, config.ambientColor.xyz);
3984
4174
  let occlusion = mix(0.75, 1.0, clamp(hit.occlusion, 0.0, 1.0));
3985
4175
  return clamp_sample_radiance(
3986
- ray.throughput.xyz *
4176
+ throughput *
3987
4177
  surfaceColor *
3988
4178
  terminal_surface_environment_source(ray, hit) *
3989
4179
  occlusion
@@ -4457,7 +4647,7 @@ fn intersect_sphere(ray: RayRecord, object: SceneObject) -> Candidate {
4457
4647
  0xffffffffu,
4458
4648
  object.objectId,
4459
4649
  object.objectId,
4460
- 0u
4650
+ object.mediumRefId
4461
4651
  );
4462
4652
  }
4463
4653
 
@@ -4509,7 +4699,7 @@ fn intersect_box(ray: RayRecord, object: SceneObject) -> Candidate {
4509
4699
  0xffffffffu,
4510
4700
  object.objectId,
4511
4701
  object.objectId,
4512
- 0u
4702
+ object.mediumRefId
4513
4703
  );
4514
4704
  }
4515
4705
 
@@ -4760,6 +4950,10 @@ fn intersectActiveQueue(@builtin(global_invocation_id) globalId: vec3<u32>) {
4760
4950
  let ray = activeQueue[index];
4761
4951
  var nearest = 1000000.0;
4762
4952
  var hitObject = SceneObject(
4953
+ 0u,
4954
+ 0u,
4955
+ 0u,
4956
+ 0u,
4763
4957
  0u,
4764
4958
  0u,
4765
4959
  0u,
@@ -4963,9 +5157,9 @@ fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult
4963
5157
  return ScatterResult(
4964
5158
  vec4<f32>(reflect(ray.direction.xyz, normal), 0.0),
4965
5159
  1.0,
5160
+ ray.mediumRefId,
4966
5161
  RAY_FLAG_DELTA_SAMPLE,
4967
5162
  0u,
4968
- 0u
4969
5163
  );
4970
5164
  }
4971
5165
 
@@ -4985,17 +5179,17 @@ fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult
4985
5179
  return ScatterResult(
4986
5180
  vec4<f32>(reflect(ray.direction.xyz, normal), 0.0),
4987
5181
  1.0,
5182
+ ray.mediumRefId,
4988
5183
  RAY_FLAG_DELTA_SAMPLE,
4989
5184
  0u,
4990
- 0u
4991
5185
  );
4992
5186
  }
4993
5187
  return ScatterResult(
4994
5188
  vec4<f32>(refract_direction(ray.direction.xyz, normal, etaRatio), 0.0),
4995
5189
  1.0,
5190
+ transmitted_medium_ref_id(ray, hit),
4996
5191
  RAY_FLAG_DELTA_SAMPLE,
4997
5192
  0u,
4998
- 0u
4999
5193
  );
5000
5194
  }
5001
5195
 
@@ -5010,9 +5204,9 @@ fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult
5010
5204
  return ScatterResult(
5011
5205
  vec4<f32>(guidedDirection, 0.0),
5012
5206
  guidedPdf,
5207
+ ray.mediumRefId,
5013
5208
  RAY_FLAG_GUIDED_EMISSIVE,
5014
5209
  0u,
5015
- 0u
5016
5210
  );
5017
5211
  }
5018
5212
  }
@@ -5020,7 +5214,7 @@ fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult
5020
5214
  let guidedDirection = sample_environment_portal_direction(hit, seed + 131u, normal);
5021
5215
  if (dot(normal, guidedDirection) > 0.000001) {
5022
5216
  let guidedPdf = max(evaluate_surface_bsdf_pdf(hit, viewDirection, guidedDirection), 0.000001);
5023
- return ScatterResult(vec4<f32>(guidedDirection, 0.0), guidedPdf, 0u, 0u, 0u);
5217
+ return ScatterResult(vec4<f32>(guidedDirection, 0.0), guidedPdf, ray.mediumRefId, 0u, 0u);
5024
5218
  }
5025
5219
  }
5026
5220
 
@@ -5054,7 +5248,7 @@ fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult
5054
5248
  );
5055
5249
  }
5056
5250
  let pdf = max(evaluate_surface_bsdf_pdf(hit, viewDirection, lightDirection), 0.000001);
5057
- return ScatterResult(vec4<f32>(lightDirection, 0.0), pdf, 0u, 0u, 0u);
5251
+ return ScatterResult(vec4<f32>(lightDirection, 0.0), pdf, ray.mediumRefId, 0u, 0u);
5058
5252
  }
5059
5253
 
5060
5254
  @compute @workgroup_size(64)
@@ -5067,15 +5261,17 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
5067
5261
 
5068
5262
  let ray = activeQueue[index];
5069
5263
  let hit = hits[index];
5264
+ let segmentTransmittance = medium_transmittance(ray.mediumRefId, hit.distance);
5265
+ let arrivingThroughput = ray.throughput.xyz * segmentTransmittance;
5070
5266
  var contribution = vec3<f32>(0.0);
5071
5267
 
5072
5268
  if (hit.hitType == 1u) {
5073
5269
  let guidedLightWeight = select(1.0, 0.24, (ray.flags & RAY_FLAG_GUIDED_EMISSIVE) != 0u);
5074
5270
  let sourceRadiance = max(hit.emission.xyz, hit.color.xyz) * guidedLightWeight;
5075
5271
  if (deferred_path_resolve_enabled()) {
5076
- record_deferred_terminal_source(ray, sourceRadiance);
5272
+ record_deferred_terminal_source(ray, sourceRadiance * segmentTransmittance);
5077
5273
  } else {
5078
- contribution = clamp_sample_radiance(ray.throughput.xyz * sourceRadiance);
5274
+ contribution = clamp_sample_radiance(arrivingThroughput * sourceRadiance);
5079
5275
  accumulation[ray.rayId] =
5080
5276
  accumulation[ray.rayId] + vec4<f32>(contribution * sample_weight(), 1.0);
5081
5277
  }
@@ -5092,9 +5288,9 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
5092
5288
  sourceRadiance = sourceRadiance * misWeight;
5093
5289
  }
5094
5290
  if (deferred_path_resolve_enabled()) {
5095
- record_deferred_terminal_source(ray, sourceRadiance);
5291
+ record_deferred_terminal_source(ray, sourceRadiance * segmentTransmittance);
5096
5292
  } else {
5097
- contribution = clamp_sample_radiance(ray.throughput.xyz * sourceRadiance);
5293
+ contribution = clamp_sample_radiance(arrivingThroughput * sourceRadiance);
5098
5294
  accumulation[ray.rayId] =
5099
5295
  accumulation[ray.rayId] + vec4<f32>(contribution * sample_weight(), 1.0);
5100
5296
  }
@@ -5102,7 +5298,11 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
5102
5298
  return;
5103
5299
  }
5104
5300
 
5105
- let response = stabilize_surface_path_response(ray, hit, surface_path_response(hit));
5301
+ let response = stabilize_surface_path_response(
5302
+ ray,
5303
+ hit,
5304
+ surface_path_response(hit) * segmentTransmittance
5305
+ );
5106
5306
  record_deferred_path_response(ray, response);
5107
5307
 
5108
5308
  let shouldEstimateDirectEnvironment =
@@ -5110,7 +5310,22 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
5110
5310
  hit.material.z >= 0.95 &&
5111
5311
  ray.bounce < 2u;
5112
5312
  if (shouldEstimateDirectEnvironment) {
5113
- let directEnvironment = surface_direct_environment_contribution(ray, hit);
5313
+ let directEnvironment = surface_direct_environment_contribution(
5314
+ RayRecord(
5315
+ ray.rayId,
5316
+ ray.parentRayId,
5317
+ ray.sourcePixelId,
5318
+ ray.sampleId,
5319
+ ray.bounce,
5320
+ ray.mediumRefId,
5321
+ ray.flags,
5322
+ 0u,
5323
+ ray.origin,
5324
+ ray.direction,
5325
+ vec4<f32>(arrivingThroughput, ray.throughput.w)
5326
+ ),
5327
+ hit
5328
+ );
5114
5329
  accumulation[ray.rayId] =
5115
5330
  accumulation[ray.rayId] + vec4<f32>(directEnvironment * sample_weight(), 0.0);
5116
5331
  }
@@ -5119,7 +5334,11 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
5119
5334
  if (deferred_path_resolve_enabled()) {
5120
5335
  record_deferred_terminal_source(ray, terminal_surface_environment_source(ray, hit));
5121
5336
  } else {
5122
- let terminalEnvironment = terminal_surface_environment_contribution(ray, hit);
5337
+ let terminalEnvironment = terminal_surface_environment_contribution(
5338
+ ray,
5339
+ arrivingThroughput,
5340
+ hit
5341
+ );
5123
5342
  accumulation[ray.rayId] =
5124
5343
  accumulation[ray.rayId] + vec4<f32>(terminalEnvironment * sample_weight(), 1.0);
5125
5344
  }
@@ -5134,7 +5353,11 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
5134
5353
  if (deferred_path_resolve_enabled()) {
5135
5354
  record_deferred_terminal_source(ray, terminal_surface_environment_source(ray, hit));
5136
5355
  } else {
5137
- let overflowEnvironment = terminal_surface_environment_contribution(ray, hit);
5356
+ let overflowEnvironment = terminal_surface_environment_contribution(
5357
+ ray,
5358
+ arrivingThroughput,
5359
+ hit
5360
+ );
5138
5361
  accumulation[ray.rayId] =
5139
5362
  accumulation[ray.rayId] + vec4<f32>(overflowEnvironment * sample_weight(), 1.0);
5140
5363
  }
@@ -5148,7 +5371,7 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
5148
5371
  ray.sourcePixelId,
5149
5372
  ray.sampleId,
5150
5373
  ray.bounce + 1u,
5151
- ray.mediumRefId,
5374
+ scatter.mediumRefId,
5152
5375
  scatter.flags,
5153
5376
  0u,
5154
5377
  vec4<f32>(offset_origin(hit.position.xyz, hit.shadingNormal.xyz), 1.0),
@@ -5409,9 +5632,22 @@ function nowMs() {
5409
5632
  }
5410
5633
  return Date.now();
5411
5634
  }
5412
- function estimateSubmittedGpuWorkTimeoutMs(config, tileCount, overrideTimeoutMs = null) {
5635
+ function estimateAccelerationBuildWaitFactor(config) {
5636
+ if (config?.gpuAccelerationBuildRequired !== true) {
5637
+ return 1;
5638
+ }
5639
+ const bvhSortStageCount = Array.isArray(config?.bvhSortStages) ? config.bvhSortStages.length : 0;
5640
+ const bvhBuildLevelCount = Array.isArray(config?.bvhBuildLevels) ? config.bvhBuildLevels.length : 0;
5641
+ const accelerationStageCount = 2 + bvhSortStageCount + bvhBuildLevelCount;
5642
+ return Math.max(1, 1 + accelerationStageCount / 96);
5643
+ }
5644
+ function estimateSubmittedGpuWorkTiming(config, tileCount, overrideTimeoutMs = null, options = {}) {
5413
5645
  if (Number.isFinite(overrideTimeoutMs)) {
5414
- return Math.max(1, Math.trunc(Number(overrideTimeoutMs)));
5646
+ const overrideMs = Math.max(1, Math.trunc(Number(overrideTimeoutMs)));
5647
+ return Object.freeze({
5648
+ timeoutMs: overrideMs,
5649
+ maxWaitMs: overrideMs
5650
+ });
5415
5651
  }
5416
5652
  const samplesPerPixel = Math.max(
5417
5653
  1,
@@ -5422,10 +5658,26 @@ function estimateSubmittedGpuWorkTimeoutMs(config, tileCount, overrideTimeoutMs
5422
5658
  const denoisePasses = config?.denoise ? samplesPerPixel < 4 ? 2 : 1 : 0;
5423
5659
  const tiles = Math.max(1, Number(tileCount ?? 1));
5424
5660
  const estimatedPasses = tiles * (samplesPerPixel * (maxDepth + 1 + deferredResolvePasses) + denoisePasses + 1);
5425
- return Math.min(
5661
+ const triangleCount = Math.max(0, Number(config?.triangleCount ?? 0));
5662
+ const geometryFactor = Math.max(1, triangleCount / 131072);
5663
+ const includeAccelerationBuild = options.includeAccelerationBuild === true;
5664
+ const accelerationFactor = includeAccelerationBuild ? estimateAccelerationBuildWaitFactor(config) : 1;
5665
+ const estimatedWindowMs = Math.round(
5666
+ (GPU_SUBMITTED_WORK_TIMEOUT_MS + estimatedPasses * 5) * geometryFactor * accelerationFactor
5667
+ );
5668
+ const timeoutMs = Math.min(
5426
5669
  GPU_MAX_SUBMITTED_WORK_TIMEOUT_MS,
5427
- GPU_SUBMITTED_WORK_TIMEOUT_MS + estimatedPasses * 5
5670
+ Math.max(GPU_SUBMITTED_WORK_TIMEOUT_MS, estimatedWindowMs)
5671
+ );
5672
+ const maxWaitMultiplier = includeAccelerationBuild ? 3 : 2;
5673
+ const maxWaitMs = Math.min(
5674
+ GPU_MAX_SUBMITTED_WORK_DEADLINE_MS,
5675
+ Math.max(timeoutMs, estimatedWindowMs * maxWaitMultiplier)
5428
5676
  );
5677
+ return Object.freeze({
5678
+ timeoutMs,
5679
+ maxWaitMs
5680
+ });
5429
5681
  }
5430
5682
  async function createWavefrontPathTracingComputeRenderer(options = {}) {
5431
5683
  assertAnalyticDisplayQualityPolicy(options);
@@ -5637,6 +5889,11 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
5637
5889
  config.environmentMap,
5638
5890
  config.environmentColor
5639
5891
  );
5892
+ let mediumTextureResource = createMediumTextureResource(
5893
+ device,
5894
+ constants,
5895
+ config.mediums
5896
+ );
5640
5897
  config = Object.freeze({
5641
5898
  ...config,
5642
5899
  environmentMap: Object.freeze({
@@ -5723,7 +5980,8 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
5723
5980
  { binding: 28, visibility: constants.shader.COMPUTE, sampler: { type: "filtering" } },
5724
5981
  { binding: 29, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
5725
5982
  { binding: 30, visibility: constants.shader.COMPUTE, sampler: { type: "filtering" } },
5726
- { binding: 31, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } }
5983
+ { binding: 31, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
5984
+ { binding: 32, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } }
5727
5985
  ]
5728
5986
  });
5729
5987
  const accelerationBindGroupLayout = device.createBindGroupLayout({
@@ -5910,14 +6168,18 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
5910
6168
  { binding: 28, resource: materialAtlasSampler },
5911
6169
  { binding: 29, resource: brdfLutResource.view },
5912
6170
  { binding: 30, resource: brdfLutResource.sampler },
5913
- { binding: 31, resource: environmentSamplingResource.view }
6171
+ { binding: 31, resource: environmentSamplingResource.view },
6172
+ { binding: 32, resource: mediumTextureResource.view }
5914
6173
  ]
5915
6174
  });
5916
6175
  }
5917
- const bindGroups = [
5918
- createTraceBindGroup(activeQueue, nextQueue, "plasius.wavefront.bind.activeNext"),
5919
- createTraceBindGroup(nextQueue, activeQueue, "plasius.wavefront.bind.nextActive")
5920
- ];
6176
+ function createTraceBindGroups() {
6177
+ return [
6178
+ createTraceBindGroup(activeQueue, nextQueue, "plasius.wavefront.bind.activeNext"),
6179
+ createTraceBindGroup(nextQueue, activeQueue, "plasius.wavefront.bind.nextActive")
6180
+ ];
6181
+ }
6182
+ let bindGroups = createTraceBindGroups();
5921
6183
  const bvhBuildBindGroup = device.createBindGroup({
5922
6184
  label: "plasius.wavefront.bind.bvhBuild",
5923
6185
  layout: accelerationBindGroupLayout,
@@ -6095,6 +6357,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
6095
6357
  emissiveTriangleCount: config.emissiveTriangleCount,
6096
6358
  environmentPortalCount: config.environmentPortalCount,
6097
6359
  environmentPortalMode: config.environmentPortalMode,
6360
+ mediumCount: config.mediumCount,
6098
6361
  environmentMap: createEnvironmentMapSnapshot(config.environmentMap),
6099
6362
  deferredPathResolve: config.deferredPathResolve,
6100
6363
  bvhNodeCount: config.bvhNodeCount,
@@ -6390,6 +6653,10 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
6390
6653
  1,
6391
6654
  Number.isFinite(options2.timeoutMs) ? Number(options2.timeoutMs) : GPU_SUBMITTED_WORK_TIMEOUT_MS
6392
6655
  );
6656
+ const maxWaitMs = Math.max(
6657
+ timeoutMs,
6658
+ Number.isFinite(options2.maxWaitMs) ? Number(options2.maxWaitMs) : timeoutMs
6659
+ );
6393
6660
  const allowTimeout = options2.allowTimeout !== false;
6394
6661
  const completionPromise = device.queue.onSubmittedWorkDone().then(
6395
6662
  () => ({ status: "done" }),
@@ -6402,43 +6669,57 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
6402
6669
  `WebGPU device lost while waiting for submitted work (${info?.reason ?? "unknown"}).`
6403
6670
  );
6404
6671
  }) : null;
6405
- let timeoutHandle = null;
6406
- let resolveTimeoutPromise = null;
6407
- let timeoutSettled = false;
6408
- const settleTimeoutPromise = (value) => {
6409
- if (timeoutSettled) {
6410
- return;
6672
+ const startedAtMs = nowMs();
6673
+ while (true) {
6674
+ const elapsedMs = Math.max(0, nowMs() - startedAtMs);
6675
+ const remainingMs = Math.max(0, maxWaitMs - elapsedMs);
6676
+ if (remainingMs <= 0) {
6677
+ if (!allowTimeout) {
6678
+ throw new Error(`Timed out after ${Math.round(maxWaitMs)} ms waiting for submitted GPU work.`);
6679
+ }
6680
+ console.warn(
6681
+ `[plasius.wavefront] Submitted GPU work did not report completion within ${Math.round(maxWaitMs)} ms; continuing.`
6682
+ );
6683
+ return false;
6411
6684
  }
6412
- timeoutSettled = true;
6413
- resolveTimeoutPromise?.(value);
6414
- };
6415
- const timeoutPromise = new Promise((resolve) => {
6416
- resolveTimeoutPromise = resolve;
6417
- timeoutHandle = setTimeout(() => settleTimeoutPromise({ status: "timeout" }), timeoutMs);
6418
- });
6419
- let result;
6420
- try {
6421
- result = await Promise.race(
6422
- [completionPromise, timeoutPromise, lossPromise].filter(Boolean)
6423
- );
6424
- } finally {
6425
- if (timeoutHandle !== null) {
6426
- clearTimeout(timeoutHandle);
6427
- settleTimeoutPromise({ status: "cancelled" });
6685
+ const waitWindowMs = Math.max(1, Math.min(timeoutMs, remainingMs));
6686
+ let timeoutHandle = null;
6687
+ let resolveTimeoutPromise = null;
6688
+ let timeoutSettled = false;
6689
+ const settleTimeoutPromise = (value) => {
6690
+ if (timeoutSettled) {
6691
+ return;
6692
+ }
6693
+ timeoutSettled = true;
6694
+ resolveTimeoutPromise?.(value);
6695
+ };
6696
+ const timeoutPromise = new Promise((resolve) => {
6697
+ resolveTimeoutPromise = resolve;
6698
+ timeoutHandle = setTimeout(
6699
+ () => settleTimeoutPromise({ status: "timeout" }),
6700
+ waitWindowMs
6701
+ );
6702
+ });
6703
+ let result;
6704
+ try {
6705
+ result = await Promise.race(
6706
+ [completionPromise, timeoutPromise, lossPromise].filter(Boolean)
6707
+ );
6708
+ } finally {
6709
+ if (timeoutHandle !== null) {
6710
+ clearTimeout(timeoutHandle);
6711
+ settleTimeoutPromise({ status: "cancelled" });
6712
+ }
6428
6713
  }
6429
- }
6430
- if (result?.status === "timeout") {
6431
- if (!allowTimeout) {
6432
- throw new Error(`Timed out after ${timeoutMs} ms waiting for submitted GPU work.`);
6714
+ if (result?.status === "done") {
6715
+ return true;
6716
+ }
6717
+ if (result?.status !== "timeout") {
6718
+ return true;
6433
6719
  }
6434
- console.warn(
6435
- `[plasius.wavefront] Submitted GPU work did not report completion within ${timeoutMs} ms; continuing.`
6436
- );
6437
- return false;
6438
6720
  }
6439
- return true;
6440
6721
  }
6441
- function dispatchFrameAwaitingGpu(frameIndex, parallelism, renderedSamplesPerPixel = config.samplesPerPixel) {
6722
+ function dispatchFrameAwaitingGpu(frameIndex, parallelism, renderedSamplesPerPixel = config.samplesPerPixel, optionsForFrame = {}) {
6442
6723
  const samplePassesPerSample = config.maxDepth + 1 + (config.deferredPathResolve ? 1 : 0);
6443
6724
  const denoisePassCount = config.denoise ? renderedSamplesPerPixel < 4 ? 2 : 1 : 0;
6444
6725
  const tailPassCount = denoisePassCount + 1;
@@ -6448,17 +6729,42 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
6448
6729
  Math.max(config.maxFramePassesPerSubmission - tailPassCount, 1) / Math.max(samplePassesPerSample, 1)
6449
6730
  )
6450
6731
  );
6451
- let submissionCount = 0;
6452
- for (const tile of tiles) {
6453
- for (let sampleStart = 0; sampleStart < renderedSamplesPerPixel; sampleStart += sampleBatchSize) {
6454
- const sampleEnd = Math.min(renderedSamplesPerPixel, sampleStart + sampleBatchSize);
6732
+ const sampleRangeStart = clamp(
6733
+ readNonNegativeInteger("sampleRangeStart", optionsForFrame.sampleRangeStart, 0),
6734
+ 0,
6735
+ renderedSamplesPerPixel
6736
+ );
6737
+ const sampleRangeEnd = clamp(
6738
+ readPositiveInteger("sampleRangeEnd", optionsForFrame.sampleRangeEnd, renderedSamplesPerPixel),
6739
+ sampleRangeStart,
6740
+ renderedSamplesPerPixel
6741
+ );
6742
+ const includeDenoise = optionsForFrame.includeDenoise === true;
6743
+ const includePresent = optionsForFrame.includePresent === true;
6744
+ const tileStartIndex = clamp(
6745
+ readNonNegativeInteger("tileStartIndex", optionsForFrame.tileStartIndex, 0),
6746
+ 0,
6747
+ tiles.length
6748
+ );
6749
+ const tileEndIndex = clamp(
6750
+ readPositiveInteger("tileEndIndex", optionsForFrame.tileEndIndex, tiles.length),
6751
+ tileStartIndex,
6752
+ tiles.length
6753
+ );
6754
+ let submissionCount = Math.max(
6755
+ 0,
6756
+ readNonNegativeInteger("startingSubmissionCount", optionsForFrame.startingSubmissionCount, 0)
6757
+ );
6758
+ let slot = Math.max(0, readNonNegativeInteger("startingSlot", optionsForFrame.startingSlot, 0));
6759
+ for (const tile of tiles.slice(tileStartIndex, tileEndIndex)) {
6760
+ for (let sampleStart = sampleRangeStart; sampleStart < sampleRangeEnd; sampleStart += sampleBatchSize) {
6761
+ const sampleEnd = Math.min(sampleRangeEnd, sampleStart + sampleBatchSize);
6455
6762
  const batch = createGpuSubmissionBatcher({
6456
6763
  device,
6457
6764
  frameIndex,
6458
6765
  maxFramePassesPerSubmission: config.maxFramePassesPerSubmission,
6459
6766
  startingSubmissionCount: submissionCount
6460
6767
  });
6461
- let slot = 0;
6462
6768
  for (let sampleIndex = sampleStart; sampleIndex < sampleEnd; sampleIndex += 1) {
6463
6769
  const configOffset = writeFrameConfigSlot(slot, tile, frameIndex, {
6464
6770
  sampleIndex,
@@ -6475,41 +6781,50 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
6475
6781
  encodeTileOutput(batch.reserve(1), tile, configOffset, parallelism);
6476
6782
  }
6477
6783
  }
6478
- if (!config.deferredPathResolve && sampleEnd >= renderedSamplesPerPixel) {
6784
+ if (!config.deferredPathResolve && sampleRangeEnd >= renderedSamplesPerPixel) {
6479
6785
  const outputConfigOffset = writeFrameConfigSlot(slot, tile, frameIndex, {
6480
6786
  sampleIndex: 0,
6481
6787
  sampleWeight: 1 / renderedSamplesPerPixel
6482
6788
  });
6789
+ slot += 1;
6483
6790
  encodeTileOutput(batch.reserve(1), tile, outputConfigOffset, parallelism);
6484
6791
  }
6485
6792
  batch.flush();
6486
6793
  submissionCount += batch.getSubmissionCount();
6487
6794
  }
6488
6795
  }
6489
- const tail = createGpuSubmissionBatcher({
6490
- device,
6491
- frameIndex,
6492
- maxFramePassesPerSubmission: config.maxFramePassesPerSubmission,
6493
- startingSubmissionCount: submissionCount
6494
- });
6495
- if (config.denoise) {
6496
- const denoiseConfigOffset = writeFrameConfigSlot(
6497
- 0,
6498
- { x: 0, y: 0, width: config.width, height: config.height },
6796
+ if (includeDenoise || includePresent) {
6797
+ const tail = createGpuSubmissionBatcher({
6798
+ device,
6499
6799
  frameIndex,
6500
- { sampleIndex: 0, sampleWeight: 1 / renderedSamplesPerPixel }
6501
- );
6502
- encodeDenoise(
6503
- tail.reserve(denoisePassCount),
6504
- denoiseConfigOffset,
6505
- parallelism,
6506
- renderedSamplesPerPixel
6507
- );
6800
+ maxFramePassesPerSubmission: config.maxFramePassesPerSubmission,
6801
+ startingSubmissionCount: submissionCount
6802
+ });
6803
+ if (includeDenoise && config.denoise) {
6804
+ const denoiseConfigOffset = writeFrameConfigSlot(
6805
+ slot,
6806
+ { x: 0, y: 0, width: config.width, height: config.height },
6807
+ frameIndex,
6808
+ { sampleIndex: 0, sampleWeight: 1 / renderedSamplesPerPixel }
6809
+ );
6810
+ slot += 1;
6811
+ encodeDenoise(
6812
+ tail.reserve(denoisePassCount),
6813
+ denoiseConfigOffset,
6814
+ parallelism,
6815
+ renderedSamplesPerPixel
6816
+ );
6817
+ }
6818
+ if (includePresent) {
6819
+ encodePresent(tail.reserve(1));
6820
+ }
6821
+ tail.flush();
6822
+ submissionCount += tail.getSubmissionCount();
6508
6823
  }
6509
- encodePresent(tail.reserve(1));
6510
- tail.flush();
6511
- submissionCount += tail.getSubmissionCount();
6512
- return submissionCount;
6824
+ return Object.freeze({
6825
+ submissionCount,
6826
+ slot
6827
+ });
6513
6828
  }
6514
6829
  async function readOutputProbe(optionsForProbe = {}) {
6515
6830
  const mapMode = constants.map;
@@ -6555,24 +6870,59 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
6555
6870
  const awaitGPUCompletion = renderOptions.awaitGPUCompletion !== false;
6556
6871
  const samplingPlan = resolveRenderedSamplesPerPixel(renderOptions, awaitGPUCompletion);
6557
6872
  const useThrottledHighSamplePath = awaitGPUCompletion && samplingPlan.renderedSamplesPerPixel >= 8;
6558
- const submittedWorkTimeoutMs = estimateSubmittedGpuWorkTimeoutMs(
6559
- { ...config, renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel },
6560
- tiles.length,
6561
- renderOptions.submittedWorkTimeoutMs
6562
- );
6563
6873
  const frameStartTimeMs = nowMs();
6564
- const submissionWaitOptions = awaitGPUCompletion ? { timeoutMs: submittedWorkTimeoutMs, allowTimeout: false } : { timeoutMs: submittedWorkTimeoutMs };
6565
6874
  let frameStats;
6566
6875
  if (useThrottledHighSamplePath) {
6567
6876
  frame += 1;
6568
6877
  const frameIndex = frame + config.frameIndex;
6569
6878
  const parallelismCounters = createGpuParallelismCounters();
6570
6879
  const accelerationBuildSubmitted = dispatchGpuAccelerationBuild(frameIndex, parallelismCounters);
6571
- const frameSubmissionCount = dispatchFrameAwaitingGpu(
6572
- frameIndex,
6573
- parallelismCounters,
6574
- samplingPlan.renderedSamplesPerPixel
6575
- );
6880
+ let frameSubmissionCount = 0;
6881
+ let frameConfigSlot = 0;
6882
+ if (accelerationBuildSubmitted) {
6883
+ const accelerationWaitOptions = {
6884
+ ...estimateSubmittedGpuWorkTiming(
6885
+ { ...config, renderedSamplesPerPixel: 1 },
6886
+ 1,
6887
+ renderOptions.submittedWorkTimeoutMs,
6888
+ { includeAccelerationBuild: true }
6889
+ ),
6890
+ allowTimeout: false
6891
+ };
6892
+ await waitForSubmittedGpuWork(accelerationWaitOptions);
6893
+ }
6894
+ for (let tileIndex = 0; tileIndex < tiles.length; tileIndex += 1) {
6895
+ const tileRangeDispatch = dispatchFrameAwaitingGpu(
6896
+ frameIndex,
6897
+ parallelismCounters,
6898
+ samplingPlan.renderedSamplesPerPixel,
6899
+ {
6900
+ sampleRangeStart: 0,
6901
+ sampleRangeEnd: samplingPlan.renderedSamplesPerPixel,
6902
+ tileStartIndex: tileIndex,
6903
+ tileEndIndex: tileIndex + 1,
6904
+ startingSubmissionCount: frameSubmissionCount,
6905
+ startingSlot: frameConfigSlot,
6906
+ includeDenoise: tileIndex + 1 >= tiles.length,
6907
+ includePresent: tileIndex + 1 >= tiles.length
6908
+ }
6909
+ );
6910
+ frameSubmissionCount = tileRangeDispatch.submissionCount;
6911
+ frameConfigSlot = tileRangeDispatch.slot;
6912
+ const tileWaitOptions = {
6913
+ ...estimateSubmittedGpuWorkTiming(
6914
+ { ...config, renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel },
6915
+ 1,
6916
+ renderOptions.submittedWorkTimeoutMs,
6917
+ {
6918
+ includeDenoise: tileIndex + 1 >= tiles.length && config.denoise,
6919
+ includePresent: tileIndex + 1 >= tiles.length
6920
+ }
6921
+ ),
6922
+ allowTimeout: false
6923
+ };
6924
+ await waitForSubmittedGpuWork(tileWaitOptions);
6925
+ }
6576
6926
  frameStats = createFrameStats({
6577
6927
  frameIndex,
6578
6928
  accelerationBuildSubmitted,
@@ -6584,10 +6934,24 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
6584
6934
  budgetConstrained: samplingPlan.budgetConstrained
6585
6935
  });
6586
6936
  } else {
6937
+ const submittedWorkTiming = estimateSubmittedGpuWorkTiming(
6938
+ { ...config, renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel },
6939
+ tiles.length,
6940
+ renderOptions.submittedWorkTimeoutMs,
6941
+ { includeAccelerationBuild: config.gpuAccelerationBuildRequired && !accelerationBuilt }
6942
+ );
6943
+ const submissionWaitOptions = awaitGPUCompletion ? {
6944
+ timeoutMs: submittedWorkTiming.timeoutMs,
6945
+ maxWaitMs: submittedWorkTiming.maxWaitMs,
6946
+ allowTimeout: false
6947
+ } : {
6948
+ timeoutMs: submittedWorkTiming.timeoutMs,
6949
+ maxWaitMs: submittedWorkTiming.maxWaitMs
6950
+ };
6587
6951
  frameStats = renderOnce(renderOptions, samplingPlan);
6588
- }
6589
- if (awaitGPUCompletion) {
6590
- await waitForSubmittedGpuWork(submissionWaitOptions);
6952
+ if (awaitGPUCompletion) {
6953
+ await waitForSubmittedGpuWork(submissionWaitOptions);
6954
+ }
6591
6955
  }
6592
6956
  const frameTimeMs = Math.max(0, nowMs() - frameStartTimeMs);
6593
6957
  if (awaitGPUCompletion) {
@@ -6642,10 +7006,22 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
6642
7006
  ...overrides
6643
7007
  });
6644
7008
  }
7009
+ function rebuildMediumResources(nextConfig) {
7010
+ const previousMediumTextureResource = mediumTextureResource;
7011
+ mediumTextureResource = createMediumTextureResource(device, constants, nextConfig.mediums);
7012
+ bindGroups = createTraceBindGroups();
7013
+ if (previousMediumTextureResource?.ownsTexture) {
7014
+ previousMediumTextureResource.texture?.destroy?.();
7015
+ }
7016
+ }
6645
7017
  function updateSceneObjects(sceneObjects) {
6646
7018
  const nextPackedScene = packWavefrontSceneObjects(sceneObjects, config.sceneObjectCapacity);
6647
7019
  packedScene = nextPackedScene;
6648
- config = rebuildLiveConfig();
7020
+ const nextConfig = rebuildLiveConfig();
7021
+ if (!mediumTablesEqual(config.mediums, nextConfig.mediums)) {
7022
+ rebuildMediumResources(nextConfig);
7023
+ }
7024
+ config = nextConfig;
6649
7025
  device.queue.writeBuffer(sceneObjectBuffer, 0, packedScene.buffer);
6650
7026
  return config;
6651
7027
  }
@@ -6669,6 +7045,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
6669
7045
  emissiveTriangleCount: config.emissiveTriangleCount,
6670
7046
  environmentPortalCount: config.environmentPortalCount,
6671
7047
  environmentPortalMode: config.environmentPortalMode,
7048
+ mediumCount: config.mediumCount,
6672
7049
  environmentMap: createEnvironmentMapSnapshot(config.environmentMap),
6673
7050
  deferredPathResolve: config.deferredPathResolve,
6674
7051
  bvhNodeCount: config.bvhNodeCount,
@@ -6709,6 +7086,9 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
6709
7086
  if (environmentSamplingResource.ownsTexture) {
6710
7087
  environmentSamplingResource.texture?.destroy?.();
6711
7088
  }
7089
+ if (mediumTextureResource.ownsTexture) {
7090
+ mediumTextureResource.texture?.destroy?.();
7091
+ }
6712
7092
  brdfLutResource.texture?.destroy?.();
6713
7093
  if (baseColorAtlasResource.ownsTexture) {
6714
7094
  baseColorAtlasResource.texture?.destroy?.();