@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.
@@ -13,17 +13,19 @@ const DEFAULT_MAX_DEPTH = 6;
13
13
  const DEFAULT_TILE_SIZE = 128;
14
14
  const DEFAULT_SAMPLES_PER_PIXEL = 1;
15
15
  const MAX_SAMPLES_PER_PIXEL = 256;
16
- const DEFAULT_BRDF_LUT_SIZE = 256;
16
+ const DEFAULT_BRDF_LUT_SIZE = 128;
17
+ const DEFAULT_BRDF_LUT_SAMPLE_COUNT = 256;
17
18
  const DEFAULT_MAX_FRAME_PASSES_PER_SUBMISSION = 256;
18
19
  const DEFAULT_SCENE_OBJECT_CAPACITY = 128;
19
20
  const DEFAULT_ENVIRONMENT_PORTAL_CAPACITY = 32;
21
+ const DEFAULT_MEDIUM_PHASE_MODEL = 0;
20
22
  const WORKGROUP_SIZE = 64;
21
23
  export const rendererWavefrontComputeMode = "webgpu-compute";
22
24
  export const rendererWavefrontComputeWorkgroupSize = WORKGROUP_SIZE;
23
25
  export const rendererWavefrontComputeStatsStride = 8;
24
26
  const RAY_RECORD_BYTES = 80;
25
27
  const HIT_RECORD_BYTES = 256;
26
- const SCENE_OBJECT_RECORD_BYTES = 144;
28
+ const SCENE_OBJECT_RECORD_BYTES = 160;
27
29
  const MESH_VERTEX_RECORD_BYTES = 48;
28
30
  const MESH_RANGE_RECORD_BYTES = 240;
29
31
  const TRIANGLE_RECORD_BYTES = 352;
@@ -32,11 +34,13 @@ const BVH_NODE_RECORD_BYTES = 48;
32
34
  const BVH_LEAF_REF_RECORD_BYTES = 16;
33
35
  const EMISSIVE_TRIANGLE_INDEX_BYTES = 4;
34
36
  const ENVIRONMENT_PORTAL_RECORD_BYTES = 96;
37
+ const MEDIUM_TABLE_ROWS = 2;
35
38
  const ACCUMULATION_RECORD_BYTES = 16;
36
39
  const PATH_VERTEX_RECORD_BYTES = 16;
37
40
  const GPU_SUBMITTED_WORK_TIMEOUT_MS = 5_000;
38
41
  const GPU_READBACK_COMPLETION_TIMEOUT_MS = 60_000;
39
42
  const GPU_MAX_SUBMITTED_WORK_TIMEOUT_MS = 60_000;
43
+ const GPU_MAX_SUBMITTED_WORK_DEADLINE_MS = 180_000;
40
44
  const CONFIG_BUFFER_BYTES = 320;
41
45
  const COUNTER_DISPATCH_ARGS_OFFSET = 16;
42
46
  const INDIRECT_DISPATCH_ARGS_BYTES = 12;
@@ -437,6 +441,183 @@ function deriveBounds(input) {
437
441
  return null;
438
442
  }
439
443
 
444
+ function deriveBeerLambertAbsorptionFromAttenuationColor(
445
+ attenuationColor,
446
+ attenuationDistance,
447
+ density = 1
448
+ ) {
449
+ const distance = Number(attenuationDistance);
450
+ const densityScale = Math.max(0, Number(density) || 0);
451
+ if (!Number.isFinite(distance) || distance <= 0 || densityScale <= 0) {
452
+ return [0, 0, 0];
453
+ }
454
+ return attenuationColor.slice(0, 3).map((channel) => {
455
+ const clamped = clamp(Number(channel) || 0, 0.0001, 1);
456
+ return Math.max(0, (-Math.log(clamped) / distance) * densityScale);
457
+ });
458
+ }
459
+
460
+ function readMediumPhaseModel(value) {
461
+ if (typeof value === "number" && Number.isFinite(value)) {
462
+ return Math.max(0, Math.trunc(value));
463
+ }
464
+ switch (String(value ?? "").trim().toLowerCase()) {
465
+ case "isotropic":
466
+ default:
467
+ return DEFAULT_MEDIUM_PHASE_MODEL;
468
+ }
469
+ }
470
+
471
+ function resolveWavefrontVolumeInput(input) {
472
+ return input?.volume ?? input?.material?.volume ?? null;
473
+ }
474
+
475
+ function normalizeWavefrontThickness(input, label) {
476
+ const volume = resolveWavefrontVolumeInput(input);
477
+ return Math.max(
478
+ 0,
479
+ readFiniteNumber(
480
+ label,
481
+ input?.thickness ?? volume?.thickness ?? input?.material?.thickness,
482
+ 0
483
+ )
484
+ );
485
+ }
486
+
487
+ function resolveWavefrontMediumId(input, fallbackId = 1) {
488
+ return (
489
+ input?.mediumRefId ??
490
+ input?.mediumId ??
491
+ input?.material?.mediumId ??
492
+ input?.materialRefId ??
493
+ input?.material?.id ??
494
+ input?.materialId ??
495
+ input?.id ??
496
+ fallbackId
497
+ );
498
+ }
499
+
500
+ function deriveWavefrontTransportMedium(input, fallbackId = 1) {
501
+ const resolvedId = resolveWavefrontMediumId(input, fallbackId);
502
+ if (input?.medium) {
503
+ return normalizeWavefrontMedium(
504
+ {
505
+ ...input.medium,
506
+ id: input.medium.id ?? input.medium.mediumId ?? resolvedId,
507
+ },
508
+ fallbackId
509
+ );
510
+ }
511
+ const volume = resolveWavefrontVolumeInput(input);
512
+ if (!volume) {
513
+ return null;
514
+ }
515
+ return normalizeWavefrontMedium(
516
+ {
517
+ id: resolvedId,
518
+ phaseModel: volume.phaseModel,
519
+ density: volume.density,
520
+ attenuationColor: volume.attenuationColor,
521
+ attenuationDistance: volume.attenuationDistance,
522
+ absorption: volume.absorption,
523
+ scattering: volume.scattering,
524
+ },
525
+ fallbackId
526
+ );
527
+ }
528
+
529
+ function normalizeWavefrontMedium(input = {}, index = 0) {
530
+ const id = readNonNegativeInteger("medium id", input.id ?? input.mediumId, index);
531
+ const density = Math.max(0, readFiniteNumber("medium density", input.density, 1));
532
+ const attenuationColor = asColor(
533
+ input.attenuationColor ?? input.color ?? input.medium?.attenuationColor,
534
+ [1, 1, 1, 1]
535
+ );
536
+ const attenuationDistance = readFiniteNumber(
537
+ "medium attenuationDistance",
538
+ input.attenuationDistance ?? input.distance ?? input.medium?.attenuationDistance,
539
+ 0
540
+ );
541
+ const absorption =
542
+ Array.isArray(input.absorption) || Array.isArray(input.medium?.absorption)
543
+ ? asVec3(input.absorption ?? input.medium?.absorption, [0, 0, 0]).map((value) =>
544
+ Math.max(0, Number(value) || 0)
545
+ )
546
+ : deriveBeerLambertAbsorptionFromAttenuationColor(
547
+ attenuationColor,
548
+ attenuationDistance,
549
+ density
550
+ );
551
+ const scattering = asVec3(
552
+ input.scattering ?? input.medium?.scattering,
553
+ [0, 0, 0]
554
+ ).map((value) => Math.max(0, Number(value) || 0));
555
+ return Object.freeze({
556
+ id,
557
+ phaseModel: readMediumPhaseModel(input.phaseModel ?? input.medium?.phaseModel),
558
+ density,
559
+ attenuationColor: Object.freeze(attenuationColor),
560
+ attenuationDistance,
561
+ absorption: Object.freeze(absorption),
562
+ scattering: Object.freeze(scattering),
563
+ });
564
+ }
565
+
566
+ function collectWavefrontMediums(options, meshes, sceneObjects = []) {
567
+ const mediumsById = new Map();
568
+ mediumsById.set(
569
+ 0,
570
+ Object.freeze({
571
+ id: 0,
572
+ phaseModel: DEFAULT_MEDIUM_PHASE_MODEL,
573
+ density: 0,
574
+ attenuationColor: Object.freeze([1, 1, 1, 1]),
575
+ attenuationDistance: 0,
576
+ absorption: Object.freeze([0, 0, 0]),
577
+ scattering: Object.freeze([0, 0, 0]),
578
+ })
579
+ );
580
+
581
+ const register = (input, fallbackId = mediumsById.size) => {
582
+ if (!input) {
583
+ return;
584
+ }
585
+ const normalized = normalizeWavefrontMedium(
586
+ typeof input === "object" ? { id: fallbackId, ...input } : { id: fallbackId },
587
+ fallbackId
588
+ );
589
+ const existing = mediumsById.get(normalized.id);
590
+ if (existing && JSON.stringify(existing) !== JSON.stringify(normalized)) {
591
+ throw new Error(`Medium id ${normalized.id} is defined more than once with different values.`);
592
+ }
593
+ mediumsById.set(normalized.id, normalized);
594
+ };
595
+
596
+ for (const medium of options.mediums ?? []) {
597
+ register(medium);
598
+ }
599
+ for (const mesh of meshes) {
600
+ register(mesh.medium, mesh.mediumRefId ?? mesh.medium?.id ?? 0);
601
+ }
602
+ for (const mesh of meshes) {
603
+ if ((mesh.mediumRefId ?? 0) > 0 && !mediumsById.has(mesh.mediumRefId)) {
604
+ register({ id: mesh.mediumRefId });
605
+ }
606
+ }
607
+ for (const object of sceneObjects) {
608
+ register(object.medium, object.mediumRefId ?? object.medium?.id ?? 0);
609
+ }
610
+ for (const object of sceneObjects) {
611
+ if ((object.mediumRefId ?? 0) > 0 && !mediumsById.has(object.mediumRefId)) {
612
+ register({ id: object.mediumRefId });
613
+ }
614
+ }
615
+
616
+ return Object.freeze(
617
+ Array.from(mediumsById.values()).sort((left, right) => left.id - right.id)
618
+ );
619
+ }
620
+
440
621
  export function normalizeWavefrontSceneObject(input = {}, index = 0) {
441
622
  const bounds = deriveBounds(input);
442
623
  const kind = readObjectKind(input.kind ?? input.type ?? (bounds ? "box" : "sphere"));
@@ -474,6 +655,7 @@ export function normalizeWavefrontSceneObject(input = {}, index = 0) {
474
655
  input.specularColor ?? input.material?.specularColor,
475
656
  [1, 1, 1, 1]
476
657
  ).map((value, componentIndex) => (componentIndex < 3 ? clamp(value, 0, 1) : 1));
658
+ const medium = deriveWavefrontTransportMedium(input, index + 1);
477
659
  const resolvedMaterialKind =
478
660
  emission[0] > 0 || emission[1] > 0 || emission[2] > 0
479
661
  ? MATERIAL_EMISSIVE
@@ -488,6 +670,12 @@ export function normalizeWavefrontSceneObject(input = {}, index = 0) {
488
670
  kind,
489
671
  materialKind: resolvedMaterialKind,
490
672
  flags: readNonNegativeInteger("flags", input.flags, 0),
673
+ mediumRefId: readNonNegativeInteger(
674
+ "mediumRefId",
675
+ input.mediumRefId ?? medium?.id ?? input.medium?.id ?? input.mediumId,
676
+ 0
677
+ ),
678
+ medium,
491
679
  center: Object.freeze(center),
492
680
  halfExtent: Object.freeze(halfExtent),
493
681
  color: Object.freeze(color),
@@ -511,6 +699,7 @@ export function normalizeWavefrontSceneObject(input = {}, index = 0) {
511
699
  ),
512
700
  specular: clamp(readFiniteNumber("specular", input.specular ?? input.material?.specular, 1), 0, 1),
513
701
  specularColor: Object.freeze(specularColor),
702
+ thickness: normalizeWavefrontThickness(input, "thickness"),
514
703
  transmission,
515
704
  });
