@plasius/gpu-renderer 0.2.5 → 0.2.6

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.
@@ -1,8 +1,19 @@
1
+ import {
2
+ createGpuParallelismCounters,
3
+ createGpuParallelismDiagnostics,
4
+ createGpuSubmissionBatcher,
5
+ createGpuWorkerJobDiagnostics,
6
+ recordDirectDispatch,
7
+ recordIndirectDispatch,
8
+ } from "./wavefront-frame-runtime.js"
9
+
1
10
  const DEFAULT_WIDTH = 1280;
2
11
  const DEFAULT_HEIGHT = 720;
3
12
  const DEFAULT_MAX_DEPTH = 6;
4
13
  const DEFAULT_TILE_SIZE = 128;
5
14
  const DEFAULT_SAMPLES_PER_PIXEL = 1;
15
+ const MAX_SAMPLES_PER_PIXEL = 256;
16
+ const DEFAULT_BRDF_LUT_SIZE = 256;
6
17
  const DEFAULT_MAX_FRAME_PASSES_PER_SUBMISSION = 256;
7
18
  const DEFAULT_SCENE_OBJECT_CAPACITY = 128;
8
19
  const DEFAULT_ENVIRONMENT_PORTAL_CAPACITY = 32;
@@ -11,22 +22,27 @@ export const rendererWavefrontComputeMode = "webgpu-compute";
11
22
  export const rendererWavefrontComputeWorkgroupSize = WORKGROUP_SIZE;
12
23
  export const rendererWavefrontComputeStatsStride = 8;
13
24
  const RAY_RECORD_BYTES = 80;
14
- const HIT_RECORD_BYTES = 208;
15
- const SCENE_OBJECT_RECORD_BYTES = 96;
25
+ const HIT_RECORD_BYTES = 256;
26
+ const SCENE_OBJECT_RECORD_BYTES = 144;
16
27
  const MESH_VERTEX_RECORD_BYTES = 48;
17
- const MESH_RANGE_RECORD_BYTES = 96;
18
- const TRIANGLE_RECORD_BYTES = 208;
28
+ const MESH_RANGE_RECORD_BYTES = 240;
29
+ const TRIANGLE_RECORD_BYTES = 352;
30
+ const GPU_MATERIAL_RECORD_BYTES = 192;
19
31
  const BVH_NODE_RECORD_BYTES = 48;
20
32
  const BVH_LEAF_REF_RECORD_BYTES = 16;
21
33
  const EMISSIVE_TRIANGLE_INDEX_BYTES = 4;
22
34
  const ENVIRONMENT_PORTAL_RECORD_BYTES = 96;
23
35
  const ACCUMULATION_RECORD_BYTES = 16;
24
36
  const PATH_VERTEX_RECORD_BYTES = 16;
25
- const CONFIG_BUFFER_BYTES = 304;
37
+ const GPU_SUBMITTED_WORK_TIMEOUT_MS = 5_000;
38
+ const GPU_READBACK_COMPLETION_TIMEOUT_MS = 60_000;
39
+ const GPU_MAX_SUBMITTED_WORK_TIMEOUT_MS = 60_000;
40
+ const CONFIG_BUFFER_BYTES = 320;
26
41
  const COUNTER_DISPATCH_ARGS_OFFSET = 16;
27
42
  const INDIRECT_DISPATCH_ARGS_BYTES = 12;
28
43
  const COUNTER_BUFFER_BYTES = 32;
29
44
  const TRACE_STORAGE_BUFFER_BINDINGS = 10;
45
+ const BRDF_LUT_UPLOAD_CACHE = new Map();
30
46
  const HIT_TYPE_SURFACE = 0;
31
47
  const HIT_TYPE_EMISSIVE = 1;
32
48
  const MATERIAL_DIFFUSE = 0;
@@ -66,6 +82,7 @@ export const wavefrontPathTracingComputeLimits = Object.freeze({
66
82
  meshVertexRecordBytes: MESH_VERTEX_RECORD_BYTES,
67
83
  meshRangeRecordBytes: MESH_RANGE_RECORD_BYTES,
68
84
  triangleRecordBytes: TRIANGLE_RECORD_BYTES,
85
+ materialRecordBytes: GPU_MATERIAL_RECORD_BYTES,
69
86
  bvhNodeRecordBytes: BVH_NODE_RECORD_BYTES,
70
87
  bvhLeafReferenceRecordBytes: BVH_LEAF_REF_RECORD_BYTES,
71
88
  emissiveTriangleIndexBytes: EMISSIVE_TRIANGLE_INDEX_BYTES,
@@ -164,6 +181,38 @@ function asColor(value, fallback = [1, 1, 1, 1]) {
164
181
  ];
165
182
  }
166
183
 
184
+ function maxComponent(value) {
185
+ return Math.max(value?.[0] ?? 0, value?.[1] ?? 0, value?.[2] ?? 0);
186
+ }
187
+
188
+ function deriveLegacySheenColor(baseColor, sheen, sheenTint) {
189
+ const sheenStrength = clamp(Number(sheen) || 0, 0, 1);
190
+ if (sheenStrength <= 0) {
191
+ return [0, 0, 0, 1];
192
+ }
193
+ const tint = clamp(Number(sheenTint) || 0, 0, 1);
194
+ const base = asColor(baseColor, [1, 1, 1, 1]);
195
+ return [
196
+ clamp((1 - tint) * sheenStrength + base[0] * tint * sheenStrength, 0, 1),
197
+ clamp((1 - tint) * sheenStrength + base[1] * tint * sheenStrength, 0, 1),
198
+ clamp((1 - tint) * sheenStrength + base[2] * tint * sheenStrength, 0, 1),
199
+ 1,
200
+ ];
201
+ }
202
+
203
+ function resolveSheenColor(input, fallbackBaseColor) {
204
+ if (input?.sheenColor || input?.material?.sheenColor) {
205
+ return asColor(input.sheenColor ?? input.material?.sheenColor, [0, 0, 0, 1]).map((value, index) =>
206
+ index < 3 ? clamp(value, 0, 1) : 1
207
+ );
208
+ }
209
+ return deriveLegacySheenColor(
210
+ fallbackBaseColor,
211
+ input?.sheen ?? input?.material?.sheen,
212
+ input?.sheenTint ?? input?.material?.sheenTint
213
+ );
214
+ }
215
+
167
216
  function resolveEnvironmentMap(input = null) {
168
217
  const source = input && typeof input === "object" ? input : null;
169
218
  const hasTexture = Boolean(source?.view || source?.texture || source?.data);
@@ -173,6 +222,11 @@ function resolveEnvironmentMap(input = null) {
173
222
  enabled: hasTexture && source?.enabled !== false,
174
223
  width,
175
224
  height,
225
+ mipLevelCount: readPositiveInteger(
226
+ "environmentMap.mipLevelCount",
227
+ source?.mipLevelCount,
228
+ 1
229
+ ),
176
230
  format: typeof source?.format === "string" ? source.format : "rgba16float",
177
231
  projection: typeof source?.projection === "string" ? source.projection : "equirectangular",
178
232
  texture: source?.texture ?? null,
@@ -185,6 +239,7 @@ function resolveEnvironmentMap(input = null) {
185
239
  0,
186
240
  readFiniteNumber("environmentMap.ambientStrength", source?.ambientStrength, 0.32)
187
241
  ),
242
+ hasImportanceData: source?.hasImportanceData === true,
188
243
  });
189
244
  }
190
245
 
@@ -394,7 +449,8 @@ export function normalizeWavefrontSceneObject(input = {}, index = 0) {
394
449
  input.halfExtent ?? input.halfExtents ?? input.extents ?? bounds?.halfExtent,
395
450
  [0.5, 0.5, 0.5]
396
451
  ).map((value) => Math.max(value, 0.001));
397
- const materialKind = readMaterialKind(input.materialKind ?? input.material?.kind);
452
+ const materialKindInput = input.materialKind ?? input.material?.kind;
453
+ const materialKind = readMaterialKind(materialKindInput);
398
454
  const color = asColor(
399
455
  input.color ??
400
456
  input.baseColor ??
@@ -407,14 +463,30 @@ export function normalizeWavefrontSceneObject(input = {}, index = 0) {
407
463
  input.emission ?? input.emissive ?? input.material?.emission ?? input.material?.emissive,
408
464
  [0, 0, 0, 1]
409
465
  );
466
+ const opacity = clamp(readFiniteNumber("opacity", input.opacity ?? input.material?.opacity, color[3] ?? 1), 0, 1);
467
+ const transmission = clamp(
468
+ readFiniteNumber("transmission", input.transmission ?? input.material?.transmission, 0),
469
+ 0,
470
+ 1
471
+ );
472
+ const sheenColor = resolveSheenColor(input, color);
473
+ const specularColor = asColor(
474
+ input.specularColor ?? input.material?.specularColor,
475
+ [1, 1, 1, 1]
476
+ ).map((value, componentIndex) => (componentIndex < 3 ? clamp(value, 0, 1) : 1));
477
+ const resolvedMaterialKind =
478
+ emission[0] > 0 || emission[1] > 0 || emission[2] > 0
479
+ ? MATERIAL_EMISSIVE
480
+ : materialKindInput === undefined || materialKindInput === null
481
+ ? transmission > 0.001 || opacity < 0.999
482
+ ? MATERIAL_TRANSPARENT
483
+ : materialKind
484
+ : materialKind;
410
485
 
411
486
  return Object.freeze({
412
487
  id: readNonNegativeInteger("id", input.id, index + 1),
413
488
  kind,
414
- materialKind:
415
- emission[0] > 0 || emission[1] > 0 || emission[2] > 0
416
- ? MATERIAL_EMISSIVE
417
- : materialKind,
489
+ materialKind: resolvedMaterialKind,
418
490
  flags: readNonNegativeInteger("flags", input.flags, 0),
419
491
  center: Object.freeze(center),
420
492
  halfExtent: Object.freeze(halfExtent),
@@ -422,8 +494,24 @@ export function normalizeWavefrontSceneObject(input = {}, index = 0) {
422
494
  emission: Object.freeze(emission),
423
495
  roughness: clamp(readFiniteNumber("roughness", input.roughness ?? input.material?.roughness, 0.72), 0, 1),
424
496
  metallic: clamp(readFiniteNumber("metallic", input.metallic ?? input.material?.metallic, 0), 0, 1),
425
- opacity: clamp(readFiniteNumber("opacity", input.opacity ?? input.material?.opacity, color[3] ?? 1), 0, 1),
497
+ opacity,
426
498
  ior: clamp(readFiniteNumber("ior", input.ior ?? input.material?.ior, 1.45), 1, 3),
499
+ sheen: clamp(readFiniteNumber("sheen", input.sheen ?? input.material?.sheen, 0), 0, 1),
500
+ sheenTint: clamp(readFiniteNumber("sheenTint", input.sheenTint ?? input.material?.sheenTint, 0), 0, 1),
501
+ sheenColor: Object.freeze(sheenColor),
502
+ clearcoat: clamp(readFiniteNumber("clearcoat", input.clearcoat ?? input.material?.clearcoat, 0), 0, 1),
503
+ clearcoatRoughness: clamp(
504
+ readFiniteNumber(
505
+ "clearcoatRoughness",
506
+ input.clearcoatRoughness ?? input.material?.clearcoatRoughness,
507
+ 0.08
508
+ ),
509
+ 0,
510
+ 1
511
+ ),
512
+ specular: clamp(readFiniteNumber("specular", input.specular ?? input.material?.specular, 1), 0, 1),
513
+ specularColor: Object.freeze(specularColor),
514
+ transmission,
427
515
  });
428
516
  }
429
517
 
@@ -507,7 +595,8 @@ export function normalizeWavefrontMesh(input = {}, meshIndex = 0) {
507
595
  readFiniteNumber("mesh uv", value, 0)
508
596
  )
509
597
  : null;
510
- const materialKind = readMaterialKind(input.materialKind ?? input.material?.kind);
598
+ const materialKindInput = input.materialKind ?? input.material?.kind;
599
+ const materialKind = readMaterialKind(materialKindInput);
511
600
  const color = asColor(
512
601
  input.color ??
513
602
  input.baseColor ??
@@ -520,6 +609,25 @@ export function normalizeWavefrontMesh(input = {}, meshIndex = 0) {
520
609
  input.emission ?? input.emissive ?? input.material?.emission ?? input.material?.emissive,
521
610
  [0, 0, 0, 1]
522
611
  );
612
+ const opacity = clamp(readFiniteNumber("opacity", input.opacity ?? input.material?.opacity, color[3] ?? 1), 0, 1);
613
+ const transmission = clamp(
614
+ readFiniteNumber("transmission", input.transmission ?? input.material?.transmission, 0),
615
+ 0,
616
+ 1
617
+ );
618
+ const sheenColor = resolveSheenColor(input, color);
619
+ const specularColor = asColor(
620
+ input.specularColor ?? input.material?.specularColor,
621
+ [1, 1, 1, 1]
622
+ ).map((value, componentIndex) => (componentIndex < 3 ? clamp(value, 0, 1) : 1));
623
+ const resolvedMaterialKind =
624
+ emission[0] > 0 || emission[1] > 0 || emission[2] > 0
625
+ ? MATERIAL_EMISSIVE
626
+ : materialKindInput === undefined || materialKindInput === null
627
+ ? transmission > 0.001 || opacity < 0.999
628
+ ? MATERIAL_TRANSPARENT
629
+ : materialKind
630
+ : materialKind;
523
631
 
524
632
  return Object.freeze({
525
633
  id: readNonNegativeInteger("mesh id", input.id, meshIndex + 1),
@@ -527,10 +635,7 @@ export function normalizeWavefrontMesh(input = {}, meshIndex = 0) {
527
635
  indices: Object.freeze(indices),
528
636
  normals: normals ? Object.freeze(normals) : null,
529
637
  uvs: uvs ? Object.freeze(uvs) : null,
530
- materialKind:
531
- emission[0] > 0 || emission[1] > 0 || emission[2] > 0
532
- ? MATERIAL_EMISSIVE
533
- : materialKind,
638
+ materialKind: resolvedMaterialKind,
534
639
  flags: readNonNegativeInteger("mesh flags", input.flags, 0),
535
640
  materialRefId: readNonNegativeInteger(
536
641
  "mesh materialRefId",
@@ -546,11 +651,189 @@ export function normalizeWavefrontMesh(input = {}, meshIndex = 0) {
546
651
  emission: Object.freeze(emission),
547
652
  roughness: clamp(readFiniteNumber("roughness", input.roughness ?? input.material?.roughness, 0.72), 0, 1),
548
653
  metallic: clamp(readFiniteNumber("metallic", input.metallic ?? input.material?.metallic, 0), 0, 1),
549
- opacity: clamp(readFiniteNumber("opacity", input.opacity ?? input.material?.opacity, color[3] ?? 1), 0, 1),
654
+ opacity,
550
655
  ior: clamp(readFiniteNumber("ior", input.ior ?? input.material?.ior, 1.45), 1, 3),
656
+ sheen: clamp(readFiniteNumber("sheen", input.sheen ?? input.material?.sheen, 0), 0, 1),
657
+ sheenTint: clamp(readFiniteNumber("sheenTint", input.sheenTint ?? input.material?.sheenTint, 0), 0, 1),
658
+ sheenColor: Object.freeze(sheenColor),
659
+ clearcoat: clamp(readFiniteNumber("clearcoat", input.clearcoat ?? input.material?.clearcoat, 0), 0, 1),
660
+ clearcoatRoughness: clamp(
661
+ readFiniteNumber(
662
+ "clearcoatRoughness",
663
+ input.clearcoatRoughness ?? input.material?.clearcoatRoughness,
664
+ 0.08
665
+ ),
666
+ 0,
667
+ 1
668
+ ),
669
+ specular: clamp(readFiniteNumber("specular", input.specular ?? input.material?.specular, 1), 0, 1),
670
+ specularColor: Object.freeze(specularColor),
671
+ transmission,
672
+ baseColorTexture: input.baseColorTexture ?? input.material?.baseColorTexture ?? null,
673
+ metallicRoughnessTexture:
674
+ input.metallicRoughnessTexture ?? input.material?.metallicRoughnessTexture ?? null,
675
+ normalTexture: input.normalTexture ?? input.material?.normalTexture ?? null,
676
+ occlusionTexture: input.occlusionTexture ?? input.material?.occlusionTexture ?? null,
677
+ emissiveTexture: input.emissiveTexture ?? input.material?.emissiveTexture ?? null,
551
678
  });
552
679
  }
553
680
 
681
+ function clampUnit(value) {
682
+ return clamp(Number(value) || 0, 0, 1);
683
+ }
684
+
685
+ function srgbToLinear(value) {
686
+ const channel = clampUnit(value);
687
+ if (channel <= 0.04045) {
688
+ return channel / 12.92;
689
+ }
690
+ return ((channel + 0.055) / 1.055) ** 2.4;
691
+ }
692
+
693
+ function sampleTextureRgba(texture, uv = [0, 0], colorSpace = "linear") {
694
+ if (
695
+ !texture ||
696
+ !Number.isFinite(texture.width) ||
697
+ !Number.isFinite(texture.height) ||
698
+ !texture.data ||
699
+ texture.width <= 0 ||
700
+ texture.height <= 0
701
+ ) {
702
+ return [1, 1, 1, 1];
703
+ }
704
+ const u = ((uv[0] % 1) + 1) % 1;
705
+ const v = ((uv[1] % 1) + 1) % 1;
706
+ const x = Math.min(texture.width - 1, Math.max(0, Math.round(u * (texture.width - 1))));
707
+ const y = Math.min(texture.height - 1, Math.max(0, Math.round((1 - v) * (texture.height - 1))));
708
+ const offset = (y * texture.width + x) * 4;
709
+ const data = texture.data;
710
+ const scale = resolveTextureSampleScale(data);
711
+ const defaultChannel = scale === 1 ? 1 : Math.round(1 / scale);
712
+ const color = [
713
+ (data[offset] ?? defaultChannel) * scale,
714
+ (data[offset + 1] ?? defaultChannel) * scale,
715
+ (data[offset + 2] ?? defaultChannel) * scale,
716
+ (data[offset + 3] ?? defaultChannel) * scale,
717
+ ];
718
+ if (colorSpace === "srgb") {
719
+ return [srgbToLinear(color[0]), srgbToLinear(color[1]), srgbToLinear(color[2]), color[3]];
720
+ }
721
+ return color;
722
+ }
723
+
724
+ function resolveTextureSampleScale(data) {
725
+ if (data instanceof Uint8Array || data instanceof Uint8ClampedArray) {
726
+ return 1 / 255;
727
+ }
728
+ if (data instanceof Uint16Array) {
729
+ return 1 / 65535;
730
+ }
731
+ if (Array.isArray(data) && data.some((value) => Number(value) > 1)) {
732
+ return 1 / 255;
733
+ }
734
+ return 1;
735
+ }
736
+
737
+ function normalizeVectorOrFallback(vector, fallback) {
738
+ return normalize(Array.isArray(vector) ? vector : fallback, fallback);
739
+ }
740
+
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
+
554
837
  function createMeshTriangleRecords(meshes) {
555
838
  const source = Array.isArray(meshes) ? meshes : [];
556
839
  let nextTriangleId = 0;
@@ -571,6 +854,16 @@ function createMeshTriangleRecords(meshes) {
571
854
  const uv0 = mesh.uvs ? readVector2(mesh.uvs, a) : [0, 0];
572
855
  const uv1 = mesh.uvs ? readVector2(mesh.uvs, b) : [0, 0];
573
856
  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
+ ];
574
867
  const bounds = triangleBounds(v0, v1, v2);
575
868
 
576
869
  triangles.push(
@@ -581,18 +874,42 @@ function createMeshTriangleRecords(meshes) {
581
874
  flags: mesh.flags,
582
875
  materialRefId: mesh.materialRefId,
583
876
  mediumRefId: mesh.mediumRefId,
877
+ materialSlot: meshIndex,
584
878
  v0: Object.freeze(v0),
585
879
  v1: Object.freeze(v1),
586
880
  v2: Object.freeze(v2),
587
- n0: Object.freeze(n0),
588
- n1: Object.freeze(n1),
589
- n2: Object.freeze(n2),
881
+ n0: Object.freeze(shadedN0),
882
+ n1: Object.freeze(shadedN1),
883
+ n2: Object.freeze(shadedN2),
590
884
  uv0: Object.freeze(uv0),
591
885
  uv1: Object.freeze(uv1),
592
886
  uv2: Object.freeze(uv2),
593
- color: mesh.color,
887
+ color: Object.freeze(averageColors(sampledColors)),
594
888
  emission: mesh.emission,
595
- material: Object.freeze([mesh.roughness, mesh.metallic, mesh.opacity, mesh.ior]),
889
+ material: Object.freeze([
890
+ averageNumbers(sampledMaterials.map((sample) => sample.roughness), mesh.roughness),
891
+ averageNumbers(sampledMaterials.map((sample) => sample.metallic), mesh.metallic),
892
+ mesh.opacity,
893
+ mesh.ior,
894
+ ]),
895
+ materialResponse: Object.freeze([
896
+ mesh.sheenColor[0] ?? 0,
897
+ mesh.sheenColor[1] ?? 0,
898
+ mesh.sheenColor[2] ?? 0,
899
+ mesh.clearcoat,
900
+ ]),
901
+ materialExtension: Object.freeze([
902
+ mesh.clearcoatRoughness,
903
+ mesh.specular,
904
+ mesh.transmission,
905
+ 0,
906
+ ]),
907
+ specularColor: Object.freeze([
908
+ mesh.specularColor[0] ?? 1,
909
+ mesh.specularColor[1] ?? 1,
910
+ mesh.specularColor[2] ?? 1,
911
+ 1,
912
+ ]),
596
913
  bounds: Object.freeze({
597
914
  min: Object.freeze(bounds.min),
598
915
  max: Object.freeze(bounds.max),
@@ -713,6 +1030,245 @@ function nextPowerOfTwo(value) {
713
1030
  return 2 ** Math.ceil(Math.log2(value));
714
1031
  }
715
1032
 
1033
+ function textureComponentToByte(value, fallback) {
1034
+ const numeric = Number(value);
1035
+ if (!Number.isFinite(numeric)) {
1036
+ return fallback;
1037
+ }
1038
+ if (numeric >= 0 && numeric <= 1) {
1039
+ return Math.max(0, Math.min(255, Math.round(numeric * 255)));
1040
+ }
1041
+ return Math.max(0, Math.min(255, Math.round(numeric)));
1042
+ }
1043
+
1044
+ function createSolidTextureSample(width, height, rgba) {
1045
+ const data = new Uint8Array(width * height * 4);
1046
+ for (let offset = 0; offset < data.length; offset += 4) {
1047
+ data[offset] = rgba[0];
1048
+ data[offset + 1] = rgba[1];
1049
+ data[offset + 2] = rgba[2];
1050
+ data[offset + 3] = rgba[3];
1051
+ }
1052
+ return Object.freeze({
1053
+ width,
1054
+ height,
1055
+ data,
1056
+ });
1057
+ }
1058
+
1059
+ function normalizeTextureSampleInput(texture, fallbackColor) {
1060
+ if (
1061
+ !texture ||
1062
+ !Number.isFinite(texture.width) ||
1063
+ !Number.isFinite(texture.height) ||
1064
+ texture.width <= 0 ||
1065
+ texture.height <= 0
1066
+ ) {
1067
+ return createSolidTextureSample(1, 1, fallbackColor);
1068
+ }
1069
+
1070
+ const pixelCount = Math.trunc(texture.width) * Math.trunc(texture.height) * 4;
1071
+ const source =
1072
+ ArrayBuffer.isView(texture.data) || Array.isArray(texture.data) ? texture.data : null;
1073
+ if (!source || source.length < pixelCount) {
1074
+ return createSolidTextureSample(1, 1, fallbackColor);
1075
+ }
1076
+
1077
+ const data = new Uint8Array(pixelCount);
1078
+ for (let index = 0; index < pixelCount; index += 1) {
1079
+ data[index] = textureComponentToByte(source[index], fallbackColor[index % 4]);
1080
+ }
1081
+
1082
+ return Object.freeze({
1083
+ width: Math.trunc(texture.width),
1084
+ height: Math.trunc(texture.height),
1085
+ data,
1086
+ });
1087
+ }
1088
+
1089
+ function buildTextureAtlas(textures, fallbackColor) {
1090
+ const padding = 1;
1091
+ const defaultTexture = createSolidTextureSample(1, 1, fallbackColor);
1092
+ const uniqueEntries = [{ source: null, texture: defaultTexture }];
1093
+ const bySource = new Map();
1094
+
1095
+ for (const texture of Array.isArray(textures) ? textures : []) {
1096
+ if (!texture || bySource.has(texture)) {
1097
+ continue;
1098
+ }
1099
+ const normalized = normalizeTextureSampleInput(texture, fallbackColor);
1100
+ bySource.set(texture, uniqueEntries.length);
1101
+ uniqueEntries.push({ source: texture, texture: normalized });
1102
+ }
1103
+
1104
+ const totalArea = uniqueEntries.reduce((sum, entry) => {
1105
+ return sum + (entry.texture.width + padding * 2) * (entry.texture.height + padding * 2);
1106
+ }, 0);
1107
+ const maxTileWidth = uniqueEntries.reduce((maxWidth, entry) => {
1108
+ return Math.max(maxWidth, entry.texture.width + padding * 2);
1109
+ }, 1);
1110
+ const targetWidth = Math.max(
1111
+ maxTileWidth,
1112
+ nextPowerOfTwo(Math.max(maxTileWidth, Math.ceil(Math.sqrt(totalArea))))
1113
+ );
1114
+
1115
+ let cursorX = 0;
1116
+ let cursorY = 0;
1117
+ let rowHeight = 0;
1118
+ let atlasWidth = 0;
1119
+ const placements = uniqueEntries.map((entry) => {
1120
+ const tileWidth = entry.texture.width + padding * 2;
1121
+ const tileHeight = entry.texture.height + padding * 2;
1122
+ if (cursorX > 0 && cursorX + tileWidth > targetWidth) {
1123
+ cursorX = 0;
1124
+ cursorY += rowHeight;
1125
+ rowHeight = 0;
1126
+ }
1127
+ const placement = Object.freeze({
1128
+ x: cursorX,
1129
+ y: cursorY,
1130
+ tileWidth,
1131
+ tileHeight,
1132
+ width: entry.texture.width,
1133
+ height: entry.texture.height,
1134
+ });
1135
+ cursorX += tileWidth;
1136
+ atlasWidth = Math.max(atlasWidth, cursorX);
1137
+ rowHeight = Math.max(rowHeight, tileHeight);
1138
+ return placement;
1139
+ });
1140
+
1141
+ const atlasHeight = Math.max(1, cursorY + rowHeight);
1142
+ const atlasData = new Uint8Array(Math.max(1, atlasWidth * atlasHeight * 4));
1143
+
1144
+ const writePixel = (x, y, rgba) => {
1145
+ const offset = (y * atlasWidth + x) * 4;
1146
+ atlasData[offset] = rgba[0];
1147
+ atlasData[offset + 1] = rgba[1];
1148
+ atlasData[offset + 2] = rgba[2];
1149
+ atlasData[offset + 3] = rgba[3];
1150
+ };
1151
+
1152
+ const rects = placements.map((placement, entryIndex) => {
1153
+ const { texture } = uniqueEntries[entryIndex];
1154
+ for (let y = 0; y < placement.tileHeight; y += 1) {
1155
+ for (let x = 0; x < placement.tileWidth; x += 1) {
1156
+ const sampleX = Math.max(0, Math.min(texture.width - 1, x - padding));
1157
+ const sampleY = Math.max(0, Math.min(texture.height - 1, y - padding));
1158
+ const sourceOffset = (sampleY * texture.width + sampleX) * 4;
1159
+ writePixel(placement.x + x, placement.y + y, texture.data.slice(sourceOffset, sourceOffset + 4));
1160
+ }
1161
+ }
1162
+ return Object.freeze([
1163
+ (placement.x + padding) / Math.max(1, atlasWidth),
1164
+ (placement.y + padding) / Math.max(1, atlasHeight),
1165
+ placement.width / Math.max(1, atlasWidth),
1166
+ placement.height / Math.max(1, atlasHeight),
1167
+ ]);
1168
+ });
1169
+
1170
+ const rectBySource = new Map();
1171
+ uniqueEntries.forEach((entry, index) => {
1172
+ if (entry.source) {
1173
+ rectBySource.set(entry.source, rects[index]);
1174
+ }
1175
+ });
1176
+
1177
+ return Object.freeze({
1178
+ width: Math.max(1, atlasWidth),
1179
+ height: Math.max(1, atlasHeight),
1180
+ data: atlasData,
1181
+ defaultRect: rects[0],
1182
+ resolveRect(texture) {
1183
+ return rectBySource.get(texture) ?? rects[0];
1184
+ },
1185
+ });
1186
+ }
1187
+
1188
+ export function createWavefrontGpuMaterialSource(meshes = []) {
1189
+ const source = Array.isArray(meshes) ? meshes : [meshes];
1190
+ const normalized = source.map((meshInput, meshIndex) => normalizeWavefrontMesh(meshInput, meshIndex));
1191
+ const baseColorAtlas = buildTextureAtlas(
1192
+ normalized.map((mesh) => mesh.baseColorTexture),
1193
+ [255, 255, 255, 255]
1194
+ );
1195
+ const metallicRoughnessAtlas = buildTextureAtlas(
1196
+ normalized.map((mesh) => mesh.metallicRoughnessTexture),
1197
+ [255, 255, 255, 255]
1198
+ );
1199
+ const normalAtlas = buildTextureAtlas(
1200
+ normalized.map((mesh) => mesh.normalTexture),
1201
+ [128, 128, 255, 255]
1202
+ );
1203
+ const occlusionAtlas = buildTextureAtlas(
1204
+ normalized.map((mesh) => mesh.occlusionTexture),
1205
+ [255, 255, 255, 255]
1206
+ );
1207
+ const emissiveAtlas = buildTextureAtlas(
1208
+ normalized.map((mesh) => mesh.emissiveTexture),
1209
+ [255, 255, 255, 255]
1210
+ );
1211
+ const bytes = new ArrayBuffer(Math.max(1, normalized.length) * GPU_MATERIAL_RECORD_BYTES);
1212
+ const floatView = new Float32Array(bytes);
1213
+
1214
+ normalized.forEach((mesh, meshIndex) => {
1215
+ const byteOffset = meshIndex * GPU_MATERIAL_RECORD_BYTES;
1216
+ writeVec4(floatView, byteOffset, mesh.color);
1217
+ writeVec4(floatView, byteOffset + 16, mesh.emission);
1218
+ writeVec4(floatView, byteOffset + 32, [
1219
+ mesh.roughness,
1220
+ mesh.metallic,
1221
+ mesh.opacity,
1222
+ mesh.ior,
1223
+ ]);
1224
+ writeVec4(floatView, byteOffset + 48, [
1225
+ mesh.sheenColor[0] ?? 0,
1226
+ mesh.sheenColor[1] ?? 0,
1227
+ mesh.sheenColor[2] ?? 0,
1228
+ mesh.clearcoat,
1229
+ ]);
1230
+ writeVec4(floatView, byteOffset + 64, [
1231
+ mesh.clearcoatRoughness,
1232
+ mesh.specular,
1233
+ mesh.transmission,
1234
+ 0,
1235
+ ]);
1236
+ writeVec4(floatView, byteOffset + 80, [
1237
+ mesh.specularColor[0] ?? 1,
1238
+ mesh.specularColor[1] ?? 1,
1239
+ mesh.specularColor[2] ?? 1,
1240
+ 1,
1241
+ ]);
1242
+ writeVec4(floatView, byteOffset + 96, baseColorAtlas.resolveRect(mesh.baseColorTexture));
1243
+ writeVec4(
1244
+ floatView,
1245
+ byteOffset + 112,
1246
+ metallicRoughnessAtlas.resolveRect(mesh.metallicRoughnessTexture)
1247
+ );
1248
+ writeVec4(floatView, byteOffset + 128, normalAtlas.resolveRect(mesh.normalTexture));
1249
+ writeVec4(floatView, byteOffset + 144, occlusionAtlas.resolveRect(mesh.occlusionTexture));
1250
+ writeVec4(floatView, byteOffset + 160, emissiveAtlas.resolveRect(mesh.emissiveTexture));
1251
+ writeVec4(floatView, byteOffset + 176, [
1252
+ clampUnit(mesh.normalTexture?.scale ?? mesh.normalTexture?.strength ?? 1),
1253
+ clampUnit(mesh.occlusionTexture?.strength ?? 1),
1254
+ clampUnit(mesh.emissiveTexture?.strength ?? 1),
1255
+ 0,
1256
+ ]);
1257
+ });
1258
+
1259
+ return Object.freeze({
1260
+ buffer: bytes,
1261
+ count: normalized.length,
1262
+ recordBytes: GPU_MATERIAL_RECORD_BYTES,
1263
+ records: Object.freeze(normalized),
1264
+ baseColorAtlas,
1265
+ metallicRoughnessAtlas,
1266
+ normalAtlas,
1267
+ occlusionAtlas,
1268
+ emissiveAtlas,
1269
+ });
1270
+ }
1271
+
716
1272
  function estimateBvhLeafSortCapacity(triangleCount) {
717
1273
  return triangleCount <= 0 ? 0 : nextPowerOfTwo(triangleCount);
718
1274
  }
@@ -783,9 +1339,10 @@ function resolveAccelerationBuildMode(options = {}) {
783
1339
  return mode;
784
1340
  }
785
1341
 
786
- export function createWavefrontGpuMeshSource(meshes = []) {
1342
+ export function createWavefrontGpuMeshSource(meshes = [], gpuMaterialSourceInput = null) {
787
1343
  const source = Array.isArray(meshes) ? meshes : [meshes];
788
1344
  const normalized = source.map((meshInput, meshIndex) => normalizeWavefrontMesh(meshInput, meshIndex));
1345
+ const gpuMaterialSource = gpuMaterialSourceInput ?? createWavefrontGpuMaterialSource(normalized);
789
1346
  const vertexCount = normalized.reduce((count, mesh) => count + mesh.positions.length / 3, 0);
790
1347
  const indexCount = normalized.reduce((count, mesh) => count + mesh.indices.length, 0);
791
1348
  const triangleCount = Math.floor(indexCount / 3);
@@ -842,7 +1399,7 @@ export function createWavefrontGpuMeshSource(meshes = []) {
842
1399
  meshUints[meshOffset + 8] = mesh.indices.length / 3;
843
1400
  meshUints[meshOffset + 9] = meshVertexBase;
844
1401
  meshUints[meshOffset + 10] = meshVertexCount;
845
- meshUints[meshOffset + 11] = 0;
1402
+ meshUints[meshOffset + 11] = meshIndex;
846
1403
  const floatOffset = meshOffset;
847
1404
  writeVec4(meshFloats, floatOffset * 4 + 48, mesh.color);
848
1405
  writeVec4(meshFloats, floatOffset * 4 + 64, mesh.emission);
@@ -852,6 +1409,55 @@ export function createWavefrontGpuMeshSource(meshes = []) {
852
1409
  mesh.opacity,
853
1410
  mesh.ior,
854
1411
  ]);
1412
+ writeVec4(meshFloats, floatOffset * 4 + 96, [
1413
+ mesh.sheenColor[0] ?? 0,
1414
+ mesh.sheenColor[1] ?? 0,
1415
+ mesh.sheenColor[2] ?? 0,
1416
+ mesh.clearcoat,
1417
+ ]);
1418
+ writeVec4(meshFloats, floatOffset * 4 + 112, [
1419
+ mesh.clearcoatRoughness,
1420
+ mesh.specular,
1421
+ mesh.transmission,
1422
+ 0,
1423
+ ]);
1424
+ writeVec4(meshFloats, floatOffset * 4 + 128, [
1425
+ mesh.specularColor[0] ?? 1,
1426
+ mesh.specularColor[1] ?? 1,
1427
+ mesh.specularColor[2] ?? 1,
1428
+ 1,
1429
+ ]);
1430
+ writeVec4(
1431
+ meshFloats,
1432
+ floatOffset * 4 + 144,
1433
+ gpuMaterialSource.baseColorAtlas.resolveRect(mesh.baseColorTexture)
1434
+ );
1435
+ writeVec4(
1436
+ meshFloats,
1437
+ floatOffset * 4 + 160,
1438
+ gpuMaterialSource.metallicRoughnessAtlas.resolveRect(mesh.metallicRoughnessTexture)
1439
+ );
1440
+ writeVec4(
1441
+ meshFloats,
1442
+ floatOffset * 4 + 176,
1443
+ gpuMaterialSource.normalAtlas.resolveRect(mesh.normalTexture)
1444
+ );
1445
+ writeVec4(
1446
+ meshFloats,
1447
+ floatOffset * 4 + 192,
1448
+ gpuMaterialSource.occlusionAtlas.resolveRect(mesh.occlusionTexture)
1449
+ );
1450
+ writeVec4(
1451
+ meshFloats,
1452
+ floatOffset * 4 + 208,
1453
+ gpuMaterialSource.emissiveAtlas.resolveRect(mesh.emissiveTexture)
1454
+ );
1455
+ writeVec4(meshFloats, floatOffset * 4 + 224, [
1456
+ clampUnit(mesh.normalTexture?.scale ?? mesh.normalTexture?.strength ?? 1),
1457
+ clampUnit(mesh.occlusionTexture?.strength ?? 1),
1458
+ clampUnit(mesh.emissiveTexture?.strength ?? 1),
1459
+ 0,
1460
+ ]);
855
1461
 
856
1462
  vertexCursor += meshVertexCount;
857
1463
  indexCursor += mesh.indices.length;
@@ -1188,12 +1794,14 @@ export function estimateWavefrontPathTracingMemory(options = {}) {
1188
1794
  options.environmentPortalCapacity,
1189
1795
  0
1190
1796
  );
1797
+ const materialCapacity = readNonNegativeInteger("materialCapacity", options.materialCapacity, 0);
1191
1798
  const queueBytes = tilePixelCapacity * RAY_RECORD_BYTES;
1192
1799
  const hitBytes = tilePixelCapacity * HIT_RECORD_BYTES;
1193
1800
  const accumulationBytes = tilePixelCapacity * ACCUMULATION_RECORD_BYTES;
1194
1801
  const pathVertexBytes = tilePixelCapacity * (maxDepth + 1) * PATH_VERTEX_RECORD_BYTES;
1195
1802
  const sceneObjectBytes = sceneObjectCapacity * SCENE_OBJECT_RECORD_BYTES;
1196
1803
  const triangleBytes = triangleCapacity * TRIANGLE_RECORD_BYTES;
1804
+ const materialTableBytes = materialCapacity * GPU_MATERIAL_RECORD_BYTES;
1197
1805
  const bvhNodeBytes = bvhNodeCapacity * BVH_NODE_RECORD_BYTES;
1198
1806
  const bvhLeafReferenceBytes = bvhLeafSortCapacity * BVH_LEAF_REF_RECORD_BYTES;
1199
1807
  const emissiveTriangleMetadataBytes =
@@ -1209,6 +1817,7 @@ export function estimateWavefrontPathTracingMemory(options = {}) {
1209
1817
  pathVertexBytes,
1210
1818
  sceneObjectBytes,
1211
1819
  triangleBytes,
1820
+ materialTableBytes,
1212
1821
  bvhNodeBytes,
1213
1822
  bvhLeafReferenceBytes,
1214
1823
  emissiveTriangleMetadataBytes,
@@ -1223,6 +1832,7 @@ export function estimateWavefrontPathTracingMemory(options = {}) {
1223
1832
  pathVertexBytes +
1224
1833
  sceneObjectBytes +
1225
1834
  triangleBytes +
1835
+ materialTableBytes +
1226
1836
  bvhNodeBytes +
1227
1837
  bvhLeafReferenceBytes +
1228
1838
  emissiveTriangleMetadataBytes +
@@ -1244,7 +1854,7 @@ export function createWavefrontPathTracingComputeConfig(options = {}) {
1244
1854
  const samplesPerPixel = clamp(
1245
1855
  readPositiveInteger("samplesPerPixel", options.samplesPerPixel, DEFAULT_SAMPLES_PER_PIXEL),
1246
1856
  1,
1247
- 64
1857
+ MAX_SAMPLES_PER_PIXEL
1248
1858
  );
1249
1859
  const maxFramePassesPerSubmission = clamp(
1250
1860
  readPositiveInteger(
@@ -1262,9 +1872,13 @@ export function createWavefrontPathTracingComputeConfig(options = {}) {
1262
1872
  );
1263
1873
  const meshes = normalizeMeshes(options);
1264
1874
  const meshSourceShape = estimateMeshSourceShape(meshes);
1875
+ const gpuMaterialSource =
1876
+ meshes.length > 0
1877
+ ? createWavefrontGpuMaterialSource(meshes)
1878
+ : createWavefrontGpuMaterialSource([]);
1265
1879
  const gpuMeshSource =
1266
1880
  meshes.length > 0
1267
- ? createWavefrontGpuMeshSource(meshes)
1881
+ ? createWavefrontGpuMeshSource(meshes, gpuMaterialSource)
1268
1882
  : createWavefrontGpuMeshSource([]);
1269
1883
  const meshAcceleration =
1270
1884
  accelerationBuildMode === "cpu-debug"
@@ -1360,6 +1974,7 @@ export function createWavefrontPathTracingComputeConfig(options = {}) {
1360
1974
  accelerationBuildMode,
1361
1975
  gpuAccelerationBuildRequired: accelerationBuildMode === "gpu" && triangleCount > 0,
1362
1976
  gpuMeshSource,
1977
+ gpuMaterialSource,
1363
1978
  meshAcceleration,
1364
1979
  emissiveTriangleIndices,
1365
1980
  emissiveTriangleCount: emissiveTriangleIndices.count,
@@ -1390,6 +2005,7 @@ export function createWavefrontPathTracingComputeConfig(options = {}) {
1390
2005
  maxDepth,
1391
2006
  sceneObjectCapacity,
1392
2007
  triangleCapacity,
2008
+ materialCapacity: gpuMaterialSource.count,
1393
2009
  bvhNodeCapacity,
1394
2010
  bvhLeafSortCapacity,
1395
2011
  emissiveTriangleCapacity: emissiveTriangleIndices.capacity,
@@ -1483,6 +2099,24 @@ export function packWavefrontSceneObjects(sceneObjects, capacity = sceneObjects.
1483
2099
  object.opacity,
1484
2100
  object.ior,
1485
2101
  ]);
2102
+ writeVec4(floatView, byteOffset + 96, [
2103
+ object.sheenColor[0] ?? 0,
2104
+ object.sheenColor[1] ?? 0,
2105
+ object.sheenColor[2] ?? 0,
2106
+ object.clearcoat,
2107
+ ]);
2108
+ writeVec4(floatView, byteOffset + 112, [
2109
+ object.clearcoatRoughness,
2110
+ object.specular,
2111
+ object.transmission,
2112
+ 0,
2113
+ ]);
2114
+ writeVec4(floatView, byteOffset + 128, [
2115
+ object.specularColor[0] ?? 1,
2116
+ object.specularColor[1] ?? 1,
2117
+ object.specularColor[2] ?? 1,
2118
+ 1,
2119
+ ]);
1486
2120
  });
1487
2121
 
1488
2122
  return Object.freeze({
@@ -1511,7 +2145,7 @@ export function packWavefrontTriangles(triangles, capacity = triangles.length) {
1511
2145
  uintView[u32 + 3] = triangle.flags;
1512
2146
  uintView[u32 + 4] = triangle.materialRefId;
1513
2147
  uintView[u32 + 5] = triangle.mediumRefId;
1514
- uintView[u32 + 6] = 0;
2148
+ uintView[u32 + 6] = triangle.materialSlot ?? 0;
1515
2149
  uintView[u32 + 7] = 0;
1516
2150
  writeVec4(floatView, byteOffset + 32, [...triangle.v0, 0]);
1517
2151
  writeVec4(floatView, byteOffset + 48, [...triangle.v1, 0]);
@@ -1524,6 +2158,15 @@ export function packWavefrontTriangles(triangles, capacity = triangles.length) {
1524
2158
  writeVec4(floatView, byteOffset + 160, triangle.color);
1525
2159
  writeVec4(floatView, byteOffset + 176, triangle.emission);
1526
2160
  writeVec4(floatView, byteOffset + 192, triangle.material);
2161
+ writeVec4(floatView, byteOffset + 208, triangle.materialResponse);
2162
+ writeVec4(floatView, byteOffset + 224, triangle.materialExtension ?? [0.08, 1, 0, 0]);
2163
+ writeVec4(floatView, byteOffset + 240, triangle.specularColor ?? [1, 1, 1, 1]);
2164
+ writeVec4(floatView, byteOffset + 256, triangle.baseColorAtlas ?? [0, 0, 1, 1]);
2165
+ writeVec4(floatView, byteOffset + 272, triangle.metallicRoughnessAtlas ?? [0, 0, 1, 1]);
2166
+ writeVec4(floatView, byteOffset + 288, triangle.normalAtlas ?? [0, 0, 1, 1]);
2167
+ writeVec4(floatView, byteOffset + 304, triangle.occlusionAtlas ?? [0, 0, 1, 1]);
2168
+ writeVec4(floatView, byteOffset + 320, triangle.emissiveAtlas ?? [0, 0, 1, 1]);
2169
+ writeVec4(floatView, byteOffset + 336, triangle.textureSettings ?? [1, 1, 1, 0]);
1527
2170
  });
1528
2171
 
1529
2172
  return Object.freeze({
@@ -1623,6 +2266,12 @@ function createConfigPayload(config, tile, frameIndex, buildRange = {}) {
1623
2266
  0,
1624
2267
  0,
1625
2268
  ]);
2269
+ writeVec4(floatView, 304, [
2270
+ config.environmentMap.width ?? 1,
2271
+ config.environmentMap.height ?? 1,
2272
+ config.environmentMap.mipLevelCount ?? 1,
2273
+ config.environmentMap.hasImportanceData ? 1 : 0,
2274
+ ]);
1626
2275
  return bytes;
1627
2276
  }
1628
2277
 
@@ -1813,6 +2462,7 @@ export function intersectWavefrontReferenceTriangle(ray, triangle, options = {})
1813
2462
  color: triangle.color,
1814
2463
  emission: triangle.emission,
1815
2464
  material: triangle.material,
2465
+ materialResponse: triangle.materialResponse,
1816
2466
  });
1817
2467
  }
1818
2468
 
@@ -1840,6 +2490,7 @@ function createWavefrontReferenceEnvironmentHit(config, ray) {
1840
2490
  color: Object.freeze([0, 0, 0, 0]),
1841
2491
  emission: radiance,
1842
2492
  material: Object.freeze([1, 0, 1, 1]),
2493
+ materialResponse: Object.freeze([0, 0, 0, 0]),
1843
2494
  });
1844
2495
  }
1845
2496
 
@@ -1932,44 +2583,26 @@ function environmentMapIntegerScale(data) {
1932
2583
  return 1;
1933
2584
  }
1934
2585
 
1935
- function readEnvironmentMapComponent(data, index, fallback, integerScale = 1) {
1936
- if (!data || index >= data.length) {
1937
- return fallback;
2586
+ function environmentMapHasSamplingData(environmentMap) {
2587
+ if (!environmentMap || !environmentMap.data) {
2588
+ return false;
1938
2589
  }
1939
- const value = Number(data[index]);
1940
- return Number.isFinite(value) ? Math.max(0, value) * integerScale : fallback;
2590
+ const width = Math.max(1, environmentMap.width ?? 1);
2591
+ const height = Math.max(1, environmentMap.height ?? 1);
2592
+ return environmentMap.data.length >= width * height * 4;
1941
2593
  }
1942
2594
 
1943
- function createEnvironmentMapUploadBytes(environmentMap, fallbackColor) {
1944
- const width = Math.max(1, environmentMap.width);
1945
- const height = Math.max(1, environmentMap.height);
1946
- const rowBytes = width * 8;
1947
- const bytesPerRow = alignTo(rowBytes, 256);
2595
+ function createRgba8TextureUpload(source) {
2596
+ const width = Math.max(1, Math.trunc(source.width));
2597
+ const height = Math.max(1, Math.trunc(source.height));
2598
+ const bytesPerRow = alignTo(width * 4, 256);
1948
2599
  const bytes = new Uint8Array(bytesPerRow * height);
1949
- const data = environmentMap.data;
1950
- const integerScale = environmentMapIntegerScale(data);
1951
- const view = new DataView(bytes.buffer);
1952
- const writeComponent = (targetOffset, sourceOffset, fallback) => {
1953
- view.setUint16(
1954
- targetOffset,
1955
- float32ToFloat16Bits(
1956
- readEnvironmentMapComponent(data, sourceOffset, fallback, integerScale)
1957
- ),
1958
- true
1959
- );
1960
- };
1961
-
2600
+ const data = source.data instanceof Uint8Array ? source.data : new Uint8Array(source.data);
1962
2601
  for (let y = 0; y < height; y += 1) {
1963
- for (let x = 0; x < width; x += 1) {
1964
- const sourceOffset = (y * width + x) * 4;
1965
- const targetOffset = y * bytesPerRow + x * 8;
1966
- writeComponent(targetOffset, sourceOffset, fallbackColor[0]);
1967
- writeComponent(targetOffset + 2, sourceOffset + 1, fallbackColor[1]);
1968
- writeComponent(targetOffset + 4, sourceOffset + 2, fallbackColor[2]);
1969
- writeComponent(targetOffset + 6, sourceOffset + 3, fallbackColor[3] ?? 1);
1970
- }
2602
+ const sourceOffset = y * width * 4;
2603
+ const targetOffset = y * bytesPerRow;
2604
+ bytes.set(data.subarray(sourceOffset, sourceOffset + width * 4), targetOffset);
1971
2605
  }
1972
-
1973
2606
  return Object.freeze({
1974
2607
  bytes,
1975
2608
  bytesPerRow,
@@ -1978,42 +2611,506 @@ function createEnvironmentMapUploadBytes(environmentMap, fallbackColor) {
1978
2611
  });
1979
2612
  }
1980
2613
 
1981
- function createEnvironmentMapResource(device, constants, environmentMap, fallbackColor) {
1982
- if (environmentMap.view) {
1983
- return Object.freeze({
1984
- view: environmentMap.view,
1985
- sampler: environmentMap.sampler ?? device.createSampler({
1986
- label: "plasius.wavefront.environmentMapSampler",
1987
- addressModeU: "repeat",
1988
- addressModeV: "clamp-to-edge",
1989
- magFilter: "linear",
1990
- minFilter: "linear",
1991
- }),
1992
- texture: null,
1993
- ownsTexture: false,
1994
- });
2614
+ function readEnvironmentMapComponent(data, index, fallback, integerScale = 1) {
2615
+ if (!data || index >= data.length) {
2616
+ return fallback;
1995
2617
  }
2618
+ const value = Number(data[index]);
2619
+ return Number.isFinite(value) ? Math.max(0, value) * integerScale : fallback;
2620
+ }
1996
2621
 
1997
- if (environmentMap.texture && typeof environmentMap.texture.createView === "function") {
1998
- return Object.freeze({
1999
- view: environmentMap.texture.createView(),
2000
- sampler: environmentMap.sampler ?? device.createSampler({
2001
- label: "plasius.wavefront.environmentMapSampler",
2002
- addressModeU: "repeat",
2003
- addressModeV: "clamp-to-edge",
2004
- magFilter: "linear",
2005
- minFilter: "linear",
2006
- }),
2007
- texture: environmentMap.texture,
2008
- ownsTexture: false,
2009
- });
2010
- }
2622
+ function reflectVector(direction, normal) {
2623
+ return subtract(direction, scale(normal, 2 * dot(direction, normal)));
2624
+ }
2011
2625
 
2012
- const upload = createEnvironmentMapUploadBytes(environmentMap, fallbackColor);
2013
- const texture = device.createTexture({
2014
- label: environmentMap.enabled
2015
- ? "plasius.wavefront.environmentMap"
2016
- : "plasius.wavefront.environmentMapFallback",
2626
+ function buildOrthonormalBasis(normal) {
2627
+ const tangentFallback = Math.abs(normal[1]) < 0.999 ? [0, 1, 0] : [1, 0, 0];
2628
+ const tangent = normalize(cross(tangentFallback, normal), [1, 0, 0]);
2629
+ const bitangent = normalize(cross(normal, tangent), [0, 0, 1]);
2630
+ return { tangent, bitangent };
2631
+ }
2632
+
2633
+ function localToWorld(local, normal) {
2634
+ const basis = buildOrthonormalBasis(normal);
2635
+ return normalize(
2636
+ add(
2637
+ add(scale(basis.tangent, local[0]), scale(basis.bitangent, local[1])),
2638
+ scale(normal, local[2])
2639
+ ),
2640
+ normal
2641
+ );
2642
+ }
2643
+
2644
+ function radicalInverseVdc(bits) {
2645
+ let value = bits >>> 0;
2646
+ value = ((value << 16) | (value >>> 16)) >>> 0;
2647
+ value = (((value & 0x55555555) << 1) | ((value & 0xaaaaaaaa) >>> 1)) >>> 0;
2648
+ value = (((value & 0x33333333) << 2) | ((value & 0xcccccccc) >>> 2)) >>> 0;
2649
+ value = (((value & 0x0f0f0f0f) << 4) | ((value & 0xf0f0f0f0) >>> 4)) >>> 0;
2650
+ value = (((value & 0x00ff00ff) << 8) | ((value & 0xff00ff00) >>> 8)) >>> 0;
2651
+ return value * 2.3283064365386963e-10;
2652
+ }
2653
+
2654
+ function hammersley(index, count) {
2655
+ return [index / Math.max(count, 1), radicalInverseVdc(index)];
2656
+ }
2657
+
2658
+ function importanceSampleGgx(sample, roughness, normal) {
2659
+ const alpha = Math.max(roughness * roughness, 0.0001);
2660
+ const phi = 2 * Math.PI * sample[0];
2661
+ const cosTheta = Math.sqrt((1 - sample[1]) / (1 + (alpha * alpha - 1) * sample[1]));
2662
+ const sinTheta = Math.sqrt(Math.max(0, 1 - cosTheta * cosTheta));
2663
+ const halfVector = localToWorld(
2664
+ [Math.cos(phi) * sinTheta, Math.sin(phi) * sinTheta, cosTheta],
2665
+ normal
2666
+ );
2667
+ return normalize(halfVector, normal);
2668
+ }
2669
+
2670
+ function distributionGgx(nDotH, roughness) {
2671
+ const alpha = Math.max(roughness * roughness, 0.0001);
2672
+ const alpha2 = alpha * alpha;
2673
+ const denom = (nDotH * nDotH) * (alpha2 - 1) + 1;
2674
+ return alpha2 / Math.max(Math.PI * denom * denom, 0.000001);
2675
+ }
2676
+
2677
+ function geometrySchlickGgx(nDotV, roughness) {
2678
+ const k = ((roughness + 1) * (roughness + 1)) / 8;
2679
+ return nDotV / Math.max(nDotV * (1 - k) + k, 0.000001);
2680
+ }
2681
+
2682
+ function geometrySmith(nDotV, nDotL, roughness) {
2683
+ return geometrySchlickGgx(nDotV, roughness) * geometrySchlickGgx(nDotL, roughness);
2684
+ }
2685
+
2686
+ function integrateBrdfSample(nDotV, roughness, sampleCount) {
2687
+ const viewDirection = [Math.sqrt(Math.max(0, 1 - nDotV * nDotV)), 0, nDotV];
2688
+ const normal = [0, 0, 1];
2689
+ let scaleTerm = 0;
2690
+ let biasTerm = 0;
2691
+ for (let index = 0; index < sampleCount; index += 1) {
2692
+ const xi = hammersley(index, sampleCount);
2693
+ const halfVector = importanceSampleGgx(xi, roughness, normal);
2694
+ const vDotH = Math.max(dot(viewDirection, halfVector), 0);
2695
+ const lightDirection = normalize(
2696
+ subtract(scale(halfVector, 2 * vDotH), viewDirection),
2697
+ normal
2698
+ );
2699
+ const nDotL = Math.max(lightDirection[2], 0);
2700
+ const nDotH = Math.max(halfVector[2], 0);
2701
+ if (nDotL <= 0 || nDotH <= 0 || vDotH <= 0) {
2702
+ continue;
2703
+ }
2704
+ const geometry = geometrySmith(nDotV, nDotL, roughness);
2705
+ const visibility = (geometry * vDotH) / Math.max(nDotH * nDotV, 0.000001);
2706
+ const fresnel = (1 - vDotH) ** 5;
2707
+ scaleTerm += (1 - fresnel) * visibility;
2708
+ biasTerm += fresnel * visibility;
2709
+ }
2710
+ return [scaleTerm / sampleCount, biasTerm / sampleCount];
2711
+ }
2712
+
2713
+ function createBrdfLutUploadBytes(size = DEFAULT_BRDF_LUT_SIZE, sampleCount = 1024) {
2714
+ const cacheKey = `${Math.max(1, Math.trunc(size))}:${Math.max(1, Math.trunc(sampleCount))}`;
2715
+ const cached = BRDF_LUT_UPLOAD_CACHE.get(cacheKey);
2716
+ if (cached) {
2717
+ return cached;
2718
+ }
2719
+ const width = Math.max(1, Math.trunc(size));
2720
+ const height = Math.max(1, Math.trunc(size));
2721
+ const rowBytes = width * 8;
2722
+ const bytesPerRow = alignTo(rowBytes, 256);
2723
+ const bytes = new Uint8Array(bytesPerRow * height);
2724
+ const view = new DataView(bytes.buffer);
2725
+ for (let y = 0; y < height; y += 1) {
2726
+ const roughness = (y + 0.5) / height;
2727
+ for (let x = 0; x < width; x += 1) {
2728
+ const nDotV = Math.max((x + 0.5) / width, 0.0001);
2729
+ const [scaleTerm, biasTerm] = integrateBrdfSample(nDotV, roughness, sampleCount);
2730
+ const offset = y * bytesPerRow + x * 8;
2731
+ view.setUint16(offset, float32ToFloat16Bits(scaleTerm), true);
2732
+ view.setUint16(offset + 2, float32ToFloat16Bits(biasTerm), true);
2733
+ view.setUint16(offset + 4, float32ToFloat16Bits(0), true);
2734
+ view.setUint16(offset + 6, float32ToFloat16Bits(1), true);
2735
+ }
2736
+ }
2737
+ const upload = Object.freeze({ bytes, bytesPerRow, width, height });
2738
+ BRDF_LUT_UPLOAD_CACHE.set(cacheKey, upload);
2739
+ return upload;
2740
+ }
2741
+
2742
+ function createLinearEnvironmentPixels(environmentMap, fallbackColor) {
2743
+ const width = Math.max(1, environmentMap.width);
2744
+ const height = Math.max(1, environmentMap.height);
2745
+ const pixels = new Float32Array(width * height * 4);
2746
+ const data = environmentMap.data;
2747
+ const integerScale = environmentMapIntegerScale(data);
2748
+ for (let index = 0; index < width * height; index += 1) {
2749
+ const sourceOffset = index * 4;
2750
+ const targetOffset = index * 4;
2751
+ pixels[targetOffset] = readEnvironmentMapComponent(data, sourceOffset, fallbackColor[0], integerScale);
2752
+ pixels[targetOffset + 1] = readEnvironmentMapComponent(data, sourceOffset + 1, fallbackColor[1], integerScale);
2753
+ pixels[targetOffset + 2] = readEnvironmentMapComponent(data, sourceOffset + 2, fallbackColor[2], integerScale);
2754
+ pixels[targetOffset + 3] = readEnvironmentMapComponent(data, sourceOffset + 3, fallbackColor[3] ?? 1, integerScale);
2755
+ }
2756
+ return pixels;
2757
+ }
2758
+
2759
+ function environmentUvToDirection(u, v, rotationRadians = 0) {
2760
+ const angle = (u - rotationRadians / (2 * Math.PI) - 0.5) * 2 * Math.PI;
2761
+ const theta = v * Math.PI;
2762
+ const sinTheta = Math.sin(theta);
2763
+ return [
2764
+ Math.cos(angle) * sinTheta,
2765
+ Math.cos(theta),
2766
+ Math.sin(angle) * sinTheta,
2767
+ ];
2768
+ }
2769
+
2770
+ function sampleEnvironmentPixelsBilinear(pixels, width, height, u, v) {
2771
+ const wrappedU = ((u % 1) + 1) % 1;
2772
+ const clampedV = clamp(v, 0, 1);
2773
+ const x = wrappedU * width - 0.5;
2774
+ const y = clampedV * height - 0.5;
2775
+ const x0 = ((Math.floor(x) % width) + width) % width;
2776
+ const y0 = clamp(Math.floor(y), 0, height - 1);
2777
+ const x1 = (x0 + 1) % width;
2778
+ const y1 = clamp(y0 + 1, 0, height - 1);
2779
+ const tx = x - Math.floor(x);
2780
+ const ty = y - Math.floor(y);
2781
+ const read = (px, py) => {
2782
+ const offset = (py * width + px) * 4;
2783
+ return [pixels[offset], pixels[offset + 1], pixels[offset + 2], pixels[offset + 3]];
2784
+ };
2785
+ const a = read(x0, y0);
2786
+ const b = read(x1, y0);
2787
+ const c = read(x0, y1);
2788
+ const d = read(x1, y1);
2789
+ const mixPair = (first, second, factor) => first * (1 - factor) + second * factor;
2790
+ return [
2791
+ mixPair(mixPair(a[0], b[0], tx), mixPair(c[0], d[0], tx), ty),
2792
+ mixPair(mixPair(a[1], b[1], tx), mixPair(c[1], d[1], tx), ty),
2793
+ mixPair(mixPair(a[2], b[2], tx), mixPair(c[2], d[2], tx), ty),
2794
+ mixPair(mixPair(a[3], b[3], tx), mixPair(c[3], d[3], tx), ty),
2795
+ ];
2796
+ }
2797
+
2798
+ function directionToEnvironmentUv(direction, rotationRadians = 0) {
2799
+ const unitDirection = normalize(direction, [0, 1, 0]);
2800
+ const rotationTurns = rotationRadians / (2 * Math.PI);
2801
+ const u = ((((Math.atan2(unitDirection[2], unitDirection[0]) / (2 * Math.PI)) + 0.5 + rotationTurns) % 1) + 1) % 1;
2802
+ const v = Math.acos(clamp(unitDirection[1], -1, 1)) / Math.PI;
2803
+ return [u, clamp(v, 0, 1)];
2804
+ }
2805
+
2806
+ function sampleEnvironmentRadiance(pixels, width, height, direction, rotationRadians = 0) {
2807
+ const [u, v] = directionToEnvironmentUv(direction, rotationRadians);
2808
+ return sampleEnvironmentPixelsBilinear(pixels, width, height, u, v);
2809
+ }
2810
+
2811
+ function createFloat16RgbaUploadFromLevels(levels) {
2812
+ return levels.map((level) => {
2813
+ const rowBytes = level.width * 8;
2814
+ const bytesPerRow = alignTo(rowBytes, 256);
2815
+ const bytes = new Uint8Array(bytesPerRow * level.height);
2816
+ const view = new DataView(bytes.buffer);
2817
+ for (let y = 0; y < level.height; y += 1) {
2818
+ for (let x = 0; x < level.width; x += 1) {
2819
+ const sourceOffset = (y * level.width + x) * 4;
2820
+ const targetOffset = y * bytesPerRow + x * 8;
2821
+ view.setUint16(targetOffset, float32ToFloat16Bits(level.data[sourceOffset]), true);
2822
+ view.setUint16(targetOffset + 2, float32ToFloat16Bits(level.data[sourceOffset + 1]), true);
2823
+ view.setUint16(targetOffset + 4, float32ToFloat16Bits(level.data[sourceOffset + 2]), true);
2824
+ view.setUint16(targetOffset + 6, float32ToFloat16Bits(level.data[sourceOffset + 3]), true);
2825
+ }
2826
+ }
2827
+ return Object.freeze({ bytes, bytesPerRow, width: level.width, height: level.height });
2828
+ });
2829
+ }
2830
+
2831
+ function createPrefilteredEnvironmentLevels(environmentMap, fallbackColor) {
2832
+ const sourcePixels = createLinearEnvironmentPixels(environmentMap, fallbackColor);
2833
+ const sourceWidth = Math.max(1, environmentMap.width);
2834
+ const sourceHeight = Math.max(1, environmentMap.height);
2835
+ const mipLevelCount = Math.max(1, Math.floor(Math.log2(Math.max(sourceWidth, sourceHeight))) + 1);
2836
+ const levels = [
2837
+ Object.freeze({
2838
+ width: sourceWidth,
2839
+ height: sourceHeight,
2840
+ data: sourcePixels,
2841
+ }),
2842
+ ];
2843
+ for (let mipLevel = 1; mipLevel < mipLevelCount; mipLevel += 1) {
2844
+ const width = Math.max(1, sourceWidth >> mipLevel);
2845
+ const height = Math.max(1, sourceHeight >> mipLevel);
2846
+ const roughness = mipLevelCount <= 1 ? 0 : mipLevel / (mipLevelCount - 1);
2847
+ const data = new Float32Array(width * height * 4);
2848
+ const sampleCount = roughness < 0.25 ? 64 : roughness < 0.6 ? 96 : 128;
2849
+ for (let y = 0; y < height; y += 1) {
2850
+ for (let x = 0; x < width; x += 1) {
2851
+ const direction = environmentUvToDirection((x + 0.5) / width, (y + 0.5) / height, environmentMap.rotationRadians);
2852
+ const normal = normalize(direction, [0, 1, 0]);
2853
+ const viewDirection = normal;
2854
+ let totalWeight = 0;
2855
+ const accum = [0, 0, 0];
2856
+ for (let sampleIndex = 0; sampleIndex < sampleCount; sampleIndex += 1) {
2857
+ const xi = hammersley(sampleIndex, sampleCount);
2858
+ const halfVector = importanceSampleGgx(xi, roughness, normal);
2859
+ const viewDotHalf = Math.max(dot(viewDirection, halfVector), 0);
2860
+ const lightDirection = normalize(
2861
+ subtract(scale(halfVector, 2 * viewDotHalf), viewDirection),
2862
+ normal
2863
+ );
2864
+ const nDotL = Math.max(dot(normal, lightDirection), 0);
2865
+ if (nDotL <= 0.000001) {
2866
+ continue;
2867
+ }
2868
+ const radiance = sampleEnvironmentRadiance(
2869
+ sourcePixels,
2870
+ sourceWidth,
2871
+ sourceHeight,
2872
+ lightDirection,
2873
+ environmentMap.rotationRadians
2874
+ );
2875
+ accum[0] += radiance[0] * nDotL;
2876
+ accum[1] += radiance[1] * nDotL;
2877
+ accum[2] += radiance[2] * nDotL;
2878
+ totalWeight += nDotL;
2879
+ }
2880
+ const offset = (y * width + x) * 4;
2881
+ data[offset] = accum[0] / Math.max(totalWeight, 0.000001);
2882
+ data[offset + 1] = accum[1] / Math.max(totalWeight, 0.000001);
2883
+ data[offset + 2] = accum[2] / Math.max(totalWeight, 0.000001);
2884
+ data[offset + 3] = 1;
2885
+ }
2886
+ }
2887
+ levels.push(Object.freeze({ width, height, data }));
2888
+ }
2889
+ return Object.freeze({
2890
+ levels,
2891
+ mipLevelCount,
2892
+ width: sourceWidth,
2893
+ height: sourceHeight,
2894
+ });
2895
+ }
2896
+
2897
+ function createEnvironmentSamplingTables(environmentMap, fallbackColor) {
2898
+ if (!environmentMapHasSamplingData(environmentMap)) {
2899
+ return Object.freeze({
2900
+ width: 1,
2901
+ height: 1,
2902
+ pdf: new Float32Array([1]),
2903
+ marginalCdf: new Float32Array([1]),
2904
+ conditionalCdf: new Float32Array([1]),
2905
+ hasImportanceData: false,
2906
+ });
2907
+ }
2908
+ const pixels = createLinearEnvironmentPixels(environmentMap, fallbackColor);
2909
+ const width = Math.max(1, environmentMap.width);
2910
+ const height = Math.max(1, environmentMap.height);
2911
+ const pdf = new Float32Array(width * height);
2912
+ const marginalCdf = new Float32Array(height);
2913
+ const conditionalCdf = new Float32Array(width * height);
2914
+ const rowSums = new Float32Array(height);
2915
+ let totalWeight = 0;
2916
+ for (let y = 0; y < height; y += 1) {
2917
+ const theta = ((y + 0.5) / height) * Math.PI;
2918
+ const sinTheta = Math.max(Math.sin(theta), 0.0001);
2919
+ let rowWeight = 0;
2920
+ for (let x = 0; x < width; x += 1) {
2921
+ const offset = (y * width + x) * 4;
2922
+ const luminance = pixels[offset] * 0.2126 + pixels[offset + 1] * 0.7152 + pixels[offset + 2] * 0.0722;
2923
+ const weight = Math.max(luminance * sinTheta, 0.000001);
2924
+ pdf[y * width + x] = weight;
2925
+ rowWeight += weight;
2926
+ conditionalCdf[y * width + x] = rowWeight;
2927
+ }
2928
+ rowSums[y] = rowWeight;
2929
+ totalWeight += rowWeight;
2930
+ if (rowWeight > 0) {
2931
+ for (let x = 0; x < width; x += 1) {
2932
+ conditionalCdf[y * width + x] /= rowWeight;
2933
+ }
2934
+ } else {
2935
+ for (let x = 0; x < width; x += 1) {
2936
+ conditionalCdf[y * width + x] = (x + 1) / width;
2937
+ }
2938
+ }
2939
+ marginalCdf[y] = totalWeight;
2940
+ }
2941
+ for (let y = 0; y < height; y += 1) {
2942
+ marginalCdf[y] /= Math.max(totalWeight, 0.000001);
2943
+ }
2944
+ for (let index = 0; index < pdf.length; index += 1) {
2945
+ pdf[index] /= Math.max(totalWeight, 0.000001);
2946
+ }
2947
+ return Object.freeze({
2948
+ width,
2949
+ height,
2950
+ pdf,
2951
+ marginalCdf,
2952
+ conditionalCdf,
2953
+ hasImportanceData: true,
2954
+ });
2955
+ }
2956
+
2957
+ function createEnvironmentMapUploadBytes(environmentMap, fallbackColor) {
2958
+ const width = Math.max(1, environmentMap.width);
2959
+ const height = Math.max(1, environmentMap.height);
2960
+ const rowBytes = width * 8;
2961
+ const bytesPerRow = alignTo(rowBytes, 256);
2962
+ const bytes = new Uint8Array(bytesPerRow * height);
2963
+ const data = environmentMap.data;
2964
+ const integerScale = environmentMapIntegerScale(data);
2965
+ const view = new DataView(bytes.buffer);
2966
+ const writeComponent = (targetOffset, sourceOffset, fallback) => {
2967
+ view.setUint16(
2968
+ targetOffset,
2969
+ float32ToFloat16Bits(
2970
+ readEnvironmentMapComponent(data, sourceOffset, fallback, integerScale)
2971
+ ),
2972
+ true
2973
+ );
2974
+ };
2975
+
2976
+ for (let y = 0; y < height; y += 1) {
2977
+ for (let x = 0; x < width; x += 1) {
2978
+ const sourceOffset = (y * width + x) * 4;
2979
+ const targetOffset = y * bytesPerRow + x * 8;
2980
+ writeComponent(targetOffset, sourceOffset, fallbackColor[0]);
2981
+ writeComponent(targetOffset + 2, sourceOffset + 1, fallbackColor[1]);
2982
+ writeComponent(targetOffset + 4, sourceOffset + 2, fallbackColor[2]);
2983
+ writeComponent(targetOffset + 6, sourceOffset + 3, fallbackColor[3] ?? 1);
2984
+ }
2985
+ }
2986
+
2987
+ const upload = Object.freeze({
2988
+ bytes,
2989
+ bytesPerRow,
2990
+ width,
2991
+ height,
2992
+ });
2993
+ return upload;
2994
+ }
2995
+
2996
+ function createEnvironmentMapResource(device, constants, environmentMap, fallbackColor) {
2997
+ if (environmentMap.view) {
2998
+ return Object.freeze({
2999
+ view: environmentMap.view,
3000
+ sampler: environmentMap.sampler ?? device.createSampler({
3001
+ label: "plasius.wavefront.environmentMapSampler",
3002
+ addressModeU: "repeat",
3003
+ addressModeV: "clamp-to-edge",
3004
+ magFilter: "linear",
3005
+ minFilter: "linear",
3006
+ mipmapFilter: "linear",
3007
+ }),
3008
+ texture: null,
3009
+ ownsTexture: false,
3010
+ width: Math.max(1, environmentMap.width),
3011
+ height: Math.max(1, environmentMap.height),
3012
+ mipLevelCount: Math.max(1, environmentMap.mipLevelCount ?? 1),
3013
+ });
3014
+ }
3015
+
3016
+ if (environmentMap.texture && typeof environmentMap.texture.createView === "function") {
3017
+ return Object.freeze({
3018
+ view: environmentMap.texture.createView(),
3019
+ sampler: environmentMap.sampler ?? device.createSampler({
3020
+ label: "plasius.wavefront.environmentMapSampler",
3021
+ addressModeU: "repeat",
3022
+ addressModeV: "clamp-to-edge",
3023
+ magFilter: "linear",
3024
+ minFilter: "linear",
3025
+ mipmapFilter: "linear",
3026
+ }),
3027
+ texture: environmentMap.texture,
3028
+ ownsTexture: false,
3029
+ width: Math.max(1, environmentMap.width),
3030
+ height: Math.max(1, environmentMap.height),
3031
+ mipLevelCount: Math.max(1, environmentMap.mipLevelCount ?? 1),
3032
+ });
3033
+ }
3034
+
3035
+ const prefiltered = createPrefilteredEnvironmentLevels(environmentMap, fallbackColor);
3036
+ const uploads = createFloat16RgbaUploadFromLevels(prefiltered.levels);
3037
+ const texture = device.createTexture({
3038
+ label: environmentMap.enabled
3039
+ ? "plasius.wavefront.environmentMap"
3040
+ : "plasius.wavefront.environmentMapFallback",
3041
+ size: { width: prefiltered.width, height: prefiltered.height },
3042
+ format: "rgba16float",
3043
+ mipLevelCount: prefiltered.mipLevelCount,
3044
+ usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST,
3045
+ });
3046
+ uploads.forEach((upload, mipLevel) => {
3047
+ device.queue.writeTexture(
3048
+ { texture, mipLevel },
3049
+ upload.bytes,
3050
+ { bytesPerRow: upload.bytesPerRow, rowsPerImage: upload.height },
3051
+ { width: upload.width, height: upload.height, depthOrArrayLayers: 1 }
3052
+ );
3053
+ });
3054
+ return Object.freeze({
3055
+ view: texture.createView(),
3056
+ sampler: environmentMap.sampler ?? device.createSampler({
3057
+ label: "plasius.wavefront.environmentMapSampler",
3058
+ addressModeU: "repeat",
3059
+ addressModeV: "clamp-to-edge",
3060
+ magFilter: "linear",
3061
+ minFilter: "linear",
3062
+ mipmapFilter: "linear",
3063
+ }),
3064
+ texture,
3065
+ ownsTexture: true,
3066
+ width: prefiltered.width,
3067
+ height: prefiltered.height,
3068
+ mipLevelCount: prefiltered.mipLevelCount,
3069
+ });
3070
+ }
3071
+
3072
+ function createEnvironmentSamplingTextureResource(device, constants, environmentMap, fallbackColor) {
3073
+ const tables = createEnvironmentSamplingTables(environmentMap, fallbackColor);
3074
+ const rowBytes = tables.width * 8;
3075
+ const bytesPerRow = alignTo(rowBytes, 256);
3076
+ const bytes = new Uint8Array(bytesPerRow * tables.height);
3077
+ const view = new DataView(bytes.buffer);
3078
+ for (let y = 0; y < tables.height; y += 1) {
3079
+ for (let x = 0; x < tables.width; x += 1) {
3080
+ const probability = tables.pdf[y * tables.width + x];
3081
+ const conditional = tables.conditionalCdf[y * tables.width + x];
3082
+ const marginal = tables.marginalCdf[y];
3083
+ const offset = y * bytesPerRow + x * 8;
3084
+ view.setUint16(offset, float32ToFloat16Bits(probability), true);
3085
+ view.setUint16(offset + 2, float32ToFloat16Bits(conditional), true);
3086
+ view.setUint16(offset + 4, float32ToFloat16Bits(marginal), true);
3087
+ view.setUint16(offset + 6, float32ToFloat16Bits(1), true);
3088
+ }
3089
+ }
3090
+ const texture = device.createTexture({
3091
+ label: "plasius.wavefront.environmentSampling",
3092
+ size: { width: tables.width, height: tables.height },
3093
+ format: "rgba16float",
3094
+ usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST,
3095
+ });
3096
+ device.queue.writeTexture(
3097
+ { texture },
3098
+ bytes,
3099
+ { bytesPerRow, rowsPerImage: tables.height },
3100
+ { width: tables.width, height: tables.height, depthOrArrayLayers: 1 }
3101
+ );
3102
+ return Object.freeze({
3103
+ view: texture.createView(),
3104
+ texture,
3105
+ ownsTexture: true,
3106
+ hasImportanceData: tables.hasImportanceData,
3107
+ });
3108
+ }
3109
+
3110
+ function createBrdfLutResource(device, constants, size = DEFAULT_BRDF_LUT_SIZE) {
3111
+ const upload = createBrdfLutUploadBytes(size);
3112
+ const texture = device.createTexture({
3113
+ label: "plasius.wavefront.brdfLut",
2017
3114
  size: { width: upload.width, height: upload.height },
2018
3115
  format: "rgba16float",
2019
3116
  usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST,
@@ -2026,15 +3123,38 @@ function createEnvironmentMapResource(device, constants, environmentMap, fallbac
2026
3123
  );
2027
3124
  return Object.freeze({
2028
3125
  view: texture.createView(),
2029
- sampler: environmentMap.sampler ?? device.createSampler({
2030
- label: "plasius.wavefront.environmentMapSampler",
2031
- addressModeU: "repeat",
3126
+ sampler: device.createSampler({
3127
+ label: "plasius.wavefront.brdfLutSampler",
3128
+ addressModeU: "clamp-to-edge",
2032
3129
  addressModeV: "clamp-to-edge",
2033
3130
  magFilter: "linear",
2034
3131
  minFilter: "linear",
2035
3132
  }),
2036
3133
  texture,
2037
3134
  ownsTexture: true,
3135
+ width: upload.width,
3136
+ height: upload.height,
3137
+ });
3138
+ }
3139
+
3140
+ function createAtlasTextureResource(device, constants, atlas, label) {
3141
+ const upload = createRgba8TextureUpload(atlas);
3142
+ const texture = device.createTexture({
3143
+ label,
3144
+ size: { width: upload.width, height: upload.height },
3145
+ format: "rgba8unorm",
3146
+ usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST,
3147
+ });
3148
+ device.queue.writeTexture(
3149
+ { texture },
3150
+ upload.bytes,
3151
+ { bytesPerRow: upload.bytesPerRow, rowsPerImage: upload.height },
3152
+ { width: upload.width, height: upload.height, depthOrArrayLayers: 1 }
3153
+ );
3154
+ return Object.freeze({
3155
+ texture,
3156
+ view: texture.createView(),
3157
+ ownsTexture: true,
2038
3158
  });
2039
3159
  }
2040
3160
 
@@ -2084,6 +3204,26 @@ async function createComputePipeline(device, shaderModule, layout, entryPoint, l
2084
3204
  }
2085
3205
  }
2086
3206
 
3207
+ async function assertShaderModuleCompiles(shaderModule, label) {
3208
+ if (typeof shaderModule?.compilationInfo !== "function") {
3209
+ return;
3210
+ }
3211
+ const info = await shaderModule.compilationInfo();
3212
+ const messages = Array.isArray(info?.messages) ? info.messages : [];
3213
+ const errors = messages.filter((message) => message?.type === "error");
3214
+ if (errors.length <= 0) {
3215
+ return;
3216
+ }
3217
+ const diagnostics = errors
3218
+ .map((message) => {
3219
+ const line = Number.isFinite(message.lineNum) ? message.lineNum : "?";
3220
+ const column = Number.isFinite(message.linePos) ? message.linePos : "?";
3221
+ return `line ${line}:${column} ${message.message}`;
3222
+ })
3223
+ .join("\n");
3224
+ throw new Error(`WGSL compilation preflight failed for ${label}:\n${diagnostics}`);
3225
+ }
3226
+
2087
3227
  async function createRenderPipeline(device, descriptor) {
2088
3228
  if (typeof device.createRenderPipelineAsync === "function") {
2089
3229
  return device.createRenderPipelineAsync(descriptor);
@@ -2093,6 +3233,7 @@ async function createRenderPipeline(device, descriptor) {
2093
3233
 
2094
3234
  const WAVEFRONT_COMPUTE_WGSL = `
2095
3235
  const RAY_FLAG_GUIDED_EMISSIVE: u32 = 1u;
3236
+ const RAY_FLAG_DELTA_SAMPLE: u32 = 2u;
2096
3237
 
2097
3238
  struct RayRecord {
2098
3239
  rayId: u32,
@@ -2118,11 +3259,12 @@ struct HitRecord {
2118
3259
  primitiveId: u32,
2119
3260
  materialRefId: u32,
2120
3261
  mediumRefId: u32,
3262
+ materialSlot: u32,
2121
3263
  pad0: u32,
2122
3264
  pad1: u32,
2123
- pad2: u32,
2124
3265
  distance: f32,
2125
- pad3: vec3<f32>,
3266
+ occlusion: f32,
3267
+ pad2: vec2<f32>,
2126
3268
  position: vec4<f32>,
2127
3269
  geometricNormal: vec4<f32>,
2128
3270
  shadingNormal: vec4<f32>,
@@ -2131,6 +3273,9 @@ struct HitRecord {
2131
3273
  color: vec4<f32>,
2132
3274
  emission: vec4<f32>,
2133
3275
  material: vec4<f32>,
3276
+ materialResponse: vec4<f32>,
3277
+ materialExtension: vec4<f32>,
3278
+ specularColor: vec4<f32>,
2134
3279
  };
2135
3280
 
2136
3281
  struct SceneObject {
@@ -2143,6 +3288,9 @@ struct SceneObject {
2143
3288
  color: vec4<f32>,
2144
3289
  emission: vec4<f32>,
2145
3290
  material: vec4<f32>,
3291
+ materialResponse: vec4<f32>,
3292
+ materialExtension: vec4<f32>,
3293
+ specularColor: vec4<f32>,
2146
3294
  };
2147
3295
 
2148
3296
  struct TriangleRecord {
@@ -2152,7 +3300,7 @@ struct TriangleRecord {
2152
3300
  flags: u32,
2153
3301
  materialRefId: u32,
2154
3302
  mediumRefId: u32,
2155
- pad0: u32,
3303
+ materialSlot: u32,
2156
3304
  pad1: u32,
2157
3305
  v0: vec4<f32>,
2158
3306
  v1: vec4<f32>,
@@ -2165,6 +3313,15 @@ struct TriangleRecord {
2165
3313
  color: vec4<f32>,
2166
3314
  emission: vec4<f32>,
2167
3315
  material: vec4<f32>,
3316
+ materialResponse: vec4<f32>,
3317
+ materialExtension: vec4<f32>,
3318
+ specularColor: vec4<f32>,
3319
+ baseColorAtlas: vec4<f32>,
3320
+ metallicRoughnessAtlas: vec4<f32>,
3321
+ normalAtlas: vec4<f32>,
3322
+ occlusionAtlas: vec4<f32>,
3323
+ emissiveAtlas: vec4<f32>,
3324
+ textureSettings: vec4<f32>,
2168
3325
  };
2169
3326
 
2170
3327
  struct BvhNode {
@@ -2185,10 +3342,10 @@ struct BvhLeafRef {
2185
3342
 
2186
3343
  struct ScatterResult {
2187
3344
  direction: vec4<f32>,
3345
+ pdf: f32,
2188
3346
  flags: u32,
2189
3347
  pad0: u32,
2190
3348
  pad1: u32,
2191
- pad2: u32,
2192
3349
  };
2193
3350
 
2194
3351
  struct MeshVertex {
@@ -2209,10 +3366,19 @@ struct MeshRange {
2209
3366
  triangleCount: u32,
2210
3367
  firstVertex: u32,
2211
3368
  vertexCount: u32,
2212
- pad0: u32,
3369
+ materialSlot: u32,
2213
3370
  color: vec4<f32>,
2214
3371
  emission: vec4<f32>,
2215
3372
  material: vec4<f32>,
3373
+ materialResponse: vec4<f32>,
3374
+ materialExtension: vec4<f32>,
3375
+ specularColor: vec4<f32>,
3376
+ baseColorAtlas: vec4<f32>,
3377
+ metallicRoughnessAtlas: vec4<f32>,
3378
+ normalAtlas: vec4<f32>,
3379
+ occlusionAtlas: vec4<f32>,
3380
+ emissiveAtlas: vec4<f32>,
3381
+ textureSettings: vec4<f32>,
2216
3382
  };
2217
3383
 
2218
3384
  struct FrameConfig {
@@ -2253,6 +3419,7 @@ struct FrameConfig {
2253
3419
  _portalPad1: u32,
2254
3420
  environmentMapSettings: vec4<f32>,
2255
3421
  pathResolveSettings: vec4<f32>,
3422
+ environmentMapMeta: vec4<f32>,
2256
3423
  };
2257
3424
 
2258
3425
  struct Counters {
@@ -2315,6 +3482,15 @@ struct EnvironmentPortal {
2315
3482
  @group(0) @binding(20) var environmentMapTexture: texture_2d<f32>;
2316
3483
  @group(0) @binding(21) var environmentMapSampler: sampler;
2317
3484
  @group(0) @binding(22) var<storage, read_write> pathVertices: array<vec4<f32>>;
3485
+ @group(0) @binding(23) var baseColorAtlasTexture: texture_2d<f32>;
3486
+ @group(0) @binding(24) var metallicRoughnessAtlasTexture: texture_2d<f32>;
3487
+ @group(0) @binding(25) var normalAtlasTexture: texture_2d<f32>;
3488
+ @group(0) @binding(26) var occlusionAtlasTexture: texture_2d<f32>;
3489
+ @group(0) @binding(27) var emissiveAtlasTexture: texture_2d<f32>;
3490
+ @group(0) @binding(28) var materialAtlasSampler: sampler;
3491
+ @group(0) @binding(29) var brdfLutTexture: texture_2d<f32>;
3492
+ @group(0) @binding(30) var brdfLutSampler: sampler;
3493
+ @group(0) @binding(31) var environmentSamplingTexture: texture_2d<f32>;
2318
3494
 
2319
3495
  fn hash_u32(value: u32) -> u32 {
2320
3496
  var x = value;
@@ -2351,6 +3527,146 @@ fn safe_normalize(value: vec3<f32>, fallback: vec3<f32>) -> vec3<f32> {
2351
3527
  return value / len;
2352
3528
  }
2353
3529
 
3530
+ struct TangentBasis {
3531
+ tangent: vec3<f32>,
3532
+ bitangent: vec3<f32>,
3533
+ };
3534
+
3535
+ struct SurfaceMaterialSample {
3536
+ color: vec4<f32>,
3537
+ emission: vec4<f32>,
3538
+ material: vec4<f32>,
3539
+ materialResponse: vec4<f32>,
3540
+ materialExtension: vec4<f32>,
3541
+ specularColor: vec4<f32>,
3542
+ shadingNormal: vec3<f32>,
3543
+ occlusion: f32,
3544
+ };
3545
+
3546
+ fn srgb_to_linear_channel(value: f32) -> f32 {
3547
+ if (value <= 0.04045) {
3548
+ return value / 12.92;
3549
+ }
3550
+ return pow((value + 0.055) / 1.055, 2.4);
3551
+ }
3552
+
3553
+ fn srgb_to_linear_vec3(value: vec3<f32>) -> vec3<f32> {
3554
+ return vec3<f32>(
3555
+ srgb_to_linear_channel(value.x),
3556
+ srgb_to_linear_channel(value.y),
3557
+ srgb_to_linear_channel(value.z)
3558
+ );
3559
+ }
3560
+
3561
+ fn wrap_uv(uv: vec2<f32>) -> vec2<f32> {
3562
+ return fract(fract(uv) + vec2<f32>(1.0));
3563
+ }
3564
+
3565
+ fn atlas_sample_uv(rect: vec4<f32>, uv: vec2<f32>) -> vec2<f32> {
3566
+ let local = wrap_uv(uv);
3567
+ let clamped = clamp(local, vec2<f32>(0.001), vec2<f32>(0.999));
3568
+ return rect.xy + clamped * rect.zw;
3569
+ }
3570
+
3571
+ fn sample_atlas(textureRef: texture_2d<f32>, rect: vec4<f32>, uv: vec2<f32>) -> vec4<f32> {
3572
+ return textureSampleLevel(textureRef, materialAtlasSampler, atlas_sample_uv(rect, uv), 0.0);
3573
+ }
3574
+
3575
+ fn build_triangle_tangent_basis(
3576
+ triangle: TriangleRecord,
3577
+ fallbackNormal: vec3<f32>
3578
+ ) -> TangentBasis {
3579
+ let edge1 = triangle.v1.xyz - triangle.v0.xyz;
3580
+ let edge2 = triangle.v2.xyz - triangle.v0.xyz;
3581
+ let uv0 = triangle.uv0uv1.xy;
3582
+ let uv1 = triangle.uv0uv1.zw;
3583
+ let uv2 = triangle.uv2Pad.xy;
3584
+ let deltaUv1 = uv1 - uv0;
3585
+ let deltaUv2 = uv2 - uv0;
3586
+ let determinant = deltaUv1.x * deltaUv2.y - deltaUv1.y * deltaUv2.x;
3587
+ if (abs(determinant) <= 0.000001) {
3588
+ let tangentFallback = select(vec3<f32>(0.0, 1.0, 0.0), vec3<f32>(1.0, 0.0, 0.0), abs(fallbackNormal.y) >= 0.999);
3589
+ let tangent = safe_normalize(cross(tangentFallback, fallbackNormal), vec3<f32>(1.0, 0.0, 0.0));
3590
+ let bitangent = safe_normalize(cross(fallbackNormal, tangent), vec3<f32>(0.0, 0.0, 1.0));
3591
+ return TangentBasis(tangent, bitangent);
3592
+ }
3593
+ let inverse = 1.0 / determinant;
3594
+ let tangent = safe_normalize(
3595
+ inverse * (edge1 * deltaUv2.y - edge2 * deltaUv1.y),
3596
+ vec3<f32>(1.0, 0.0, 0.0)
3597
+ );
3598
+ let bitangent = safe_normalize(
3599
+ inverse * (-edge1 * deltaUv2.x + edge2 * deltaUv1.x),
3600
+ vec3<f32>(0.0, 0.0, 1.0)
3601
+ );
3602
+ return TangentBasis(tangent, bitangent);
3603
+ }
3604
+
3605
+ fn sample_surface_material(
3606
+ triangle: TriangleRecord,
3607
+ uv: vec2<f32>,
3608
+ geometricNormal: vec3<f32>,
3609
+ shadingNormal: vec3<f32>
3610
+ ) -> SurfaceMaterialSample {
3611
+ let baseColorTexel = sample_atlas(baseColorAtlasTexture, triangle.baseColorAtlas, uv);
3612
+ let baseColor = vec4<f32>(
3613
+ clamp(triangle.color.rgb * srgb_to_linear_vec3(baseColorTexel.rgb), vec3<f32>(0.0), vec3<f32>(1.0)),
3614
+ clamp(triangle.color.a * baseColorTexel.a, 0.0, 1.0)
3615
+ );
3616
+ let metallicRoughnessTexel = sample_atlas(
3617
+ metallicRoughnessAtlasTexture,
3618
+ triangle.metallicRoughnessAtlas,
3619
+ uv
3620
+ );
3621
+ let normalTexel = sample_atlas(normalAtlasTexture, triangle.normalAtlas, uv);
3622
+ let occlusionTexel = sample_atlas(occlusionAtlasTexture, triangle.occlusionAtlas, uv);
3623
+ let emissiveTexel = sample_atlas(emissiveAtlasTexture, triangle.emissiveAtlas, uv);
3624
+ let normalScale = clamp(triangle.textureSettings.x, 0.0, 1.0);
3625
+ let tangentBasis = build_triangle_tangent_basis(triangle, geometricNormal);
3626
+ let tangentNormal = safe_normalize(
3627
+ vec3<f32>(
3628
+ (normalTexel.x * 2.0 - 1.0) * normalScale,
3629
+ (normalTexel.y * 2.0 - 1.0) * normalScale,
3630
+ 1.0 + ((normalTexel.z * 2.0 - 1.0) - 1.0) * normalScale
3631
+ ),
3632
+ vec3<f32>(0.0, 0.0, 1.0)
3633
+ );
3634
+ let mappedNormal = safe_normalize(
3635
+ tangentBasis.tangent * tangentNormal.x +
3636
+ tangentBasis.bitangent * tangentNormal.y +
3637
+ shadingNormal * tangentNormal.z,
3638
+ shadingNormal
3639
+ );
3640
+ let emission = vec4<f32>(
3641
+ max(
3642
+ triangle.emission.rgb *
3643
+ srgb_to_linear_vec3(emissiveTexel.rgb) *
3644
+ max(triangle.textureSettings.z, 0.0),
3645
+ vec3<f32>(0.0)
3646
+ ),
3647
+ clamp(triangle.emission.a * emissiveTexel.a, 0.0, 1.0)
3648
+ );
3649
+ return SurfaceMaterialSample(
3650
+ baseColor,
3651
+ emission,
3652
+ vec4<f32>(
3653
+ clamp(triangle.material.x * metallicRoughnessTexel.y, 0.0, 1.0),
3654
+ clamp(triangle.material.y * metallicRoughnessTexel.z, 0.0, 1.0),
3655
+ clamp(triangle.material.z * baseColor.a, 0.0, 1.0),
3656
+ clamp(triangle.material.w, 1.0, 3.0)
3657
+ ),
3658
+ triangle.materialResponse,
3659
+ triangle.materialExtension,
3660
+ triangle.specularColor,
3661
+ repair_shading_normal(geometricNormal, mappedNormal),
3662
+ clamp(
3663
+ mix(1.0, occlusionTexel.x, clamp(triangle.textureSettings.y, 0.0, 1.0)),
3664
+ 0.0,
3665
+ 1.0
3666
+ )
3667
+ );
3668
+ }
3669
+
2354
3670
  fn saturate(value: f32) -> f32 {
2355
3671
  return clamp(value, 0.0, 1.0);
2356
3672
  }
@@ -2359,6 +3675,10 @@ fn max_component(value: vec3<f32>) -> f32 {
2359
3675
  return max(max(value.x, value.y), value.z);
2360
3676
  }
2361
3677
 
3678
+ fn radiance_luminance(value: vec3<f32>) -> f32 {
3679
+ return dot(value, vec3<f32>(0.2126, 0.7152, 0.0722));
3680
+ }
3681
+
2362
3682
  fn environment_map_enabled() -> bool {
2363
3683
  return config.environmentMapSettings.x > 0.5;
2364
3684
  }
@@ -2487,6 +3807,343 @@ fn environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
2487
3807
  select(vec3<f32>(1.0), portalScale, portalHit);
2488
3808
  }
2489
3809
 
3810
+ fn direct_environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
3811
+ let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
3812
+ let portalScale = environment_portal_radiance_scale(origin, rayDirection);
3813
+ let portalHit = max_component(portalScale) > 0.0001;
3814
+ if (
3815
+ config.environmentPortalCount > 0u &&
3816
+ config.environmentPortalMode == 2u &&
3817
+ !portalHit
3818
+ ) {
3819
+ return vec3<f32>(0.0);
3820
+ }
3821
+ return base_environment_radiance(rayDirection) *
3822
+ select(vec3<f32>(1.0), portalScale, portalHit);
3823
+ }
3824
+
3825
+ fn radical_inverse_vdc(bitsValue: u32) -> f32 {
3826
+ var bits = bitsValue;
3827
+ bits = (bits << 16u) | (bits >> 16u);
3828
+ bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xaaaaaaaau) >> 1u);
3829
+ bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xccccccccu) >> 2u);
3830
+ bits = ((bits & 0x0f0f0f0fu) << 4u) | ((bits & 0xf0f0f0f0u) >> 4u);
3831
+ bits = ((bits & 0x00ff00ffu) << 8u) | ((bits & 0xff00ff00u) >> 8u);
3832
+ return f32(bits) * 2.3283064365386963e-10;
3833
+ }
3834
+
3835
+ fn hammersley_2d(index: u32, count: u32) -> vec2<f32> {
3836
+ return vec2<f32>(f32(index) / max(f32(count), 1.0), radical_inverse_vdc(index));
3837
+ }
3838
+
3839
+ fn build_basis_tangent(normal: vec3<f32>) -> vec3<f32> {
3840
+ let tangentFallback = select(vec3<f32>(0.0, 1.0, 0.0), vec3<f32>(1.0, 0.0, 0.0), abs(normal.y) >= 0.999);
3841
+ return safe_normalize(cross(tangentFallback, normal), vec3<f32>(1.0, 0.0, 0.0));
3842
+ }
3843
+
3844
+ fn local_to_world(local: vec3<f32>, normal: vec3<f32>) -> vec3<f32> {
3845
+ let tangent = build_basis_tangent(normal);
3846
+ let bitangent = safe_normalize(cross(normal, tangent), vec3<f32>(0.0, 0.0, 1.0));
3847
+ return safe_normalize(tangent * local.x + bitangent * local.y + normal * local.z, normal);
3848
+ }
3849
+
3850
+ fn cosine_sample_hemisphere(sample: vec2<f32>, normal: vec3<f32>) -> vec3<f32> {
3851
+ let phi = 6.28318530718 * sample.x;
3852
+ let radius = sqrt(sample.y);
3853
+ let x = cos(phi) * radius;
3854
+ let y = sin(phi) * radius;
3855
+ let z = sqrt(max(0.0, 1.0 - sample.y));
3856
+ return local_to_world(vec3<f32>(x, y, z), normal);
3857
+ }
3858
+
3859
+ fn importance_sample_ggx(sample: vec2<f32>, roughness: f32, normal: vec3<f32>) -> vec3<f32> {
3860
+ let alpha = max(roughness * roughness, 0.0001);
3861
+ let phi = 6.28318530718 * sample.x;
3862
+ let cosTheta = sqrt((1.0 - sample.y) / max(1.0 + (alpha * alpha - 1.0) * sample.y, 0.0001));
3863
+ let sinTheta = sqrt(max(0.0, 1.0 - cosTheta * cosTheta));
3864
+ let localHalf = vec3<f32>(cos(phi) * sinTheta, sin(phi) * sinTheta, cosTheta);
3865
+ return local_to_world(localHalf, normal);
3866
+ }
3867
+
3868
+ fn distribution_ggx(normal: vec3<f32>, halfVector: vec3<f32>, roughness: f32) -> f32 {
3869
+ let alpha = max(roughness * roughness, 0.0001);
3870
+ let alpha2 = alpha * alpha;
3871
+ let nDotH = saturate(dot(normal, halfVector));
3872
+ let denominator = nDotH * nDotH * (alpha2 - 1.0) + 1.0;
3873
+ return alpha2 / max(3.14159265359 * denominator * denominator, 0.000001);
3874
+ }
3875
+
3876
+ fn geometry_schlick_ggx(nDotValue: f32, roughness: f32) -> f32 {
3877
+ let k = ((roughness + 1.0) * (roughness + 1.0)) / 8.0;
3878
+ return nDotValue / max(nDotValue * (1.0 - k) + k, 0.000001);
3879
+ }
3880
+
3881
+ fn geometry_smith(normal: vec3<f32>, viewDirection: vec3<f32>, lightDirection: vec3<f32>, roughness: f32) -> f32 {
3882
+ let nDotV = saturate(dot(normal, viewDirection));
3883
+ let nDotL = saturate(dot(normal, lightDirection));
3884
+ return geometry_schlick_ggx(nDotV, roughness) * geometry_schlick_ggx(nDotL, roughness);
3885
+ }
3886
+
3887
+ fn fresnel_schlick(cosine: f32, f0: vec3<f32>) -> vec3<f32> {
3888
+ return f0 + (vec3<f32>(1.0) - f0) * pow(1.0 - cosine, 5.0);
3889
+ }
3890
+
3891
+ fn sample_brdf_lut(nDotV: f32, roughness: f32) -> vec2<f32> {
3892
+ let uv = vec2<f32>(clamp(nDotV, 0.0, 1.0), clamp(roughness, 0.0, 1.0));
3893
+ return textureSampleLevel(brdfLutTexture, brdfLutSampler, uv, 0.0).xy;
3894
+ }
3895
+
3896
+ fn prefiltered_environment_radiance(direction: vec3<f32>, roughness: f32) -> vec3<f32> {
3897
+ let uv = environment_map_uv(direction);
3898
+ let maxLevel = max(config.environmentMapMeta.z - 1.0, 0.0);
3899
+ let lod = clamp(roughness, 0.0, 1.0) * maxLevel;
3900
+ let texel = max(textureSampleLevel(environmentMapTexture, environmentMapSampler, uv, lod).rgb, vec3<f32>(0.0));
3901
+ return texel * max(config.environmentMapSettings.y, 0.0);
3902
+ }
3903
+
3904
+ fn environment_pdf_dimensions() -> vec2<u32> {
3905
+ return vec2<u32>(
3906
+ max(u32(config.environmentMapMeta.x), 1u),
3907
+ max(u32(config.environmentMapMeta.y), 1u)
3908
+ );
3909
+ }
3910
+
3911
+ fn environment_importance_sampling_enabled() -> bool {
3912
+ return config.environmentMapMeta.w > 0.5;
3913
+ }
3914
+
3915
+ fn uniform_sphere_pdf() -> f32 {
3916
+ return 1.0 / (4.0 * 3.14159265359);
3917
+ }
3918
+
3919
+ fn sample_uniform_sphere_direction(sample: vec2<f32>) -> vec3<f32> {
3920
+ let z = 1.0 - 2.0 * sample.y;
3921
+ let radial = sqrt(max(1.0 - z * z, 0.0));
3922
+ let phi = sample.x * 6.28318530718;
3923
+ return vec3<f32>(cos(phi) * radial, z, sin(phi) * radial);
3924
+ }
3925
+
3926
+ fn environment_sampling_texel(x: u32, y: u32) -> vec4<f32> {
3927
+ return textureLoad(environmentSamplingTexture, vec2<i32>(i32(x), i32(y)), 0);
3928
+ }
3929
+
3930
+ fn environment_pdf_texel(x: u32, y: u32) -> f32 {
3931
+ return environment_sampling_texel(x, y).x;
3932
+ }
3933
+
3934
+ fn environment_row_cdf_texel(y: u32) -> f32 {
3935
+ return environment_sampling_texel(0u, y).z;
3936
+ }
3937
+
3938
+ fn environment_column_cdf_texel(x: u32, y: u32) -> f32 {
3939
+ return environment_sampling_texel(x, y).y;
3940
+ }
3941
+
3942
+ fn environment_direction_pdf(direction: vec3<f32>) -> f32 {
3943
+ if (!environment_importance_sampling_enabled()) {
3944
+ return uniform_sphere_pdf();
3945
+ }
3946
+ let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
3947
+ let uv = environment_map_uv(rayDirection);
3948
+ let dimensions = environment_pdf_dimensions();
3949
+ let width = max(f32(dimensions.x), 1.0);
3950
+ let height = max(f32(dimensions.y), 1.0);
3951
+ let x = min(u32(uv.x * width), dimensions.x - 1u);
3952
+ let y = min(u32(uv.y * height), dimensions.y - 1u);
3953
+ let discretePdf = max(environment_pdf_texel(x, y), 0.0);
3954
+ let sinTheta = sqrt(max(1.0 - rayDirection.y * rayDirection.y, 0.0));
3955
+ let solidAngle = max((2.0 * 3.14159265359 * 3.14159265359 * sinTheta) / (width * height), 0.000001);
3956
+ return discretePdf / solidAngle;
3957
+ }
3958
+
3959
+ fn sample_row_cdf(count: u32, sampleValue: f32) -> u32 {
3960
+ if (count == 0u) {
3961
+ return 0u;
3962
+ }
3963
+ var low = 0u;
3964
+ var high = count - 1u;
3965
+ loop {
3966
+ if (low >= high) {
3967
+ break;
3968
+ }
3969
+ let mid = (low + high) / 2u;
3970
+ let cdfValue = environment_row_cdf_texel(mid);
3971
+ if (sampleValue <= cdfValue) {
3972
+ high = mid;
3973
+ } else {
3974
+ low = mid + 1u;
3975
+ }
3976
+ }
3977
+ return min(low, count - 1u);
3978
+ }
3979
+
3980
+ fn sample_column_cdf(row: u32, count: u32, sampleValue: f32) -> u32 {
3981
+ if (count == 0u) {
3982
+ return 0u;
3983
+ }
3984
+ var low = 0u;
3985
+ var high = count - 1u;
3986
+ loop {
3987
+ if (low >= high) {
3988
+ break;
3989
+ }
3990
+ let mid = (low + high) / 2u;
3991
+ let cdfValue = environment_column_cdf_texel(mid, row);
3992
+ if (sampleValue <= cdfValue) {
3993
+ high = mid;
3994
+ } else {
3995
+ low = mid + 1u;
3996
+ }
3997
+ }
3998
+ return min(low, count - 1u);
3999
+ }
4000
+
4001
+ struct EnvironmentSample {
4002
+ direction: vec3<f32>,
4003
+ radiance: vec3<f32>,
4004
+ pdf: f32,
4005
+ };
4006
+
4007
+ fn sample_environment_importance(sample: vec2<f32>) -> EnvironmentSample {
4008
+ if (!environment_importance_sampling_enabled()) {
4009
+ let direction = sample_uniform_sphere_direction(sample);
4010
+ return EnvironmentSample(direction, base_environment_radiance(direction), uniform_sphere_pdf());
4011
+ }
4012
+ let dimensions = environment_pdf_dimensions();
4013
+ let row = sample_row_cdf(dimensions.y, sample.y);
4014
+ let column = sample_column_cdf(row, dimensions.x, sample.x);
4015
+ let uv = vec2<f32>(
4016
+ (f32(column) + 0.5) / max(f32(dimensions.x), 1.0),
4017
+ (f32(row) + 0.5) / max(f32(dimensions.y), 1.0)
4018
+ );
4019
+ let theta = uv.y * 3.14159265359;
4020
+ let phi = (uv.x - 0.5 - config.environmentMapSettings.z / 6.28318530718) * 6.28318530718;
4021
+ let sinTheta = sin(theta);
4022
+ let direction = vec3<f32>(cos(phi) * sinTheta, cos(theta), sin(phi) * sinTheta);
4023
+ let pdf = environment_direction_pdf(direction);
4024
+ return EnvironmentSample(direction, base_environment_radiance(direction), pdf);
4025
+ }
4026
+
4027
+ fn power_heuristic(pdfA: f32, pdfB: f32) -> f32 {
4028
+ let a2 = pdfA * pdfA;
4029
+ let b2 = pdfB * pdfB;
4030
+ return a2 / max(a2 + b2, 0.000001);
4031
+ }
4032
+
4033
+ fn visible_environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
4034
+ let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
4035
+ let visible = !scene_visibility_blocked(origin, rayDirection, 1000000.0);
4036
+ return select(vec3<f32>(0.0), direct_environment_radiance(origin, rayDirection), visible);
4037
+ }
4038
+
4039
+ fn glossy_environment_direction(
4040
+ incidentDirection: vec3<f32>,
4041
+ normal: vec3<f32>,
4042
+ roughness: f32,
4043
+ normalBlendScale: f32
4044
+ ) -> vec3<f32> {
4045
+ let reflectionDirection = reflect(incidentDirection, normal);
4046
+ let blend = clamp(roughness * roughness * normalBlendScale, 0.0, 0.92);
4047
+ return safe_normalize(mix(reflectionDirection, normal, blend), normal);
4048
+ }
4049
+
4050
+ fn surface_glossiness(hit: HitRecord) -> f32 {
4051
+ let roughness = clamp(hit.material.x, 0.0, 1.0);
4052
+ let metallic = clamp(hit.material.y, 0.0, 1.0);
4053
+ let sheen = clamp(max_component(hit.materialResponse.xyz), 0.0, 1.0);
4054
+ let clearcoat = clamp(hit.materialResponse.w, 0.0, 1.0);
4055
+ let specularWeight = clamp(hit.materialExtension.y, 0.0, 1.0);
4056
+ let transmission = clamp(hit.materialExtension.z, 0.0, 1.0);
4057
+ let baseGloss =
4058
+ max(
4059
+ clearcoat,
4060
+ max(sheen * 0.72, max(specularWeight * (0.38 + metallic * 0.62), transmission))
4061
+ );
4062
+ return clamp(baseGloss * (1.0 - roughness * 0.72) + metallic * (1.0 - roughness) * 0.35, 0.0, 1.0);
4063
+ }
4064
+
4065
+ fn surface_specular_f0(hit: HitRecord, surfaceColor: vec3<f32>) -> vec3<f32> {
4066
+ let metallic = clamp(hit.material.y, 0.0, 1.0);
4067
+ let specularWeight = clamp(hit.materialExtension.y, 0.0, 1.0);
4068
+ let specularColor = clamp(hit.specularColor.xyz, vec3<f32>(0.0), vec3<f32>(1.0));
4069
+ let dielectricF0 = vec3<f32>(0.04) * specularWeight * specularColor;
4070
+ return mix(dielectricF0, surfaceColor, metallic);
4071
+ }
4072
+
4073
+ fn surface_bsdf_sampling_weights(hit: HitRecord) -> vec3<f32> {
4074
+ let metallic = clamp(hit.material.y, 0.0, 1.0);
4075
+ let clearcoat = clamp(hit.materialResponse.w, 0.0, 1.0);
4076
+ let specularWeight = clamp(hit.materialExtension.y, 0.0, 1.0);
4077
+ let diffuseWeight = clamp(
4078
+ (1.0 - metallic) * max(1.0 - specularWeight * 0.5 - clearcoat * 0.25, 0.15),
4079
+ 0.0,
4080
+ 1.0
4081
+ );
4082
+ let specWeight = clamp(max(metallic, specularWeight * 0.75) * (1.0 - clearcoat * 0.5), 0.0, 1.0);
4083
+ let clearcoatWeight = clamp(clearcoat, 0.0, 1.0);
4084
+ let totalWeight = max(diffuseWeight + specWeight + clearcoatWeight, 0.000001);
4085
+ return vec3<f32>(
4086
+ diffuseWeight / totalWeight,
4087
+ specWeight / totalWeight,
4088
+ clearcoatWeight / totalWeight
4089
+ );
4090
+ }
4091
+
4092
+ fn evaluate_surface_bsdf(hit: HitRecord, viewDirection: vec3<f32>, lightDirection: vec3<f32>) -> vec3<f32> {
4093
+ let normal = safe_normalize(hit.shadingNormal.xyz, vec3<f32>(0.0, 1.0, 0.0));
4094
+ let surfaceColor = clamp(max(hit.color.xyz, config.ambientColor.xyz * 0.35), vec3<f32>(0.0), vec3<f32>(1.0));
4095
+ let roughness = clamp(hit.material.x, 0.0, 1.0);
4096
+ let metallic = clamp(hit.material.y, 0.0, 1.0);
4097
+ let clearcoat = clamp(hit.materialResponse.w, 0.0, 1.0);
4098
+ let clearcoatRoughness = clamp(hit.materialExtension.x, 0.0, 1.0);
4099
+ let occlusion = clamp(hit.occlusion, 0.0, 1.0);
4100
+ let nDotV = saturate(dot(normal, viewDirection));
4101
+ let nDotL = saturate(dot(normal, lightDirection));
4102
+ if (nDotV <= 0.0 || nDotL <= 0.0) {
4103
+ return vec3<f32>(0.0);
4104
+ }
4105
+ let halfVector = safe_normalize(viewDirection + lightDirection, normal);
4106
+ let vDotH = saturate(dot(viewDirection, halfVector));
4107
+ let f0 = surface_specular_f0(hit, surfaceColor);
4108
+ let fresnel = fresnel_schlick(vDotH, f0);
4109
+ let distribution = distribution_ggx(normal, halfVector, roughness);
4110
+ let geometry = geometry_smith(normal, viewDirection, lightDirection, roughness);
4111
+ let specular = (distribution * geometry * fresnel) / max(4.0 * nDotV * nDotL, 0.000001);
4112
+ let diffuseWeight = (1.0 - metallic) * (1.0 - clearcoat * 0.24) * (1.0 - clamp(max_component(fresnel), 0.0, 0.98));
4113
+ let diffuse = surfaceColor * diffuseWeight / 3.14159265359;
4114
+ let clearcoatHalf = safe_normalize(viewDirection + lightDirection, normal);
4115
+ let clearcoatDistribution = distribution_ggx(normal, clearcoatHalf, max(clearcoatRoughness, 0.02));
4116
+ let clearcoatGeometry = geometry_smith(normal, viewDirection, lightDirection, max(clearcoatRoughness, 0.02));
4117
+ let clearcoatFresnel = fresnel_schlick(saturate(dot(viewDirection, clearcoatHalf)), vec3<f32>(0.04));
4118
+ let clearcoatTerm =
4119
+ (clearcoatDistribution * clearcoatGeometry * clearcoatFresnel) /
4120
+ max(4.0 * nDotV * nDotL, 0.000001) *
4121
+ clearcoat;
4122
+ return (diffuse + specular + clearcoatTerm) * mix(0.42, 1.0, occlusion);
4123
+ }
4124
+
4125
+ fn diffuse_pdf(normal: vec3<f32>, lightDirection: vec3<f32>) -> f32 {
4126
+ return saturate(dot(normal, lightDirection)) / 3.14159265359;
4127
+ }
4128
+
4129
+ fn ggx_pdf(normal: vec3<f32>, viewDirection: vec3<f32>, lightDirection: vec3<f32>, roughness: f32) -> f32 {
4130
+ let halfVector = safe_normalize(viewDirection + lightDirection, normal);
4131
+ let nDotH = saturate(dot(normal, halfVector));
4132
+ let vDotH = saturate(dot(viewDirection, halfVector));
4133
+ let distribution = distribution_ggx(normal, halfVector, roughness);
4134
+ return (distribution * nDotH) / max(4.0 * vDotH, 0.000001);
4135
+ }
4136
+
4137
+ fn evaluate_surface_bsdf_pdf(hit: HitRecord, viewDirection: vec3<f32>, lightDirection: vec3<f32>) -> f32 {
4138
+ let normal = safe_normalize(hit.shadingNormal.xyz, vec3<f32>(0.0, 1.0, 0.0));
4139
+ let roughness = clamp(hit.material.x, 0.0, 1.0);
4140
+ let weights = surface_bsdf_sampling_weights(hit);
4141
+ let diffuseTerm = diffuse_pdf(normal, lightDirection);
4142
+ let specTerm = ggx_pdf(normal, viewDirection, lightDirection, max(roughness, 0.02));
4143
+ let clearcoatTerm = ggx_pdf(normal, viewDirection, lightDirection, max(clamp(hit.materialExtension.x, 0.0, 1.0), 0.02));
4144
+ return weights.x * diffuseTerm + weights.y * specTerm + weights.z * clearcoatTerm;
4145
+ }
4146
+
2490
4147
  fn gated_environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
2491
4148
  let portalScale = environment_portal_radiance_scale(origin, safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0)));
2492
4149
  if (
@@ -2502,9 +4159,45 @@ fn gated_environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f
2502
4159
  fn surface_path_response(hit: HitRecord) -> vec3<f32> {
2503
4160
  let color = clamp(hit.color.xyz, vec3<f32>(0.0), vec3<f32>(1.0));
2504
4161
  let opacity = clamp(hit.material.z, 0.0, 1.0);
4162
+ let occlusion = clamp(hit.occlusion, 0.0, 1.0);
2505
4163
  let materialEnergy = select(0.68, 0.92, hit.materialKind == 1u || hit.materialKind == 2u);
2506
4164
  let transparentEnergy = select(materialEnergy, 0.9, hit.hitType == 3u);
2507
- return mix(vec3<f32>(1.0), color, max(opacity, 0.18)) * transparentEnergy;
4165
+ return mix(vec3<f32>(1.0), color, max(opacity, 0.18)) * transparentEnergy * mix(0.55, 1.0, occlusion);
4166
+ }
4167
+
4168
+ fn bounded_path_response_luminance(ray: RayRecord, hit: HitRecord) -> f32 {
4169
+ let daylightFloor = max(config.pathResolveSettings.y, 0.0) * 0.08;
4170
+ let hdriFloor = max(config.environmentMapSettings.w, 0.0) * 0.02;
4171
+ let sceneFloor = max(daylightFloor, hdriFloor);
4172
+ if (sceneFloor <= 0.000001) {
4173
+ return 0.0;
4174
+ }
4175
+ let bounceRatio = select(
4176
+ 0.0,
4177
+ f32(ray.bounce) / max(f32(config.maxDepth - 1u), 1.0),
4178
+ config.maxDepth > 1u
4179
+ );
4180
+ let bounceScale = 1.0 - bounceRatio * 0.55;
4181
+ let materialScale = select(1.0, 0.34, hit.materialKind == 1u || hit.materialKind == 2u);
4182
+ let transparentScale = select(materialScale, 0.58, hit.hitType == 3u);
4183
+ let opacityScale = mix(0.55, 1.0, clamp(hit.material.z, 0.0, 1.0));
4184
+ return sceneFloor * bounceScale * transparentScale * opacityScale;
4185
+ }
4186
+
4187
+ fn stabilize_surface_path_response(ray: RayRecord, hit: HitRecord, response: vec3<f32>) -> vec3<f32> {
4188
+ let minimumLuminance = bounded_path_response_luminance(ray, hit);
4189
+ let responseLuminance = radiance_luminance(response);
4190
+ if (minimumLuminance <= 0.000001 || responseLuminance >= minimumLuminance) {
4191
+ return response;
4192
+ }
4193
+ let tintBase = max(response, max(hit.color.xyz * 0.65, config.ambientColor.xyz * 0.35));
4194
+ let tint = tintBase / max(max_component(tintBase), 0.0001);
4195
+ let lifted = select(
4196
+ tint * minimumLuminance,
4197
+ response * (minimumLuminance / max(responseLuminance, 0.0001)),
4198
+ responseLuminance > 0.0001
4199
+ );
4200
+ return clamp(lifted, vec3<f32>(0.0), vec3<f32>(0.98));
2508
4201
  }
2509
4202
 
2510
4203
  fn sunlit_baseline_radiance(normal: vec3<f32>) -> vec3<f32> {
@@ -2523,12 +4216,24 @@ fn sunlit_baseline_radiance(normal: vec3<f32>) -> vec3<f32> {
2523
4216
  return clamp_sample_radiance(sunTint * baseline * skyFacing * directionalWeight * 0.04);
2524
4217
  }
2525
4218
 
2526
- fn terminal_surface_environment_source(hit: HitRecord) -> vec3<f32> {
4219
+ fn terminal_surface_environment_source(ray: RayRecord, hit: HitRecord) -> vec3<f32> {
2527
4220
  let normal = safe_normalize(hit.shadingNormal.xyz, vec3<f32>(0.0, 1.0, 0.0));
2528
- let normalEnvironment = gated_environment_radiance(
2529
- hit.position.xyz + normal * 0.003,
2530
- normal
4221
+ let origin = hit.position.xyz + normal * 0.003;
4222
+ let roughness = clamp(hit.material.x, 0.0, 1.0);
4223
+ let glossiness = surface_glossiness(hit);
4224
+ let normalEnvironment = gated_environment_radiance(origin, normal);
4225
+ let viewDirection = safe_normalize(-ray.direction.xyz, normal);
4226
+ let reflectionDirection = glossy_environment_direction(
4227
+ ray.direction.xyz,
4228
+ normal,
4229
+ roughness,
4230
+ mix(0.88, 0.38, glossiness)
2531
4231
  );
4232
+ let reflectionEnvironment = prefiltered_environment_radiance(reflectionDirection, roughness);
4233
+ let surfaceColor = clamp(max(hit.color.xyz, config.ambientColor.xyz * 0.35), vec3<f32>(0.0), vec3<f32>(1.0));
4234
+ let f0 = surface_specular_f0(hit, surfaceColor);
4235
+ let brdfTerm = sample_brdf_lut(saturate(dot(normal, viewDirection)), roughness);
4236
+ let specularEnvironment = reflectionEnvironment * (f0 * brdfTerm.x + vec3<f32>(brdfTerm.y));
2532
4237
  let sunlitFloor = sunlit_baseline_radiance(normal);
2533
4238
  let ambientFloor = select(
2534
4239
  max(config.ambientColor.xyz, sunlitFloor * 0.82),
@@ -2540,17 +4245,23 @@ fn terminal_surface_environment_source(hit: HitRecord) -> vec3<f32> {
2540
4245
  max(config.environmentMapSettings.w, max(0.12, config.pathResolveSettings.y * 0.42)),
2541
4246
  environment_map_enabled()
2542
4247
  );
2543
- let environmentFloor = max(ambientFloor, max(sunlitFloor, normalEnvironment * environmentInfluence));
4248
+ let glossyEnvironment = max(
4249
+ normalEnvironment,
4250
+ max(reflectionEnvironment * mix(0.24, 0.92, glossiness), specularEnvironment)
4251
+ );
4252
+ let environmentFloor = max(ambientFloor, max(sunlitFloor, glossyEnvironment * environmentInfluence));
2544
4253
  let materialFloor = select(0.7, 1.0, hit.materialKind == 0u || hit.materialKind == 3u);
2545
4254
  return clamp_sample_radiance(environmentFloor * materialFloor);
2546
4255
  }
2547
4256
 
2548
4257
  fn terminal_surface_environment_contribution(ray: RayRecord, hit: HitRecord) -> vec3<f32> {
2549
4258
  let surfaceColor = max(hit.color.xyz, config.ambientColor.xyz);
4259
+ let occlusion = mix(0.75, 1.0, clamp(hit.occlusion, 0.0, 1.0));
2550
4260
  return clamp_sample_radiance(
2551
4261
  ray.throughput.xyz *
2552
4262
  surfaceColor *
2553
- terminal_surface_environment_source(hit)
4263
+ terminal_surface_environment_source(ray, hit) *
4264
+ occlusion
2554
4265
  );
2555
4266
  }
2556
4267
 
@@ -2583,6 +4294,10 @@ fn direct_environment_portal_irradiance(origin: vec3<f32>, normal: vec3<f32>) ->
2583
4294
  );
2584
4295
  let area = max(portal.position.w, 0.0001);
2585
4296
  let distanceFalloff = clamp(area / max(distanceSquared, area * 0.25), 0.0, 2.5);
4297
+ let traceDistance = max(sqrt(distanceSquared) - 0.01, 0.01);
4298
+ if (scene_visibility_blocked(origin, direction, traceDistance)) {
4299
+ continue;
4300
+ }
2586
4301
  irradiance = irradiance +
2587
4302
  portal.color.rgb *
2588
4303
  portal.normal.w *
@@ -2594,48 +4309,79 @@ fn direct_environment_portal_irradiance(origin: vec3<f32>, normal: vec3<f32>) ->
2594
4309
  return irradiance;
2595
4310
  }
2596
4311
 
2597
- fn surface_direct_environment_contribution(ray: RayRecord, hit: HitRecord) -> vec3<f32> {
2598
- let normal = safe_normalize(hit.shadingNormal.xyz, vec3<f32>(0.0, 1.0, 0.0));
2599
- let origin = hit.position.xyz + normal * 0.003;
2600
- let viewDirection = safe_normalize(-ray.direction.xyz, normal);
2601
- let surfaceColor = clamp(max(hit.color.xyz, config.ambientColor.xyz * 0.35), vec3<f32>(0.0), vec3<f32>(1.0));
2602
- let roughness = clamp(hit.material.x, 0.0, 1.0);
2603
- let metallic = clamp(hit.material.y, 0.0, 1.0);
2604
-
2605
- let normalEnvironment = gated_environment_radiance(origin, normal);
2606
- let skyVisibility = 0.35 + saturate(normal.y * 0.5 + 0.5) * 0.45;
2607
- let sunlitFloor = sunlit_baseline_radiance(normal);
2608
- let ambientIrradiance = max(
2609
- select(config.ambientColor.xyz * 0.72, config.ambientColor.xyz * 0.28, environment_map_enabled()),
2610
- sunlitFloor * select(0.72, 0.45, environment_map_enabled())
2611
- );
2612
- let environmentIrradianceScale = select(
2613
- max(0.16, config.pathResolveSettings.y * 0.45),
2614
- max(config.environmentMapSettings.w, max(0.16, config.pathResolveSettings.y * 0.45)),
2615
- environment_map_enabled()
4312
+ fn visibility_test_ray(origin: vec3<f32>, direction: vec3<f32>) -> RayRecord {
4313
+ let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
4314
+ return RayRecord(
4315
+ 0u,
4316
+ 0u,
4317
+ 0u,
4318
+ 0u,
4319
+ 0u,
4320
+ 0u,
4321
+ 0u,
4322
+ 0u,
4323
+ vec4<f32>(origin, 1.0),
4324
+ vec4<f32>(rayDirection, 0.0),
4325
+ vec4<f32>(1.0, 1.0, 1.0, 1.0)
2616
4326
  );
2617
- let skyIrradiance = max(ambientIrradiance, normalEnvironment * skyVisibility * environmentIrradianceScale);
4327
+ }
2618
4328
 
2619
- let sunDirection = safe_normalize(
2620
- config.environmentSunDirectionIntensity.xyz,
2621
- vec3<f32>(0.0, 1.0, 0.0)
2622
- );
2623
- let sunFacing = saturate(dot(normal, sunDirection));
2624
- let sunRadiance = gated_environment_radiance(origin, sunDirection);
2625
- let sunIrradiance = sunRadiance * sunFacing * 0.2;
2626
- let portalIrradiance = direct_environment_portal_irradiance(origin, normal);
4329
+ fn scene_visibility_blocked(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> bool {
4330
+ let testRay = visibility_test_ray(origin, direction);
4331
+ let nearest = max(maxDistance, 0.001);
2627
4332
 
2628
- let diffuseWeight = select(1.0 - metallic * 0.65, 0.22, hit.materialKind == 1u);
2629
- let diffuse = surfaceColor * (skyIrradiance + sunIrradiance + portalIrradiance) * diffuseWeight;
4333
+ for (var objectIndex = 0u; objectIndex < config.sceneObjectCount; objectIndex = objectIndex + 1u) {
4334
+ let object = sceneObjects[objectIndex];
4335
+ var current = no_candidate();
4336
+ if (object.kind == 1u) {
4337
+ current = intersect_sphere(testRay, object);
4338
+ } else if (object.kind == 2u) {
4339
+ current = intersect_box(testRay, object);
4340
+ }
4341
+ if (current.hit == 1u && current.distance < nearest) {
4342
+ return true;
4343
+ }
4344
+ }
2630
4345
 
2631
- let halfVector = safe_normalize(sunDirection + viewDirection, normal);
2632
- let specularPower = 8.0 + (1.0 - roughness) * 96.0;
2633
- let specularFacing = pow(saturate(dot(normal, halfVector)), specularPower) * sunFacing;
2634
- let specularTint = mix(vec3<f32>(0.04), surfaceColor, metallic);
2635
- let specular = specularTint * sunRadiance * specularFacing * select(0.16, 0.48, hit.materialKind == 1u || hit.materialKind == 2u);
4346
+ let meshCandidate = intersect_bvh(testRay, nearest);
4347
+ return meshCandidate.hit == 1u && meshCandidate.distance < nearest;
4348
+ }
2636
4349
 
2637
- let bounceWeight = select(1.0, 0.38, ray.bounce > 0u);
2638
- return clamp_sample_radiance(ray.throughput.xyz * (diffuse + specular) * bounceWeight);
4350
+ fn surface_direct_environment_contribution(ray: RayRecord, hit: HitRecord) -> vec3<f32> {
4351
+ let normal = safe_normalize(hit.shadingNormal.xyz, vec3<f32>(0.0, 1.0, 0.0));
4352
+ let origin = hit.position.xyz + normal * 0.003;
4353
+ let viewDirection = safe_normalize(-ray.direction.xyz, normal);
4354
+ let lightSample = sample_environment_importance(vec2<f32>(
4355
+ random01(mix_seed(ray.sourcePixelId, ray.sampleId, ray.bounce, config.frameIndex, 41u)),
4356
+ random01(mix_seed(ray.sourcePixelId, ray.sampleId, ray.bounce, config.frameIndex, 43u))
4357
+ ));
4358
+ if (lightSample.pdf <= 0.000001) {
4359
+ return vec3<f32>(0.0);
4360
+ }
4361
+ let lightDirection = safe_normalize(lightSample.direction, normal);
4362
+ let nDotL = saturate(dot(normal, lightDirection));
4363
+ if (nDotL <= 0.000001) {
4364
+ return vec3<f32>(0.0);
4365
+ }
4366
+ if (scene_visibility_blocked(origin, lightDirection, 1000000.0)) {
4367
+ return vec3<f32>(0.0);
4368
+ }
4369
+ let incidentRadiance = direct_environment_radiance(origin, lightDirection);
4370
+ if (max_component(incidentRadiance) <= 0.000001) {
4371
+ return vec3<f32>(0.0);
4372
+ }
4373
+ let bsdf = evaluate_surface_bsdf(hit, viewDirection, lightDirection);
4374
+ if (max_component(bsdf) <= 0.000001) {
4375
+ return vec3<f32>(0.0);
4376
+ }
4377
+ let bsdfPdf = evaluate_surface_bsdf_pdf(hit, viewDirection, lightDirection);
4378
+ let misWeight = power_heuristic(lightSample.pdf, bsdfPdf);
4379
+ let contribution =
4380
+ ray.throughput.xyz *
4381
+ bsdf *
4382
+ incidentRadiance *
4383
+ (nDotL * misWeight / max(lightSample.pdf, 0.000001));
4384
+ return clamp_sample_radiance(contribution);
2639
4385
  }
2640
4386
 
2641
4387
  fn default_mesh_range() -> MeshRange {
@@ -2654,7 +4400,16 @@ fn default_mesh_range() -> MeshRange {
2654
4400
  0u,
2655
4401
  vec4<f32>(0.72, 0.72, 0.68, 1.0),
2656
4402
  vec4<f32>(0.0),
2657
- vec4<f32>(0.72, 0.0, 1.0, 1.45)
4403
+ vec4<f32>(0.72, 0.0, 1.0, 1.45),
4404
+ vec4<f32>(0.0, 0.0, 0.0, 0.08),
4405
+ vec4<f32>(1.0, 1.0, 1.0, 1.0),
4406
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
4407
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
4408
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
4409
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
4410
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
4411
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
4412
+ vec4<f32>(1.0, 1.0, 1.0, 0.0)
2658
4413
  );
2659
4414
  }
2660
4415
 
@@ -2750,7 +4505,7 @@ fn prepareMeshTrianglesAndLeaves(@builtin(global_invocation_id) globalId: vec3<u
2750
4505
  mesh.flags,
2751
4506
  mesh.materialRefId,
2752
4507
  mesh.mediumRefId,
2753
- 0u,
4508
+ mesh.materialSlot,
2754
4509
  0u,
2755
4510
  vec4<f32>(vertex0.position.xyz, 0.0),
2756
4511
  vec4<f32>(vertex1.position.xyz, 0.0),
@@ -2762,7 +4517,16 @@ fn prepareMeshTrianglesAndLeaves(@builtin(global_invocation_id) globalId: vec3<u
2762
4517
  vec4<f32>(uv2, 0.0, 0.0),
2763
4518
  mesh.color,
2764
4519
  mesh.emission,
2765
- mesh.material
4520
+ mesh.material,
4521
+ mesh.materialResponse,
4522
+ mesh.materialExtension,
4523
+ mesh.specularColor,
4524
+ mesh.baseColorAtlas,
4525
+ mesh.metallicRoughnessAtlas,
4526
+ mesh.normalAtlas,
4527
+ mesh.occlusionAtlas,
4528
+ mesh.emissiveAtlas,
4529
+ mesh.textureSettings
2766
4530
  );
2767
4531
 
2768
4532
  let leafBase = config.triangleCount - 1u;
@@ -2921,7 +4685,8 @@ fn make_miss(ray: RayRecord) -> HitRecord {
2921
4685
  0u,
2922
4686
  0u,
2923
4687
  -1.0,
2924
- vec3<f32>(0.0),
4688
+ 1.0,
4689
+ vec2<f32>(0.0),
2925
4690
  vec4<f32>(ray.origin.xyz + ray.direction.xyz * 1000.0, 1.0),
2926
4691
  vec4<f32>(-ray.direction.xyz, 0.0),
2927
4692
  vec4<f32>(-ray.direction.xyz, 0.0),
@@ -2929,7 +4694,10 @@ fn make_miss(ray: RayRecord) -> HitRecord {
2929
4694
  vec4<f32>(0.0),
2930
4695
  vec4<f32>(radiance, 1.0),
2931
4696
  vec4<f32>(0.0),
2932
- vec4<f32>(1.0, 0.0, 1.0, 1.0)
4697
+ vec4<f32>(1.0, 0.0, 1.0, 1.0),
4698
+ vec4<f32>(0.0, 0.0, 0.0, 0.08),
4699
+ vec4<f32>(0.08, 1.0, 0.0, 0.0),
4700
+ vec4<f32>(1.0, 1.0, 1.0, 1.0)
2933
4701
  );
2934
4702
  }
2935
4703
 
@@ -3224,6 +4992,19 @@ fn denoise_range_space(value: vec3<f32>) -> vec3<f32> {
3224
4992
  return value / (vec3<f32>(1.0) + value);
3225
4993
  }
3226
4994
 
4995
+ fn denoise_sample_count() -> f32 {
4996
+ return clamp(1.0 / max(config.projectionAndSampling.z, 0.000001), 1.0, 256.0);
4997
+ }
4998
+
4999
+ fn denoise_strength() -> f32 {
5000
+ let spp = denoise_sample_count();
5001
+ return clamp(0.44 / sqrt(spp), 0.08, 0.44);
5002
+ }
5003
+
5004
+ fn denoise_kernel_radius() -> i32 {
5005
+ return select(1i, 2i, denoise_sample_count() < 2.5);
5006
+ }
5007
+
3227
5008
  @compute @workgroup_size(64)
3228
5009
  fn generatePrimaryRays(@builtin(global_invocation_id) globalId: vec3<u32>) {
3229
5010
  let index = globalId.x;
@@ -3262,7 +5043,10 @@ fn intersectActiveQueue(@builtin(global_invocation_id) globalId: vec3<u32>) {
3262
5043
  vec4<f32>(0.0),
3263
5044
  vec4<f32>(0.0),
3264
5045
  vec4<f32>(0.0),
3265
- vec4<f32>(1.0, 0.0, 1.0, 1.0)
5046
+ vec4<f32>(1.0, 0.0, 1.0, 1.0),
5047
+ vec4<f32>(0.0, 0.0, 0.0, 0.08),
5048
+ vec4<f32>(0.08, 1.0, 0.0, 0.0),
5049
+ vec4<f32>(1.0, 1.0, 1.0, 1.0)
3266
5050
  );
3267
5051
  var candidate = no_candidate();
3268
5052
  var hitTriangle = TriangleRecord(
@@ -3284,7 +5068,16 @@ fn intersectActiveQueue(@builtin(global_invocation_id) globalId: vec3<u32>) {
3284
5068
  vec4<f32>(0.0),
3285
5069
  vec4<f32>(0.0),
3286
5070
  vec4<f32>(0.0),
3287
- vec4<f32>(1.0, 0.0, 1.0, 1.0)
5071
+ vec4<f32>(1.0, 0.0, 1.0, 1.0),
5072
+ vec4<f32>(0.0, 0.0, 0.0, 0.08),
5073
+ vec4<f32>(0.08, 1.0, 0.0, 0.0),
5074
+ vec4<f32>(1.0, 1.0, 1.0, 1.0),
5075
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
5076
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
5077
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
5078
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
5079
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
5080
+ vec4<f32>(1.0, 1.0, 1.0, 0.0)
3288
5081
  );
3289
5082
 
3290
5083
  for (var objectIndex = 0u; objectIndex < config.sceneObjectCount; objectIndex = objectIndex + 1u) {
@@ -3317,16 +5110,28 @@ fn intersectActiveQueue(@builtin(global_invocation_id) globalId: vec3<u32>) {
3317
5110
  let position = ray.origin.xyz + ray.direction.xyz * candidate.distance;
3318
5111
  let hitMaterialKind = select(hitObject.materialKind, hitTriangle.materialKind, candidate.triangleIndex != 0xffffffffu);
3319
5112
  let hitObjectId = select(hitObject.objectId, hitTriangle.meshId, candidate.triangleIndex != 0xffffffffu);
3320
- let hitColor = select(hitObject.color, hitTriangle.color, candidate.triangleIndex != 0xffffffffu);
3321
- let hitEmission = select(hitObject.emission, hitTriangle.emission, candidate.triangleIndex != 0xffffffffu);
3322
- let hitMaterial = select(hitObject.material, hitTriangle.material, candidate.triangleIndex != 0xffffffffu);
5113
+ let meshSurface = sample_surface_material(
5114
+ hitTriangle,
5115
+ candidate.uv,
5116
+ candidate.geometricNormal,
5117
+ candidate.shadingNormal
5118
+ );
5119
+ let hitColor = select(hitObject.color, meshSurface.color, candidate.triangleIndex != 0xffffffffu);
5120
+ let hitEmission = select(hitObject.emission, meshSurface.emission, candidate.triangleIndex != 0xffffffffu);
5121
+ let hitMaterial = select(hitObject.material, meshSurface.material, candidate.triangleIndex != 0xffffffffu);
5122
+ let hitMaterialResponse = select(hitObject.materialResponse, meshSurface.materialResponse, candidate.triangleIndex != 0xffffffffu);
5123
+ let hitMaterialExtension = select(hitObject.materialExtension, meshSurface.materialExtension, candidate.triangleIndex != 0xffffffffu);
5124
+ let hitSpecularColor = select(hitObject.specularColor, meshSurface.specularColor, candidate.triangleIndex != 0xffffffffu);
5125
+ let hitShadingNormal = select(candidate.shadingNormal, meshSurface.shadingNormal, candidate.triangleIndex != 0xffffffffu);
3323
5126
  let hitPrimitiveId = select(candidate.primitiveId, hitTriangle.triangleId, candidate.triangleIndex != 0xffffffffu);
3324
5127
  let hitMaterialRefId = select(candidate.materialRefId, hitTriangle.materialRefId, candidate.triangleIndex != 0xffffffffu);
3325
5128
  let hitMediumRefId = select(candidate.mediumRefId, hitTriangle.mediumRefId, candidate.triangleIndex != 0xffffffffu);
5129
+ let hitMaterialSlot = select(0u, hitTriangle.materialSlot, candidate.triangleIndex != 0xffffffffu);
5130
+ let hitOcclusion = select(1.0, meshSurface.occlusion, candidate.triangleIndex != 0xffffffffu);
3326
5131
  var hitType = 0u;
3327
5132
  if (hitMaterialKind == 4u || emission_power(hitEmission) > 0.0001) {
3328
5133
  hitType = 1u;
3329
- } else if (hitMaterialKind == 3u || hitMaterial.z < 0.999) {
5134
+ } else if (hitMaterialKind == 3u || hitMaterial.z < 0.999 || hitMaterialExtension.z > 0.001) {
3330
5135
  hitType = 3u;
3331
5136
  }
3332
5137
  atomicAdd(&counters.hitCount, 1u);
@@ -3340,19 +5145,23 @@ fn intersectActiveQueue(@builtin(global_invocation_id) globalId: vec3<u32>) {
3340
5145
  hitPrimitiveId,
3341
5146
  hitMaterialRefId,
3342
5147
  hitMediumRefId,
3343
- 0u,
5148
+ hitMaterialSlot,
3344
5149
  0u,
3345
5150
  0u,
3346
5151
  candidate.distance,
3347
- vec3<f32>(0.0),
5152
+ hitOcclusion,
5153
+ vec2<f32>(0.0),
3348
5154
  vec4<f32>(position, 1.0),
3349
5155
  vec4<f32>(candidate.geometricNormal, 0.0),
3350
- vec4<f32>(candidate.shadingNormal, 0.0),
5156
+ vec4<f32>(hitShadingNormal, 0.0),
3351
5157
  vec4<f32>(candidate.barycentric, 0.0),
3352
5158
  vec4<f32>(candidate.uv, 0.0, 0.0),
3353
5159
  hitColor,
3354
5160
  hitEmission,
3355
- hitMaterial
5161
+ hitMaterial,
5162
+ hitMaterialResponse,
5163
+ hitMaterialExtension,
5164
+ hitSpecularColor
3356
5165
  );
3357
5166
  }
3358
5167
 
@@ -3421,60 +5230,106 @@ fn sample_environment_portal_direction(hit: HitRecord, seed: u32, fallback: vec3
3421
5230
  }
3422
5231
 
3423
5232
  fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult {
5233
+ let normal = safe_normalize(hit.shadingNormal.xyz, vec3<f32>(0.0, 1.0, 0.0));
5234
+ let viewDirection = safe_normalize(-ray.direction.xyz, normal);
3424
5235
  let roughness = clamp(hit.material.x, 0.0, 1.0);
3425
- if (hit.materialKind == 1u) {
5236
+ let transmission = clamp(hit.materialExtension.z, 0.0, 1.0);
5237
+ if (hit.materialKind == 1u && roughness <= 0.02) {
3426
5238
  return ScatterResult(
3427
- vec4<f32>(
3428
- safe_normalize(
3429
- reflect(ray.direction.xyz, hit.shadingNormal.xyz) + random_unit_vector(seed) * roughness,
3430
- hit.shadingNormal.xyz
3431
- ),
3432
- 0.0
3433
- ),
3434
- 0u,
3435
- 0u,
5239
+ vec4<f32>(reflect(ray.direction.xyz, normal), 0.0),
5240
+ 1.0,
5241
+ RAY_FLAG_DELTA_SAMPLE,
3436
5242
  0u,
3437
5243
  0u
3438
5244
  );
3439
5245
  }
3440
5246
 
3441
- if (hit.materialKind == 2u || hit.materialKind == 3u) {
5247
+ if (hit.materialKind == 2u || hit.materialKind == 3u || transmission > 0.001) {
3442
5248
  let ior = max(hit.material.w, 1.01);
3443
5249
  let etaRatio = select(ior, 1.0 / ior, hit.frontFace == 1u);
3444
- let cosTheta = min(dot(-ray.direction.xyz, hit.shadingNormal.xyz), 1.0);
5250
+ let cosTheta = min(dot(-ray.direction.xyz, normal), 1.0);
3445
5251
  let sinTheta = sqrt(max(0.0, 1.0 - cosTheta * cosTheta));
3446
5252
  let cannotRefract = etaRatio * sinTheta > 1.0;
3447
5253
  let reflectChance = schlick(cosTheta, etaRatio);
3448
- if (cannotRefract || random01(seed + 23u) < reflectChance) {
3449
- return ScatterResult(vec4<f32>(reflect(ray.direction.xyz, hit.shadingNormal.xyz), 0.0), 0u, 0u, 0u, 0u);
5254
+ let transmissionReflectChance = select(
5255
+ reflectChance,
5256
+ max(reflectChance, 1.0 - transmission),
5257
+ transmission > 0.001
5258
+ );
5259
+ if (cannotRefract || random01(seed + 23u) < transmissionReflectChance) {
5260
+ return ScatterResult(
5261
+ vec4<f32>(reflect(ray.direction.xyz, normal), 0.0),
5262
+ 1.0,
5263
+ RAY_FLAG_DELTA_SAMPLE,
5264
+ 0u,
5265
+ 0u
5266
+ );
3450
5267
  }
3451
- return ScatterResult(vec4<f32>(refract_direction(ray.direction.xyz, hit.shadingNormal.xyz, etaRatio), 0.0), 0u, 0u, 0u, 0u);
3452
- }
3453
-
3454
- let randomDiffuse = safe_normalize(
3455
- hit.shadingNormal.xyz + random_unit_vector(seed),
3456
- hit.shadingNormal.xyz
3457
- );
3458
- let guidedLight = sample_emissive_triangle_direction(hit, seed, randomDiffuse);
3459
- let canSampleLight = dot(hit.shadingNormal.xyz, guidedLight) > -0.04;
3460
- let guideProbability = select(0.38, 0.72, ray.bounce == 0u);
3461
- let useGuidedLight = canSampleLight && random01(seed + 37u) < guideProbability;
3462
- let guidedPortal = sample_environment_portal_direction(hit, seed, randomDiffuse);
3463
- let canSamplePortal = dot(hit.shadingNormal.xyz, guidedPortal) > -0.04;
3464
- let useGuidedPortal =
3465
- !useGuidedLight &&
3466
- canSamplePortal &&
3467
- config.environmentPortalCount > 0u &&
3468
- config.environmentPortalMode > 0u &&
3469
- random01(seed + 89u) < 0.58;
3470
- let guidedDirection = select(randomDiffuse, guidedPortal, useGuidedPortal);
3471
- return ScatterResult(
3472
- vec4<f32>(select(guidedDirection, guidedLight, useGuidedLight), 0.0),
3473
- select(0u, RAY_FLAG_GUIDED_EMISSIVE, useGuidedLight),
3474
- 0u,
3475
- 0u,
3476
- 0u
3477
- );
5268
+ return ScatterResult(
5269
+ vec4<f32>(refract_direction(ray.direction.xyz, normal, etaRatio), 0.0),
5270
+ 1.0,
5271
+ RAY_FLAG_DELTA_SAMPLE,
5272
+ 0u,
5273
+ 0u
5274
+ );
5275
+ }
5276
+
5277
+ let guidedEmissiveAvailable = config.emissiveTriangleCount > 0u;
5278
+ let guidedPortalAvailable =
5279
+ config.environmentPortalCount > 0u && config.environmentPortalMode != 0u;
5280
+ let guidedSelector = random01(seed + 17u);
5281
+ if (guidedEmissiveAvailable && guidedSelector < 0.18) {
5282
+ let guidedDirection = sample_emissive_triangle_direction(hit, seed + 101u, normal);
5283
+ if (dot(normal, guidedDirection) > 0.000001) {
5284
+ let guidedPdf = max(evaluate_surface_bsdf_pdf(hit, viewDirection, guidedDirection), 0.000001);
5285
+ return ScatterResult(
5286
+ vec4<f32>(guidedDirection, 0.0),
5287
+ guidedPdf,
5288
+ RAY_FLAG_GUIDED_EMISSIVE,
5289
+ 0u,
5290
+ 0u
5291
+ );
5292
+ }
5293
+ }
5294
+ if (guidedPortalAvailable && guidedSelector < 0.32) {
5295
+ let guidedDirection = sample_environment_portal_direction(hit, seed + 131u, normal);
5296
+ if (dot(normal, guidedDirection) > 0.000001) {
5297
+ let guidedPdf = max(evaluate_surface_bsdf_pdf(hit, viewDirection, guidedDirection), 0.000001);
5298
+ return ScatterResult(vec4<f32>(guidedDirection, 0.0), guidedPdf, 0u, 0u, 0u);
5299
+ }
5300
+ }
5301
+
5302
+ let weights = surface_bsdf_sampling_weights(hit);
5303
+ let selector = random01(seed + 31u);
5304
+ var lightDirection = normal;
5305
+ if (selector < weights.x) {
5306
+ lightDirection = cosine_sample_hemisphere(
5307
+ vec2<f32>(random01(seed + 37u), random01(seed + 41u)),
5308
+ normal
5309
+ );
5310
+ } else if (selector < weights.x + weights.y) {
5311
+ let halfVector = importance_sample_ggx(
5312
+ vec2<f32>(random01(seed + 47u), random01(seed + 53u)),
5313
+ max(roughness, 0.02),
5314
+ normal
5315
+ );
5316
+ lightDirection = safe_normalize(reflect(-viewDirection, halfVector), normal);
5317
+ } else {
5318
+ let halfVector = importance_sample_ggx(
5319
+ vec2<f32>(random01(seed + 59u), random01(seed + 61u)),
5320
+ max(clamp(hit.materialExtension.x, 0.0, 1.0), 0.02),
5321
+ normal
5322
+ );
5323
+ lightDirection = safe_normalize(reflect(-viewDirection, halfVector), normal);
5324
+ }
5325
+ if (dot(normal, lightDirection) <= 0.000001) {
5326
+ lightDirection = cosine_sample_hemisphere(
5327
+ vec2<f32>(random01(seed + 67u), random01(seed + 71u)),
5328
+ normal
5329
+ );
5330
+ }
5331
+ let pdf = max(evaluate_surface_bsdf_pdf(hit, viewDirection, lightDirection), 0.000001);
5332
+ return ScatterResult(vec4<f32>(lightDirection, 0.0), pdf, 0u, 0u, 0u);
3478
5333
  }
3479
5334
 
3480
5335
  @compute @workgroup_size(64)
@@ -3504,10 +5359,17 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
3504
5359
  }
3505
5360
 
3506
5361
  if (hit.hitType == 2u) {
5362
+ var sourceRadiance = hit.color.xyz;
5363
+ if ((ray.flags & RAY_FLAG_DELTA_SAMPLE) == 0u) {
5364
+ let bsdfPdf = max(ray.throughput.w, 0.000001);
5365
+ let lightPdf = environment_direction_pdf(ray.direction.xyz);
5366
+ let misWeight = power_heuristic(bsdfPdf, lightPdf);
5367
+ sourceRadiance = sourceRadiance * misWeight;
5368
+ }
3507
5369
  if (deferred_path_resolve_enabled()) {
3508
- record_deferred_terminal_source(ray, hit.color.xyz);
5370
+ record_deferred_terminal_source(ray, sourceRadiance);
3509
5371
  } else {
3510
- contribution = clamp_sample_radiance(ray.throughput.xyz * max(hit.color.xyz, config.ambientColor.xyz));
5372
+ contribution = clamp_sample_radiance(ray.throughput.xyz * sourceRadiance);
3511
5373
  accumulation[ray.rayId] =
3512
5374
  accumulation[ray.rayId] + vec4<f32>(contribution * sample_weight(), 1.0);
3513
5375
  }
@@ -3515,13 +5377,13 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
3515
5377
  return;
3516
5378
  }
3517
5379
 
3518
- let response = surface_path_response(hit);
5380
+ let response = stabilize_surface_path_response(ray, hit, surface_path_response(hit));
3519
5381
  record_deferred_path_response(ray, response);
3520
5382
 
3521
5383
  let shouldEstimateDirectEnvironment =
3522
- !deferred_path_resolve_enabled() &&
3523
5384
  (hit.materialKind == 0u || hit.materialKind == 1u) &&
3524
- hit.material.z >= 0.95;
5385
+ hit.material.z >= 0.95 &&
5386
+ ray.bounce < 2u;
3525
5387
  if (shouldEstimateDirectEnvironment) {
3526
5388
  let directEnvironment = surface_direct_environment_contribution(ray, hit);
3527
5389
  accumulation[ray.rayId] =
@@ -3530,7 +5392,7 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
3530
5392
 
3531
5393
  if (ray.bounce + 1u >= config.maxDepth) {
3532
5394
  if (deferred_path_resolve_enabled()) {
3533
- record_deferred_terminal_source(ray, terminal_surface_environment_source(hit));
5395
+ record_deferred_terminal_source(ray, terminal_surface_environment_source(ray, hit));
3534
5396
  } else {
3535
5397
  let terminalEnvironment = terminal_surface_environment_contribution(ray, hit);
3536
5398
  accumulation[ray.rayId] =
@@ -3545,7 +5407,7 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
3545
5407
  let nextIndex = atomicAdd(&counters.nextCount, 1u);
3546
5408
  if (nextIndex >= config.tilePixelCount) {
3547
5409
  if (deferred_path_resolve_enabled()) {
3548
- record_deferred_terminal_source(ray, terminal_surface_environment_source(hit));
5410
+ record_deferred_terminal_source(ray, terminal_surface_environment_source(ray, hit));
3549
5411
  } else {
3550
5412
  let overflowEnvironment = terminal_surface_environment_contribution(ray, hit);
3551
5413
  accumulation[ray.rayId] =
@@ -3566,7 +5428,7 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
3566
5428
  0u,
3567
5429
  vec4<f32>(offset_origin(hit.position.xyz, hit.shadingNormal.xyz), 1.0),
3568
5430
  scatter.direction,
3569
- vec4<f32>(throughput, ray.throughput.w)
5431
+ vec4<f32>(throughput, scatter.pdf)
3570
5432
  );
3571
5433
  }
3572
5434
 
@@ -3635,8 +5497,11 @@ fn denoiseLinearRadiance(@builtin(global_invocation_id) globalId: vec3<u32>) {
3635
5497
 
3636
5498
  let pixel = vec2<i32>(i32(x), i32(y));
3637
5499
  let center = textureLoad(denoiseInputRadiance, pixel, 0).xyz;
3638
- var sum = center * 1.4;
3639
- var totalWeight = 1.4;
5500
+ let strength = denoise_strength();
5501
+ let kernelRadius = denoise_kernel_radius();
5502
+ let centerWeight = 1.7 - strength * 0.35;
5503
+ var sum = center * centerWeight;
5504
+ var totalWeight = centerWeight;
3640
5505
  let centerRange = denoise_range_space(center);
3641
5506
 
3642
5507
  for (var oy = -2i; oy <= 2i; oy = oy + 1i) {
@@ -3644,13 +5509,16 @@ fn denoiseLinearRadiance(@builtin(global_invocation_id) globalId: vec3<u32>) {
3644
5509
  if (ox == 0i && oy == 0i) {
3645
5510
  continue;
3646
5511
  }
5512
+ if (abs(ox) > kernelRadius || abs(oy) > kernelRadius) {
5513
+ continue;
5514
+ }
3647
5515
  let sx = clamp(i32(x) + ox, 0i, i32(config.canvasWidth) - 1i);
3648
5516
  let sy = clamp(i32(y) + oy, 0i, i32(config.canvasHeight) - 1i);
3649
5517
  let sampleColor = textureLoad(denoiseInputRadiance, vec2<i32>(sx, sy), 0).xyz;
3650
5518
  let colorDistance = length(denoise_range_space(sampleColor) - centerRange);
3651
- let rangeWeight = 1.0 / (1.0 + colorDistance * 7.0);
3652
- let distanceWeight = 1.0 / (1.0 + f32(ox * ox + oy * oy) * 0.24);
3653
- let diagonalWeight = select(1.0, 0.78, abs(ox) + abs(oy) > 2i);
5519
+ let rangeWeight = 1.0 / (1.0 + colorDistance * (11.0 + strength * 6.0));
5520
+ let distanceWeight = 1.0 / (1.0 + f32(ox * ox + oy * oy) * (0.62 + strength * 0.24));
5521
+ let diagonalWeight = select(1.0, 0.92, abs(ox) + abs(oy) > 1i);
3654
5522
  let weight = rangeWeight * diagonalWeight * distanceWeight;
3655
5523
  sum = sum + sampleColor * weight;
3656
5524
  totalWeight = totalWeight + weight;
@@ -3658,8 +5526,9 @@ fn denoiseLinearRadiance(@builtin(global_invocation_id) globalId: vec3<u32>) {
3658
5526
  }
3659
5527
 
3660
5528
  let filtered = sum / max(totalWeight, 0.0001);
3661
- let outlier = saturate(length(denoise_range_space(center) - denoise_range_space(filtered)) * 2.4);
3662
- let color = min(mix(center, filtered, 0.52 + outlier * 0.18), vec3<f32>(16.0));
5529
+ let outlier = saturate(length(denoise_range_space(center) - denoise_range_space(filtered)) * 2.1);
5530
+ let blend = min(0.3, strength * (0.62 + outlier * 0.12));
5531
+ let color = min(mix(center, filtered, blend), vec3<f32>(16.0));
3663
5532
  textureStore(denoisedRadianceImage, pixel, vec4<f32>(color, 1.0));
3664
5533
  }
3665
5534
 
@@ -3673,8 +5542,10 @@ fn resolveDenoisedOutputImage(@builtin(global_invocation_id) globalId: vec3<u32>
3673
5542
 
3674
5543
  let pixel = vec2<i32>(i32(x), i32(y));
3675
5544
  let center = textureLoad(finalDenoiseInputRadiance, pixel, 0).xyz;
3676
- var sum = center * 1.25;
3677
- var totalWeight = 1.25;
5545
+ let strength = denoise_strength();
5546
+ let centerWeight = 1.35 - strength * 0.25;
5547
+ var sum = center * centerWeight;
5548
+ var totalWeight = centerWeight;
3678
5549
  let centerRange = denoise_range_space(center);
3679
5550
 
3680
5551
  for (var oy = -1i; oy <= 1i; oy = oy + 1i) {
@@ -3686,8 +5557,8 @@ fn resolveDenoisedOutputImage(@builtin(global_invocation_id) globalId: vec3<u32>
3686
5557
  let sy = clamp(i32(y) + oy, 0i, i32(config.canvasHeight) - 1i);
3687
5558
  let sampleColor = textureLoad(finalDenoiseInputRadiance, vec2<i32>(sx, sy), 0).xyz;
3688
5559
  let colorDistance = length(denoise_range_space(sampleColor) - centerRange);
3689
- let rangeWeight = 1.0 / (1.0 + colorDistance * 9.0);
3690
- let distanceWeight = 1.0 / (1.0 + f32(ox * ox + oy * oy) * 0.4);
5560
+ let rangeWeight = 1.0 / (1.0 + colorDistance * (12.0 + strength * 8.0));
5561
+ let distanceWeight = 1.0 / (1.0 + f32(ox * ox + oy * oy) * (0.82 + strength * 0.28));
3691
5562
  let weight = rangeWeight * distanceWeight;
3692
5563
  sum = sum + sampleColor * weight;
3693
5564
  totalWeight = totalWeight + weight;
@@ -3695,8 +5566,9 @@ fn resolveDenoisedOutputImage(@builtin(global_invocation_id) globalId: vec3<u32>
3695
5566
  }
3696
5567
 
3697
5568
  let filtered = sum / max(totalWeight, 0.0001);
3698
- let outlier = saturate(length(denoise_range_space(center) - denoise_range_space(filtered)) * 2.8);
3699
- let radiance = min(mix(center, filtered, 0.28 + outlier * 0.12), vec3<f32>(16.0));
5569
+ let outlier = saturate(length(denoise_range_space(center) - denoise_range_space(filtered)) * 2.2);
5570
+ let blend = min(0.18, strength * (0.42 + outlier * 0.08));
5571
+ let radiance = min(mix(center, filtered, blend), vec3<f32>(16.0));
3700
5572
  textureStore(denoisedOutputImage, pixel, vec4<f32>(tone_map_radiance(radiance), 1.0));
3701
5573
  }
3702
5574
  `;
@@ -3801,96 +5673,47 @@ function createGpuAdapterParallelismDiagnostics(adapter, device) {
3801
5673
  });
3802
5674
  }
3803
5675
 
3804
- function createGpuParallelismCounters() {
3805
- return {
3806
- directDispatches: 0,
3807
- directWorkgroups: 0,
3808
- directShaderInvocations: 0,
3809
- multiWorkgroupDispatches: 0,
3810
- largestDirectWorkgroupsPerDispatch: 0,
3811
- indirectDispatches: 0,
3812
- estimatedIndirectWorkgroupsUpperBound: 0,
3813
- estimatedIndirectShaderInvocationsUpperBound: 0,
3814
- indirectDispatchesWithMultiWorkgroupCapacity: 0,
3815
- largestEstimatedIndirectWorkgroupsPerDispatch: 0,
3816
- };
3817
- }
3818
-
3819
- function countDispatchWorkgroups(groups) {
3820
- return groups.reduce((product, value) => {
3821
- const numeric = Number(value ?? 1);
3822
- const count = Number.isFinite(numeric) ? Math.max(1, Math.trunc(numeric)) : 1;
3823
- return product * count;
3824
- }, 1);
3825
- }
3826
-
3827
- function recordDirectDispatch(parallelism, groups, invocationsPerWorkgroup = WORKGROUP_SIZE) {
3828
- const workgroups = countDispatchWorkgroups(groups);
3829
- parallelism.directDispatches += 1;
3830
- parallelism.directWorkgroups += workgroups;
3831
- parallelism.directShaderInvocations += workgroups * invocationsPerWorkgroup;
3832
- parallelism.largestDirectWorkgroupsPerDispatch = Math.max(
3833
- parallelism.largestDirectWorkgroupsPerDispatch,
3834
- workgroups
3835
- );
3836
- if (workgroups > 1) {
3837
- parallelism.multiWorkgroupDispatches += 1;
3838
- }
3839
- }
3840
-
3841
- function recordIndirectDispatch(parallelism, estimatedWorkgroupsUpperBound, invocationsPerWorkgroup = WORKGROUP_SIZE) {
3842
- const workgroups = Math.max(1, Math.trunc(Number(estimatedWorkgroupsUpperBound) || 1));
3843
- parallelism.indirectDispatches += 1;
3844
- parallelism.estimatedIndirectWorkgroupsUpperBound += workgroups;
3845
- parallelism.estimatedIndirectShaderInvocationsUpperBound += workgroups * invocationsPerWorkgroup;
3846
- parallelism.largestEstimatedIndirectWorkgroupsPerDispatch = Math.max(
3847
- parallelism.largestEstimatedIndirectWorkgroupsPerDispatch,
3848
- workgroups
3849
- );
3850
- if (workgroups > 1) {
3851
- parallelism.indirectDispatchesWithMultiWorkgroupCapacity += 1;
3852
- }
3853
- }
3854
-
3855
- function createGpuParallelismDiagnostics(adapterDiagnostics, counters) {
3856
- const totalEstimatedWorkgroupsUpperBound =
3857
- counters.directWorkgroups + counters.estimatedIndirectWorkgroupsUpperBound;
3858
- const totalEstimatedShaderInvocationsUpperBound =
3859
- counters.directShaderInvocations + counters.estimatedIndirectShaderInvocationsUpperBound;
3860
- const exposesMultiWorkgroupParallelism =
3861
- counters.multiWorkgroupDispatches > 0 || counters.indirectDispatchesWithMultiWorkgroupCapacity > 0;
3862
- return Object.freeze({
3863
- ...adapterDiagnostics,
3864
- directDispatches: counters.directDispatches,
3865
- directWorkgroups: counters.directWorkgroups,
3866
- directShaderInvocations: counters.directShaderInvocations,
3867
- multiWorkgroupDispatches: counters.multiWorkgroupDispatches,
3868
- largestDirectWorkgroupsPerDispatch: counters.largestDirectWorkgroupsPerDispatch,
3869
- indirectDispatches: counters.indirectDispatches,
3870
- estimatedIndirectWorkgroupsUpperBound: counters.estimatedIndirectWorkgroupsUpperBound,
3871
- estimatedIndirectShaderInvocationsUpperBound: counters.estimatedIndirectShaderInvocationsUpperBound,
3872
- indirectDispatchesWithMultiWorkgroupCapacity: counters.indirectDispatchesWithMultiWorkgroupCapacity,
3873
- largestEstimatedIndirectWorkgroupsPerDispatch: counters.largestEstimatedIndirectWorkgroupsPerDispatch,
3874
- totalEstimatedWorkgroupsUpperBound,
3875
- totalEstimatedShaderInvocationsUpperBound,
3876
- exposesMultiWorkgroupParallelism,
3877
- likelyUsesMoreThanOnePhysicalGpuCore: null,
3878
- coreUtilizationStatus: "not-exposed-by-webgpu",
3879
- });
3880
- }
3881
-
3882
5676
  function createEnvironmentMapSnapshot(environmentMap) {
3883
5677
  return Object.freeze({
3884
5678
  enabled: environmentMap.enabled,
3885
5679
  width: environmentMap.width,
3886
5680
  height: environmentMap.height,
5681
+ mipLevelCount: environmentMap.mipLevelCount ?? 1,
3887
5682
  projection: environmentMap.projection,
3888
5683
  intensity: environmentMap.intensity,
3889
5684
  rotationRadians: environmentMap.rotationRadians,
3890
5685
  ambientStrength: environmentMap.ambientStrength,
5686
+ hasImportanceData: environmentMap.hasImportanceData === true,
3891
5687
  });
3892
5688
  }
3893
5689
 
5690
+ function nowMs() {
5691
+ if (typeof performance !== "undefined" && typeof performance.now === "function") {
5692
+ return performance.now();
5693
+ }
5694
+ return Date.now();
5695
+ }
5696
+
5697
+ function estimateSubmittedGpuWorkTimeoutMs(config, tileCount, overrideTimeoutMs = null) {
5698
+ if (Number.isFinite(overrideTimeoutMs)) {
5699
+ return Math.max(1, Math.trunc(Number(overrideTimeoutMs)));
5700
+ }
5701
+ const samplesPerPixel = Math.max(
5702
+ 1,
5703
+ Number(config?.renderedSamplesPerPixel ?? config?.samplesPerPixel ?? 1)
5704
+ );
5705
+ const maxDepth = Math.max(1, Number(config?.maxDepth ?? 1));
5706
+ const deferredResolvePasses = config?.deferredPathResolve ? 1 : 0;
5707
+ const denoisePasses = config?.denoise ? (samplesPerPixel < 4 ? 2 : 1) : 0;
5708
+ const tiles = Math.max(1, Number(tileCount ?? 1));
5709
+ const estimatedPasses =
5710
+ tiles * (samplesPerPixel * (maxDepth + 1 + deferredResolvePasses) + denoisePasses + 1);
5711
+ return Math.min(
5712
+ GPU_MAX_SUBMITTED_WORK_TIMEOUT_MS,
5713
+ GPU_SUBMITTED_WORK_TIMEOUT_MS + estimatedPasses * 5
5714
+ );
5715
+ }
5716
+
3894
5717
  export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3895
5718
  assertAnalyticDisplayQualityPolicy(options);
3896
5719
  const constants = getGpuUsageConstants();
@@ -4116,6 +5939,60 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4116
5939
  config.environmentMap,
4117
5940
  config.environmentColor
4118
5941
  );
5942
+ const environmentSamplingResource = createEnvironmentSamplingTextureResource(
5943
+ device,
5944
+ constants,
5945
+ config.environmentMap,
5946
+ config.environmentColor
5947
+ );
5948
+ config = Object.freeze({
5949
+ ...config,
5950
+ environmentMap: Object.freeze({
5951
+ ...config.environmentMap,
5952
+ width: environmentMapResource.width,
5953
+ height: environmentMapResource.height,
5954
+ mipLevelCount: environmentMapResource.mipLevelCount,
5955
+ hasImportanceData: environmentSamplingResource.hasImportanceData,
5956
+ }),
5957
+ });
5958
+ const brdfLutResource = createBrdfLutResource(device, constants);
5959
+ const baseColorAtlasResource = createAtlasTextureResource(
5960
+ device,
5961
+ constants,
5962
+ config.gpuMaterialSource.baseColorAtlas,
5963
+ "plasius.wavefront.materialAtlas.baseColor"
5964
+ );
5965
+ const metallicRoughnessAtlasResource = createAtlasTextureResource(
5966
+ device,
5967
+ constants,
5968
+ config.gpuMaterialSource.metallicRoughnessAtlas,
5969
+ "plasius.wavefront.materialAtlas.metallicRoughness"
5970
+ );
5971
+ const normalAtlasResource = createAtlasTextureResource(
5972
+ device,
5973
+ constants,
5974
+ config.gpuMaterialSource.normalAtlas,
5975
+ "plasius.wavefront.materialAtlas.normal"
5976
+ );
5977
+ const occlusionAtlasResource = createAtlasTextureResource(
5978
+ device,
5979
+ constants,
5980
+ config.gpuMaterialSource.occlusionAtlas,
5981
+ "plasius.wavefront.materialAtlas.occlusion"
5982
+ );
5983
+ const emissiveAtlasResource = createAtlasTextureResource(
5984
+ device,
5985
+ constants,
5986
+ config.gpuMaterialSource.emissiveAtlas,
5987
+ "plasius.wavefront.materialAtlas.emissive"
5988
+ );
5989
+ const materialAtlasSampler = device.createSampler({
5990
+ label: "plasius.wavefront.materialAtlasSampler",
5991
+ addressModeU: "clamp-to-edge",
5992
+ addressModeV: "clamp-to-edge",
5993
+ magFilter: "linear",
5994
+ minFilter: "linear",
5995
+ });
4119
5996
 
4120
5997
  const traceBindGroupLayout = device.createBindGroupLayout({
4121
5998
  label: "plasius.wavefront.traceBindGroupLayout",
@@ -4147,6 +6024,15 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4147
6024
  { binding: 20, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
4148
6025
  { binding: 21, visibility: constants.shader.COMPUTE, sampler: { type: "filtering" } },
4149
6026
  { binding: 22, visibility: constants.shader.COMPUTE, buffer: { type: "storage" } },
6027
+ { binding: 23, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
6028
+ { binding: 24, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
6029
+ { binding: 25, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
6030
+ { binding: 26, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
6031
+ { binding: 27, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
6032
+ { binding: 28, visibility: constants.shader.COMPUTE, sampler: { type: "filtering" } },
6033
+ { binding: 29, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
6034
+ { binding: 30, visibility: constants.shader.COMPUTE, sampler: { type: "filtering" } },
6035
+ { binding: 31, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
4150
6036
  ],
4151
6037
  });
4152
6038
  const accelerationBindGroupLayout = device.createBindGroupLayout({
@@ -4225,6 +6111,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4225
6111
  label: "plasius.wavefront.computeShader",
4226
6112
  code: WAVEFRONT_COMPUTE_WGSL,
4227
6113
  });
6114
+ await assertShaderModuleCompiles(computeShader, "plasius.wavefront.computeShader");
4228
6115
 
4229
6116
  const pipelines = {
4230
6117
  prepareMeshTrianglesAndLeaves: await createComputePipeline(
@@ -4326,6 +6213,15 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4326
6213
  { binding: 20, resource: environmentMapResource.view },
4327
6214
  { binding: 21, resource: environmentMapResource.sampler },
4328
6215
  { binding: 22, resource: { buffer: pathVertexBuffer } },
6216
+ { binding: 23, resource: baseColorAtlasResource.view },
6217
+ { binding: 24, resource: metallicRoughnessAtlasResource.view },
6218
+ { binding: 25, resource: normalAtlasResource.view },
6219
+ { binding: 26, resource: occlusionAtlasResource.view },
6220
+ { binding: 27, resource: emissiveAtlasResource.view },
6221
+ { binding: 28, resource: materialAtlasSampler },
6222
+ { binding: 29, resource: brdfLutResource.view },
6223
+ { binding: 30, resource: brdfLutResource.sampler },
6224
+ { binding: 31, resource: environmentSamplingResource.view },
4329
6225
  ],
4330
6226
  });
4331
6227
  }
@@ -4381,6 +6277,11 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4381
6277
  outputView,
4382
6278
  "plasius.wavefront.bind.denoise.scratchToOutput"
4383
6279
  );
6280
+ const denoiseDirectResolveBindGroup = createDenoiseResolveBindGroup(
6281
+ radianceView,
6282
+ outputView,
6283
+ "plasius.wavefront.bind.denoise.radianceToOutput"
6284
+ );
4384
6285
 
4385
6286
  const presentBindGroupLayout = device.createBindGroupLayout({
4386
6287
  label: "plasius.wavefront.presentBindGroupLayout",
@@ -4420,24 +6321,137 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4420
6321
  let accelerationBuilt = !config.gpuAccelerationBuildRequired;
4421
6322
  let accelerationBuildCount = 0;
4422
6323
  let activeCameraOptions = options.camera ?? DEFAULT_CAMERA;
6324
+ let lastCompletedFrameTimeMs = null;
6325
+ let lastCompletedSamplesPerPixel = Math.max(1, config.samplesPerPixel);
4423
6326
  let lastGpuParallelism = createGpuParallelismDiagnostics(
4424
6327
  gpuAdapterParallelism,
4425
6328
  createGpuParallelismCounters()
4426
6329
  );
4427
6330
 
6331
+ function resolveRenderedSamplesPerPixel(renderOptions = {}, awaitGPUCompletion = true) {
6332
+ const targetSamplesPerPixel = clamp(
6333
+ readPositiveInteger(
6334
+ "samplesPerPixel",
6335
+ renderOptions.samplesPerPixel,
6336
+ config.samplesPerPixel
6337
+ ),
6338
+ 1,
6339
+ config.samplesPerPixel
6340
+ );
6341
+ const frameTimeBudgetMs = Number.isFinite(renderOptions.frameTimeBudgetMs)
6342
+ ? Math.max(0, Number(renderOptions.frameTimeBudgetMs))
6343
+ : null;
6344
+ const minimumSamplesPerPixel = clamp(
6345
+ readPositiveInteger(
6346
+ "minimumSamplesPerPixel",
6347
+ renderOptions.minimumSamplesPerPixel,
6348
+ frameTimeBudgetMs !== null && targetSamplesPerPixel > 1 ? 1 : targetSamplesPerPixel
6349
+ ),
6350
+ 1,
6351
+ targetSamplesPerPixel
6352
+ );
6353
+ if (frameTimeBudgetMs === null || !awaitGPUCompletion || targetSamplesPerPixel <= minimumSamplesPerPixel) {
6354
+ return Object.freeze({
6355
+ renderedSamplesPerPixel: targetSamplesPerPixel,
6356
+ targetSamplesPerPixel,
6357
+ minimumSamplesPerPixel,
6358
+ frameTimeBudgetMs,
6359
+ budgetConstrained: false,
6360
+ });
6361
+ }
6362
+ const estimatedSampleTimeMs =
6363
+ Number.isFinite(lastCompletedFrameTimeMs) && lastCompletedFrameTimeMs > 0
6364
+ ? lastCompletedFrameTimeMs / Math.max(1, lastCompletedSamplesPerPixel)
6365
+ : null;
6366
+ if (!Number.isFinite(estimatedSampleTimeMs) || estimatedSampleTimeMs <= 0) {
6367
+ return Object.freeze({
6368
+ renderedSamplesPerPixel: minimumSamplesPerPixel,
6369
+ targetSamplesPerPixel,
6370
+ minimumSamplesPerPixel,
6371
+ frameTimeBudgetMs,
6372
+ budgetConstrained: minimumSamplesPerPixel < targetSamplesPerPixel,
6373
+ });
6374
+ }
6375
+ const budgetLimitedSamples = clamp(
6376
+ Math.floor(frameTimeBudgetMs / estimatedSampleTimeMs),
6377
+ minimumSamplesPerPixel,
6378
+ targetSamplesPerPixel
6379
+ );
6380
+ return Object.freeze({
6381
+ renderedSamplesPerPixel: budgetLimitedSamples,
6382
+ targetSamplesPerPixel,
6383
+ minimumSamplesPerPixel,
6384
+ frameTimeBudgetMs,
6385
+ budgetConstrained: budgetLimitedSamples < targetSamplesPerPixel,
6386
+ });
6387
+ }
6388
+
6389
+ function createFrameStats({
6390
+ frameIndex,
6391
+ accelerationBuildSubmitted,
6392
+ frameSubmissionCount,
6393
+ parallelismCounters,
6394
+ renderedSamplesPerPixel,
6395
+ targetSamplesPerPixel,
6396
+ frameTimeBudgetMs,
6397
+ budgetConstrained,
6398
+ }) {
6399
+ lastGpuParallelism = createGpuParallelismDiagnostics(gpuAdapterParallelism, parallelismCounters);
6400
+ const commandSubmissions = frameSubmissionCount + (accelerationBuildSubmitted ? 1 : 0);
6401
+ return Object.freeze({
6402
+ frame,
6403
+ frameIndex,
6404
+ width: config.width,
6405
+ height: config.height,
6406
+ maxDepth: config.maxDepth,
6407
+ tiles: tiles.length,
6408
+ tileSize: config.tileSize,
6409
+ samplesPerPixel: targetSamplesPerPixel,
6410
+ renderedSamplesPerPixel,
6411
+ frameTimeBudgetMs,
6412
+ budgetConstrained,
6413
+ maxFramePassesPerSubmission: config.maxFramePassesPerSubmission,
6414
+ screenRays: config.width * config.height,
6415
+ primaryRays: config.width * config.height * renderedSamplesPerPixel,
6416
+ sceneObjectCount: config.sceneObjectCount,
6417
+ triangleCount: config.triangleCount,
6418
+ emissiveTriangleCount: config.emissiveTriangleCount,
6419
+ environmentPortalCount: config.environmentPortalCount,
6420
+ environmentPortalMode: config.environmentPortalMode,
6421
+ environmentMap: createEnvironmentMapSnapshot(config.environmentMap),
6422
+ deferredPathResolve: config.deferredPathResolve,
6423
+ bvhNodeCount: config.bvhNodeCount,
6424
+ displayQuality: config.displayQuality,
6425
+ accelerationBuildMode: config.accelerationBuildMode,
6426
+ gpuAccelerationBuildRequired: config.gpuAccelerationBuildRequired,
6427
+ accelerationBuildSubmitted,
6428
+ accelerationBuilt,
6429
+ accelerationBuildCount,
6430
+ commandSubmissions,
6431
+ frameConfigSlots: frameConfigSlotCount,
6432
+ gpuParallelism: lastGpuParallelism,
6433
+ memory: config.memory,
6434
+ });
6435
+ }
6436
+
6437
+ function writeFrameConfigSlot(slot, tile, frameIndex, buildRange = {}) {
6438
+ if (slot >= frameConfigSlotCount) {
6439
+ throw new Error("Wavefront frame config slot capacity exceeded.");
6440
+ }
6441
+ const offset = slot * configBufferStride;
6442
+ device.queue.writeBuffer(
6443
+ configBuffer,
6444
+ offset,
6445
+ createConfigPayload(config, tile, frameIndex, buildRange)
6446
+ );
6447
+ return offset;
6448
+ }
6449
+
4428
6450
  function createFrameConfigWriter(frameIndex) {
4429
6451
  let slot = 0;
4430
6452
  return (tile, buildRange = {}) => {
4431
- if (slot >= frameConfigSlotCount) {
4432
- throw new Error("Wavefront frame config slot capacity exceeded.");
4433
- }
4434
- const offset = slot * configBufferStride;
6453
+ const offset = writeFrameConfigSlot(slot, tile, frameIndex, buildRange);
4435
6454
  slot += 1;
4436
- device.queue.writeBuffer(
4437
- configBuffer,
4438
- offset,
4439
- createConfigPayload(config, tile, frameIndex, buildRange)
4440
- );
4441
6455
  return offset;
4442
6456
  };
4443
6457
  }
@@ -4483,7 +6497,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4483
6497
  passEncoder.setPipeline(pipelines.prepareMeshTrianglesAndLeaves);
4484
6498
  const prepareWorkgroups = Math.ceil(config.bvhLeafSortCapacity / WORKGROUP_SIZE);
4485
6499
  passEncoder.dispatchWorkgroups(prepareWorkgroups);
4486
- recordDirectDispatch(parallelism, [prepareWorkgroups]);
6500
+ recordDirectDispatch(parallelism, [prepareWorkgroups], WORKGROUP_SIZE);
4487
6501
  passEncoder.setPipeline(pipelines.sortBvhLeafRefs);
4488
6502
  for (let stageIndex = 0; stageIndex < config.bvhSortStages.length; stageIndex += 1) {
4489
6503
  passEncoder.setBindGroup(0, bvhBuildBindGroup, [
@@ -4491,13 +6505,13 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4491
6505
  ]);
4492
6506
  const sortWorkgroups = Math.ceil(config.bvhLeafSortCapacity / WORKGROUP_SIZE);
4493
6507
  passEncoder.dispatchWorkgroups(sortWorkgroups);
4494
- recordDirectDispatch(parallelism, [sortWorkgroups]);
6508
+ recordDirectDispatch(parallelism, [sortWorkgroups], WORKGROUP_SIZE);
4495
6509
  }
4496
6510
  passEncoder.setBindGroup(0, bvhBuildBindGroup, [0]);
4497
6511
  passEncoder.setPipeline(pipelines.writeSortedBvhLeaves);
4498
6512
  const leafWriteWorkgroups = Math.ceil(config.triangleCount / WORKGROUP_SIZE);
4499
6513
  passEncoder.dispatchWorkgroups(leafWriteWorkgroups);
4500
- recordDirectDispatch(parallelism, [leafWriteWorkgroups]);
6514
+ recordDirectDispatch(parallelism, [leafWriteWorkgroups], WORKGROUP_SIZE);
4501
6515
  passEncoder.setPipeline(pipelines.buildBvhInternalLevel);
4502
6516
  for (let levelIndex = 0; levelIndex < config.bvhBuildLevels.length; levelIndex += 1) {
4503
6517
  const buildLevel = config.bvhBuildLevels[levelIndex];
@@ -4506,7 +6520,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4506
6520
  ]);
4507
6521
  const levelWorkgroups = Math.ceil(buildLevel.count / WORKGROUP_SIZE);
4508
6522
  passEncoder.dispatchWorkgroups(levelWorkgroups);
4509
- recordDirectDispatch(parallelism, [levelWorkgroups]);
6523
+ recordDirectDispatch(parallelism, [levelWorkgroups], WORKGROUP_SIZE);
4510
6524
  }
4511
6525
  passEncoder.end();
4512
6526
  device.queue.submit([encoder.finish()]);
@@ -4524,7 +6538,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4524
6538
  generatePass.setBindGroup(0, bindGroups[0], [configOffset]);
4525
6539
  generatePass.setPipeline(pipelines.generatePrimaryRays);
4526
6540
  generatePass.dispatchWorkgroups(tileWorkgroups);
4527
- recordDirectDispatch(parallelism, [tileWorkgroups]);
6541
+ recordDirectDispatch(parallelism, [tileWorkgroups], WORKGROUP_SIZE);
4528
6542
  generatePass.end();
4529
6543
 
4530
6544
  for (let bounceIndex = 0; bounceIndex < config.maxDepth; bounceIndex += 1) {
@@ -4541,10 +6555,10 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4541
6555
  passEncoder.setBindGroup(0, bindGroups[bounceIndex % 2], [configOffset]);
4542
6556
  passEncoder.setPipeline(pipelines.intersectActiveQueue);
4543
6557
  passEncoder.dispatchWorkgroupsIndirect(activeDispatchBuffer, 0);
4544
- recordIndirectDispatch(parallelism, tileWorkgroups);
6558
+ recordIndirectDispatch(parallelism, tileWorkgroups, WORKGROUP_SIZE);
4545
6559
  passEncoder.setPipeline(pipelines.resolveSurfaceRecords);
4546
6560
  passEncoder.dispatchWorkgroupsIndirect(activeDispatchBuffer, 0);
4547
- recordIndirectDispatch(parallelism, tileWorkgroups);
6561
+ recordIndirectDispatch(parallelism, tileWorkgroups, WORKGROUP_SIZE);
4548
6562
  passEncoder.setPipeline(pipelines.compactAndSwapQueues);
4549
6563
  passEncoder.dispatchWorkgroups(1);
4550
6564
  recordDirectDispatch(parallelism, [1], 1);
@@ -4561,32 +6575,47 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4561
6575
  passEncoder.setBindGroup(0, bindGroups[0], [configOffset]);
4562
6576
  passEncoder.setPipeline(pipelines.accumulateTerminalRadiance);
4563
6577
  passEncoder.dispatchWorkgroups(tileWorkgroups);
4564
- recordDirectDispatch(parallelism, [tileWorkgroups]);
6578
+ recordDirectDispatch(parallelism, [tileWorkgroups], WORKGROUP_SIZE);
4565
6579
  passEncoder.end();
4566
6580
  }
4567
6581
 
4568
- function encodeDenoise(encoder, configOffset, parallelism) {
6582
+ function encodeDenoise(encoder, configOffset, parallelism, renderedSamplesPerPixel = config.samplesPerPixel) {
4569
6583
  if (!config.denoise) {
4570
6584
  return;
4571
6585
  }
4572
6586
  const denoiseWorkgroupsX = Math.ceil(config.width / 8);
4573
6587
  const denoiseWorkgroupsY = Math.ceil(config.height / 8);
4574
- const radiancePass = encoder.beginComputePass({
4575
- label: "plasius.wavefront.denoiseRadiancePass",
4576
- });
4577
- radiancePass.setBindGroup(0, denoiseRadianceBindGroup, [configOffset]);
4578
- radiancePass.setPipeline(pipelines.denoiseLinearRadiance);
4579
- radiancePass.dispatchWorkgroups(denoiseWorkgroupsX, denoiseWorkgroupsY);
4580
- recordDirectDispatch(parallelism, [denoiseWorkgroupsX, denoiseWorkgroupsY]);
4581
- radiancePass.end();
6588
+ const useTwoPassDenoise = renderedSamplesPerPixel < 4;
6589
+ if (useTwoPassDenoise) {
6590
+ const radiancePass = encoder.beginComputePass({
6591
+ label: "plasius.wavefront.denoiseRadiancePass",
6592
+ });
6593
+ radiancePass.setBindGroup(0, denoiseRadianceBindGroup, [configOffset]);
6594
+ radiancePass.setPipeline(pipelines.denoiseLinearRadiance);
6595
+ radiancePass.dispatchWorkgroups(denoiseWorkgroupsX, denoiseWorkgroupsY);
6596
+ recordDirectDispatch(
6597
+ parallelism,
6598
+ [denoiseWorkgroupsX, denoiseWorkgroupsY],
6599
+ WORKGROUP_SIZE
6600
+ );
6601
+ radiancePass.end();
6602
+ }
4582
6603
 
4583
6604
  const resolvePass = encoder.beginComputePass({
4584
6605
  label: "plasius.wavefront.denoiseResolvePass",
4585
6606
  });
4586
- resolvePass.setBindGroup(0, denoiseResolveBindGroup, [configOffset]);
6607
+ resolvePass.setBindGroup(
6608
+ 0,
6609
+ useTwoPassDenoise ? denoiseResolveBindGroup : denoiseDirectResolveBindGroup,
6610
+ [configOffset]
6611
+ );
4587
6612
  resolvePass.setPipeline(pipelines.resolveDenoisedOutputImage);
4588
6613
  resolvePass.dispatchWorkgroups(denoiseWorkgroupsX, denoiseWorkgroupsY);
4589
- recordDirectDispatch(parallelism, [denoiseWorkgroupsX, denoiseWorkgroupsY]);
6614
+ recordDirectDispatch(
6615
+ parallelism,
6616
+ [denoiseWorkgroupsX, denoiseWorkgroupsY],
6617
+ WORKGROUP_SIZE
6618
+ );
4590
6619
  resolvePass.end();
4591
6620
  }
4592
6621
 
@@ -4609,105 +6638,233 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4609
6638
  passEncoder.end();
4610
6639
  }
4611
6640
 
4612
- function dispatchFrame(frameIndex, parallelism) {
6641
+ function dispatchFrame(frameIndex, parallelism, renderedSamplesPerPixel = config.samplesPerPixel) {
4613
6642
  const writeFrameConfig = createFrameConfigWriter(frameIndex);
4614
- let submissionCount = 0;
4615
- let encodedFramePasses = 0;
4616
- let encoder = device.createCommandEncoder({
4617
- label: `plasius.wavefront.frame.${frameIndex}.batched.${submissionCount + 1}`,
6643
+ const batch = createGpuSubmissionBatcher({
6644
+ device,
6645
+ frameIndex,
6646
+ maxFramePassesPerSubmission: config.maxFramePassesPerSubmission,
4618
6647
  });
4619
6648
 
4620
- function submitCurrentEncoder() {
4621
- if (encodedFramePasses <= 0) {
4622
- return;
4623
- }
4624
- device.queue.submit([encoder.finish()]);
4625
- submissionCount += 1;
4626
- encodedFramePasses = 0;
4627
- encoder = device.createCommandEncoder({
4628
- label: `plasius.wavefront.frame.${frameIndex}.batched.${submissionCount + 1}`,
4629
- });
4630
- }
4631
-
4632
- function reserveEncoder(passCount = 1) {
4633
- if (
4634
- encodedFramePasses > 0 &&
4635
- encodedFramePasses + passCount > config.maxFramePassesPerSubmission
4636
- ) {
4637
- submitCurrentEncoder();
4638
- }
4639
- encodedFramePasses += passCount;
4640
- return encoder;
4641
- }
4642
-
4643
6649
  for (const tile of tiles) {
4644
- for (let sampleIndex = 0; sampleIndex < config.samplesPerPixel; sampleIndex += 1) {
6650
+ for (let sampleIndex = 0; sampleIndex < renderedSamplesPerPixel; sampleIndex += 1) {
4645
6651
  const configOffset = writeFrameConfig(tile, {
4646
6652
  sampleIndex,
4647
- sampleWeight: 1 / config.samplesPerPixel,
6653
+ sampleWeight: 1 / renderedSamplesPerPixel,
4648
6654
  });
4649
- encodeTileSample(reserveEncoder(), tile, configOffset, parallelism);
6655
+ encodeTileSample(
6656
+ batch.reserve(config.maxDepth + 1),
6657
+ tile,
6658
+ configOffset,
6659
+ parallelism
6660
+ );
4650
6661
  if (config.deferredPathResolve) {
4651
- encodeTileOutput(reserveEncoder(), tile, configOffset, parallelism);
6662
+ encodeTileOutput(batch.reserve(1), tile, configOffset, parallelism);
4652
6663
  }
4653
6664
  }
4654
6665
  if (!config.deferredPathResolve) {
4655
6666
  const outputConfigOffset = writeFrameConfig(tile, {
4656
6667
  sampleIndex: 0,
4657
- sampleWeight: 1 / config.samplesPerPixel,
6668
+ sampleWeight: 1 / renderedSamplesPerPixel,
4658
6669
  });
4659
- encodeTileOutput(reserveEncoder(), tile, outputConfigOffset, parallelism);
6670
+ encodeTileOutput(batch.reserve(1), tile, outputConfigOffset, parallelism);
4660
6671
  }
4661
6672
  }
4662
6673
  if (config.denoise) {
4663
6674
  const denoiseConfigOffset = writeFrameConfig(
4664
6675
  { x: 0, y: 0, width: config.width, height: config.height },
4665
- { sampleIndex: 0, sampleWeight: 1 / config.samplesPerPixel }
6676
+ { sampleIndex: 0, sampleWeight: 1 / renderedSamplesPerPixel }
6677
+ );
6678
+ const denoisePassCount = renderedSamplesPerPixel < 4 ? 2 : 1;
6679
+ encodeDenoise(
6680
+ batch.reserve(denoisePassCount),
6681
+ denoiseConfigOffset,
6682
+ parallelism,
6683
+ renderedSamplesPerPixel
4666
6684
  );
4667
- encodeDenoise(reserveEncoder(), denoiseConfigOffset, parallelism);
4668
6685
  }
4669
- encodePresent(reserveEncoder());
4670
- submitCurrentEncoder();
4671
- return submissionCount;
6686
+ encodePresent(batch.reserve(1));
6687
+ return batch.flush();
4672
6688
  }
4673
6689
 
4674
- function renderOnce() {
6690
+ function renderOnce(renderOptions = {}, resolvedSamplingPlan = null) {
6691
+ const frameStartTimeMs = nowMs();
4675
6692
  frame += 1;
4676
6693
  const frameIndex = frame + config.frameIndex;
6694
+ const samplingPlan = resolvedSamplingPlan ?? resolveRenderedSamplesPerPixel(renderOptions, false);
4677
6695
  const parallelismCounters = createGpuParallelismCounters();
4678
6696
  const accelerationBuildSubmitted = dispatchGpuAccelerationBuild(frameIndex, parallelismCounters);
4679
- const frameSubmissionCount = dispatchFrame(frameIndex, parallelismCounters);
4680
- lastGpuParallelism = createGpuParallelismDiagnostics(gpuAdapterParallelism, parallelismCounters);
6697
+ const frameSubmissionCount = dispatchFrame(
6698
+ frameIndex,
6699
+ parallelismCounters,
6700
+ samplingPlan.renderedSamplesPerPixel
6701
+ );
6702
+ const frameTimeMs = Math.max(0, nowMs() - frameStartTimeMs);
4681
6703
  return Object.freeze({
4682
- frame,
4683
- width: config.width,
4684
- height: config.height,
4685
- maxDepth: config.maxDepth,
4686
- tiles: tiles.length,
4687
- tileSize: config.tileSize,
4688
- samplesPerPixel: config.samplesPerPixel,
6704
+ ...createFrameStats({
6705
+ frameIndex,
6706
+ accelerationBuildSubmitted,
6707
+ frameSubmissionCount,
6708
+ parallelismCounters,
6709
+ renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel,
6710
+ targetSamplesPerPixel: samplingPlan.targetSamplesPerPixel,
6711
+ frameTimeBudgetMs: samplingPlan.frameTimeBudgetMs,
6712
+ budgetConstrained: samplingPlan.budgetConstrained,
6713
+ }),
6714
+ gpuWorkerJobs: createGpuWorkerJobDiagnostics(
6715
+ lastGpuParallelism,
6716
+ frameSubmissionCount + (accelerationBuildSubmitted ? 1 : 0),
6717
+ frameTimeMs,
6718
+ false
6719
+ ),
6720
+ });
6721
+ }
6722
+
6723
+ async function waitForSubmittedGpuWork(options = {}) {
6724
+ if (typeof device.queue.onSubmittedWorkDone !== "function") {
6725
+ return true;
6726
+ }
6727
+ const timeoutMs = Math.max(
6728
+ 1,
6729
+ Number.isFinite(options.timeoutMs)
6730
+ ? Number(options.timeoutMs)
6731
+ : GPU_SUBMITTED_WORK_TIMEOUT_MS
6732
+ );
6733
+ const allowTimeout = options.allowTimeout !== false;
6734
+ const completionPromise = device.queue.onSubmittedWorkDone().then(
6735
+ () => ({ status: "done" }),
6736
+ (error) => {
6737
+ throw error;
6738
+ }
6739
+ );
6740
+ const lossPromise =
6741
+ typeof device.lost?.then === "function"
6742
+ ? device.lost.then((info) => {
6743
+ throw new Error(
6744
+ `WebGPU device lost while waiting for submitted work (${info?.reason ?? "unknown"}).`
6745
+ );
6746
+ })
6747
+ : null;
6748
+ let timeoutHandle = null;
6749
+ let resolveTimeoutPromise = null;
6750
+ let timeoutSettled = false;
6751
+ const settleTimeoutPromise = (value) => {
6752
+ if (timeoutSettled) {
6753
+ return;
6754
+ }
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" });
6771
+ }
6772
+ }
6773
+ if (result?.status === "timeout") {
6774
+ if (!allowTimeout) {
6775
+ throw new Error(`Timed out after ${timeoutMs} ms waiting for submitted GPU work.`);
6776
+ }
6777
+ console.warn(
6778
+ `[plasius.wavefront] Submitted GPU work did not report completion within ${timeoutMs} ms; continuing.`
6779
+ );
6780
+ return false;
6781
+ }
6782
+ return true;
6783
+ }
6784
+
6785
+ function dispatchFrameAwaitingGpu(
6786
+ frameIndex,
6787
+ parallelism,
6788
+ renderedSamplesPerPixel = config.samplesPerPixel
6789
+ ) {
6790
+ const samplePassesPerSample = config.maxDepth + 1 + (config.deferredPathResolve ? 1 : 0);
6791
+ const denoisePassCount = config.denoise ? (renderedSamplesPerPixel < 4 ? 2 : 1) : 0;
6792
+ const tailPassCount = denoisePassCount + 1;
6793
+ const sampleBatchSize = Math.max(
6794
+ 1,
6795
+ Math.floor(
6796
+ Math.max(config.maxFramePassesPerSubmission - tailPassCount, 1) /
6797
+ Math.max(samplePassesPerSample, 1)
6798
+ )
6799
+ );
6800
+ let submissionCount = 0;
6801
+
6802
+ for (const tile of tiles) {
6803
+ for (
6804
+ let sampleStart = 0;
6805
+ sampleStart < renderedSamplesPerPixel;
6806
+ sampleStart += sampleBatchSize
6807
+ ) {
6808
+ const sampleEnd = Math.min(renderedSamplesPerPixel, sampleStart + sampleBatchSize);
6809
+ const batch = createGpuSubmissionBatcher({
6810
+ device,
6811
+ frameIndex,
6812
+ maxFramePassesPerSubmission: config.maxFramePassesPerSubmission,
6813
+ startingSubmissionCount: submissionCount,
6814
+ });
6815
+ let slot = 0;
6816
+ for (let sampleIndex = sampleStart; sampleIndex < sampleEnd; sampleIndex += 1) {
6817
+ const configOffset = writeFrameConfigSlot(slot, tile, frameIndex, {
6818
+ sampleIndex,
6819
+ sampleWeight: 1 / renderedSamplesPerPixel,
6820
+ });
6821
+ slot += 1;
6822
+ encodeTileSample(
6823
+ batch.reserve(config.maxDepth + 1),
6824
+ tile,
6825
+ configOffset,
6826
+ parallelism
6827
+ );
6828
+ if (config.deferredPathResolve) {
6829
+ encodeTileOutput(batch.reserve(1), tile, configOffset, parallelism);
6830
+ }
6831
+ }
6832
+ if (!config.deferredPathResolve && sampleEnd >= renderedSamplesPerPixel) {
6833
+ const outputConfigOffset = writeFrameConfigSlot(slot, tile, frameIndex, {
6834
+ sampleIndex: 0,
6835
+ sampleWeight: 1 / renderedSamplesPerPixel,
6836
+ });
6837
+ encodeTileOutput(batch.reserve(1), tile, outputConfigOffset, parallelism);
6838
+ }
6839
+ batch.flush();
6840
+ submissionCount += batch.getSubmissionCount();
6841
+ }
6842
+ }
6843
+
6844
+ const tail = createGpuSubmissionBatcher({
6845
+ device,
6846
+ frameIndex,
4689
6847
  maxFramePassesPerSubmission: config.maxFramePassesPerSubmission,
4690
- screenRays: config.width * config.height,
4691
- primaryRays: config.width * config.height * config.samplesPerPixel,
4692
- sceneObjectCount: config.sceneObjectCount,
4693
- triangleCount: config.triangleCount,
4694
- emissiveTriangleCount: config.emissiveTriangleCount,
4695
- environmentPortalCount: config.environmentPortalCount,
4696
- environmentPortalMode: config.environmentPortalMode,
4697
- environmentMap: createEnvironmentMapSnapshot(config.environmentMap),
4698
- deferredPathResolve: config.deferredPathResolve,
4699
- bvhNodeCount: config.bvhNodeCount,
4700
- displayQuality: config.displayQuality,
4701
- accelerationBuildMode: config.accelerationBuildMode,
4702
- gpuAccelerationBuildRequired: config.gpuAccelerationBuildRequired,
4703
- accelerationBuildSubmitted,
4704
- accelerationBuilt,
4705
- accelerationBuildCount,
4706
- commandSubmissions: frameSubmissionCount + (accelerationBuildSubmitted ? 1 : 0),
4707
- frameConfigSlots: frameConfigSlotCount,
4708
- gpuParallelism: lastGpuParallelism,
4709
- memory: config.memory,
6848
+ startingSubmissionCount: submissionCount,
4710
6849
  });
6850
+ if (config.denoise) {
6851
+ const denoiseConfigOffset = writeFrameConfigSlot(
6852
+ 0,
6853
+ { x: 0, y: 0, width: config.width, height: config.height },
6854
+ frameIndex,
6855
+ { sampleIndex: 0, sampleWeight: 1 / renderedSamplesPerPixel }
6856
+ );
6857
+ encodeDenoise(
6858
+ tail.reserve(denoisePassCount),
6859
+ denoiseConfigOffset,
6860
+ parallelism,
6861
+ renderedSamplesPerPixel
6862
+ );
6863
+ }
6864
+ encodePresent(tail.reserve(1));
6865
+ tail.flush();
6866
+ submissionCount += tail.getSubmissionCount();
6867
+ return submissionCount;
4711
6868
  }
4712
6869
 
4713
6870
  async function readOutputProbe(optionsForProbe = {}) {
@@ -4722,6 +6879,10 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4722
6879
  size: 256,
4723
6880
  usage: constants.buffer.COPY_DST | constants.buffer.MAP_READ,
4724
6881
  });
6882
+ await waitForSubmittedGpuWork({
6883
+ timeoutMs: GPU_READBACK_COMPLETION_TIMEOUT_MS,
6884
+ allowTimeout: false,
6885
+ });
4725
6886
  const encoder = device.createCommandEncoder({
4726
6887
  label: "plasius.wavefront.outputProbe.copy",
4727
6888
  });
@@ -4731,6 +6892,10 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4731
6892
  { width: 1, height: 1, depthOrArrayLayers: 1 }
4732
6893
  );
4733
6894
  device.queue.submit([encoder.finish()]);
6895
+ await waitForSubmittedGpuWork({
6896
+ timeoutMs: GPU_READBACK_COMPLETION_TIMEOUT_MS,
6897
+ allowTimeout: false,
6898
+ });
4734
6899
  await readback.mapAsync(mapMode.READ);
4735
6900
  const bytes = new Uint8Array(readback.getMappedRange()).slice(0, 4);
4736
6901
  readback.unmap();
@@ -4744,7 +6909,60 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4744
6909
  }
4745
6910
 
4746
6911
  async function renderFrame(renderOptions = {}) {
4747
- const frameStats = renderOnce();
6912
+ const awaitGPUCompletion = renderOptions.awaitGPUCompletion !== false;
6913
+ const samplingPlan = resolveRenderedSamplesPerPixel(renderOptions, awaitGPUCompletion);
6914
+ const useThrottledHighSamplePath =
6915
+ awaitGPUCompletion && samplingPlan.renderedSamplesPerPixel >= 8;
6916
+ const submittedWorkTimeoutMs = estimateSubmittedGpuWorkTimeoutMs(
6917
+ { ...config, renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel },
6918
+ tiles.length,
6919
+ renderOptions.submittedWorkTimeoutMs
6920
+ );
6921
+ const frameStartTimeMs = nowMs();
6922
+ const submissionWaitOptions = awaitGPUCompletion
6923
+ ? { timeoutMs: submittedWorkTimeoutMs, allowTimeout: false }
6924
+ : { timeoutMs: submittedWorkTimeoutMs };
6925
+ let frameStats;
6926
+ if (useThrottledHighSamplePath) {
6927
+ frame += 1;
6928
+ const frameIndex = frame + config.frameIndex;
6929
+ const parallelismCounters = createGpuParallelismCounters();
6930
+ const accelerationBuildSubmitted = dispatchGpuAccelerationBuild(frameIndex, parallelismCounters);
6931
+ const frameSubmissionCount = dispatchFrameAwaitingGpu(
6932
+ frameIndex,
6933
+ parallelismCounters,
6934
+ samplingPlan.renderedSamplesPerPixel
6935
+ );
6936
+ frameStats = createFrameStats({
6937
+ frameIndex,
6938
+ accelerationBuildSubmitted,
6939
+ frameSubmissionCount,
6940
+ parallelismCounters,
6941
+ renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel,
6942
+ targetSamplesPerPixel: samplingPlan.targetSamplesPerPixel,
6943
+ frameTimeBudgetMs: samplingPlan.frameTimeBudgetMs,
6944
+ budgetConstrained: samplingPlan.budgetConstrained,
6945
+ });
6946
+ } else {
6947
+ frameStats = renderOnce(renderOptions, samplingPlan);
6948
+ }
6949
+ if (awaitGPUCompletion) {
6950
+ await waitForSubmittedGpuWork(submissionWaitOptions);
6951
+ }
6952
+ const frameTimeMs = Math.max(0, nowMs() - frameStartTimeMs);
6953
+ if (awaitGPUCompletion) {
6954
+ lastCompletedFrameTimeMs = frameTimeMs;
6955
+ lastCompletedSamplesPerPixel = frameStats.renderedSamplesPerPixel ?? frameStats.samplesPerPixel;
6956
+ }
6957
+ frameStats = Object.freeze({
6958
+ ...frameStats,
6959
+ gpuWorkerJobs: createGpuWorkerJobDiagnostics(
6960
+ frameStats.gpuParallelism,
6961
+ frameStats.commandSubmissions,
6962
+ frameTimeMs,
6963
+ awaitGPUCompletion
6964
+ ),
6965
+ });
4748
6966
  const probe =
4749
6967
  renderOptions.readOutputProbe === false ? null : await readOutputProbe(renderOptions.probe);
4750
6968
  const maxChannel = probe ? Math.max(...probe.rgba.slice(0, 3)) : 0;
@@ -4769,10 +6987,8 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4769
6987
  });
4770
6988
  }
4771
6989
 
4772
- function updateSceneObjects(sceneObjects) {
4773
- const nextPackedScene = packWavefrontSceneObjects(sceneObjects, config.sceneObjectCapacity);
4774
- packedScene = nextPackedScene;
4775
- config = createWavefrontPathTracingComputeConfig({
6990
+ function rebuildLiveConfig(overrides = {}) {
6991
+ return createWavefrontPathTracingComputeConfig({
4776
6992
  ...options,
4777
6993
  canvas,
4778
6994
  width: config.width,
@@ -4783,27 +6999,25 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4783
6999
  sceneObjectCapacity: config.sceneObjectCapacity,
4784
7000
  sceneObjects: packedScene.objects,
4785
7001
  camera: activeCameraOptions,
7002
+ environmentMap: {
7003
+ ...config.environmentMap,
7004
+ },
4786
7005
  frameIndex: config.frameIndex,
7006
+ ...overrides,
4787
7007
  });
7008
+ }
7009
+
7010
+ function updateSceneObjects(sceneObjects) {
7011
+ const nextPackedScene = packWavefrontSceneObjects(sceneObjects, config.sceneObjectCapacity);
7012
+ packedScene = nextPackedScene;
7013
+ config = rebuildLiveConfig();
4788
7014
  device.queue.writeBuffer(sceneObjectBuffer, 0, packedScene.buffer);
4789
7015
  return config;
4790
7016
  }
4791
7017
 
4792
7018
  function updateCamera(cameraOptions = {}) {
4793
7019
  activeCameraOptions = cameraOptions;
4794
- config = createWavefrontPathTracingComputeConfig({
4795
- ...options,
4796
- canvas,
4797
- width: config.width,
4798
- height: config.height,
4799
- maxDepth: config.maxDepth,
4800
- tileSize: config.tileSize,
4801
- samplesPerPixel: config.samplesPerPixel,
4802
- sceneObjectCapacity: config.sceneObjectCapacity,
4803
- sceneObjects: packedScene.objects,
4804
- camera: activeCameraOptions,
4805
- frameIndex: config.frameIndex,
4806
- });
7020
+ config = rebuildLiveConfig();
4807
7021
  return config;
4808
7022
  }
4809
7023
 
@@ -4856,9 +7070,28 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4856
7070
  activeDispatchBuffer.destroy?.();
4857
7071
  radianceTexture.destroy?.();
4858
7072
  denoiseScratchTexture.destroy?.();
4859
- outputTexture.destroy?.();
4860
- if (environmentMapResource.ownsTexture) {
4861
- environmentMapResource.texture?.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
+ }
7084
+ if (metallicRoughnessAtlasResource.ownsTexture) {
7085
+ metallicRoughnessAtlasResource.texture?.destroy?.();
7086
+ }
7087
+ if (normalAtlasResource.ownsTexture) {
7088
+ normalAtlasResource.texture?.destroy?.();
7089
+ }
7090
+ if (occlusionAtlasResource.ownsTexture) {
7091
+ occlusionAtlasResource.texture?.destroy?.();
7092
+ }
7093
+ if (emissiveAtlasResource.ownsTexture) {
7094
+ emissiveAtlasResource.texture?.destroy?.();
4862
7095
  }
4863
7096
  context.unconfigure?.();
4864
7097
  }