516
705
  }
@@ -620,6 +809,7 @@ export function normalizeWavefrontMesh(input = {}, meshIndex = 0) {
620
809
  input.specularColor ?? input.material?.specularColor,
621
810
  [1, 1, 1, 1]
622
811
  ).map((value, componentIndex) => (componentIndex < 3 ? clamp(value, 0, 1) : 1));
812
+ const medium = deriveWavefrontTransportMedium(input, meshIndex + 1);
623
813
  const resolvedMaterialKind =
624
814
  emission[0] > 0 || emission[1] > 0 || emission[2] > 0
625
815
  ? MATERIAL_EMISSIVE
@@ -644,9 +834,14 @@ export function normalizeWavefrontMesh(input = {}, meshIndex = 0) {
644
834
  ),
645
835
  mediumRefId: readNonNegativeInteger(
646
836
  "mesh mediumRefId",
647
- input.mediumRefId ?? input.medium?.id ?? input.mediumId,
837
+ input.mediumRefId ??
838
+ medium?.id ??
839
+ input.medium?.id ??
840
+ input.mediumId ??
841
+ input.material?.mediumId,
648
842
  0
649
843
  ),
844
+ medium,
650
845
  color: Object.freeze(color),
651
846
  emission: Object.freeze(emission),
652
847
  roughness: clamp(readFiniteNumber("roughness", input.roughness ?? input.material?.roughness, 0.72), 0, 1),
@@ -668,6 +863,7 @@ export function normalizeWavefrontMesh(input = {}, meshIndex = 0) {
668
863
  ),
669
864
  specular: clamp(readFiniteNumber("specular", input.specular ?? input.material?.specular, 1), 0, 1),
670
865
  specularColor: Object.freeze(specularColor),
866
+ thickness: normalizeWavefrontThickness(input, "mesh thickness"),
671
867
  transmission,
672
868
  baseColorTexture: input.baseColorTexture ?? input.material?.baseColorTexture ?? null,
673
869
  metallicRoughnessTexture:
@@ -738,104 +934,9 @@ function normalizeVectorOrFallback(vector, fallback) {
738
934
  return normalize(Array.isArray(vector) ? vector : fallback, fallback);
739
935
  }
740
936
 
741
- function buildTriangleTangentBasis(v0, v1, v2, uv0, uv1, uv2, fallbackNormal) {
742
- const edge1 = subtract(v1, v0);
743
- const edge2 = subtract(v2, v0);
744
- const deltaUv1 = [uv1[0] - uv0[0], uv1[1] - uv0[1]];
745
- const deltaUv2 = [uv2[0] - uv0[0], uv2[1] - uv0[1]];
746
- const determinant = deltaUv1[0] * deltaUv2[1] - deltaUv1[1] * deltaUv2[0];
747
- if (Math.abs(determinant) < 1e-6) {
748
- const tangentFallback = Math.abs(fallbackNormal[1]) < 0.999 ? [0, 1, 0] : [1, 0, 0];
749
- const tangent = normalize(cross(tangentFallback, fallbackNormal), [1, 0, 0]);
750
- const bitangent = normalize(cross(fallbackNormal, tangent), [0, 0, 1]);
751
- return { tangent, bitangent };
752
- }
753
- const inverse = 1 / determinant;
754
- const tangent = normalize(
755
- [
756
- inverse * (edge1[0] * deltaUv2[1] - edge2[0] * deltaUv1[1]),
757
- inverse * (edge1[1] * deltaUv2[1] - edge2[1] * deltaUv1[1]),
758
- inverse * (edge1[2] * deltaUv2[1] - edge2[2] * deltaUv1[1]),
759
- ],
760
- [1, 0, 0]
761
- );
762
- const bitangent = normalize(
763
- [
764
- inverse * (-edge1[0] * deltaUv2[0] + edge2[0] * deltaUv1[0]),
765
- inverse * (-edge1[1] * deltaUv2[0] + edge2[1] * deltaUv1[0]),
766
- inverse * (-edge1[2] * deltaUv2[0] + edge2[2] * deltaUv1[0]),
767
- ],
768
- [0, 0, 1]
769
- );
770
- return { tangent, bitangent };
771
- }
772
-
773
- function applyNormalMap(normal, tangent, bitangent, normalTexture, uv) {
774
- if (!normalTexture) {
775
- return normalizeVectorOrFallback(normal, [0, 1, 0]);
776
- }
777
- const sample = sampleTextureRgba(normalTexture, uv, "linear");
778
- const strength = clampUnit(normalTexture.scale ?? 1);
779
- const tangentNormal = normalize(
780
- [
781
- (sample[0] * 2 - 1) * strength,
782
- (sample[1] * 2 - 1) * strength,
783
- 1 + (sample[2] * 2 - 1 - 1) * strength,
784
- ],
785
- [0, 0, 1]
786
- );
787
- return normalize(
788
- [
789
- tangent[0] * tangentNormal[0] + bitangent[0] * tangentNormal[1] + normal[0] * tangentNormal[2],
790
- tangent[1] * tangentNormal[0] + bitangent[1] * tangentNormal[1] + normal[1] * tangentNormal[2],
791
- tangent[2] * tangentNormal[0] + bitangent[2] * tangentNormal[1] + normal[2] * tangentNormal[2],
792
- ],
793
- normal
794
- );
795
- }
796
-
797
- function sampleBaseColor(mesh, uv) {
798
- const sample = mesh.baseColorTexture ? sampleTextureRgba(mesh.baseColorTexture, uv, "srgb") : [1, 1, 1, 1];
799
- return [
800
- clampUnit(mesh.color[0] * sample[0]),
801
- clampUnit(mesh.color[1] * sample[1]),
802
- clampUnit(mesh.color[2] * sample[2]),
803
- clampUnit((mesh.color[3] ?? 1) * sample[3]),
804
- ];
805
- }
806
-
807
- function sampleSurfaceMaterial(mesh, uv) {
808
- const textureSample = mesh.metallicRoughnessTexture
809
- ? sampleTextureRgba(mesh.metallicRoughnessTexture, uv, "linear")
810
- : [1, 1, 1, 1];
811
- return {
812
- roughness: clamp(mesh.roughness * textureSample[1], 0, 1),
813
- metallic: clamp(mesh.metallic * textureSample[2], 0, 1),
814
- };
815
- }
816
-
817
- function averageColors(colors) {
818
- const count = Math.max(colors.length, 1);
819
- return colors.reduce(
820
- (accumulator, color) => [
821
- accumulator[0] + color[0] / count,
822
- accumulator[1] + color[1] / count,
823
- accumulator[2] + color[2] / count,
824
- accumulator[3] + color[3] / count,
825
- ],
826
- [0, 0, 0, 0]
827
- );
828
- }
829
-
830
- function averageNumbers(values, fallback = 0) {
831
- if (!Array.isArray(values) || values.length === 0) {
832
- return fallback;
833
- }
834
- return values.reduce((sum, value) => sum + value, 0) / values.length;
835
- }
836
-
837
- function createMeshTriangleRecords(meshes) {
937
+ function createMeshTriangleRecords(meshes, gpuMaterialSource = null) {
838
938
  const source = Array.isArray(meshes) ? meshes : [];
939
+ const resolvedMaterialSource = gpuMaterialSource ?? createWavefrontGpuMaterialSource(source);
839
940
  let nextTriangleId = 0;
840
941
  return source.flatMap((meshInput, meshIndex) => {
841
942
  const mesh = normalizeWavefrontMesh(meshInput, meshIndex);
@@ -854,16 +955,6 @@ function createMeshTriangleRecords(meshes) {
854
955
  const uv0 = mesh.uvs ? readVector2(mesh.uvs, a) : [0, 0];
855
956
  const uv1 = mesh.uvs ? readVector2(mesh.uvs, b) : [0, 0];
856
957
  const uv2 = mesh.uvs ? readVector2(mesh.uvs, c) : [0, 0];
857
- const tangentBasis = buildTriangleTangentBasis(v0, v1, v2, uv0, uv1, uv2, faceNormal);
858
- const shadedN0 = applyNormalMap(n0, tangentBasis.tangent, tangentBasis.bitangent, mesh.normalTexture, uv0);
859
- const shadedN1 = applyNormalMap(n1, tangentBasis.tangent, tangentBasis.bitangent, mesh.normalTexture, uv1);
860
- const shadedN2 = applyNormalMap(n2, tangentBasis.tangent, tangentBasis.bitangent, mesh.normalTexture, uv2);
861
- const sampledColors = [sampleBaseColor(mesh, uv0), sampleBaseColor(mesh, uv1), sampleBaseColor(mesh, uv2)];
862
- const sampledMaterials = [
863
- sampleSurfaceMaterial(mesh, uv0),
864
- sampleSurfaceMaterial(mesh, uv1),
865
- sampleSurfaceMaterial(mesh, uv2),
866
- ];
867
958
  const bounds = triangleBounds(v0, v1, v2);
868
959
 
869
960
  triangles.push(
@@ -878,17 +969,17 @@ function createMeshTriangleRecords(meshes) {
878
969
  v0: Object.freeze(v0),
879
970
  v1: Object.freeze(v1),
880
971
  v2: Object.freeze(v2),
881
- n0: Object.freeze(shadedN0),
882
- n1: Object.freeze(shadedN1),
883
- n2: Object.freeze(shadedN2),
972
+ n0: Object.freeze(n0),
973
+ n1: Object.freeze(n1),
974
+ n2: Object.freeze(n2),
884
975
  uv0: Object.freeze(uv0),
885
976
  uv1: Object.freeze(uv1),
886
977
  uv2: Object.freeze(uv2),
887
- color: Object.freeze(averageColors(sampledColors)),
978
+ color: mesh.color,
888
979
  emission: mesh.emission,
889
980
  material: Object.freeze([
890
- averageNumbers(sampledMaterials.map((sample) => sample.roughness), mesh.roughness),
891
- averageNumbers(sampledMaterials.map((sample) => sample.metallic), mesh.metallic),
981
+ mesh.roughness,
982
+ mesh.metallic,
892
983
  mesh.opacity,
893
984
  mesh.ior,
894
985
  ]),
@@ -902,7 +993,7 @@ function createMeshTriangleRecords(meshes) {
902
993
  mesh.clearcoatRoughness,
903
994
  mesh.specular,
904
995
  mesh.transmission,
905
- 0,
996
+ mesh.thickness,
906
997
  ]),
907
998
  specularColor: Object.freeze([
908
999
  mesh.specularColor[0] ?? 1,
@@ -910,6 +1001,27 @@ function createMeshTriangleRecords(meshes) {
910
1001
  mesh.specularColor[2] ?? 1,
911
1002
  1,
912
1003
  ]),
1004
+ baseColorAtlas: Object.freeze(
1005
+ resolvedMaterialSource.baseColorAtlas.resolveRect(mesh.baseColorTexture)
1006
+ ),
1007
+ metallicRoughnessAtlas: Object.freeze(
1008
+ resolvedMaterialSource.metallicRoughnessAtlas.resolveRect(mesh.metallicRoughnessTexture)
1009
+ ),
1010
+ normalAtlas: Object.freeze(
1011
+ resolvedMaterialSource.normalAtlas.resolveRect(mesh.normalTexture)
1012
+ ),
1013
+ occlusionAtlas: Object.freeze(
1014
+ resolvedMaterialSource.occlusionAtlas.resolveRect(mesh.occlusionTexture)
1015
+ ),
1016
+ emissiveAtlas: Object.freeze(
1017
+ resolvedMaterialSource.emissiveAtlas.resolveRect(mesh.emissiveTexture)
1018
+ ),
1019
+ textureSettings: Object.freeze([
1020
+ clampUnit(mesh.normalTexture?.scale ?? mesh.normalTexture?.strength ?? 1),
1021
+ clampUnit(mesh.occlusionTexture?.strength ?? 1),
1022
+ clampUnit(mesh.emissiveTexture?.strength ?? 1),
1023
+ 0,
1024
+ ]),
913
1025
  bounds: Object.freeze({
914
1026
  min: Object.freeze(bounds.min),
915
1027
  max: Object.freeze(bounds.max),
@@ -992,9 +1104,10 @@ function buildBvh(triangles, maxLeafTriangles = 4) {
992
1104
  });
993
1105
  }
994
1106
 
995
- export function createWavefrontMeshAcceleration(meshes = []) {
1107
+ export function createWavefrontMeshAcceleration(meshes = [], gpuMaterialSource = null) {
996
1108
  const source = Array.isArray(meshes) ? meshes : [meshes];
997
- const triangles = createMeshTriangleRecords(source);
1109
+ const resolvedMaterialSource = gpuMaterialSource ?? createWavefrontGpuMaterialSource(source);
1110
+ const triangles = createMeshTriangleRecords(source, resolvedMaterialSource);
998
1111
  return buildBvh(triangles);
999
1112
  }
1000
1113
 
@@ -1231,7 +1344,7 @@ export function createWavefrontGpuMaterialSource(meshes = []) {
1231
1344
  mesh.clearcoatRoughness,
1232
1345
  mesh.specular,
1233
1346
  mesh.transmission,
1234
- 0,
1347
+ mesh.thickness,
1235
1348
  ]);
1236
1349
  writeVec4(floatView, byteOffset + 80, [
1237
1350
  mesh.specularColor[0] ?? 1,
@@ -1329,12 +1442,13 @@ export function createWavefrontBvhBuildLevels(triangleCountInput) {
1329
1442
  }
1330
1443
 
1331
1444
  function resolveAccelerationBuildMode(options = {}) {
1332
- const mode = options.accelerationBuildMode ?? (options.displayQuality === true ? "gpu" : "cpu-debug");
1333
- if (mode !== "gpu" && mode !== "cpu-debug") {
1334
- throw new Error("accelerationBuildMode must be either \"gpu\" or \"cpu-debug\".");
1335
- }
1336
- if (options.displayQuality === true && mode !== "gpu") {
1337
- throw new Error("Display-quality path tracing requires GPU-built mesh acceleration.");
1445
+ const requestedMode =
1446
+ options.accelerationBuildMode ?? (options.displayQuality === true ? "cpu-upload" : "cpu-debug");
1447
+ const mode = requestedMode === "cpu-debug" ? "cpu-upload" : requestedMode;
1448
+ if (mode !== "gpu" && mode !== "cpu-upload") {
1449
+ throw new Error(
1450
+ "accelerationBuildMode must be either \"gpu\", \"cpu-upload\", or the legacy alias \"cpu-debug\"."
1451
+ );
1338
1452
  }
1339
1453
  return mode;
1340
1454
  }
@@ -1419,7 +1533,7 @@ export function createWavefrontGpuMeshSource(meshes = [], gpuMaterialSourceInput
1419
1533
  mesh.clearcoatRoughness,
1420
1534
  mesh.specular,
1421
1535
  mesh.transmission,
1422
- 0,
1536
+ mesh.thickness,
1423
1537
  ]);
1424
1538
  writeVec4(meshFloats, floatOffset * 4 + 128, [
1425
1539
  mesh.specularColor[0] ?? 1,
@@ -1534,12 +1648,17 @@ function normalizeSceneObjects(sceneObjects, useDefaultScene = true) {
1534
1648
  return source.map((object, index) => normalizeWavefrontSceneObject(object, index));
1535
1649
  }
1536
1650
 
1651
+ function normalizeWavefrontMeshes(meshes) {
1652
+ const source = Array.isArray(meshes) ? meshes : [];
1653
+ return source.map((mesh, index) => normalizeWavefrontMesh(mesh, index));
1654
+ }
1655
+
1537
1656
  function normalizeMeshes(options = {}) {
1538
1657
  if (Array.isArray(options.meshes)) {
1539
- return options.meshes;
1658
+ return normalizeWavefrontMeshes(options.meshes);
1540
1659
  }
1541
1660
  if (options.mesh) {
1542
- return [options.mesh];
1661
+ return normalizeWavefrontMeshes([options.mesh]);
1543
1662
  }
1544
1663
  return [];
1545
1664
  }
@@ -1881,8 +2000,8 @@ export function createWavefrontPathTracingComputeConfig(options = {}) {
1881
2000
  ? createWavefrontGpuMeshSource(meshes, gpuMaterialSource)
1882
2001
  : createWavefrontGpuMeshSource([]);
1883
2002
  const meshAcceleration =
1884
- accelerationBuildMode === "cpu-debug"
1885
- ? createWavefrontMeshAcceleration(meshes)
2003
+ accelerationBuildMode === "cpu-upload"
2004
+ ? createWavefrontMeshAcceleration(meshes, gpuMaterialSource)
1886
2005
  : Object.freeze({ nodes: Object.freeze([]), triangles: Object.freeze([]) });
1887
2006
  const emissiveTriangleIndices = createWavefrontEmissiveTriangleIndexSource(
1888
2007
  meshes,
@@ -1899,6 +2018,7 @@ export function createWavefrontPathTracingComputeConfig(options = {}) {
1899
2018
  const sceneObjects = Object.freeze(
1900
2019
  normalizeSceneObjects(options.sceneObjects, meshes.length === 0)
1901
2020
  );
2021
+ const mediums = collectWavefrontMediums(options, meshes, sceneObjects);
1902
2022
  const sceneObjectCapacity = Math.max(
1903
2023
  sceneObjects.length,
1904
2024
  readPositiveInteger("sceneObjectCapacity", options.sceneObjectCapacity, DEFAULT_SCENE_OBJECT_CAPACITY)
@@ -1971,6 +2091,8 @@ export function createWavefrontPathTracingComputeConfig(options = {}) {
1971
2091
  sceneObjects,
1972
2092
  sceneObjectCount: sceneObjects.length,
1973
2093
  sceneObjectCapacity,
2094
+ mediums,
2095
+ mediumCount: mediums.length,
1974
2096
  accelerationBuildMode,
1975
2097
  gpuAccelerationBuildRequired: accelerationBuildMode === "gpu" && triangleCount > 0,
1976
2098
  gpuMeshSource,
@@ -2089,29 +2211,30 @@ export function packWavefrontSceneObjects(sceneObjects, capacity = sceneObjects.
2089
2211
  uintView[u32 + 1] = object.id;
2090
2212
  uintView[u32 + 2] = object.materialKind;
2091
2213
  uintView[u32 + 3] = object.flags;
2092
- writeVec4(floatView, byteOffset + 16, [...object.center, 0]);
2093
- writeVec4(floatView, byteOffset + 32, [...object.halfExtent, 0]);
2094
- writeVec4(floatView, byteOffset + 48, object.color);
2095
- writeVec4(floatView, byteOffset + 64, object.emission);
2096
- writeVec4(floatView, byteOffset + 80, [
2214
+ uintView[u32 + 4] = object.mediumRefId;
2215
+ writeVec4(floatView, byteOffset + 32, [...object.center, 0]);
2216
+ writeVec4(floatView, byteOffset + 48, [...object.halfExtent, 0]);
2217
+ writeVec4(floatView, byteOffset + 64, object.color);
2218
+ writeVec4(floatView, byteOffset + 80, object.emission);
2219
+ writeVec4(floatView, byteOffset + 96, [
2097
2220
  object.roughness,
2098
2221
  object.metallic,
2099
2222
  object.opacity,
2100
2223
  object.ior,
2101
2224
  ]);
2102
- writeVec4(floatView, byteOffset + 96, [
2225
+ writeVec4(floatView, byteOffset + 112, [
2103
2226
  object.sheenColor[0] ?? 0,
2104
2227
  object.sheenColor[1] ?? 0,
2105
2228
  object.sheenColor[2] ?? 0,
2106
2229
  object.clearcoat,
2107
2230
  ]);
2108
- writeVec4(floatView, byteOffset + 112, [
2231
+ writeVec4(floatView, byteOffset + 128, [
2109
2232
  object.clearcoatRoughness,
2110
2233
  object.specular,
2111
2234
  object.transmission,
2112
- 0,
2235
+ object.thickness,
2113
2236
  ]);
2114
- writeVec4(floatView, byteOffset + 128, [
2237
+ writeVec4(floatView, byteOffset + 144, [
2115
2238
  object.specularColor[0] ?? 1,
2116
2239
  object.specularColor[1] ?? 1,
2117
2240
  object.specularColor[2] ?? 1,
@@ -2710,7 +2833,10 @@ function integrateBrdfSample(nDotV, roughness, sampleCount) {
2710
2833
  return [scaleTerm / sampleCount, biasTerm / sampleCount];
2711
2834
  }
2712
2835
 
2713
- function createBrdfLutUploadBytes(size = DEFAULT_BRDF_LUT_SIZE, sampleCount = 1024) {
2836
+ function createBrdfLutUploadBytes(
2837
+ size = DEFAULT_BRDF_LUT_SIZE,
2838
+ sampleCount = DEFAULT_BRDF_LUT_SAMPLE_COUNT
2839
+ ) {
2714
2840
  const cacheKey = `${Math.max(1, Math.trunc(size))}:${Math.max(1, Math.trunc(sampleCount))}`;
2715
2841
  const cached = BRDF_LUT_UPLOAD_CACHE.get(cacheKey);
2716
2842
  if (cached) {
@@ -3137,6 +3263,85 @@ function createBrdfLutResource(device, constants, size = DEFAULT_BRDF_LUT_SIZE)
3137
3263
  });
3138
3264
  }
3139
3265
 
3266
+ function createMediumTextureResource(device, constants, mediums) {
3267
+ const normalized = Array.isArray(mediums) && mediums.length > 0 ? mediums : [{ id: 0 }];
3268
+ const width = Math.max(
3269
+ 1,
3270
+ normalized.reduce((maximum, medium) => Math.max(maximum, medium.id ?? 0), 0) + 1
3271
+ );
3272
+ const level = {
3273
+ width,
3274
+ height: MEDIUM_TABLE_ROWS,
3275
+ data: new Float32Array(width * MEDIUM_TABLE_ROWS * 4),
3276
+ };
3277
+
3278
+ for (const medium of normalized) {
3279
+ const mediumId = Math.max(0, Math.trunc(Number(medium.id) || 0));
3280
+ const absorptionOffset = mediumId * 4;
3281
+ level.data[absorptionOffset] = Math.max(0, medium.absorption?.[0] ?? 0);
3282
+ level.data[absorptionOffset + 1] = Math.max(0, medium.absorption?.[1] ?? 0);
3283
+ level.data[absorptionOffset + 2] = Math.max(0, medium.absorption?.[2] ?? 0);
3284
+ level.data[absorptionOffset + 3] = Math.max(0, medium.phaseModel ?? 0);
3285
+
3286
+ const scatteringOffset = (width + mediumId) * 4;
3287
+ level.data[scatteringOffset] = Math.max(0, medium.scattering?.[0] ?? 0);
3288
+ level.data[scatteringOffset + 1] = Math.max(0, medium.scattering?.[1] ?? 0);
3289
+ level.data[scatteringOffset + 2] = Math.max(0, medium.scattering?.[2] ?? 0);
3290
+ level.data[scatteringOffset + 3] = Math.max(0, medium.density ?? 0);
3291
+ }
3292
+
3293
+ const upload = createFloat16RgbaUploadFromLevels([level])[0];
3294
+ const texture = device.createTexture({
3295
+ label: "plasius.wavefront.mediumTable",
3296
+ size: { width, height: MEDIUM_TABLE_ROWS },
3297
+ format: "rgba16float",
3298
+ usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST,
3299
+ });
3300
+ device.queue.writeTexture(
3301
+ { texture },
3302
+ upload.bytes,
3303
+ { bytesPerRow: upload.bytesPerRow, rowsPerImage: upload.height },
3304
+ { width, height: MEDIUM_TABLE_ROWS, depthOrArrayLayers: 1 }
3305
+ );
3306
+ return Object.freeze({
3307
+ texture,
3308
+ view: texture.createView(),
3309
+ ownsTexture: true,
3310
+ count: normalized.length,
3311
+ width,
3312
+ });
3313
+ }
3314
+
3315
+ function mediumTablesEqual(left, right) {
3316
+ const leftMediums = Array.isArray(left) ? left : [];
3317
+ const rightMediums = Array.isArray(right) ? right : [];
3318
+ if (leftMediums.length !== rightMediums.length) {
3319
+ return false;
3320
+ }
3321
+ for (let index = 0; index < leftMediums.length; index += 1) {
3322
+ const leftMedium = leftMediums[index];
3323
+ const rightMedium = rightMediums[index];
3324
+ if ((leftMedium?.id ?? 0) !== (rightMedium?.id ?? 0)) {
3325
+ return false;
3326
+ }
3327
+ if ((leftMedium?.phaseModel ?? 0) !== (rightMedium?.phaseModel ?? 0)) {
3328
+ return false;
3329
+ }
3330
+ if ((leftMedium?.density ?? 0) !== (rightMedium?.density ?? 0)) {
3331
+ return false;
3332
+ }
3333
+ for (let component = 0; component < 3; component += 1) {
3334
+ if ((leftMedium?.absorption?.[component] ?? 0) !== (rightMedium?.absorption?.[component] ?? 0)) {
3335
+ return false;
3336
+ }
3337
+ if ((leftMedium?.scattering?.[component] ?? 0) !== (rightMedium?.scattering?.[component] ?? 0)) {
3338
+ return false;
3339
+ }
3340
+ }
3341
+ }
3342
+ return true;
3343
+ }
3344
+
3140
3345
  function createAtlasTextureResource(device, constants, atlas, label) {
3141
3346
  const upload = createRgba8TextureUpload(atlas);
3142
3347
  const texture = device.createTexture({
@@ -3283,6 +3488,10 @@ struct SceneObject {
3283
3488
  objectId: u32,
3284
3489
  materialKind: u32,
3285
3490
  flags: u32,
3491
+ mediumRefId: u32,
3492
+ pad0: u32,
3493
+ pad1: u32,
3494
+ pad2: u32,
3286
3495
  center: vec4<f32>,
3287
3496
  halfExtent: vec4<f32>,
3288
3497
  color: vec4<f32>,
@@ -3343,9 +3552,9 @@ struct BvhLeafRef {
3343
3552
  struct ScatterResult {
3344
3553
  direction: vec4<f32>,
3345
3554
  pdf: f32,
3555
+ mediumRefId: u32,
3346
3556
  flags: u32,
3347
3557
  pad0: u32,
3348
- pad1: u32,
3349
3558
  };
3350
3559
 
3351
3560
  struct MeshVertex {
@@ -3491,6 +3700,7 @@ struct EnvironmentPortal {
3491
3700
  @group(0) @binding(29) var brdfLutTexture: texture_2d<f32>;
3492
3701
  @group(0) @binding(30) var brdfLutSampler: sampler;
3493
3702
  @group(0) @binding(31) var environmentSamplingTexture: texture_2d<f32>;
3703
+ @group(0) @binding(32) var mediumTableTexture: texture_2d<f32>;
3494
3704
 
3495
3705
  fn hash_u32(value: u32) -> u32 {
3496
3706
  var x = value;
@@ -4156,6 +4366,60 @@ fn gated_environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f
4156
4366
  return environment_radiance(origin, direction);
4157
4367
  }
4158
4368
 
4369
+ fn medium_dimensions() -> vec2<u32> {
4370
+ return textureDimensions(mediumTableTexture);
4371
+ }
4372
+
4373
+ fn medium_valid(mediumRefId: u32) -> bool {
4374
+ let dimensions = medium_dimensions();
4375
+ return mediumRefId > 0u && mediumRefId < dimensions.x;
4376
+ }
4377
+
4378
+ fn medium_absorption(mediumRefId: u32) -> vec3<f32> {
4379
+ if (!medium_valid(mediumRefId)) {
4380
+ return vec3<f32>(0.0);
4381
+ }
4382
+ return max(
4383
+ textureLoad(mediumTableTexture, vec2<i32>(i32(mediumRefId), 0), 0).xyz,
4384
+ vec3<f32>(0.0)
4385
+ );
4386
+ }
4387
+
4388
+ fn medium_scattering(mediumRefId: u32) -> vec3<f32> {
4389
+ if (!medium_valid(mediumRefId)) {
4390
+ return vec3<f32>(0.0);
4391
+ }
4392
+ return max(
4393
+ textureLoad(mediumTableTexture, vec2<i32>(i32(mediumRefId), 1), 0).xyz,
4394
+ vec3<f32>(0.0)
4395
+ );
4396
+ }
4397
+
4398
+ fn medium_transmittance(mediumRefId: u32, distance: f32) -> vec3<f32> {
4399
+ if (!medium_valid(mediumRefId) || distance <= 0.000001) {
4400
+ return vec3<f32>(1.0);
4401
+ }
4402
+ let extinction = medium_absorption(mediumRefId) + medium_scattering(mediumRefId);
4403
+ return vec3<f32>(
4404
+ exp(-extinction.x * distance),
4405
+ exp(-extinction.y * distance),
4406
+ exp(-extinction.z * distance)
4407
+ );
4408
+ }
4409
+
4410
+ fn transmitted_medium_ref_id(ray: RayRecord, hit: HitRecord) -> u32 {
4411
+ if (hit.mediumRefId == 0u) {
4412
+ return ray.mediumRefId;
4413
+ }
4414
+ if (hit.frontFace == 1u) {
4415
+ return hit.mediumRefId;
4416
+ }
4417
+ if (ray.mediumRefId == hit.mediumRefId) {
4418
+ return 0u;
4419
+ }
4420
+ return ray.mediumRefId;
4421
+ }
4422
+
4159
4423
  fn surface_path_response(hit: HitRecord) -> vec3<f32> {
4160
4424
  let color = clamp(hit.color.xyz, vec3<f32>(0.0), vec3<f32>(1.0));
4161
4425
  let opacity = clamp(hit.material.z, 0.0, 1.0);
@@ -4254,11 +4518,15 @@ fn terminal_surface_environment_source(ray: RayRecord, hit: HitRecord) -> vec3<f
4254
4518
  return clamp_sample_radiance(environmentFloor * materialFloor);
4255
4519
  }
4256
4520
 
4257
- fn terminal_surface_environment_contribution(ray: RayRecord, hit: HitRecord) -> vec3<f32> {
4521
+ fn terminal_surface_environment_contribution(
4522
+ ray: RayRecord,
4523
+ throughput: vec3<f32>,
4524
+ hit: HitRecord
4525
+ ) -> vec3<f32> {
4258
4526
  let surfaceColor = max(hit.color.xyz, config.ambientColor.xyz);
4259
4527
  let occlusion = mix(0.75, 1.0, clamp(hit.occlusion, 0.0, 1.0));
4260
4528
  return clamp_sample_radiance(
4261
- ray.throughput.xyz *
4529
+ throughput *
4262
4530
  surfaceColor *
4263
4531
  terminal_surface_environment_source(ray, hit) *
4264
4532
  occlusion
@@ -4732,7 +5000,7 @@ fn intersect_sphere(ray: RayRecord, object: SceneObject) -> Candidate {
4732
5000
  0xffffffffu,
4733
5001
  object.objectId,
4734
5002
  object.objectId,
4735
- 0u
5003
+ object.mediumRefId
4736
5004
  );
4737
5005
  }
4738
5006
 
@@ -4784,7 +5052,7 @@ fn intersect_box(ray: RayRecord, object: SceneObject) -> Candidate {
4784
5052
  0xffffffffu,
4785
5053
  object.objectId,
4786
5054
  object.objectId,
4787
- 0u
5055
+ object.mediumRefId
4788
5056
  );
4789
5057
  }
4790
5058
 
@@ -5035,6 +5303,10 @@ fn intersectActiveQueue(@builtin(global_invocation_id) globalId: vec3<u32>) {
5035
5303
  let ray = activeQueue[index];
5036
5304
  var nearest = 1000000.0;
5037
5305
  var hitObject = SceneObject(
5306
+ 0u,
5307
+ 0u,
5308
+ 0u,
5309
+ 0u,
5038
5310
  0u,
5039
5311
  0u,
5040
5312
  0u,
@@ -5238,9 +5510,9 @@ fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult
5238
5510
  return ScatterResult(
5239
5511
  vec4<f32>(reflect(ray.direction.xyz, normal), 0.0),
5240
5512
  1.0,
5513
+ ray.mediumRefId,
5241
5514
  RAY_FLAG_DELTA_SAMPLE,
5242
5515
  0u,
5243
- 0u
5244
5516
  );
5245
5517
  }
5246
5518
 
@@ -5260,17 +5532,17 @@ fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult
5260
5532
  return ScatterResult(
5261
5533
  vec4<f32>(reflect(ray.direction.xyz, normal), 0.0),
5262
5534
  1.0,
5535
+ ray.mediumRefId,
5263
5536
  RAY_FLAG_DELTA_SAMPLE,
5264
5537
  0u,
5265
- 0u
5266
5538
  );
5267
5539
  }
5268
5540
  return ScatterResult(
5269
5541
  vec4<f32>(refract_direction(ray.direction.xyz, normal, etaRatio), 0.0),
5270
5542
  1.0,
5543
+ transmitted_medium_ref_id(ray, hit),
5271
5544
  RAY_FLAG_DELTA_SAMPLE,
5272
5545
  0u,
5273
- 0u
5274
5546
  );
5275
5547
  }
5276
5548
 
@@ -5285,9 +5557,9 @@ fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult
5285
5557
  return ScatterResult(
5286
5558
  vec4<f32>(guidedDirection, 0.0),
5287
5559
  guidedPdf,
5560
+ ray.mediumRefId,
5288
5561
  RAY_FLAG_GUIDED_EMISSIVE,
5289
5562
  0u,
5290
- 0u
5291
5563
  );
5292
5564
  }
5293
5565
  }
@@ -5295,7 +5567,7 @@ fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult
5295
5567
  let guidedDirection = sample_environment_portal_direction(hit, seed + 131u, normal);
5296
5568
  if (dot(normal, guidedDirection) > 0.000001) {
5297
5569
  let guidedPdf = max(evaluate_surface_bsdf_pdf(hit, viewDirection, guidedDirection), 0.000001);
5298
- return ScatterResult(vec4<f32>(guidedDirection, 0.0), guidedPdf, 0u, 0u, 0u);
5570
+ return ScatterResult(vec4<f32>(guidedDirection, 0.0), guidedPdf, ray.mediumRefId, 0u, 0u);
5299
5571
  }
5300
5572
  }
5301
5573
 
@@ -5329,7 +5601,7 @@ fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult
5329
5601
  );
5330
5602
  }
5331
5603
  let pdf = max(evaluate_surface_bsdf_pdf(hit, viewDirection, lightDirection), 0.000001);
5332
- return ScatterResult(vec4<f32>(lightDirection, 0.0), pdf, 0u, 0u, 0u);
5604
+ return ScatterResult(vec4<f32>(lightDirection, 0.0), pdf, ray.mediumRefId, 0u, 0u);
5333
5605
  }
5334
5606
 
5335
5607
  @compute @workgroup_size(64)
@@ -5342,15 +5614,17 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
5342
5614
 
5343
5615
  let ray = activeQueue[index];
5344
5616
  let hit = hits[index];
5617
+ let segmentTransmittance = medium_transmittance(ray.mediumRefId, hit.distance);
5618
+ let arrivingThroughput = ray.throughput.xyz * segmentTransmittance;
5345
5619
  var contribution = vec3<f32>(0.0);
5346
5620
 
5347
5621
  if (hit.hitType == 1u) {
5348
5622
  let guidedLightWeight = select(1.0, 0.24, (ray.flags & RAY_FLAG_GUIDED_EMISSIVE) != 0u);
5349
5623
  let sourceRadiance = max(hit.emission.xyz, hit.color.xyz) * guidedLightWeight;
5350
5624
  if (deferred_path_resolve_enabled()) {
5351
- record_deferred_terminal_source(ray, sourceRadiance);
5625
+ record_deferred_terminal_source(ray, sourceRadiance * segmentTransmittance);
5352
5626
  } else {
5353
- contribution = clamp_sample_radiance(ray.throughput.xyz * sourceRadiance);
5627
+ contribution = clamp_sample_radiance(arrivingThroughput * sourceRadiance);
5354
5628
  accumulation[ray.rayId] =
5355
5629
  accumulation[ray.rayId] + vec4<f32>(contribution * sample_weight(), 1.0);
5356
5630
  }
@@ -5367,9 +5641,9 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
5367
5641
  sourceRadiance = sourceRadiance * misWeight;
5368
5642
  }
5369
5643
  if (deferred_path_resolve_enabled()) {
5370
- record_deferred_terminal_source(ray, sourceRadiance);
5644
+ record_deferred_terminal_source(ray, sourceRadiance * segmentTransmittance);
5371
5645
  } else {
5372
- contribution = clamp_sample_radiance(ray.throughput.xyz * sourceRadiance);
5646
+ contribution = clamp_sample_radiance(arrivingThroughput * sourceRadiance);
5373
5647
  accumulation[ray.rayId] =
5374
5648
  accumulation[ray.rayId] + vec4<f32>(contribution * sample_weight(), 1.0);
5375
5649
  }
@@ -5377,7 +5651,11 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
5377
5651
  return;
5378
5652
  }
5379
5653
 
5380
- let response = stabilize_surface_path_response(ray, hit, surface_path_response(hit));
5654
+ let response = stabilize_surface_path_response(
5655
+ ray,
5656
+ hit,
5657
+ surface_path_response(hit) * segmentTransmittance
5658
+ );
5381
5659
  record_deferred_path_response(ray, response);
5382
5660
 
5383
5661
  let shouldEstimateDirectEnvironment =
@@ -5385,7 +5663,22 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
5385
5663
  hit.material.z >= 0.95 &&
5386
5664
  ray.bounce < 2u;
5387
5665
  if (shouldEstimateDirectEnvironment) {
5388
- let directEnvironment = surface_direct_environment_contribution(ray, hit);
5666
+ let directEnvironment = surface_direct_environment_contribution(
5667
+ RayRecord(
5668
+ ray.rayId,
5669
+ ray.parentRayId,
5670
+ ray.sourcePixelId,
5671
+ ray.sampleId,
5672
+ ray.bounce,
5673
+ ray.mediumRefId,
5674
+ ray.flags,
5675
+ 0u,
5676
+ ray.origin,
5677
+ ray.direction,
5678
+ vec4<f32>(arrivingThroughput, ray.throughput.w)
5679
+ ),
5680
+ hit
5681
+ );
5389
5682
  accumulation[ray.rayId] =
5390
5683
  accumulation[ray.rayId] + vec4<f32>(directEnvironment * sample_weight(), 0.0);
5391
5684
  }
@@ -5394,7 +5687,11 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
5394
5687
  if (deferred_path_resolve_enabled()) {
5395
5688
  record_deferred_terminal_source(ray, terminal_surface_environment_source(ray, hit));
5396
5689
  } else {
5397
- let terminalEnvironment = terminal_surface_environment_contribution(ray, hit);
5690
+ let terminalEnvironment = terminal_surface_environment_contribution(
5691
+ ray,
5692
+ arrivingThroughput,
5693
+ hit
5694
+ );
5398
5695
  accumulation[ray.rayId] =
5399
5696
  accumulation[ray.rayId] + vec4<f32>(terminalEnvironment * sample_weight(), 1.0);
5400
5697
  }
@@ -5409,7 +5706,11 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
5409
5706
  if (deferred_path_resolve_enabled()) {
5410
5707
  record_deferred_terminal_source(ray, terminal_surface_environment_source(ray, hit));
5411
5708
  } else {
5412
- let overflowEnvironment = terminal_surface_environment_contribution(ray, hit);
5709
+ let overflowEnvironment = terminal_surface_environment_contribution(
5710
+ ray,
5711
+ arrivingThroughput,
5712
+ hit
5713
+ );
5413
5714
  accumulation[ray.rayId] =
5414
5715
  accumulation[ray.rayId] + vec4<f32>(overflowEnvironment * sample_weight(), 1.0);
5415
5716
  }
@@ -5423,7 +5724,7 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
5423
5724
  ray.sourcePixelId,
5424
5725
  ray.sampleId,
5425
5726
  ray.bounce + 1u,
5426
- ray.mediumRefId,
5727
+ scatter.mediumRefId,
5427
5728
  scatter.flags,
5428
5729
  0u,
5429
5730
  vec4<f32>(offset_origin(hit.position.xyz, hit.shadingNormal.xyz), 1.0),
@@ -5694,9 +5995,28 @@ function nowMs() {
5694
5995
  return Date.now();
5695
5996
  }
5696
5997
 
5697
- function estimateSubmittedGpuWorkTimeoutMs(config, tileCount, overrideTimeoutMs = null) {
5998
+ function estimateAccelerationBuildWaitFactor(config) {
5999
+ if (config?.gpuAccelerationBuildRequired !== true) {
6000
+ return 1;
6001
+ }
6002
+ const bvhSortStageCount = Array.isArray(config?.bvhSortStages) ? config.bvhSortStages.length : 0;
6003
+ const bvhBuildLevelCount = Array.isArray(config?.bvhBuildLevels) ? config.bvhBuildLevels.length : 0;
6004
+ const accelerationStageCount = 2 + bvhSortStageCount + bvhBuildLevelCount;
6005
+ return Math.max(1, 1 + accelerationStageCount / 96);
6006
+ }
6007
+
6008
+ function estimateSubmittedGpuWorkTiming(
6009
+ config,
6010
+ tileCount,
6011
+ overrideTimeoutMs = null,
6012
+ options = {}
6013
+ ) {
5698
6014
  if (Number.isFinite(overrideTimeoutMs)) {
5699
- return Math.max(1, Math.trunc(Number(overrideTimeoutMs)));
6015
+ const overrideMs = Math.max(1, Math.trunc(Number(overrideTimeoutMs)));
6016
+ return Object.freeze({
6017
+ timeoutMs: overrideMs,
6018
+ maxWaitMs: overrideMs,
6019
+ });
5700
6020
  }
5701
6021
  const samplesPerPixel = Math.max(
5702
6022
  1,
@@ -5708,10 +6028,28 @@ function estimateSubmittedGpuWorkTimeoutMs(config, tileCount, overrideTimeoutMs
5708
6028
  const tiles = Math.max(1, Number(tileCount ?? 1));
5709
6029
  const estimatedPasses =
5710
6030
  tiles * (samplesPerPixel * (maxDepth + 1 + deferredResolvePasses) + denoisePasses + 1);
5711
- return Math.min(
6031
+ const triangleCount = Math.max(0, Number(config?.triangleCount ?? 0));
6032
+ const geometryFactor = Math.max(1, triangleCount / 131072);
6033
+ const includeAccelerationBuild = options.includeAccelerationBuild === true;
6034
+ const accelerationFactor = includeAccelerationBuild
6035
+ ? estimateAccelerationBuildWaitFactor(config)
6036
+ : 1;
6037
+ const estimatedWindowMs = Math.round(
6038
+ (GPU_SUBMITTED_WORK_TIMEOUT_MS + estimatedPasses * 5) * geometryFactor * accelerationFactor
6039
+ );
6040
+ const timeoutMs = Math.min(
5712
6041
  GPU_MAX_SUBMITTED_WORK_TIMEOUT_MS,
5713
- GPU_SUBMITTED_WORK_TIMEOUT_MS + estimatedPasses * 5
6042
+ Math.max(GPU_SUBMITTED_WORK_TIMEOUT_MS, estimatedWindowMs)
6043
+ );
6044
+ const maxWaitMultiplier = includeAccelerationBuild ? 3 : 2;
6045
+ const maxWaitMs = Math.min(
6046
+ GPU_MAX_SUBMITTED_WORK_DEADLINE_MS,
6047
+ Math.max(timeoutMs, estimatedWindowMs * maxWaitMultiplier)
5714
6048
  );
6049
+ return Object.freeze({
6050
+ timeoutMs,
6051
+ maxWaitMs,
6052
+ });
5715
6053
  }
5716
6054
 
5717
6055
  export async function createWavefrontPathTracingComputeRenderer(options = {}) {
@@ -5945,6 +6283,11 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
5945
6283
  config.environmentMap,
5946
6284
  config.environmentColor
5947
6285
  );
6286
+ let mediumTextureResource = createMediumTextureResource(
6287
+ device,
6288
+ constants,
6289
+ config.mediums
6290
+ );
5948
6291
  config = Object.freeze({
5949
6292
  ...config,
5950
6293
  environmentMap: Object.freeze({
@@ -6033,6 +6376,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
6033
6376
  { binding: 29, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
6034
6377
  { binding: 30, visibility: constants.shader.COMPUTE, sampler: { type: "filtering" } },
6035
6378
  { binding: 31, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
6379
+ { binding: 32, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
6036
6380
  ],
6037
6381
  });
6038
6382
  const accelerationBindGroupLayout = device.createBindGroupLayout({
@@ -6222,14 +6566,19 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
6222
6566
  { binding: 29, resource: brdfLutResource.view },
6223
6567
  { binding: 30, resource: brdfLutResource.sampler },
6224
6568
  { binding: 31, resource: environmentSamplingResource.view },
6569
+ { binding: 32, resource: mediumTextureResource.view },
6225
6570
  ],
6226
6571
  });
6227
6572
  }
6228
6573
 
6229
- const bindGroups = [
6230
- createTraceBindGroup(activeQueue, nextQueue, "plasius.wavefront.bind.activeNext"),
6231
- createTraceBindGroup(nextQueue, activeQueue, "plasius.wavefront.bind.nextActive"),
6232
- ];
6574
+ function createTraceBindGroups() {
6575
+ return [
6576
+ createTraceBindGroup(activeQueue, nextQueue, "plasius.wavefront.bind.activeNext"),
6577
+ createTraceBindGroup(nextQueue, activeQueue, "plasius.wavefront.bind.nextActive"),
6578
+ ];
6579
+ }
6580
+
6581
+ let bindGroups = createTraceBindGroups();
6233
6582
  const bvhBuildBindGroup = device.createBindGroup({
6234
6583
  label: "plasius.wavefront.bind.bvhBuild",
6235
6584
  layout: accelerationBindGroupLayout,
@@ -6418,6 +6767,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
6418
6767
  emissiveTriangleCount: config.emissiveTriangleCount,
6419
6768
  environmentPortalCount: config.environmentPortalCount,
6420
6769
  environmentPortalMode: config.environmentPortalMode,
6770
+ mediumCount: config.mediumCount,
6421
6771
  environmentMap: createEnvironmentMapSnapshot(config.environmentMap),
6422
6772
  deferredPathResolve: config.deferredPathResolve,
6423
6773
  bvhNodeCount: config.bvhNodeCount,
@@ -6730,6 +7080,10 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
6730
7080
  ? Number(options.timeoutMs)
6731
7081
  : GPU_SUBMITTED_WORK_TIMEOUT_MS
6732
7082
  );
7083
+ const maxWaitMs = Math.max(
7084
+ timeoutMs,
7085
+ Number.isFinite(options.maxWaitMs) ? Number(options.maxWaitMs) : timeoutMs
7086
+ );
6733
7087
  const allowTimeout = options.allowTimeout !== false;
6734
7088
  const completionPromise = device.queue.onSubmittedWorkDone().then(
6735
7089
  () => ({ status: "done" }),
@@ -6745,47 +7099,62 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
6745
7099
  );
6746
7100
  })
6747
7101
  : null;
6748
- let timeoutHandle = null;
6749
- let resolveTimeoutPromise = null;
6750
- let timeoutSettled = false;
6751
- const settleTimeoutPromise = (value) => {
6752
- if (timeoutSettled) {
6753
- return;
7102
+ const startedAtMs = nowMs();
7103
+ while (true) {
7104
+ const elapsedMs = Math.max(0, nowMs() - startedAtMs);
7105
+ const remainingMs = Math.max(0, maxWaitMs - elapsedMs);
7106
+ if (remainingMs <= 0) {
7107
+ if (!allowTimeout) {
7108
+ throw new Error(`Timed out after ${Math.round(maxWaitMs)} ms waiting for submitted GPU work.`);
7109
+ }
7110
+ console.warn(
7111
+ `[plasius.wavefront] Submitted GPU work did not report completion within ${Math.round(maxWaitMs)} ms; continuing.`
7112
+ );
7113
+ return false;
6754
7114
  }
6755
- timeoutSettled = true;
6756
- resolveTimeoutPromise?.(value);
6757
- };
6758
- const timeoutPromise = new Promise((resolve) => {
6759
- resolveTimeoutPromise = resolve;
6760
- timeoutHandle = setTimeout(() => settleTimeoutPromise({ status: "timeout" }), timeoutMs);
6761
- });
6762
- let result;
6763
- try {
6764
- result = await Promise.race(
6765
- [completionPromise, timeoutPromise, lossPromise].filter(Boolean)
6766
- );
6767
- } finally {
6768
- if (timeoutHandle !== null) {
6769
- clearTimeout(timeoutHandle);
6770
- settleTimeoutPromise({ status: "cancelled" });
7115
+ const waitWindowMs = Math.max(1, Math.min(timeoutMs, remainingMs));
7116
+ let timeoutHandle = null;
7117
+ let resolveTimeoutPromise = null;
7118
+ let timeoutSettled = false;
7119
+ const settleTimeoutPromise = (value) => {
7120
+ if (timeoutSettled) {
7121
+ return;
7122
+ }
7123
+ timeoutSettled = true;
7124
+ resolveTimeoutPromise?.(value);
7125
+ };
7126
+ const timeoutPromise = new Promise((resolve) => {
7127
+ resolveTimeoutPromise = resolve;
7128
+ timeoutHandle = setTimeout(
7129
+ () => settleTimeoutPromise({ status: "timeout" }),
7130
+ waitWindowMs
7131
+ );
7132
+ });
7133
+ let result;
7134
+ try {
7135
+ result = await Promise.race(
7136
+ [completionPromise, timeoutPromise, lossPromise].filter(Boolean)
7137
+ );
7138
+ } finally {
7139
+ if (timeoutHandle !== null) {
7140
+ clearTimeout(timeoutHandle);
7141
+ settleTimeoutPromise({ status: "cancelled" });
7142
+ }
6771
7143
  }
6772
- }
6773
- if (result?.status === "timeout") {
6774
- if (!allowTimeout) {
6775
- throw new Error(`Timed out after ${timeoutMs} ms waiting for submitted GPU work.`);
7144
+ if (result?.status === "done") {
7145
+ return true;
7146
+ }
7147
+ if (result?.status !== "timeout") {
7148
+ return true;
6776
7149
  }
6777
- console.warn(
6778
- `[plasius.wavefront] Submitted GPU work did not report completion within ${timeoutMs} ms; continuing.`
6779
- );
6780
- return false;
6781
7150
  }
6782
- return true;
6783
7151
  }
6784
7152
 
6785
7153
  function dispatchFrameAwaitingGpu(
6786
7154
  frameIndex,
6787
7155
  parallelism,
6788
- renderedSamplesPerPixel = config.samplesPerPixel
7156
+ renderedSamplesPerPixel = config.samplesPerPixel,
7157
+ optionsForFrame = {}
6789
7158
  ) {
6790
7159
  const samplePassesPerSample = config.maxDepth + 1 + (config.deferredPathResolve ? 1 : 0);
6791
7160
  const denoisePassCount = config.denoise ? (renderedSamplesPerPixel < 4 ? 2 : 1) : 0;
@@ -6794,25 +7163,50 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
6794
7163
  1,
6795
7164
  Math.floor(
6796
7165
  Math.max(config.maxFramePassesPerSubmission - tailPassCount, 1) /
6797
- Math.max(samplePassesPerSample, 1)
7166
+ Math.max(samplePassesPerSample, 1)
6798
7167
  )
6799
7168
  );
6800
- let submissionCount = 0;
7169
+ const sampleRangeStart = clamp(
7170
+ readNonNegativeInteger("sampleRangeStart", optionsForFrame.sampleRangeStart, 0),
7171
+ 0,
7172
+ renderedSamplesPerPixel
7173
+ );
7174
+ const sampleRangeEnd = clamp(
7175
+ readPositiveInteger("sampleRangeEnd", optionsForFrame.sampleRangeEnd, renderedSamplesPerPixel),
7176
+ sampleRangeStart,
7177
+ renderedSamplesPerPixel
7178
+ );
7179
+ const includeDenoise = optionsForFrame.includeDenoise === true;
7180
+ const includePresent = optionsForFrame.includePresent === true;
7181
+ const tileStartIndex = clamp(
7182
+ readNonNegativeInteger("tileStartIndex", optionsForFrame.tileStartIndex, 0),
7183
+ 0,
7184
+ tiles.length
7185
+ );
7186
+ const tileEndIndex = clamp(
7187
+ readPositiveInteger("tileEndIndex", optionsForFrame.tileEndIndex, tiles.length),
7188
+ tileStartIndex,
7189
+ tiles.length
7190
+ );
7191
+ let submissionCount = Math.max(
7192
+ 0,
7193
+ readNonNegativeInteger("startingSubmissionCount", optionsForFrame.startingSubmissionCount, 0)
7194
+ );
7195
+ let slot = Math.max(0, readNonNegativeInteger("startingSlot", optionsForFrame.startingSlot, 0));
6801
7196
 
6802
- for (const tile of tiles) {
7197
+ for (const tile of tiles.slice(tileStartIndex, tileEndIndex)) {
6803
7198
  for (
6804
- let sampleStart = 0;
6805
- sampleStart < renderedSamplesPerPixel;
7199
+ let sampleStart = sampleRangeStart;
7200
+ sampleStart < sampleRangeEnd;
6806
7201
  sampleStart += sampleBatchSize
6807
7202
  ) {
6808
- const sampleEnd = Math.min(renderedSamplesPerPixel, sampleStart + sampleBatchSize);
7203
+ const sampleEnd = Math.min(sampleRangeEnd, sampleStart + sampleBatchSize);
6809
7204
  const batch = createGpuSubmissionBatcher({
6810
7205
  device,
6811
7206
  frameIndex,
6812
7207
  maxFramePassesPerSubmission: config.maxFramePassesPerSubmission,
6813
7208
  startingSubmissionCount: submissionCount,
6814
7209
  });
6815
- let slot = 0;
6816
7210
  for (let sampleIndex = sampleStart; sampleIndex < sampleEnd; sampleIndex += 1) {
6817
7211
  const configOffset = writeFrameConfigSlot(slot, tile, frameIndex, {
6818
7212
  sampleIndex,
@@ -6829,11 +7223,12 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
6829
7223
  encodeTileOutput(batch.reserve(1), tile, configOffset, parallelism);
6830
7224
  }
6831
7225
  }
6832
- if (!config.deferredPathResolve && sampleEnd >= renderedSamplesPerPixel) {
7226
+ if (!config.deferredPathResolve && sampleRangeEnd >= renderedSamplesPerPixel) {
6833
7227
  const outputConfigOffset = writeFrameConfigSlot(slot, tile, frameIndex, {
6834
7228
  sampleIndex: 0,
6835
7229
  sampleWeight: 1 / renderedSamplesPerPixel,
6836
7230
  });
7231
+ slot += 1;
6837
7232
  encodeTileOutput(batch.reserve(1), tile, outputConfigOffset, parallelism);
6838
7233
  }
6839
7234
  batch.flush();
@@ -6841,30 +7236,38 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
6841
7236
  }
6842
7237
  }
6843
7238
 
6844
- const tail = createGpuSubmissionBatcher({
6845
- device,
6846
- frameIndex,
6847
- maxFramePassesPerSubmission: config.maxFramePassesPerSubmission,
6848
- startingSubmissionCount: submissionCount,
6849
- });
6850
- if (config.denoise) {
6851
- const denoiseConfigOffset = writeFrameConfigSlot(
6852
- 0,
6853
- { x: 0, y: 0, width: config.width, height: config.height },
7239
+ if (includeDenoise || includePresent) {
7240
+ const tail = createGpuSubmissionBatcher({
7241
+ device,
6854
7242
  frameIndex,
6855
- { sampleIndex: 0, sampleWeight: 1 / renderedSamplesPerPixel }
6856
- );
6857
- encodeDenoise(
6858
- tail.reserve(denoisePassCount),
6859
- denoiseConfigOffset,
6860
- parallelism,
6861
- renderedSamplesPerPixel
6862
- );
7243
+ maxFramePassesPerSubmission: config.maxFramePassesPerSubmission,
7244
+ startingSubmissionCount: submissionCount,
7245
+ });
7246
+ if (includeDenoise && config.denoise) {
7247
+ const denoiseConfigOffset = writeFrameConfigSlot(
7248
+ slot,
7249
+ { x: 0, y: 0, width: config.width, height: config.height },
7250
+ frameIndex,
7251
+ { sampleIndex: 0, sampleWeight: 1 / renderedSamplesPerPixel }
7252
+ );
7253
+ slot += 1;
7254
+ encodeDenoise(
7255
+ tail.reserve(denoisePassCount),
7256
+ denoiseConfigOffset,
7257
+ parallelism,
7258
+ renderedSamplesPerPixel
7259
+ );
7260
+ }
7261
+ if (includePresent) {
7262
+ encodePresent(tail.reserve(1));
7263
+ }
7264
+ tail.flush();
7265
+ submissionCount += tail.getSubmissionCount();
6863
7266
  }
6864
- encodePresent(tail.reserve(1));
6865
- tail.flush();
6866
- submissionCount += tail.getSubmissionCount();
6867
- return submissionCount;
7267
+ return Object.freeze({
7268
+ submissionCount,
7269
+ slot,
7270
+ });
6868
7271
  }
6869
7272
 
6870
7273
  async function readOutputProbe(optionsForProbe = {}) {
@@ -6913,26 +7316,59 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
6913
7316
  const samplingPlan = resolveRenderedSamplesPerPixel(renderOptions, awaitGPUCompletion);
6914
7317
  const useThrottledHighSamplePath =
6915
7318
  awaitGPUCompletion && samplingPlan.renderedSamplesPerPixel >= 8;
6916
- const submittedWorkTimeoutMs = estimateSubmittedGpuWorkTimeoutMs(
6917
- { ...config, renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel },
6918
- tiles.length,
6919
- renderOptions.submittedWorkTimeoutMs
6920
- );
6921
7319
  const frameStartTimeMs = nowMs();
6922
- const submissionWaitOptions = awaitGPUCompletion
6923
- ? { timeoutMs: submittedWorkTimeoutMs, allowTimeout: false }
6924
- : { timeoutMs: submittedWorkTimeoutMs };
6925
7320
  let frameStats;
6926
7321
  if (useThrottledHighSamplePath) {
6927
7322
  frame += 1;
6928
7323
  const frameIndex = frame + config.frameIndex;
6929
7324
  const parallelismCounters = createGpuParallelismCounters();
6930
7325
  const accelerationBuildSubmitted = dispatchGpuAccelerationBuild(frameIndex, parallelismCounters);
6931
- const frameSubmissionCount = dispatchFrameAwaitingGpu(
6932
- frameIndex,
6933
- parallelismCounters,
6934
- samplingPlan.renderedSamplesPerPixel
6935
- );
7326
+ let frameSubmissionCount = 0;
7327
+ let frameConfigSlot = 0;
7328
+ if (accelerationBuildSubmitted) {
7329
+ const accelerationWaitOptions = {
7330
+ ...estimateSubmittedGpuWorkTiming(
7331
+ { ...config, renderedSamplesPerPixel: 1 },
7332
+ 1,
7333
+ renderOptions.submittedWorkTimeoutMs,
7334
+ { includeAccelerationBuild: true }
7335
+ ),
7336
+ allowTimeout: false,
7337
+ };
7338
+ await waitForSubmittedGpuWork(accelerationWaitOptions);
7339
+ }
7340
+ for (let tileIndex = 0; tileIndex < tiles.length; tileIndex += 1) {
7341
+ const tileRangeDispatch = dispatchFrameAwaitingGpu(
7342
+ frameIndex,
7343
+ parallelismCounters,
7344
+ samplingPlan.renderedSamplesPerPixel,
7345
+ {
7346
+ sampleRangeStart: 0,
7347
+ sampleRangeEnd: samplingPlan.renderedSamplesPerPixel,
7348
+ tileStartIndex: tileIndex,
7349
+ tileEndIndex: tileIndex + 1,
7350
+ startingSubmissionCount: frameSubmissionCount,
7351
+ startingSlot: frameConfigSlot,
7352
+ includeDenoise: tileIndex + 1 >= tiles.length,
7353
+ includePresent: tileIndex + 1 >= tiles.length,
7354
+ }
7355
+ );
7356
+ frameSubmissionCount = tileRangeDispatch.submissionCount;
7357
+ frameConfigSlot = tileRangeDispatch.slot;
7358
+ const tileWaitOptions = {
7359
+ ...estimateSubmittedGpuWorkTiming(
7360
+ { ...config, renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel },
7361
+ 1,
7362
+ renderOptions.submittedWorkTimeoutMs,
7363
+ {
7364
+ includeDenoise: tileIndex + 1 >= tiles.length && config.denoise,
7365
+ includePresent: tileIndex + 1 >= tiles.length,
7366
+ }
7367
+ ),
7368
+ allowTimeout: false,
7369
+ };
7370
+ await waitForSubmittedGpuWork(tileWaitOptions);
7371
+ }
6936
7372
  frameStats = createFrameStats({
6937
7373
  frameIndex,
6938
7374
  accelerationBuildSubmitted,
@@ -6944,10 +7380,26 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
6944
7380
  budgetConstrained: samplingPlan.budgetConstrained,
6945
7381
  });
6946
7382
  } else {
7383
+ const submittedWorkTiming = estimateSubmittedGpuWorkTiming(
7384
+ { ...config, renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel },
7385
+ tiles.length,
7386
+ renderOptions.submittedWorkTimeoutMs,
7387
+ { includeAccelerationBuild: config.gpuAccelerationBuildRequired && !accelerationBuilt }
7388
+ );
7389
+ const submissionWaitOptions = awaitGPUCompletion
7390
+ ? {
7391
+ timeoutMs: submittedWorkTiming.timeoutMs,
7392
+ maxWaitMs: submittedWorkTiming.maxWaitMs,
7393
+ allowTimeout: false,
7394
+ }
7395
+ : {
7396
+ timeoutMs: submittedWorkTiming.timeoutMs,
7397
+ maxWaitMs: submittedWorkTiming.maxWaitMs,
7398
+ };
6947
7399
  frameStats = renderOnce(renderOptions, samplingPlan);
6948
- }
6949
- if (awaitGPUCompletion) {
6950
- await waitForSubmittedGpuWork(submissionWaitOptions);
7400
+ if (awaitGPUCompletion) {
7401
+ await waitForSubmittedGpuWork(submissionWaitOptions);
7402
+ }
6951
7403
  }
6952
7404
  const frameTimeMs = Math.max(0, nowMs() - frameStartTimeMs);
6953
7405
  if (awaitGPUCompletion) {
@@ -7007,10 +7459,23 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
7007
7459
  });
7008
7460
  }
7009
7461
 
7462
+ function rebuildMediumResources(nextConfig) {
7463
+ const previousMediumTextureResource = mediumTextureResource;
7464
+ mediumTextureResource = createMediumTextureResource(device, constants, nextConfig.mediums);
7465
+ bindGroups = createTraceBindGroups();
7466
+ if (previousMediumTextureResource?.ownsTexture) {
7467
+ previousMediumTextureResource.texture?.destroy?.();
7468
+ }
7469
+ }
7470
+
7010
7471
  function updateSceneObjects(sceneObjects) {
7011
7472
  const nextPackedScene = packWavefrontSceneObjects(sceneObjects, config.sceneObjectCapacity);
7012
7473
  packedScene = nextPackedScene;
7013
- config = rebuildLiveConfig();
7474
+ const nextConfig = rebuildLiveConfig();
7475
+ if (!mediumTablesEqual(config.mediums, nextConfig.mediums)) {
7476
+ rebuildMediumResources(nextConfig);
7477
+ }
7478
+ config = nextConfig;
7014
7479
  device.queue.writeBuffer(sceneObjectBuffer, 0, packedScene.buffer);
7015
7480
  return config;
7016
7481
  }
@@ -7036,6 +7501,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
7036
7501
  emissiveTriangleCount: config.emissiveTriangleCount,
7037
7502
  environmentPortalCount: config.environmentPortalCount,
7038
7503
  environmentPortalMode: config.environmentPortalMode,
7504
+ mediumCount: config.mediumCount,
7039
7505
  environmentMap: createEnvironmentMapSnapshot(config.environmentMap),
7040
7506
  deferredPathResolve: config.deferredPathResolve,
7041
7507
  bvhNodeCount: config.bvhNodeCount,
@@ -7070,17 +7536,20 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
7070
7536
  activeDispatchBuffer.destroy?.();
7071
7537
  radianceTexture.destroy?.();
7072
7538
  denoiseScratchTexture.destroy?.();
7073
- outputTexture.destroy?.();
7074
- if (environmentMapResource.ownsTexture) {
7075
- environmentMapResource.texture?.destroy?.();
7076
- }
7077
- if (environmentSamplingResource.ownsTexture) {
7078
- environmentSamplingResource.texture?.destroy?.();
7079
- }
7080
- brdfLutResource.texture?.destroy?.();
7081
- if (baseColorAtlasResource.ownsTexture) {
7082
- baseColorAtlasResource.texture?.destroy?.();
7083
- }
7539
+ outputTexture.destroy?.();
7540
+ if (environmentMapResource.ownsTexture) {
7541
+ environmentMapResource.texture?.destroy?.();
7542
+ }
7543
+ if (environmentSamplingResource.ownsTexture) {
7544
+ environmentSamplingResource.texture?.destroy?.();
7545
+ }
7546
+ if (mediumTextureResource.ownsTexture) {
7547
+ mediumTextureResource.texture?.destroy?.();
7548
+ }
7549
+ brdfLutResource.texture?.destroy?.();
7550
+ if (baseColorAtlasResource.ownsTexture) {
7551
+ baseColorAtlasResource.texture?.destroy?.();
7552
+ }
7084
7553
  if (metallicRoughnessAtlasResource.ownsTexture) {
7085
7554
  metallicRoughnessAtlasResource.texture?.destroy?.();
7086
7555
  }