@plasius/gpu-renderer 0.2.5 → 0.2.7

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,32 +1,51 @@
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 = 128;
17
+ const DEFAULT_BRDF_LUT_SAMPLE_COUNT = 256;
6
18
  const DEFAULT_MAX_FRAME_PASSES_PER_SUBMISSION = 256;
7
19
  const DEFAULT_SCENE_OBJECT_CAPACITY = 128;
8
20
  const DEFAULT_ENVIRONMENT_PORTAL_CAPACITY = 32;
21
+ const DEFAULT_MEDIUM_PHASE_MODEL = 0;
9
22
  const WORKGROUP_SIZE = 64;
10
23
  export const rendererWavefrontComputeMode = "webgpu-compute";
11
24
  export const rendererWavefrontComputeWorkgroupSize = WORKGROUP_SIZE;
12
25
  export const rendererWavefrontComputeStatsStride = 8;
13
26
  const RAY_RECORD_BYTES = 80;
14
- const HIT_RECORD_BYTES = 208;
15
- const SCENE_OBJECT_RECORD_BYTES = 96;
27
+ const HIT_RECORD_BYTES = 256;
28
+ const SCENE_OBJECT_RECORD_BYTES = 160;
16
29
  const MESH_VERTEX_RECORD_BYTES = 48;
17
- const MESH_RANGE_RECORD_BYTES = 96;
18
- const TRIANGLE_RECORD_BYTES = 208;
30
+ const MESH_RANGE_RECORD_BYTES = 240;
31
+ const TRIANGLE_RECORD_BYTES = 352;
32
+ const GPU_MATERIAL_RECORD_BYTES = 192;
19
33
  const BVH_NODE_RECORD_BYTES = 48;
20
34
  const BVH_LEAF_REF_RECORD_BYTES = 16;
21
35
  const EMISSIVE_TRIANGLE_INDEX_BYTES = 4;
22
36
  const ENVIRONMENT_PORTAL_RECORD_BYTES = 96;
37
+ const MEDIUM_TABLE_ROWS = 2;
23
38
  const ACCUMULATION_RECORD_BYTES = 16;
24
39
  const PATH_VERTEX_RECORD_BYTES = 16;
25
- const CONFIG_BUFFER_BYTES = 304;
40
+ const GPU_SUBMITTED_WORK_TIMEOUT_MS = 5_000;
41
+ const GPU_READBACK_COMPLETION_TIMEOUT_MS = 60_000;
42
+ const GPU_MAX_SUBMITTED_WORK_TIMEOUT_MS = 60_000;
43
+ const CONFIG_BUFFER_BYTES = 320;
26
44
  const COUNTER_DISPATCH_ARGS_OFFSET = 16;
27
45
  const INDIRECT_DISPATCH_ARGS_BYTES = 12;
28
46
  const COUNTER_BUFFER_BYTES = 32;
29
47
  const TRACE_STORAGE_BUFFER_BINDINGS = 10;
48
+ const BRDF_LUT_UPLOAD_CACHE = new Map();
30
49
  const HIT_TYPE_SURFACE = 0;
31
50
  const HIT_TYPE_EMISSIVE = 1;
32
51
  const MATERIAL_DIFFUSE = 0;
@@ -66,6 +85,7 @@ export const wavefrontPathTracingComputeLimits = Object.freeze({
66
85
  meshVertexRecordBytes: MESH_VERTEX_RECORD_BYTES,
67
86
  meshRangeRecordBytes: MESH_RANGE_RECORD_BYTES,
68
87
  triangleRecordBytes: TRIANGLE_RECORD_BYTES,
88
+ materialRecordBytes: GPU_MATERIAL_RECORD_BYTES,
69
89
  bvhNodeRecordBytes: BVH_NODE_RECORD_BYTES,
70
90
  bvhLeafReferenceRecordBytes: BVH_LEAF_REF_RECORD_BYTES,
71
91
  emissiveTriangleIndexBytes: EMISSIVE_TRIANGLE_INDEX_BYTES,
@@ -164,6 +184,38 @@ function asColor(value, fallback = [1, 1, 1, 1]) {
164
184
  ];
165
185
  }
166
186
 
187
+ function maxComponent(value) {
188
+ return Math.max(value?.[0] ?? 0, value?.[1] ?? 0, value?.[2] ?? 0);
189
+ }
190
+
191
+ function deriveLegacySheenColor(baseColor, sheen, sheenTint) {
192
+ const sheenStrength = clamp(Number(sheen) || 0, 0, 1);
193
+ if (sheenStrength <= 0) {
194
+ return [0, 0, 0, 1];
195
+ }
196
+ const tint = clamp(Number(sheenTint) || 0, 0, 1);
197
+ const base = asColor(baseColor, [1, 1, 1, 1]);
198
+ return [
199
+ clamp((1 - tint) * sheenStrength + base[0] * tint * sheenStrength, 0, 1),
200
+ clamp((1 - tint) * sheenStrength + base[1] * tint * sheenStrength, 0, 1),
201
+ clamp((1 - tint) * sheenStrength + base[2] * tint * sheenStrength, 0, 1),
202
+ 1,
203
+ ];
204
+ }
205
+
206
+ function resolveSheenColor(input, fallbackBaseColor) {
207
+ if (input?.sheenColor || input?.material?.sheenColor) {
208
+ return asColor(input.sheenColor ?? input.material?.sheenColor, [0, 0, 0, 1]).map((value, index) =>
209
+ index < 3 ? clamp(value, 0, 1) : 1
210
+ );
211
+ }
212
+ return deriveLegacySheenColor(
213
+ fallbackBaseColor,
214
+ input?.sheen ?? input?.material?.sheen,
215
+ input?.sheenTint ?? input?.material?.sheenTint
216
+ );
217
+ }
218
+
167
219
  function resolveEnvironmentMap(input = null) {
168
220
  const source = input && typeof input === "object" ? input : null;
169
221
  const hasTexture = Boolean(source?.view || source?.texture || source?.data);
@@ -173,6 +225,11 @@ function resolveEnvironmentMap(input = null) {
173
225
  enabled: hasTexture && source?.enabled !== false,
174
226
  width,
175
227
  height,
228
+ mipLevelCount: readPositiveInteger(
229
+ "environmentMap.mipLevelCount",
230
+ source?.mipLevelCount,
231
+ 1
232
+ ),
176
233
  format: typeof source?.format === "string" ? source.format : "rgba16float",
177
234
  projection: typeof source?.projection === "string" ? source.projection : "equirectangular",
178
235
  texture: source?.texture ?? null,
@@ -185,6 +242,7 @@ function resolveEnvironmentMap(input = null) {
185
242
  0,
186
243
  readFiniteNumber("environmentMap.ambientStrength", source?.ambientStrength, 0.32)
187
244
  ),
245
+ hasImportanceData: source?.hasImportanceData === true,
188
246
  });
189
247
  }
190
248
 
@@ -382,6 +440,183 @@ function deriveBounds(input) {
382
440
  return null;
383
441
  }
384
442
 
443
+ function deriveBeerLambertAbsorptionFromAttenuationColor(
444
+ attenuationColor,
445
+ attenuationDistance,
446
+ density = 1
447
+ ) {
448
+ const distance = Number(attenuationDistance);
449
+ const densityScale = Math.max(0, Number(density) || 0);
450
+ if (!Number.isFinite(distance) || distance <= 0 || densityScale <= 0) {
451
+ return [0, 0, 0];
452
+ }
453
+ return attenuationColor.slice(0, 3).map((channel) => {
454
+ const clamped = clamp(Number(channel) || 0, 0.0001, 1);
455
+ return Math.max(0, (-Math.log(clamped) / distance) * densityScale);
456
+ });
457
+ }
458
+
459
+ function readMediumPhaseModel(value) {
460
+ if (typeof value === "number" && Number.isFinite(value)) {
461
+ return Math.max(0, Math.trunc(value));
462
+ }
463
+ switch (String(value ?? "").trim().toLowerCase()) {
464
+ case "isotropic":
465
+ default:
466
+ return DEFAULT_MEDIUM_PHASE_MODEL;
467
+ }
468
+ }
469
+
470
+ function resolveWavefrontVolumeInput(input) {
471
+ return input?.volume ?? input?.material?.volume ?? null;
472
+ }
473
+
474
+ function normalizeWavefrontThickness(input, label) {
475
+ const volume = resolveWavefrontVolumeInput(input);
476
+ return Math.max(
477
+ 0,
478
+ readFiniteNumber(
479
+ label,
480
+ input?.thickness ?? volume?.thickness ?? input?.material?.thickness,
481
+ 0
482
+ )
483
+ );
484
+ }
485
+
486
+ function resolveWavefrontMediumId(input, fallbackId = 1) {
487
+ return (
488
+ input?.mediumRefId ??
489
+ input?.mediumId ??
490
+ input?.material?.mediumId ??
491
+ input?.materialRefId ??
492
+ input?.material?.id ??
493
+ input?.materialId ??
494
+ input?.id ??
495
+ fallbackId
496
+ );
497
+ }
498
+
499
+ function deriveWavefrontTransportMedium(input, fallbackId = 1) {
500
+ const resolvedId = resolveWavefrontMediumId(input, fallbackId);
501
+ if (input?.medium) {
502
+ return normalizeWavefrontMedium(
503
+ {
504
+ ...input.medium,
505
+ id: input.medium.id ?? input.medium.mediumId ?? resolvedId,
506
+ },
507
+ fallbackId
508
+ );
509
+ }
510
+ const volume = resolveWavefrontVolumeInput(input);
511
+ if (!volume) {
512
+ return null;
513
+ }
514
+ return normalizeWavefrontMedium(
515
+ {
516
+ id: resolvedId,
517
+ phaseModel: volume.phaseModel,
518
+ density: volume.density,
519
+ attenuationColor: volume.attenuationColor,
520
+ attenuationDistance: volume.attenuationDistance,
521
+ absorption: volume.absorption,
522
+ scattering: volume.scattering,
523
+ },
524
+ fallbackId
525
+ );
526
+ }
527
+
528
+ function normalizeWavefrontMedium(input = {}, index = 0) {
529
+ const id = readNonNegativeInteger("medium id", input.id ?? input.mediumId, index);
530
+ const density = Math.max(0, readFiniteNumber("medium density", input.density, 1));
531
+ const attenuationColor = asColor(
532
+ input.attenuationColor ?? input.color ?? input.medium?.attenuationColor,
533
+ [1, 1, 1, 1]
534
+ );
535
+ const attenuationDistance = readFiniteNumber(
536
+ "medium attenuationDistance",
537
+ input.attenuationDistance ?? input.distance ?? input.medium?.attenuationDistance,
538
+ 0
539
+ );
540
+ const absorption =
541
+ Array.isArray(input.absorption) || Array.isArray(input.medium?.absorption)
542
+ ? asVec3(input.absorption ?? input.medium?.absorption, [0, 0, 0]).map((value) =>
543
+ Math.max(0, Number(value) || 0)
544
+ )
545
+ : deriveBeerLambertAbsorptionFromAttenuationColor(
546
+ attenuationColor,
547
+ attenuationDistance,
548
+ density
549
+ );
550
+ const scattering = asVec3(
551
+ input.scattering ?? input.medium?.scattering,
552
+ [0, 0, 0]
553
+ ).map((value) => Math.max(0, Number(value) || 0));
554
+ return Object.freeze({
555
+ id,
556
+ phaseModel: readMediumPhaseModel(input.phaseModel ?? input.medium?.phaseModel),
557
+ density,
558
+ attenuationColor: Object.freeze(attenuationColor),
559
+ attenuationDistance,
560
+ absorption: Object.freeze(absorption),
561
+ scattering: Object.freeze(scattering),
562
+ });
563
+ }
564
+
565
+ function collectWavefrontMediums(options, meshes, sceneObjects = []) {
566
+ const mediumsById = new Map();
567
+ mediumsById.set(
568
+ 0,
569
+ Object.freeze({
570
+ id: 0,
571
+ phaseModel: DEFAULT_MEDIUM_PHASE_MODEL,
572
+ density: 0,
573
+ attenuationColor: Object.freeze([1, 1, 1, 1]),
574
+ attenuationDistance: 0,
575
+ absorption: Object.freeze([0, 0, 0]),
576
+ scattering: Object.freeze([0, 0, 0]),
577
+ })
578
+ );
579
+
580
+ const register = (input, fallbackId = mediumsById.size) => {
581
+ if (!input) {
582
+ return;
583
+ }
584
+ const normalized = normalizeWavefrontMedium(
585
+ typeof input === "object" ? { id: fallbackId, ...input } : { id: fallbackId },
586
+ fallbackId
587
+ );
588
+ const existing = mediumsById.get(normalized.id);
589
+ if (existing && JSON.stringify(existing) !== JSON.stringify(normalized)) {
590
+ throw new Error(`Medium id ${normalized.id} is defined more than once with different values.`);
591
+ }
592
+ mediumsById.set(normalized.id, normalized);
593
+ };
594
+
595
+ for (const medium of options.mediums ?? []) {
596
+ register(medium);
597
+ }
598
+ for (const mesh of meshes) {
599
+ register(mesh.medium, mesh.mediumRefId ?? mesh.medium?.id ?? 0);
600
+ }
601
+ for (const mesh of meshes) {
602
+ if ((mesh.mediumRefId ?? 0) > 0 && !mediumsById.has(mesh.mediumRefId)) {
603
+ register({ id: mesh.mediumRefId });
604
+ }
605
+ }
606
+ for (const object of sceneObjects) {
607
+ register(object.medium, object.mediumRefId ?? object.medium?.id ?? 0);
608
+ }
609
+ for (const object of sceneObjects) {
610
+ if ((object.mediumRefId ?? 0) > 0 && !mediumsById.has(object.mediumRefId)) {
611
+ register({ id: object.mediumRefId });
612
+ }
613
+ }
614
+
615
+ return Object.freeze(
616
+ Array.from(mediumsById.values()).sort((left, right) => left.id - right.id)
617
+ );
618
+ }
619
+
385
620
  export function normalizeWavefrontSceneObject(input = {}, index = 0) {
386
621
  const bounds = deriveBounds(input);
387
622
  const kind = readObjectKind(input.kind ?? input.type ?? (bounds ? "box" : "sphere"));
@@ -394,7 +629,8 @@ export function normalizeWavefrontSceneObject(input = {}, index = 0) {
394
629
  input.halfExtent ?? input.halfExtents ?? input.extents ?? bounds?.halfExtent,
395
630
  [0.5, 0.5, 0.5]
396
631
  ).map((value) => Math.max(value, 0.001));
397
- const materialKind = readMaterialKind(input.materialKind ?? input.material?.kind);
632
+ const materialKindInput = input.materialKind ?? input.material?.kind;
633
+ const materialKind = readMaterialKind(materialKindInput);
398
634
  const color = asColor(
399
635
  input.color ??
400
636
  input.baseColor ??
@@ -407,23 +643,63 @@ export function normalizeWavefrontSceneObject(input = {}, index = 0) {
407
643
  input.emission ?? input.emissive ?? input.material?.emission ?? input.material?.emissive,
408
644
  [0, 0, 0, 1]
409
645
  );
646
+ const opacity = clamp(readFiniteNumber("opacity", input.opacity ?? input.material?.opacity, color[3] ?? 1), 0, 1);
647
+ const transmission = clamp(
648
+ readFiniteNumber("transmission", input.transmission ?? input.material?.transmission, 0),
649
+ 0,
650
+ 1
651
+ );
652
+ const sheenColor = resolveSheenColor(input, color);
653
+ const specularColor = asColor(
654
+ input.specularColor ?? input.material?.specularColor,
655
+ [1, 1, 1, 1]
656
+ ).map((value, componentIndex) => (componentIndex < 3 ? clamp(value, 0, 1) : 1));
657
+ const medium = deriveWavefrontTransportMedium(input, index + 1);
658
+ const resolvedMaterialKind =
659
+ emission[0] > 0 || emission[1] > 0 || emission[2] > 0
660
+ ? MATERIAL_EMISSIVE
661
+ : materialKindInput === undefined || materialKindInput === null
662
+ ? transmission > 0.001 || opacity < 0.999
663
+ ? MATERIAL_TRANSPARENT
664
+ : materialKind
665
+ : materialKind;
410
666
 
411
667
  return Object.freeze({
412
668
  id: readNonNegativeInteger("id", input.id, index + 1),
413
669
  kind,
414
- materialKind:
415
- emission[0] > 0 || emission[1] > 0 || emission[2] > 0
416
- ? MATERIAL_EMISSIVE
417
- : materialKind,
670
+ materialKind: resolvedMaterialKind,
418
671
  flags: readNonNegativeInteger("flags", input.flags, 0),
672
+ mediumRefId: readNonNegativeInteger(
673
+ "mediumRefId",
674
+ input.mediumRefId ?? medium?.id ?? input.medium?.id ?? input.mediumId,
675
+ 0
676
+ ),
677
+ medium,
419
678
  center: Object.freeze(center),
420
679
  halfExtent: Object.freeze(halfExtent),
421
680
  color: Object.freeze(color),
422
681
  emission: Object.freeze(emission),
423
682
  roughness: clamp(readFiniteNumber("roughness", input.roughness ?? input.material?.roughness, 0.72), 0, 1),
424
683
  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),
684
+ opacity,
426
685
  ior: clamp(readFiniteNumber("ior", input.ior ?? input.material?.ior, 1.45), 1, 3),
686
+ sheen: clamp(readFiniteNumber("sheen", input.sheen ?? input.material?.sheen, 0), 0, 1),
687
+ sheenTint: clamp(readFiniteNumber("sheenTint", input.sheenTint ?? input.material?.sheenTint, 0), 0, 1),
688
+ sheenColor: Object.freeze(sheenColor),
689
+ clearcoat: clamp(readFiniteNumber("clearcoat", input.clearcoat ?? input.material?.clearcoat, 0), 0, 1),
690
+ clearcoatRoughness: clamp(
691
+ readFiniteNumber(
692
+ "clearcoatRoughness",
693
+ input.clearcoatRoughness ?? input.material?.clearcoatRoughness,
694
+ 0.08
695
+ ),
696
+ 0,
697
+ 1
698
+ ),
699
+ specular: clamp(readFiniteNumber("specular", input.specular ?? input.material?.specular, 1), 0, 1),
700
+ specularColor: Object.freeze(specularColor),
701
+ thickness: normalizeWavefrontThickness(input, "thickness"),
702
+ transmission,
427
703
  });
428
704
  }
429
705
 
@@ -507,7 +783,8 @@ export function normalizeWavefrontMesh(input = {}, meshIndex = 0) {
507
783
  readFiniteNumber("mesh uv", value, 0)
508
784
  )
509
785
  : null;
510
- const materialKind = readMaterialKind(input.materialKind ?? input.material?.kind);
786
+ const materialKindInput = input.materialKind ?? input.material?.kind;
787
+ const materialKind = readMaterialKind(materialKindInput);
511
788
  const color = asColor(
512
789
  input.color ??
513
790
  input.baseColor ??
@@ -520,6 +797,26 @@ export function normalizeWavefrontMesh(input = {}, meshIndex = 0) {
520
797
  input.emission ?? input.emissive ?? input.material?.emission ?? input.material?.emissive,
521
798
  [0, 0, 0, 1]
522
799
  );
800
+ const opacity = clamp(readFiniteNumber("opacity", input.opacity ?? input.material?.opacity, color[3] ?? 1), 0, 1);
801
+ const transmission = clamp(
802
+ readFiniteNumber("transmission", input.transmission ?? input.material?.transmission, 0),
803
+ 0,
804
+ 1
805
+ );
806
+ const sheenColor = resolveSheenColor(input, color);
807
+ const specularColor = asColor(
808
+ input.specularColor ?? input.material?.specularColor,
809
+ [1, 1, 1, 1]
810
+ ).map((value, componentIndex) => (componentIndex < 3 ? clamp(value, 0, 1) : 1));
811
+ const medium = deriveWavefrontTransportMedium(input, meshIndex + 1);
812
+ const resolvedMaterialKind =
813
+ emission[0] > 0 || emission[1] > 0 || emission[2] > 0
814
+ ? MATERIAL_EMISSIVE
815
+ : materialKindInput === undefined || materialKindInput === null
816
+ ? transmission > 0.001 || opacity < 0.999
817
+ ? MATERIAL_TRANSPARENT
818
+ : materialKind
819
+ : materialKind;
523
820
 
524
821
  return Object.freeze({
525
822
  id: readNonNegativeInteger("mesh id", input.id, meshIndex + 1),
@@ -527,10 +824,7 @@ export function normalizeWavefrontMesh(input = {}, meshIndex = 0) {
527
824
  indices: Object.freeze(indices),
528
825
  normals: normals ? Object.freeze(normals) : null,
529
826
  uvs: uvs ? Object.freeze(uvs) : null,
530
- materialKind:
531
- emission[0] > 0 || emission[1] > 0 || emission[2] > 0
532
- ? MATERIAL_EMISSIVE
533
- : materialKind,
827
+ materialKind: resolvedMaterialKind,
534
828
  flags: readNonNegativeInteger("mesh flags", input.flags, 0),
535
829
  materialRefId: readNonNegativeInteger(
536
830
  "mesh materialRefId",
@@ -539,18 +833,202 @@ export function normalizeWavefrontMesh(input = {}, meshIndex = 0) {
539
833
  ),
540
834
  mediumRefId: readNonNegativeInteger(
541
835
  "mesh mediumRefId",
542
- input.mediumRefId ?? input.medium?.id ?? input.mediumId,
836
+ input.mediumRefId ??
837
+ medium?.id ??
838
+ input.medium?.id ??
839
+ input.mediumId ??
840
+ input.material?.mediumId,
543
841
  0
544
842
  ),
843
+ medium,
545
844
  color: Object.freeze(color),
546
845
  emission: Object.freeze(emission),
547
846
  roughness: clamp(readFiniteNumber("roughness", input.roughness ?? input.material?.roughness, 0.72), 0, 1),
548
847
  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),
848
+ opacity,
550
849
  ior: clamp(readFiniteNumber("ior", input.ior ?? input.material?.ior, 1.45), 1, 3),
850
+ sheen: clamp(readFiniteNumber("sheen", input.sheen ?? input.material?.sheen, 0), 0, 1),
851
+ sheenTint: clamp(readFiniteNumber("sheenTint", input.sheenTint ?? input.material?.sheenTint, 0), 0, 1),
852
+ sheenColor: Object.freeze(sheenColor),
853
+ clearcoat: clamp(readFiniteNumber("clearcoat", input.clearcoat ?? input.material?.clearcoat, 0), 0, 1),
854
+ clearcoatRoughness: clamp(
855
+ readFiniteNumber(
856
+ "clearcoatRoughness",
857
+ input.clearcoatRoughness ?? input.material?.clearcoatRoughness,
858
+ 0.08
859
+ ),
860
+ 0,
861
+ 1
862
+ ),
863
+ specular: clamp(readFiniteNumber("specular", input.specular ?? input.material?.specular, 1), 0, 1),
864
+ specularColor: Object.freeze(specularColor),
865
+ thickness: normalizeWavefrontThickness(input, "mesh thickness"),
866
+ transmission,
867
+ baseColorTexture: input.baseColorTexture ?? input.material?.baseColorTexture ?? null,
868
+ metallicRoughnessTexture:
869
+ input.metallicRoughnessTexture ?? input.material?.metallicRoughnessTexture ?? null,
870
+ normalTexture: input.normalTexture ?? input.material?.normalTexture ?? null,
871
+ occlusionTexture: input.occlusionTexture ?? input.material?.occlusionTexture ?? null,
872
+ emissiveTexture: input.emissiveTexture ?? input.material?.emissiveTexture ?? null,
551
873
  });
552
874
  }
553
875
 
876
+ function clampUnit(value) {
877
+ return clamp(Number(value) || 0, 0, 1);
878
+ }
879
+
880
+ function srgbToLinear(value) {
881
+ const channel = clampUnit(value);
882
+ if (channel <= 0.04045) {
883
+ return channel / 12.92;
884
+ }
885
+ return ((channel + 0.055) / 1.055) ** 2.4;
886
+ }
887
+
888
+ function sampleTextureRgba(texture, uv = [0, 0], colorSpace = "linear") {
889
+ if (
890
+ !texture ||
891
+ !Number.isFinite(texture.width) ||
892
+ !Number.isFinite(texture.height) ||
893
+ !texture.data ||
894
+ texture.width <= 0 ||
895
+ texture.height <= 0
896
+ ) {
897
+ return [1, 1, 1, 1];
898
+ }
899
+ const u = ((uv[0] % 1) + 1) % 1;
900
+ const v = ((uv[1] % 1) + 1) % 1;
901
+ const x = Math.min(texture.width - 1, Math.max(0, Math.round(u * (texture.width - 1))));
902
+ const y = Math.min(texture.height - 1, Math.max(0, Math.round((1 - v) * (texture.height - 1))));
903
+ const offset = (y * texture.width + x) * 4;
904
+ const data = texture.data;
905
+ const scale = resolveTextureSampleScale(data);
906
+ const defaultChannel = scale === 1 ? 1 : Math.round(1 / scale);
907
+ const color = [
908
+ (data[offset] ?? defaultChannel) * scale,
909
+ (data[offset + 1] ?? defaultChannel) * scale,
910
+ (data[offset + 2] ?? defaultChannel) * scale,
911
+ (data[offset + 3] ?? defaultChannel) * scale,
912
+ ];
913
+ if (colorSpace === "srgb") {
914
+ return [srgbToLinear(color[0]), srgbToLinear(color[1]), srgbToLinear(color[2]), color[3]];
915
+ }
916
+ return color;
917
+ }
918
+
919
+ function resolveTextureSampleScale(data) {
920
+ if (data instanceof Uint8Array || data instanceof Uint8ClampedArray) {
921
+ return 1 / 255;
922
+ }
923
+ if (data instanceof Uint16Array) {
924
+ return 1 / 65535;
925
+ }
926
+ if (Array.isArray(data) && data.some((value) => Number(value) > 1)) {
927
+ return 1 / 255;
928
+ }
929
+ return 1;
930
+ }
931
+
932
+ function normalizeVectorOrFallback(vector, fallback) {
933
+ return normalize(Array.isArray(vector) ? vector : fallback, fallback);
934
+ }
935
+
936
+ function buildTriangleTangentBasis(v0, v1, v2, uv0, uv1, uv2, fallbackNormal) {
937
+ const edge1 = subtract(v1, v0);
938
+ const edge2 = subtract(v2, v0);
939
+ const deltaUv1 = [uv1[0] - uv0[0], uv1[1] - uv0[1]];
940
+ const deltaUv2 = [uv2[0] - uv0[0], uv2[1] - uv0[1]];
941
+ const determinant = deltaUv1[0] * deltaUv2[1] - deltaUv1[1] * deltaUv2[0];
942
+ if (Math.abs(determinant) < 1e-6) {
943
+ const tangentFallback = Math.abs(fallbackNormal[1]) < 0.999 ? [0, 1, 0] : [1, 0, 0];
944
+ const tangent = normalize(cross(tangentFallback, fallbackNormal), [1, 0, 0]);
945
+ const bitangent = normalize(cross(fallbackNormal, tangent), [0, 0, 1]);
946
+ return { tangent, bitangent };
947
+ }
948
+ const inverse = 1 / determinant;
949
+ const tangent = normalize(
950
+ [
951
+ inverse * (edge1[0] * deltaUv2[1] - edge2[0] * deltaUv1[1]),
952
+ inverse * (edge1[1] * deltaUv2[1] - edge2[1] * deltaUv1[1]),
953
+ inverse * (edge1[2] * deltaUv2[1] - edge2[2] * deltaUv1[1]),
954
+ ],
955
+ [1, 0, 0]
956
+ );
957
+ const bitangent = normalize(
958
+ [
959
+ inverse * (-edge1[0] * deltaUv2[0] + edge2[0] * deltaUv1[0]),
960
+ inverse * (-edge1[1] * deltaUv2[0] + edge2[1] * deltaUv1[0]),
961
+ inverse * (-edge1[2] * deltaUv2[0] + edge2[2] * deltaUv1[0]),
962
+ ],
963
+ [0, 0, 1]
964
+ );
965
+ return { tangent, bitangent };
966
+ }
967
+
968
+ function applyNormalMap(normal, tangent, bitangent, normalTexture, uv) {
969
+ if (!normalTexture) {
970
+ return normalizeVectorOrFallback(normal, [0, 1, 0]);
971
+ }
972
+ const sample = sampleTextureRgba(normalTexture, uv, "linear");
973
+ const strength = clampUnit(normalTexture.scale ?? 1);
974
+ const tangentNormal = normalize(
975
+ [
976
+ (sample[0] * 2 - 1) * strength,
977
+ (sample[1] * 2 - 1) * strength,
978
+ 1 + (sample[2] * 2 - 1 - 1) * strength,
979
+ ],
980
+ [0, 0, 1]
981
+ );
982
+ return normalize(
983
+ [
984
+ tangent[0] * tangentNormal[0] + bitangent[0] * tangentNormal[1] + normal[0] * tangentNormal[2],
985
+ tangent[1] * tangentNormal[0] + bitangent[1] * tangentNormal[1] + normal[1] * tangentNormal[2],
986
+ tangent[2] * tangentNormal[0] + bitangent[2] * tangentNormal[1] + normal[2] * tangentNormal[2],
987
+ ],
988
+ normal
989
+ );
990
+ }
991
+
992
+ function sampleBaseColor(mesh, uv) {
993
+ const sample = mesh.baseColorTexture ? sampleTextureRgba(mesh.baseColorTexture, uv, "srgb") : [1, 1, 1, 1];
994
+ return [
995
+ clampUnit(mesh.color[0] * sample[0]),
996
+ clampUnit(mesh.color[1] * sample[1]),
997
+ clampUnit(mesh.color[2] * sample[2]),
998
+ clampUnit((mesh.color[3] ?? 1) * sample[3]),
999
+ ];
1000
+ }
1001
+
1002
+ function sampleSurfaceMaterial(mesh, uv) {
1003
+ const textureSample = mesh.metallicRoughnessTexture
1004
+ ? sampleTextureRgba(mesh.metallicRoughnessTexture, uv, "linear")
1005
+ : [1, 1, 1, 1];
1006
+ return {
1007
+ roughness: clamp(mesh.roughness * textureSample[1], 0, 1),
1008
+ metallic: clamp(mesh.metallic * textureSample[2], 0, 1),
1009
+ };
1010
+ }
1011
+
1012
+ function averageColors(colors) {
1013
+ const count = Math.max(colors.length, 1);
1014
+ return colors.reduce(
1015
+ (accumulator, color) => [
1016
+ accumulator[0] + color[0] / count,
1017
+ accumulator[1] + color[1] / count,
1018
+ accumulator[2] + color[2] / count,
1019
+ accumulator[3] + color[3] / count,
1020
+ ],
1021
+ [0, 0, 0, 0]
1022
+ );
1023
+ }
1024
+
1025
+ function averageNumbers(values, fallback = 0) {
1026
+ if (!Array.isArray(values) || values.length === 0) {
1027
+ return fallback;
1028
+ }
1029
+ return values.reduce((sum, value) => sum + value, 0) / values.length;
1030
+ }
1031
+
554
1032
  function createMeshTriangleRecords(meshes) {
555
1033
  const source = Array.isArray(meshes) ? meshes : [];
556
1034
  let nextTriangleId = 0;
@@ -571,6 +1049,16 @@ function createMeshTriangleRecords(meshes) {
571
1049
  const uv0 = mesh.uvs ? readVector2(mesh.uvs, a) : [0, 0];
572
1050
  const uv1 = mesh.uvs ? readVector2(mesh.uvs, b) : [0, 0];
573
1051
  const uv2 = mesh.uvs ? readVector2(mesh.uvs, c) : [0, 0];
1052
+ const tangentBasis = buildTriangleTangentBasis(v0, v1, v2, uv0, uv1, uv2, faceNormal);
1053
+ const shadedN0 = applyNormalMap(n0, tangentBasis.tangent, tangentBasis.bitangent, mesh.normalTexture, uv0);
1054
+ const shadedN1 = applyNormalMap(n1, tangentBasis.tangent, tangentBasis.bitangent, mesh.normalTexture, uv1);
1055
+ const shadedN2 = applyNormalMap(n2, tangentBasis.tangent, tangentBasis.bitangent, mesh.normalTexture, uv2);
1056
+ const sampledColors = [sampleBaseColor(mesh, uv0), sampleBaseColor(mesh, uv1), sampleBaseColor(mesh, uv2)];
1057
+ const sampledMaterials = [
1058
+ sampleSurfaceMaterial(mesh, uv0),
1059
+ sampleSurfaceMaterial(mesh, uv1),
1060
+ sampleSurfaceMaterial(mesh, uv2),
1061
+ ];
574
1062
  const bounds = triangleBounds(v0, v1, v2);
575
1063
 
576
1064
  triangles.push(
@@ -581,18 +1069,42 @@ function createMeshTriangleRecords(meshes) {
581
1069
  flags: mesh.flags,
582
1070
  materialRefId: mesh.materialRefId,
583
1071
  mediumRefId: mesh.mediumRefId,
1072
+ materialSlot: meshIndex,
584
1073
  v0: Object.freeze(v0),
585
1074
  v1: Object.freeze(v1),
586
1075
  v2: Object.freeze(v2),
587
- n0: Object.freeze(n0),
588
- n1: Object.freeze(n1),
589
- n2: Object.freeze(n2),
1076
+ n0: Object.freeze(shadedN0),
1077
+ n1: Object.freeze(shadedN1),
1078
+ n2: Object.freeze(shadedN2),
590
1079
  uv0: Object.freeze(uv0),
591
1080
  uv1: Object.freeze(uv1),
592
1081
  uv2: Object.freeze(uv2),
593
- color: mesh.color,
1082
+ color: Object.freeze(averageColors(sampledColors)),
594
1083
  emission: mesh.emission,
595
- material: Object.freeze([mesh.roughness, mesh.metallic, mesh.opacity, mesh.ior]),
1084
+ material: Object.freeze([
1085
+ averageNumbers(sampledMaterials.map((sample) => sample.roughness), mesh.roughness),
1086
+ averageNumbers(sampledMaterials.map((sample) => sample.metallic), mesh.metallic),
1087
+ mesh.opacity,
1088
+ mesh.ior,
1089
+ ]),
1090
+ materialResponse: Object.freeze([
1091
+ mesh.sheenColor[0] ?? 0,
1092
+ mesh.sheenColor[1] ?? 0,
1093
+ mesh.sheenColor[2] ?? 0,
1094
+ mesh.clearcoat,
1095
+ ]),
1096
+ materialExtension: Object.freeze([
1097
+ mesh.clearcoatRoughness,
1098
+ mesh.specular,
1099
+ mesh.transmission,
1100
+ mesh.thickness,
1101
+ ]),
1102
+ specularColor: Object.freeze([
1103
+ mesh.specularColor[0] ?? 1,
1104
+ mesh.specularColor[1] ?? 1,
1105
+ mesh.specularColor[2] ?? 1,
1106
+ 1,
1107
+ ]),
596
1108
  bounds: Object.freeze({
597
1109
  min: Object.freeze(bounds.min),
598
1110
  max: Object.freeze(bounds.max),
@@ -713,6 +1225,245 @@ function nextPowerOfTwo(value) {
713
1225
  return 2 ** Math.ceil(Math.log2(value));
714
1226
  }
715
1227
 
1228
+ function textureComponentToByte(value, fallback) {
1229
+ const numeric = Number(value);
1230
+ if (!Number.isFinite(numeric)) {
1231
+ return fallback;
1232
+ }
1233
+ if (numeric >= 0 && numeric <= 1) {
1234
+ return Math.max(0, Math.min(255, Math.round(numeric * 255)));
1235
+ }
1236
+ return Math.max(0, Math.min(255, Math.round(numeric)));
1237
+ }
1238
+
1239
+ function createSolidTextureSample(width, height, rgba) {
1240
+ const data = new Uint8Array(width * height * 4);
1241
+ for (let offset = 0; offset < data.length; offset += 4) {
1242
+ data[offset] = rgba[0];
1243
+ data[offset + 1] = rgba[1];
1244
+ data[offset + 2] = rgba[2];
1245
+ data[offset + 3] = rgba[3];
1246
+ }
1247
+ return Object.freeze({
1248
+ width,
1249
+ height,
1250
+ data,
1251
+ });
1252
+ }
1253
+
1254
+ function normalizeTextureSampleInput(texture, fallbackColor) {
1255
+ if (
1256
+ !texture ||
1257
+ !Number.isFinite(texture.width) ||
1258
+ !Number.isFinite(texture.height) ||
1259
+ texture.width <= 0 ||
1260
+ texture.height <= 0
1261
+ ) {
1262
+ return createSolidTextureSample(1, 1, fallbackColor);
1263
+ }
1264
+
1265
+ const pixelCount = Math.trunc(texture.width) * Math.trunc(texture.height) * 4;
1266
+ const source =
1267
+ ArrayBuffer.isView(texture.data) || Array.isArray(texture.data) ? texture.data : null;
1268
+ if (!source || source.length < pixelCount) {
1269
+ return createSolidTextureSample(1, 1, fallbackColor);
1270
+ }
1271
+
1272
+ const data = new Uint8Array(pixelCount);
1273
+ for (let index = 0; index < pixelCount; index += 1) {
1274
+ data[index] = textureComponentToByte(source[index], fallbackColor[index % 4]);
1275
+ }
1276
+
1277
+ return Object.freeze({
1278
+ width: Math.trunc(texture.width),
1279
+ height: Math.trunc(texture.height),
1280
+ data,
1281
+ });
1282
+ }
1283
+
1284
+ function buildTextureAtlas(textures, fallbackColor) {
1285
+ const padding = 1;
1286
+ const defaultTexture = createSolidTextureSample(1, 1, fallbackColor);
1287
+ const uniqueEntries = [{ source: null, texture: defaultTexture }];
1288
+ const bySource = new Map();
1289
+
1290
+ for (const texture of Array.isArray(textures) ? textures : []) {
1291
+ if (!texture || bySource.has(texture)) {
1292
+ continue;
1293
+ }
1294
+ const normalized = normalizeTextureSampleInput(texture, fallbackColor);
1295
+ bySource.set(texture, uniqueEntries.length);
1296
+ uniqueEntries.push({ source: texture, texture: normalized });
1297
+ }
1298
+
1299
+ const totalArea = uniqueEntries.reduce((sum, entry) => {
1300
+ return sum + (entry.texture.width + padding * 2) * (entry.texture.height + padding * 2);
1301
+ }, 0);
1302
+ const maxTileWidth = uniqueEntries.reduce((maxWidth, entry) => {
1303
+ return Math.max(maxWidth, entry.texture.width + padding * 2);
1304
+ }, 1);
1305
+ const targetWidth = Math.max(
1306
+ maxTileWidth,
1307
+ nextPowerOfTwo(Math.max(maxTileWidth, Math.ceil(Math.sqrt(totalArea))))
1308
+ );
1309
+
1310
+ let cursorX = 0;
1311
+ let cursorY = 0;
1312
+ let rowHeight = 0;
1313
+ let atlasWidth = 0;
1314
+ const placements = uniqueEntries.map((entry) => {
1315
+ const tileWidth = entry.texture.width + padding * 2;
1316
+ const tileHeight = entry.texture.height + padding * 2;
1317
+ if (cursorX > 0 && cursorX + tileWidth > targetWidth) {
1318
+ cursorX = 0;
1319
+ cursorY += rowHeight;
1320
+ rowHeight = 0;
1321
+ }
1322
+ const placement = Object.freeze({
1323
+ x: cursorX,
1324
+ y: cursorY,
1325
+ tileWidth,
1326
+ tileHeight,
1327
+ width: entry.texture.width,
1328
+ height: entry.texture.height,
1329
+ });
1330
+ cursorX += tileWidth;
1331
+ atlasWidth = Math.max(atlasWidth, cursorX);
1332
+ rowHeight = Math.max(rowHeight, tileHeight);
1333
+ return placement;
1334
+ });
1335
+
1336
+ const atlasHeight = Math.max(1, cursorY + rowHeight);
1337
+ const atlasData = new Uint8Array(Math.max(1, atlasWidth * atlasHeight * 4));
1338
+
1339
+ const writePixel = (x, y, rgba) => {
1340
+ const offset = (y * atlasWidth + x) * 4;
1341
+ atlasData[offset] = rgba[0];
1342
+ atlasData[offset + 1] = rgba[1];
1343
+ atlasData[offset + 2] = rgba[2];
1344
+ atlasData[offset + 3] = rgba[3];
1345
+ };
1346
+
1347
+ const rects = placements.map((placement, entryIndex) => {
1348
+ const { texture } = uniqueEntries[entryIndex];
1349
+ for (let y = 0; y < placement.tileHeight; y += 1) {
1350
+ for (let x = 0; x < placement.tileWidth; x += 1) {
1351
+ const sampleX = Math.max(0, Math.min(texture.width - 1, x - padding));
1352
+ const sampleY = Math.max(0, Math.min(texture.height - 1, y - padding));
1353
+ const sourceOffset = (sampleY * texture.width + sampleX) * 4;
1354
+ writePixel(placement.x + x, placement.y + y, texture.data.slice(sourceOffset, sourceOffset + 4));
1355
+ }
1356
+ }
1357
+ return Object.freeze([
1358
+ (placement.x + padding) / Math.max(1, atlasWidth),
1359
+ (placement.y + padding) / Math.max(1, atlasHeight),
1360
+ placement.width / Math.max(1, atlasWidth),
1361
+ placement.height / Math.max(1, atlasHeight),
1362
+ ]);
1363
+ });
1364
+
1365
+ const rectBySource = new Map();
1366
+ uniqueEntries.forEach((entry, index) => {
1367
+ if (entry.source) {
1368
+ rectBySource.set(entry.source, rects[index]);
1369
+ }
1370
+ });
1371
+
1372
+ return Object.freeze({
1373
+ width: Math.max(1, atlasWidth),
1374
+ height: Math.max(1, atlasHeight),
1375
+ data: atlasData,
1376
+ defaultRect: rects[0],
1377
+ resolveRect(texture) {
1378
+ return rectBySource.get(texture) ?? rects[0];
1379
+ },
1380
+ });
1381
+ }
1382
+
1383
+ export function createWavefrontGpuMaterialSource(meshes = []) {
1384
+ const source = Array.isArray(meshes) ? meshes : [meshes];
1385
+ const normalized = source.map((meshInput, meshIndex) => normalizeWavefrontMesh(meshInput, meshIndex));
1386
+ const baseColorAtlas = buildTextureAtlas(
1387
+ normalized.map((mesh) => mesh.baseColorTexture),
1388
+ [255, 255, 255, 255]
1389
+ );
1390
+ const metallicRoughnessAtlas = buildTextureAtlas(
1391
+ normalized.map((mesh) => mesh.metallicRoughnessTexture),
1392
+ [255, 255, 255, 255]
1393
+ );
1394
+ const normalAtlas = buildTextureAtlas(
1395
+ normalized.map((mesh) => mesh.normalTexture),
1396
+ [128, 128, 255, 255]
1397
+ );
1398
+ const occlusionAtlas = buildTextureAtlas(
1399
+ normalized.map((mesh) => mesh.occlusionTexture),
1400
+ [255, 255, 255, 255]
1401
+ );
1402
+ const emissiveAtlas = buildTextureAtlas(
1403
+ normalized.map((mesh) => mesh.emissiveTexture),
1404
+ [255, 255, 255, 255]
1405
+ );
1406
+ const bytes = new ArrayBuffer(Math.max(1, normalized.length) * GPU_MATERIAL_RECORD_BYTES);
1407
+ const floatView = new Float32Array(bytes);
1408
+
1409
+ normalized.forEach((mesh, meshIndex) => {
1410
+ const byteOffset = meshIndex * GPU_MATERIAL_RECORD_BYTES;
1411
+ writeVec4(floatView, byteOffset, mesh.color);
1412
+ writeVec4(floatView, byteOffset + 16, mesh.emission);
1413
+ writeVec4(floatView, byteOffset + 32, [
1414
+ mesh.roughness,
1415
+ mesh.metallic,
1416
+ mesh.opacity,
1417
+ mesh.ior,
1418
+ ]);
1419
+ writeVec4(floatView, byteOffset + 48, [
1420
+ mesh.sheenColor[0] ?? 0,
1421
+ mesh.sheenColor[1] ?? 0,
1422
+ mesh.sheenColor[2] ?? 0,
1423
+ mesh.clearcoat,
1424
+ ]);
1425
+ writeVec4(floatView, byteOffset + 64, [
1426
+ mesh.clearcoatRoughness,
1427
+ mesh.specular,
1428
+ mesh.transmission,
1429
+ mesh.thickness,
1430
+ ]);
1431
+ writeVec4(floatView, byteOffset + 80, [
1432
+ mesh.specularColor[0] ?? 1,
1433
+ mesh.specularColor[1] ?? 1,
1434
+ mesh.specularColor[2] ?? 1,
1435
+ 1,
1436
+ ]);
1437
+ writeVec4(floatView, byteOffset + 96, baseColorAtlas.resolveRect(mesh.baseColorTexture));
1438
+ writeVec4(
1439
+ floatView,
1440
+ byteOffset + 112,
1441
+ metallicRoughnessAtlas.resolveRect(mesh.metallicRoughnessTexture)
1442
+ );
1443
+ writeVec4(floatView, byteOffset + 128, normalAtlas.resolveRect(mesh.normalTexture));
1444
+ writeVec4(floatView, byteOffset + 144, occlusionAtlas.resolveRect(mesh.occlusionTexture));
1445
+ writeVec4(floatView, byteOffset + 160, emissiveAtlas.resolveRect(mesh.emissiveTexture));
1446
+ writeVec4(floatView, byteOffset + 176, [
1447
+ clampUnit(mesh.normalTexture?.scale ?? mesh.normalTexture?.strength ?? 1),
1448
+ clampUnit(mesh.occlusionTexture?.strength ?? 1),
1449
+ clampUnit(mesh.emissiveTexture?.strength ?? 1),
1450
+ 0,
1451
+ ]);
1452
+ });
1453
+
1454
+ return Object.freeze({
1455
+ buffer: bytes,
1456
+ count: normalized.length,
1457
+ recordBytes: GPU_MATERIAL_RECORD_BYTES,
1458
+ records: Object.freeze(normalized),
1459
+ baseColorAtlas,
1460
+ metallicRoughnessAtlas,
1461
+ normalAtlas,
1462
+ occlusionAtlas,
1463
+ emissiveAtlas,
1464
+ });
1465
+ }
1466
+
716
1467
  function estimateBvhLeafSortCapacity(triangleCount) {
717
1468
  return triangleCount <= 0 ? 0 : nextPowerOfTwo(triangleCount);
718
1469
  }
@@ -783,9 +1534,10 @@ function resolveAccelerationBuildMode(options = {}) {
783
1534
  return mode;
784
1535
  }
785
1536
 
786
- export function createWavefrontGpuMeshSource(meshes = []) {
1537
+ export function createWavefrontGpuMeshSource(meshes = [], gpuMaterialSourceInput = null) {
787
1538
  const source = Array.isArray(meshes) ? meshes : [meshes];
788
1539
  const normalized = source.map((meshInput, meshIndex) => normalizeWavefrontMesh(meshInput, meshIndex));
1540
+ const gpuMaterialSource = gpuMaterialSourceInput ?? createWavefrontGpuMaterialSource(normalized);
789
1541
  const vertexCount = normalized.reduce((count, mesh) => count + mesh.positions.length / 3, 0);
790
1542
  const indexCount = normalized.reduce((count, mesh) => count + mesh.indices.length, 0);
791
1543
  const triangleCount = Math.floor(indexCount / 3);
@@ -842,7 +1594,7 @@ export function createWavefrontGpuMeshSource(meshes = []) {
842
1594
  meshUints[meshOffset + 8] = mesh.indices.length / 3;
843
1595
  meshUints[meshOffset + 9] = meshVertexBase;
844
1596
  meshUints[meshOffset + 10] = meshVertexCount;
845
- meshUints[meshOffset + 11] = 0;
1597
+ meshUints[meshOffset + 11] = meshIndex;
846
1598
  const floatOffset = meshOffset;
847
1599
  writeVec4(meshFloats, floatOffset * 4 + 48, mesh.color);
848
1600
  writeVec4(meshFloats, floatOffset * 4 + 64, mesh.emission);
@@ -852,6 +1604,55 @@ export function createWavefrontGpuMeshSource(meshes = []) {
852
1604
  mesh.opacity,
853
1605
  mesh.ior,
854
1606
  ]);
1607
+ writeVec4(meshFloats, floatOffset * 4 + 96, [
1608
+ mesh.sheenColor[0] ?? 0,
1609
+ mesh.sheenColor[1] ?? 0,
1610
+ mesh.sheenColor[2] ?? 0,
1611
+ mesh.clearcoat,
1612
+ ]);
1613
+ writeVec4(meshFloats, floatOffset * 4 + 112, [
1614
+ mesh.clearcoatRoughness,
1615
+ mesh.specular,
1616
+ mesh.transmission,
1617
+ mesh.thickness,
1618
+ ]);
1619
+ writeVec4(meshFloats, floatOffset * 4 + 128, [
1620
+ mesh.specularColor[0] ?? 1,
1621
+ mesh.specularColor[1] ?? 1,
1622
+ mesh.specularColor[2] ?? 1,
1623
+ 1,
1624
+ ]);
1625
+ writeVec4(
1626
+ meshFloats,
1627
+ floatOffset * 4 + 144,
1628
+ gpuMaterialSource.baseColorAtlas.resolveRect(mesh.baseColorTexture)
1629
+ );
1630
+ writeVec4(
1631
+ meshFloats,
1632
+ floatOffset * 4 + 160,
1633
+ gpuMaterialSource.metallicRoughnessAtlas.resolveRect(mesh.metallicRoughnessTexture)
1634
+ );
1635
+ writeVec4(
1636
+ meshFloats,
1637
+ floatOffset * 4 + 176,
1638
+ gpuMaterialSource.normalAtlas.resolveRect(mesh.normalTexture)
1639
+ );
1640
+ writeVec4(
1641
+ meshFloats,
1642
+ floatOffset * 4 + 192,
1643
+ gpuMaterialSource.occlusionAtlas.resolveRect(mesh.occlusionTexture)
1644
+ );
1645
+ writeVec4(
1646
+ meshFloats,
1647
+ floatOffset * 4 + 208,
1648
+ gpuMaterialSource.emissiveAtlas.resolveRect(mesh.emissiveTexture)
1649
+ );
1650
+ writeVec4(meshFloats, floatOffset * 4 + 224, [
1651
+ clampUnit(mesh.normalTexture?.scale ?? mesh.normalTexture?.strength ?? 1),
1652
+ clampUnit(mesh.occlusionTexture?.strength ?? 1),
1653
+ clampUnit(mesh.emissiveTexture?.strength ?? 1),
1654
+ 0,
1655
+ ]);
855
1656
 
856
1657
  vertexCursor += meshVertexCount;
857
1658
  indexCursor += mesh.indices.length;
@@ -928,12 +1729,17 @@ function normalizeSceneObjects(sceneObjects, useDefaultScene = true) {
928
1729
  return source.map((object, index) => normalizeWavefrontSceneObject(object, index));
929
1730
  }
930
1731
 
1732
+ function normalizeWavefrontMeshes(meshes) {
1733
+ const source = Array.isArray(meshes) ? meshes : [];
1734
+ return source.map((mesh, index) => normalizeWavefrontMesh(mesh, index));
1735
+ }
1736
+
931
1737
  function normalizeMeshes(options = {}) {
932
1738
  if (Array.isArray(options.meshes)) {
933
- return options.meshes;
1739
+ return normalizeWavefrontMeshes(options.meshes);
934
1740
  }
935
1741
  if (options.mesh) {
936
- return [options.mesh];
1742
+ return normalizeWavefrontMeshes([options.mesh]);
937
1743
  }
938
1744
  return [];
939
1745
  }
@@ -1188,12 +1994,14 @@ export function estimateWavefrontPathTracingMemory(options = {}) {
1188
1994
  options.environmentPortalCapacity,
1189
1995
  0
1190
1996
  );
1997
+ const materialCapacity = readNonNegativeInteger("materialCapacity", options.materialCapacity, 0);
1191
1998
  const queueBytes = tilePixelCapacity * RAY_RECORD_BYTES;
1192
1999
  const hitBytes = tilePixelCapacity * HIT_RECORD_BYTES;
1193
2000
  const accumulationBytes = tilePixelCapacity * ACCUMULATION_RECORD_BYTES;
1194
2001
  const pathVertexBytes = tilePixelCapacity * (maxDepth + 1) * PATH_VERTEX_RECORD_BYTES;
1195
2002
  const sceneObjectBytes = sceneObjectCapacity * SCENE_OBJECT_RECORD_BYTES;
1196
2003
  const triangleBytes = triangleCapacity * TRIANGLE_RECORD_BYTES;
2004
+ const materialTableBytes = materialCapacity * GPU_MATERIAL_RECORD_BYTES;
1197
2005
  const bvhNodeBytes = bvhNodeCapacity * BVH_NODE_RECORD_BYTES;
1198
2006
  const bvhLeafReferenceBytes = bvhLeafSortCapacity * BVH_LEAF_REF_RECORD_BYTES;
1199
2007
  const emissiveTriangleMetadataBytes =
@@ -1209,6 +2017,7 @@ export function estimateWavefrontPathTracingMemory(options = {}) {
1209
2017
  pathVertexBytes,
1210
2018
  sceneObjectBytes,
1211
2019
  triangleBytes,
2020
+ materialTableBytes,
1212
2021
  bvhNodeBytes,
1213
2022
  bvhLeafReferenceBytes,
1214
2023
  emissiveTriangleMetadataBytes,
@@ -1223,6 +2032,7 @@ export function estimateWavefrontPathTracingMemory(options = {}) {
1223
2032
  pathVertexBytes +
1224
2033
  sceneObjectBytes +
1225
2034
  triangleBytes +
2035
+ materialTableBytes +
1226
2036
  bvhNodeBytes +
1227
2037
  bvhLeafReferenceBytes +
1228
2038
  emissiveTriangleMetadataBytes +
@@ -1244,7 +2054,7 @@ export function createWavefrontPathTracingComputeConfig(options = {}) {
1244
2054
  const samplesPerPixel = clamp(
1245
2055
  readPositiveInteger("samplesPerPixel", options.samplesPerPixel, DEFAULT_SAMPLES_PER_PIXEL),
1246
2056
  1,
1247
- 64
2057
+ MAX_SAMPLES_PER_PIXEL
1248
2058
  );
1249
2059
  const maxFramePassesPerSubmission = clamp(
1250
2060
  readPositiveInteger(
@@ -1262,9 +2072,13 @@ export function createWavefrontPathTracingComputeConfig(options = {}) {
1262
2072
  );
1263
2073
  const meshes = normalizeMeshes(options);
1264
2074
  const meshSourceShape = estimateMeshSourceShape(meshes);
2075
+ const gpuMaterialSource =
2076
+ meshes.length > 0
2077
+ ? createWavefrontGpuMaterialSource(meshes)
2078
+ : createWavefrontGpuMaterialSource([]);
1265
2079
  const gpuMeshSource =
1266
2080
  meshes.length > 0
1267
- ? createWavefrontGpuMeshSource(meshes)
2081
+ ? createWavefrontGpuMeshSource(meshes, gpuMaterialSource)
1268
2082
  : createWavefrontGpuMeshSource([]);
1269
2083
  const meshAcceleration =
1270
2084
  accelerationBuildMode === "cpu-debug"
@@ -1285,6 +2099,7 @@ export function createWavefrontPathTracingComputeConfig(options = {}) {
1285
2099
  const sceneObjects = Object.freeze(
1286
2100
  normalizeSceneObjects(options.sceneObjects, meshes.length === 0)
1287
2101
  );
2102
+ const mediums = collectWavefrontMediums(options, meshes, sceneObjects);
1288
2103
  const sceneObjectCapacity = Math.max(
1289
2104
  sceneObjects.length,
1290
2105
  readPositiveInteger("sceneObjectCapacity", options.sceneObjectCapacity, DEFAULT_SCENE_OBJECT_CAPACITY)
@@ -1357,9 +2172,12 @@ export function createWavefrontPathTracingComputeConfig(options = {}) {
1357
2172
  sceneObjects,
1358
2173
  sceneObjectCount: sceneObjects.length,
1359
2174
  sceneObjectCapacity,
2175
+ mediums,
2176
+ mediumCount: mediums.length,
1360
2177
  accelerationBuildMode,
1361
2178
  gpuAccelerationBuildRequired: accelerationBuildMode === "gpu" && triangleCount > 0,
1362
2179
  gpuMeshSource,
2180
+ gpuMaterialSource,
1363
2181
  meshAcceleration,
1364
2182
  emissiveTriangleIndices,
1365
2183
  emissiveTriangleCount: emissiveTriangleIndices.count,
@@ -1390,6 +2208,7 @@ export function createWavefrontPathTracingComputeConfig(options = {}) {
1390
2208
  maxDepth,
1391
2209
  sceneObjectCapacity,
1392
2210
  triangleCapacity,
2211
+ materialCapacity: gpuMaterialSource.count,
1393
2212
  bvhNodeCapacity,
1394
2213
  bvhLeafSortCapacity,
1395
2214
  emissiveTriangleCapacity: emissiveTriangleIndices.capacity,
@@ -1473,16 +2292,35 @@ export function packWavefrontSceneObjects(sceneObjects, capacity = sceneObjects.
1473
2292
  uintView[u32 + 1] = object.id;
1474
2293
  uintView[u32 + 2] = object.materialKind;
1475
2294
  uintView[u32 + 3] = object.flags;
1476
- writeVec4(floatView, byteOffset + 16, [...object.center, 0]);
1477
- writeVec4(floatView, byteOffset + 32, [...object.halfExtent, 0]);
1478
- writeVec4(floatView, byteOffset + 48, object.color);
1479
- writeVec4(floatView, byteOffset + 64, object.emission);
1480
- writeVec4(floatView, byteOffset + 80, [
2295
+ uintView[u32 + 4] = object.mediumRefId;
2296
+ writeVec4(floatView, byteOffset + 32, [...object.center, 0]);
2297
+ writeVec4(floatView, byteOffset + 48, [...object.halfExtent, 0]);
2298
+ writeVec4(floatView, byteOffset + 64, object.color);
2299
+ writeVec4(floatView, byteOffset + 80, object.emission);
2300
+ writeVec4(floatView, byteOffset + 96, [
1481
2301
  object.roughness,
1482
2302
  object.metallic,
1483
2303
  object.opacity,
1484
2304
  object.ior,
1485
2305
  ]);
2306
+ writeVec4(floatView, byteOffset + 112, [
2307
+ object.sheenColor[0] ?? 0,
2308
+ object.sheenColor[1] ?? 0,
2309
+ object.sheenColor[2] ?? 0,
2310
+ object.clearcoat,
2311
+ ]);
2312
+ writeVec4(floatView, byteOffset + 128, [
2313
+ object.clearcoatRoughness,
2314
+ object.specular,
2315
+ object.transmission,
2316
+ object.thickness,
2317
+ ]);
2318
+ writeVec4(floatView, byteOffset + 144, [
2319
+ object.specularColor[0] ?? 1,
2320
+ object.specularColor[1] ?? 1,
2321
+ object.specularColor[2] ?? 1,
2322
+ 1,
2323
+ ]);
1486
2324
  });
1487
2325
 
1488
2326
  return Object.freeze({
@@ -1511,7 +2349,7 @@ export function packWavefrontTriangles(triangles, capacity = triangles.length) {
1511
2349
  uintView[u32 + 3] = triangle.flags;
1512
2350
  uintView[u32 + 4] = triangle.materialRefId;
1513
2351
  uintView[u32 + 5] = triangle.mediumRefId;
1514
- uintView[u32 + 6] = 0;
2352
+ uintView[u32 + 6] = triangle.materialSlot ?? 0;
1515
2353
  uintView[u32 + 7] = 0;
1516
2354
  writeVec4(floatView, byteOffset + 32, [...triangle.v0, 0]);
1517
2355
  writeVec4(floatView, byteOffset + 48, [...triangle.v1, 0]);
@@ -1524,6 +2362,15 @@ export function packWavefrontTriangles(triangles, capacity = triangles.length) {
1524
2362
  writeVec4(floatView, byteOffset + 160, triangle.color);
1525
2363
  writeVec4(floatView, byteOffset + 176, triangle.emission);
1526
2364
  writeVec4(floatView, byteOffset + 192, triangle.material);
2365
+ writeVec4(floatView, byteOffset + 208, triangle.materialResponse);
2366
+ writeVec4(floatView, byteOffset + 224, triangle.materialExtension ?? [0.08, 1, 0, 0]);
2367
+ writeVec4(floatView, byteOffset + 240, triangle.specularColor ?? [1, 1, 1, 1]);
2368
+ writeVec4(floatView, byteOffset + 256, triangle.baseColorAtlas ?? [0, 0, 1, 1]);
2369
+ writeVec4(floatView, byteOffset + 272, triangle.metallicRoughnessAtlas ?? [0, 0, 1, 1]);
2370
+ writeVec4(floatView, byteOffset + 288, triangle.normalAtlas ?? [0, 0, 1, 1]);
2371
+ writeVec4(floatView, byteOffset + 304, triangle.occlusionAtlas ?? [0, 0, 1, 1]);
2372
+ writeVec4(floatView, byteOffset + 320, triangle.emissiveAtlas ?? [0, 0, 1, 1]);
2373
+ writeVec4(floatView, byteOffset + 336, triangle.textureSettings ?? [1, 1, 1, 0]);
1527
2374
  });
1528
2375
 
1529
2376
  return Object.freeze({
@@ -1623,6 +2470,12 @@ function createConfigPayload(config, tile, frameIndex, buildRange = {}) {
1623
2470
  0,
1624
2471
  0,
1625
2472
  ]);
2473
+ writeVec4(floatView, 304, [
2474
+ config.environmentMap.width ?? 1,
2475
+ config.environmentMap.height ?? 1,
2476
+ config.environmentMap.mipLevelCount ?? 1,
2477
+ config.environmentMap.hasImportanceData ? 1 : 0,
2478
+ ]);
1626
2479
  return bytes;
1627
2480
  }
1628
2481
 
@@ -1813,6 +2666,7 @@ export function intersectWavefrontReferenceTriangle(ray, triangle, options = {})
1813
2666
  color: triangle.color,
1814
2667
  emission: triangle.emission,
1815
2668
  material: triangle.material,
2669
+ materialResponse: triangle.materialResponse,
1816
2670
  });
1817
2671
  }
1818
2672
 
@@ -1840,6 +2694,7 @@ function createWavefrontReferenceEnvironmentHit(config, ray) {
1840
2694
  color: Object.freeze([0, 0, 0, 0]),
1841
2695
  emission: radiance,
1842
2696
  material: Object.freeze([1, 0, 1, 1]),
2697
+ materialResponse: Object.freeze([0, 0, 0, 0]),
1843
2698
  });
1844
2699
  }
1845
2700
 
@@ -1932,12 +2787,378 @@ function environmentMapIntegerScale(data) {
1932
2787
  return 1;
1933
2788
  }
1934
2789
 
1935
- function readEnvironmentMapComponent(data, index, fallback, integerScale = 1) {
1936
- if (!data || index >= data.length) {
1937
- return fallback;
2790
+ function environmentMapHasSamplingData(environmentMap) {
2791
+ if (!environmentMap || !environmentMap.data) {
2792
+ return false;
1938
2793
  }
1939
- const value = Number(data[index]);
1940
- return Number.isFinite(value) ? Math.max(0, value) * integerScale : fallback;
2794
+ const width = Math.max(1, environmentMap.width ?? 1);
2795
+ const height = Math.max(1, environmentMap.height ?? 1);
2796
+ return environmentMap.data.length >= width * height * 4;
2797
+ }
2798
+
2799
+ function createRgba8TextureUpload(source) {
2800
+ const width = Math.max(1, Math.trunc(source.width));
2801
+ const height = Math.max(1, Math.trunc(source.height));
2802
+ const bytesPerRow = alignTo(width * 4, 256);
2803
+ const bytes = new Uint8Array(bytesPerRow * height);
2804
+ const data = source.data instanceof Uint8Array ? source.data : new Uint8Array(source.data);
2805
+ for (let y = 0; y < height; y += 1) {
2806
+ const sourceOffset = y * width * 4;
2807
+ const targetOffset = y * bytesPerRow;
2808
+ bytes.set(data.subarray(sourceOffset, sourceOffset + width * 4), targetOffset);
2809
+ }
2810
+ return Object.freeze({
2811
+ bytes,
2812
+ bytesPerRow,
2813
+ width,
2814
+ height,
2815
+ });
2816
+ }
2817
+
2818
+ function readEnvironmentMapComponent(data, index, fallback, integerScale = 1) {
2819
+ if (!data || index >= data.length) {
2820
+ return fallback;
2821
+ }
2822
+ const value = Number(data[index]);
2823
+ return Number.isFinite(value) ? Math.max(0, value) * integerScale : fallback;
2824
+ }
2825
+
2826
+ function reflectVector(direction, normal) {
2827
+ return subtract(direction, scale(normal, 2 * dot(direction, normal)));
2828
+ }
2829
+
2830
+ function buildOrthonormalBasis(normal) {
2831
+ const tangentFallback = Math.abs(normal[1]) < 0.999 ? [0, 1, 0] : [1, 0, 0];
2832
+ const tangent = normalize(cross(tangentFallback, normal), [1, 0, 0]);
2833
+ const bitangent = normalize(cross(normal, tangent), [0, 0, 1]);
2834
+ return { tangent, bitangent };
2835
+ }
2836
+
2837
+ function localToWorld(local, normal) {
2838
+ const basis = buildOrthonormalBasis(normal);
2839
+ return normalize(
2840
+ add(
2841
+ add(scale(basis.tangent, local[0]), scale(basis.bitangent, local[1])),
2842
+ scale(normal, local[2])
2843
+ ),
2844
+ normal
2845
+ );
2846
+ }
2847
+
2848
+ function radicalInverseVdc(bits) {
2849
+ let value = bits >>> 0;
2850
+ value = ((value << 16) | (value >>> 16)) >>> 0;
2851
+ value = (((value & 0x55555555) << 1) | ((value & 0xaaaaaaaa) >>> 1)) >>> 0;
2852
+ value = (((value & 0x33333333) << 2) | ((value & 0xcccccccc) >>> 2)) >>> 0;
2853
+ value = (((value & 0x0f0f0f0f) << 4) | ((value & 0xf0f0f0f0) >>> 4)) >>> 0;
2854
+ value = (((value & 0x00ff00ff) << 8) | ((value & 0xff00ff00) >>> 8)) >>> 0;
2855
+ return value * 2.3283064365386963e-10;
2856
+ }
2857
+
2858
+ function hammersley(index, count) {
2859
+ return [index / Math.max(count, 1), radicalInverseVdc(index)];
2860
+ }
2861
+
2862
+ function importanceSampleGgx(sample, roughness, normal) {
2863
+ const alpha = Math.max(roughness * roughness, 0.0001);
2864
+ const phi = 2 * Math.PI * sample[0];
2865
+ const cosTheta = Math.sqrt((1 - sample[1]) / (1 + (alpha * alpha - 1) * sample[1]));
2866
+ const sinTheta = Math.sqrt(Math.max(0, 1 - cosTheta * cosTheta));
2867
+ const halfVector = localToWorld(
2868
+ [Math.cos(phi) * sinTheta, Math.sin(phi) * sinTheta, cosTheta],
2869
+ normal
2870
+ );
2871
+ return normalize(halfVector, normal);
2872
+ }
2873
+
2874
+ function distributionGgx(nDotH, roughness) {
2875
+ const alpha = Math.max(roughness * roughness, 0.0001);
2876
+ const alpha2 = alpha * alpha;
2877
+ const denom = (nDotH * nDotH) * (alpha2 - 1) + 1;
2878
+ return alpha2 / Math.max(Math.PI * denom * denom, 0.000001);
2879
+ }
2880
+
2881
+ function geometrySchlickGgx(nDotV, roughness) {
2882
+ const k = ((roughness + 1) * (roughness + 1)) / 8;
2883
+ return nDotV / Math.max(nDotV * (1 - k) + k, 0.000001);
2884
+ }
2885
+
2886
+ function geometrySmith(nDotV, nDotL, roughness) {
2887
+ return geometrySchlickGgx(nDotV, roughness) * geometrySchlickGgx(nDotL, roughness);
2888
+ }
2889
+
2890
+ function integrateBrdfSample(nDotV, roughness, sampleCount) {
2891
+ const viewDirection = [Math.sqrt(Math.max(0, 1 - nDotV * nDotV)), 0, nDotV];
2892
+ const normal = [0, 0, 1];
2893
+ let scaleTerm = 0;
2894
+ let biasTerm = 0;
2895
+ for (let index = 0; index < sampleCount; index += 1) {
2896
+ const xi = hammersley(index, sampleCount);
2897
+ const halfVector = importanceSampleGgx(xi, roughness, normal);
2898
+ const vDotH = Math.max(dot(viewDirection, halfVector), 0);
2899
+ const lightDirection = normalize(
2900
+ subtract(scale(halfVector, 2 * vDotH), viewDirection),
2901
+ normal
2902
+ );
2903
+ const nDotL = Math.max(lightDirection[2], 0);
2904
+ const nDotH = Math.max(halfVector[2], 0);
2905
+ if (nDotL <= 0 || nDotH <= 0 || vDotH <= 0) {
2906
+ continue;
2907
+ }
2908
+ const geometry = geometrySmith(nDotV, nDotL, roughness);
2909
+ const visibility = (geometry * vDotH) / Math.max(nDotH * nDotV, 0.000001);
2910
+ const fresnel = (1 - vDotH) ** 5;
2911
+ scaleTerm += (1 - fresnel) * visibility;
2912
+ biasTerm += fresnel * visibility;
2913
+ }
2914
+ return [scaleTerm / sampleCount, biasTerm / sampleCount];
2915
+ }
2916
+
2917
+ function createBrdfLutUploadBytes(
2918
+ size = DEFAULT_BRDF_LUT_SIZE,
2919
+ sampleCount = DEFAULT_BRDF_LUT_SAMPLE_COUNT
2920
+ ) {
2921
+ const cacheKey = `${Math.max(1, Math.trunc(size))}:${Math.max(1, Math.trunc(sampleCount))}`;
2922
+ const cached = BRDF_LUT_UPLOAD_CACHE.get(cacheKey);
2923
+ if (cached) {
2924
+ return cached;
2925
+ }
2926
+ const width = Math.max(1, Math.trunc(size));
2927
+ const height = Math.max(1, Math.trunc(size));
2928
+ const rowBytes = width * 8;
2929
+ const bytesPerRow = alignTo(rowBytes, 256);
2930
+ const bytes = new Uint8Array(bytesPerRow * height);
2931
+ const view = new DataView(bytes.buffer);
2932
+ for (let y = 0; y < height; y += 1) {
2933
+ const roughness = (y + 0.5) / height;
2934
+ for (let x = 0; x < width; x += 1) {
2935
+ const nDotV = Math.max((x + 0.5) / width, 0.0001);
2936
+ const [scaleTerm, biasTerm] = integrateBrdfSample(nDotV, roughness, sampleCount);
2937
+ const offset = y * bytesPerRow + x * 8;
2938
+ view.setUint16(offset, float32ToFloat16Bits(scaleTerm), true);
2939
+ view.setUint16(offset + 2, float32ToFloat16Bits(biasTerm), true);
2940
+ view.setUint16(offset + 4, float32ToFloat16Bits(0), true);
2941
+ view.setUint16(offset + 6, float32ToFloat16Bits(1), true);
2942
+ }
2943
+ }
2944
+ const upload = Object.freeze({ bytes, bytesPerRow, width, height });
2945
+ BRDF_LUT_UPLOAD_CACHE.set(cacheKey, upload);
2946
+ return upload;
2947
+ }
2948
+
2949
+ function createLinearEnvironmentPixels(environmentMap, fallbackColor) {
2950
+ const width = Math.max(1, environmentMap.width);
2951
+ const height = Math.max(1, environmentMap.height);
2952
+ const pixels = new Float32Array(width * height * 4);
2953
+ const data = environmentMap.data;
2954
+ const integerScale = environmentMapIntegerScale(data);
2955
+ for (let index = 0; index < width * height; index += 1) {
2956
+ const sourceOffset = index * 4;
2957
+ const targetOffset = index * 4;
2958
+ pixels[targetOffset] = readEnvironmentMapComponent(data, sourceOffset, fallbackColor[0], integerScale);
2959
+ pixels[targetOffset + 1] = readEnvironmentMapComponent(data, sourceOffset + 1, fallbackColor[1], integerScale);
2960
+ pixels[targetOffset + 2] = readEnvironmentMapComponent(data, sourceOffset + 2, fallbackColor[2], integerScale);
2961
+ pixels[targetOffset + 3] = readEnvironmentMapComponent(data, sourceOffset + 3, fallbackColor[3] ?? 1, integerScale);
2962
+ }
2963
+ return pixels;
2964
+ }
2965
+
2966
+ function environmentUvToDirection(u, v, rotationRadians = 0) {
2967
+ const angle = (u - rotationRadians / (2 * Math.PI) - 0.5) * 2 * Math.PI;
2968
+ const theta = v * Math.PI;
2969
+ const sinTheta = Math.sin(theta);
2970
+ return [
2971
+ Math.cos(angle) * sinTheta,
2972
+ Math.cos(theta),
2973
+ Math.sin(angle) * sinTheta,
2974
+ ];
2975
+ }
2976
+
2977
+ function sampleEnvironmentPixelsBilinear(pixels, width, height, u, v) {
2978
+ const wrappedU = ((u % 1) + 1) % 1;
2979
+ const clampedV = clamp(v, 0, 1);
2980
+ const x = wrappedU * width - 0.5;
2981
+ const y = clampedV * height - 0.5;
2982
+ const x0 = ((Math.floor(x) % width) + width) % width;
2983
+ const y0 = clamp(Math.floor(y), 0, height - 1);
2984
+ const x1 = (x0 + 1) % width;
2985
+ const y1 = clamp(y0 + 1, 0, height - 1);
2986
+ const tx = x - Math.floor(x);
2987
+ const ty = y - Math.floor(y);
2988
+ const read = (px, py) => {
2989
+ const offset = (py * width + px) * 4;
2990
+ return [pixels[offset], pixels[offset + 1], pixels[offset + 2], pixels[offset + 3]];
2991
+ };
2992
+ const a = read(x0, y0);
2993
+ const b = read(x1, y0);
2994
+ const c = read(x0, y1);
2995
+ const d = read(x1, y1);
2996
+ const mixPair = (first, second, factor) => first * (1 - factor) + second * factor;
2997
+ return [
2998
+ mixPair(mixPair(a[0], b[0], tx), mixPair(c[0], d[0], tx), ty),
2999
+ mixPair(mixPair(a[1], b[1], tx), mixPair(c[1], d[1], tx), ty),
3000
+ mixPair(mixPair(a[2], b[2], tx), mixPair(c[2], d[2], tx), ty),
3001
+ mixPair(mixPair(a[3], b[3], tx), mixPair(c[3], d[3], tx), ty),
3002
+ ];
3003
+ }
3004
+
3005
+ function directionToEnvironmentUv(direction, rotationRadians = 0) {
3006
+ const unitDirection = normalize(direction, [0, 1, 0]);
3007
+ const rotationTurns = rotationRadians / (2 * Math.PI);
3008
+ const u = ((((Math.atan2(unitDirection[2], unitDirection[0]) / (2 * Math.PI)) + 0.5 + rotationTurns) % 1) + 1) % 1;
3009
+ const v = Math.acos(clamp(unitDirection[1], -1, 1)) / Math.PI;
3010
+ return [u, clamp(v, 0, 1)];
3011
+ }
3012
+
3013
+ function sampleEnvironmentRadiance(pixels, width, height, direction, rotationRadians = 0) {
3014
+ const [u, v] = directionToEnvironmentUv(direction, rotationRadians);
3015
+ return sampleEnvironmentPixelsBilinear(pixels, width, height, u, v);
3016
+ }
3017
+
3018
+ function createFloat16RgbaUploadFromLevels(levels) {
3019
+ return levels.map((level) => {
3020
+ const rowBytes = level.width * 8;
3021
+ const bytesPerRow = alignTo(rowBytes, 256);
3022
+ const bytes = new Uint8Array(bytesPerRow * level.height);
3023
+ const view = new DataView(bytes.buffer);
3024
+ for (let y = 0; y < level.height; y += 1) {
3025
+ for (let x = 0; x < level.width; x += 1) {
3026
+ const sourceOffset = (y * level.width + x) * 4;
3027
+ const targetOffset = y * bytesPerRow + x * 8;
3028
+ view.setUint16(targetOffset, float32ToFloat16Bits(level.data[sourceOffset]), true);
3029
+ view.setUint16(targetOffset + 2, float32ToFloat16Bits(level.data[sourceOffset + 1]), true);
3030
+ view.setUint16(targetOffset + 4, float32ToFloat16Bits(level.data[sourceOffset + 2]), true);
3031
+ view.setUint16(targetOffset + 6, float32ToFloat16Bits(level.data[sourceOffset + 3]), true);
3032
+ }
3033
+ }
3034
+ return Object.freeze({ bytes, bytesPerRow, width: level.width, height: level.height });
3035
+ });
3036
+ }
3037
+
3038
+ function createPrefilteredEnvironmentLevels(environmentMap, fallbackColor) {
3039
+ const sourcePixels = createLinearEnvironmentPixels(environmentMap, fallbackColor);
3040
+ const sourceWidth = Math.max(1, environmentMap.width);
3041
+ const sourceHeight = Math.max(1, environmentMap.height);
3042
+ const mipLevelCount = Math.max(1, Math.floor(Math.log2(Math.max(sourceWidth, sourceHeight))) + 1);
3043
+ const levels = [
3044
+ Object.freeze({
3045
+ width: sourceWidth,
3046
+ height: sourceHeight,
3047
+ data: sourcePixels,
3048
+ }),
3049
+ ];
3050
+ for (let mipLevel = 1; mipLevel < mipLevelCount; mipLevel += 1) {
3051
+ const width = Math.max(1, sourceWidth >> mipLevel);
3052
+ const height = Math.max(1, sourceHeight >> mipLevel);
3053
+ const roughness = mipLevelCount <= 1 ? 0 : mipLevel / (mipLevelCount - 1);
3054
+ const data = new Float32Array(width * height * 4);
3055
+ const sampleCount = roughness < 0.25 ? 64 : roughness < 0.6 ? 96 : 128;
3056
+ for (let y = 0; y < height; y += 1) {
3057
+ for (let x = 0; x < width; x += 1) {
3058
+ const direction = environmentUvToDirection((x + 0.5) / width, (y + 0.5) / height, environmentMap.rotationRadians);
3059
+ const normal = normalize(direction, [0, 1, 0]);
3060
+ const viewDirection = normal;
3061
+ let totalWeight = 0;
3062
+ const accum = [0, 0, 0];
3063
+ for (let sampleIndex = 0; sampleIndex < sampleCount; sampleIndex += 1) {
3064
+ const xi = hammersley(sampleIndex, sampleCount);
3065
+ const halfVector = importanceSampleGgx(xi, roughness, normal);
3066
+ const viewDotHalf = Math.max(dot(viewDirection, halfVector), 0);
3067
+ const lightDirection = normalize(
3068
+ subtract(scale(halfVector, 2 * viewDotHalf), viewDirection),
3069
+ normal
3070
+ );
3071
+ const nDotL = Math.max(dot(normal, lightDirection), 0);
3072
+ if (nDotL <= 0.000001) {
3073
+ continue;
3074
+ }
3075
+ const radiance = sampleEnvironmentRadiance(
3076
+ sourcePixels,
3077
+ sourceWidth,
3078
+ sourceHeight,
3079
+ lightDirection,
3080
+ environmentMap.rotationRadians
3081
+ );
3082
+ accum[0] += radiance[0] * nDotL;
3083
+ accum[1] += radiance[1] * nDotL;
3084
+ accum[2] += radiance[2] * nDotL;
3085
+ totalWeight += nDotL;
3086
+ }
3087
+ const offset = (y * width + x) * 4;
3088
+ data[offset] = accum[0] / Math.max(totalWeight, 0.000001);
3089
+ data[offset + 1] = accum[1] / Math.max(totalWeight, 0.000001);
3090
+ data[offset + 2] = accum[2] / Math.max(totalWeight, 0.000001);
3091
+ data[offset + 3] = 1;
3092
+ }
3093
+ }
3094
+ levels.push(Object.freeze({ width, height, data }));
3095
+ }
3096
+ return Object.freeze({
3097
+ levels,
3098
+ mipLevelCount,
3099
+ width: sourceWidth,
3100
+ height: sourceHeight,
3101
+ });
3102
+ }
3103
+
3104
+ function createEnvironmentSamplingTables(environmentMap, fallbackColor) {
3105
+ if (!environmentMapHasSamplingData(environmentMap)) {
3106
+ return Object.freeze({
3107
+ width: 1,
3108
+ height: 1,
3109
+ pdf: new Float32Array([1]),
3110
+ marginalCdf: new Float32Array([1]),
3111
+ conditionalCdf: new Float32Array([1]),
3112
+ hasImportanceData: false,
3113
+ });
3114
+ }
3115
+ const pixels = createLinearEnvironmentPixels(environmentMap, fallbackColor);
3116
+ const width = Math.max(1, environmentMap.width);
3117
+ const height = Math.max(1, environmentMap.height);
3118
+ const pdf = new Float32Array(width * height);
3119
+ const marginalCdf = new Float32Array(height);
3120
+ const conditionalCdf = new Float32Array(width * height);
3121
+ const rowSums = new Float32Array(height);
3122
+ let totalWeight = 0;
3123
+ for (let y = 0; y < height; y += 1) {
3124
+ const theta = ((y + 0.5) / height) * Math.PI;
3125
+ const sinTheta = Math.max(Math.sin(theta), 0.0001);
3126
+ let rowWeight = 0;
3127
+ for (let x = 0; x < width; x += 1) {
3128
+ const offset = (y * width + x) * 4;
3129
+ const luminance = pixels[offset] * 0.2126 + pixels[offset + 1] * 0.7152 + pixels[offset + 2] * 0.0722;
3130
+ const weight = Math.max(luminance * sinTheta, 0.000001);
3131
+ pdf[y * width + x] = weight;
3132
+ rowWeight += weight;
3133
+ conditionalCdf[y * width + x] = rowWeight;
3134
+ }
3135
+ rowSums[y] = rowWeight;
3136
+ totalWeight += rowWeight;
3137
+ if (rowWeight > 0) {
3138
+ for (let x = 0; x < width; x += 1) {
3139
+ conditionalCdf[y * width + x] /= rowWeight;
3140
+ }
3141
+ } else {
3142
+ for (let x = 0; x < width; x += 1) {
3143
+ conditionalCdf[y * width + x] = (x + 1) / width;
3144
+ }
3145
+ }
3146
+ marginalCdf[y] = totalWeight;
3147
+ }
3148
+ for (let y = 0; y < height; y += 1) {
3149
+ marginalCdf[y] /= Math.max(totalWeight, 0.000001);
3150
+ }
3151
+ for (let index = 0; index < pdf.length; index += 1) {
3152
+ pdf[index] /= Math.max(totalWeight, 0.000001);
3153
+ }
3154
+ return Object.freeze({
3155
+ width,
3156
+ height,
3157
+ pdf,
3158
+ marginalCdf,
3159
+ conditionalCdf,
3160
+ hasImportanceData: true,
3161
+ });
1941
3162
  }
1942
3163
 
1943
3164
  function createEnvironmentMapUploadBytes(environmentMap, fallbackColor) {
@@ -1970,12 +3191,13 @@ function createEnvironmentMapUploadBytes(environmentMap, fallbackColor) {
1970
3191
  }
1971
3192
  }
1972
3193
 
1973
- return Object.freeze({
3194
+ const upload = Object.freeze({
1974
3195
  bytes,
1975
3196
  bytesPerRow,
1976
3197
  width,
1977
3198
  height,
1978
3199
  });
3200
+ return upload;
1979
3201
  }
1980
3202
 
1981
3203
  function createEnvironmentMapResource(device, constants, environmentMap, fallbackColor) {
@@ -1988,9 +3210,13 @@ function createEnvironmentMapResource(device, constants, environmentMap, fallbac
1988
3210
  addressModeV: "clamp-to-edge",
1989
3211
  magFilter: "linear",
1990
3212
  minFilter: "linear",
3213
+ mipmapFilter: "linear",
1991
3214
  }),
1992
3215
  texture: null,
1993
3216
  ownsTexture: false,
3217
+ width: Math.max(1, environmentMap.width),
3218
+ height: Math.max(1, environmentMap.height),
3219
+ mipLevelCount: Math.max(1, environmentMap.mipLevelCount ?? 1),
1994
3220
  });
1995
3221
  }
1996
3222
 
@@ -2003,17 +3229,95 @@ function createEnvironmentMapResource(device, constants, environmentMap, fallbac
2003
3229
  addressModeV: "clamp-to-edge",
2004
3230
  magFilter: "linear",
2005
3231
  minFilter: "linear",
3232
+ mipmapFilter: "linear",
2006
3233
  }),
2007
3234
  texture: environmentMap.texture,
2008
3235
  ownsTexture: false,
3236
+ width: Math.max(1, environmentMap.width),
3237
+ height: Math.max(1, environmentMap.height),
3238
+ mipLevelCount: Math.max(1, environmentMap.mipLevelCount ?? 1),
2009
3239
  });
2010
3240
  }
2011
3241
 
2012
- const upload = createEnvironmentMapUploadBytes(environmentMap, fallbackColor);
3242
+ const prefiltered = createPrefilteredEnvironmentLevels(environmentMap, fallbackColor);
3243
+ const uploads = createFloat16RgbaUploadFromLevels(prefiltered.levels);
2013
3244
  const texture = device.createTexture({
2014
3245
  label: environmentMap.enabled
2015
3246
  ? "plasius.wavefront.environmentMap"
2016
3247
  : "plasius.wavefront.environmentMapFallback",
3248
+ size: { width: prefiltered.width, height: prefiltered.height },
3249
+ format: "rgba16float",
3250
+ mipLevelCount: prefiltered.mipLevelCount,
3251
+ usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST,
3252
+ });
3253
+ uploads.forEach((upload, mipLevel) => {
3254
+ device.queue.writeTexture(
3255
+ { texture, mipLevel },
3256
+ upload.bytes,
3257
+ { bytesPerRow: upload.bytesPerRow, rowsPerImage: upload.height },
3258
+ { width: upload.width, height: upload.height, depthOrArrayLayers: 1 }
3259
+ );
3260
+ });
3261
+ return Object.freeze({
3262
+ view: texture.createView(),
3263
+ sampler: environmentMap.sampler ?? device.createSampler({
3264
+ label: "plasius.wavefront.environmentMapSampler",
3265
+ addressModeU: "repeat",
3266
+ addressModeV: "clamp-to-edge",
3267
+ magFilter: "linear",
3268
+ minFilter: "linear",
3269
+ mipmapFilter: "linear",
3270
+ }),
3271
+ texture,
3272
+ ownsTexture: true,
3273
+ width: prefiltered.width,
3274
+ height: prefiltered.height,
3275
+ mipLevelCount: prefiltered.mipLevelCount,
3276
+ });
3277
+ }
3278
+
3279
+ function createEnvironmentSamplingTextureResource(device, constants, environmentMap, fallbackColor) {
3280
+ const tables = createEnvironmentSamplingTables(environmentMap, fallbackColor);
3281
+ const rowBytes = tables.width * 8;
3282
+ const bytesPerRow = alignTo(rowBytes, 256);
3283
+ const bytes = new Uint8Array(bytesPerRow * tables.height);
3284
+ const view = new DataView(bytes.buffer);
3285
+ for (let y = 0; y < tables.height; y += 1) {
3286
+ for (let x = 0; x < tables.width; x += 1) {
3287
+ const probability = tables.pdf[y * tables.width + x];
3288
+ const conditional = tables.conditionalCdf[y * tables.width + x];
3289
+ const marginal = tables.marginalCdf[y];
3290
+ const offset = y * bytesPerRow + x * 8;
3291
+ view.setUint16(offset, float32ToFloat16Bits(probability), true);
3292
+ view.setUint16(offset + 2, float32ToFloat16Bits(conditional), true);
3293
+ view.setUint16(offset + 4, float32ToFloat16Bits(marginal), true);
3294
+ view.setUint16(offset + 6, float32ToFloat16Bits(1), true);
3295
+ }
3296
+ }
3297
+ const texture = device.createTexture({
3298
+ label: "plasius.wavefront.environmentSampling",
3299
+ size: { width: tables.width, height: tables.height },
3300
+ format: "rgba16float",
3301
+ usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST,
3302
+ });
3303
+ device.queue.writeTexture(
3304
+ { texture },
3305
+ bytes,
3306
+ { bytesPerRow, rowsPerImage: tables.height },
3307
+ { width: tables.width, height: tables.height, depthOrArrayLayers: 1 }
3308
+ );
3309
+ return Object.freeze({
3310
+ view: texture.createView(),
3311
+ texture,
3312
+ ownsTexture: true,
3313
+ hasImportanceData: tables.hasImportanceData,
3314
+ });
3315
+ }
3316
+
3317
+ function createBrdfLutResource(device, constants, size = DEFAULT_BRDF_LUT_SIZE) {
3318
+ const upload = createBrdfLutUploadBytes(size);
3319
+ const texture = device.createTexture({
3320
+ label: "plasius.wavefront.brdfLut",
2017
3321
  size: { width: upload.width, height: upload.height },
2018
3322
  format: "rgba16float",
2019
3323
  usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST,
@@ -2026,15 +3330,117 @@ function createEnvironmentMapResource(device, constants, environmentMap, fallbac
2026
3330
  );
2027
3331
  return Object.freeze({
2028
3332
  view: texture.createView(),
2029
- sampler: environmentMap.sampler ?? device.createSampler({
2030
- label: "plasius.wavefront.environmentMapSampler",
2031
- addressModeU: "repeat",
3333
+ sampler: device.createSampler({
3334
+ label: "plasius.wavefront.brdfLutSampler",
3335
+ addressModeU: "clamp-to-edge",
2032
3336
  addressModeV: "clamp-to-edge",
2033
3337
  magFilter: "linear",
2034
3338
  minFilter: "linear",
2035
3339
  }),
2036
3340
  texture,
2037
3341
  ownsTexture: true,
3342
+ width: upload.width,
3343
+ height: upload.height,
3344
+ });
3345
+ }
3346
+
3347
+ function createMediumTextureResource(device, constants, mediums) {
3348
+ const normalized = Array.isArray(mediums) && mediums.length > 0 ? mediums : [{ id: 0 }];
3349
+ const width = Math.max(
3350
+ 1,
3351
+ normalized.reduce((maximum, medium) => Math.max(maximum, medium.id ?? 0), 0) + 1
3352
+ );
3353
+ const level = {
3354
+ width,
3355
+ height: MEDIUM_TABLE_ROWS,
3356
+ data: new Float32Array(width * MEDIUM_TABLE_ROWS * 4),
3357
+ };
3358
+
3359
+ for (const medium of normalized) {
3360
+ const mediumId = Math.max(0, Math.trunc(Number(medium.id) || 0));
3361
+ const absorptionOffset = mediumId * 4;
3362
+ level.data[absorptionOffset] = Math.max(0, medium.absorption?.[0] ?? 0);
3363
+ level.data[absorptionOffset + 1] = Math.max(0, medium.absorption?.[1] ?? 0);
3364
+ level.data[absorptionOffset + 2] = Math.max(0, medium.absorption?.[2] ?? 0);
3365
+ level.data[absorptionOffset + 3] = Math.max(0, medium.phaseModel ?? 0);
3366
+
3367
+ const scatteringOffset = (width + mediumId) * 4;
3368
+ level.data[scatteringOffset] = Math.max(0, medium.scattering?.[0] ?? 0);
3369
+ level.data[scatteringOffset + 1] = Math.max(0, medium.scattering?.[1] ?? 0);
3370
+ level.data[scatteringOffset + 2] = Math.max(0, medium.scattering?.[2] ?? 0);
3371
+ level.data[scatteringOffset + 3] = Math.max(0, medium.density ?? 0);
3372
+ }
3373
+
3374
+ const upload = createFloat16RgbaUploadFromLevels([level])[0];
3375
+ const texture = device.createTexture({
3376
+ label: "plasius.wavefront.mediumTable",
3377
+ size: { width, height: MEDIUM_TABLE_ROWS },
3378
+ format: "rgba16float",
3379
+ usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST,
3380
+ });
3381
+ device.queue.writeTexture(
3382
+ { texture },
3383
+ upload.bytes,
3384
+ { bytesPerRow: upload.bytesPerRow, rowsPerImage: upload.height },
3385
+ { width, height: MEDIUM_TABLE_ROWS, depthOrArrayLayers: 1 }
3386
+ );
3387
+ return Object.freeze({
3388
+ texture,
3389
+ view: texture.createView(),
3390
+ ownsTexture: true,
3391
+ count: normalized.length,
3392
+ width,
3393
+ });
3394
+ }
3395
+
3396
+ function mediumTablesEqual(left, right) {
3397
+ const leftMediums = Array.isArray(left) ? left : [];
3398
+ const rightMediums = Array.isArray(right) ? right : [];
3399
+ if (leftMediums.length !== rightMediums.length) {
3400
+ return false;
3401
+ }
3402
+ for (let index = 0; index < leftMediums.length; index += 1) {
3403
+ const leftMedium = leftMediums[index];
3404
+ const rightMedium = rightMediums[index];
3405
+ if ((leftMedium?.id ?? 0) !== (rightMedium?.id ?? 0)) {
3406
+ return false;
3407
+ }
3408
+ if ((leftMedium?.phaseModel ?? 0) !== (rightMedium?.phaseModel ?? 0)) {
3409
+ return false;
3410
+ }
3411
+ if ((leftMedium?.density ?? 0) !== (rightMedium?.density ?? 0)) {
3412
+ return false;
3413
+ }
3414
+ for (let component = 0; component < 3; component += 1) {
3415
+ if ((leftMedium?.absorption?.[component] ?? 0) !== (rightMedium?.absorption?.[component] ?? 0)) {
3416
+ return false;
3417
+ }
3418
+ if ((leftMedium?.scattering?.[component] ?? 0) !== (rightMedium?.scattering?.[component] ?? 0)) {
3419
+ return false;
3420
+ }
3421
+ }
3422
+ }
3423
+ return true;
3424
+ }
3425
+
3426
+ function createAtlasTextureResource(device, constants, atlas, label) {
3427
+ const upload = createRgba8TextureUpload(atlas);
3428
+ const texture = device.createTexture({
3429
+ label,
3430
+ size: { width: upload.width, height: upload.height },
3431
+ format: "rgba8unorm",
3432
+ usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST,
3433
+ });
3434
+ device.queue.writeTexture(
3435
+ { texture },
3436
+ upload.bytes,
3437
+ { bytesPerRow: upload.bytesPerRow, rowsPerImage: upload.height },
3438
+ { width: upload.width, height: upload.height, depthOrArrayLayers: 1 }
3439
+ );
3440
+ return Object.freeze({
3441
+ texture,
3442
+ view: texture.createView(),
3443
+ ownsTexture: true,
2038
3444
  });
2039
3445
  }
2040
3446
 
@@ -2084,6 +3490,26 @@ async function createComputePipeline(device, shaderModule, layout, entryPoint, l
2084
3490
  }
2085
3491
  }
2086
3492
 
3493
+ async function assertShaderModuleCompiles(shaderModule, label) {
3494
+ if (typeof shaderModule?.compilationInfo !== "function") {
3495
+ return;
3496
+ }
3497
+ const info = await shaderModule.compilationInfo();
3498
+ const messages = Array.isArray(info?.messages) ? info.messages : [];
3499
+ const errors = messages.filter((message) => message?.type === "error");
3500
+ if (errors.length <= 0) {
3501
+ return;
3502
+ }
3503
+ const diagnostics = errors
3504
+ .map((message) => {
3505
+ const line = Number.isFinite(message.lineNum) ? message.lineNum : "?";
3506
+ const column = Number.isFinite(message.linePos) ? message.linePos : "?";
3507
+ return `line ${line}:${column} ${message.message}`;
3508
+ })
3509
+ .join("\n");
3510
+ throw new Error(`WGSL compilation preflight failed for ${label}:\n${diagnostics}`);
3511
+ }
3512
+
2087
3513
  async function createRenderPipeline(device, descriptor) {
2088
3514
  if (typeof device.createRenderPipelineAsync === "function") {
2089
3515
  return device.createRenderPipelineAsync(descriptor);
@@ -2093,6 +3519,7 @@ async function createRenderPipeline(device, descriptor) {
2093
3519
 
2094
3520
  const WAVEFRONT_COMPUTE_WGSL = `
2095
3521
  const RAY_FLAG_GUIDED_EMISSIVE: u32 = 1u;
3522
+ const RAY_FLAG_DELTA_SAMPLE: u32 = 2u;
2096
3523
 
2097
3524
  struct RayRecord {
2098
3525
  rayId: u32,
@@ -2118,11 +3545,12 @@ struct HitRecord {
2118
3545
  primitiveId: u32,
2119
3546
  materialRefId: u32,
2120
3547
  mediumRefId: u32,
3548
+ materialSlot: u32,
2121
3549
  pad0: u32,
2122
3550
  pad1: u32,
2123
- pad2: u32,
2124
3551
  distance: f32,
2125
- pad3: vec3<f32>,
3552
+ occlusion: f32,
3553
+ pad2: vec2<f32>,
2126
3554
  position: vec4<f32>,
2127
3555
  geometricNormal: vec4<f32>,
2128
3556
  shadingNormal: vec4<f32>,
@@ -2131,6 +3559,9 @@ struct HitRecord {
2131
3559
  color: vec4<f32>,
2132
3560
  emission: vec4<f32>,
2133
3561
  material: vec4<f32>,
3562
+ materialResponse: vec4<f32>,
3563
+ materialExtension: vec4<f32>,
3564
+ specularColor: vec4<f32>,
2134
3565
  };
2135
3566
 
2136
3567
  struct SceneObject {
@@ -2138,11 +3569,18 @@ struct SceneObject {
2138
3569
  objectId: u32,
2139
3570
  materialKind: u32,
2140
3571
  flags: u32,
3572
+ mediumRefId: u32,
3573
+ pad0: u32,
3574
+ pad1: u32,
3575
+ pad2: u32,
2141
3576
  center: vec4<f32>,
2142
3577
  halfExtent: vec4<f32>,
2143
3578
  color: vec4<f32>,
2144
3579
  emission: vec4<f32>,
2145
3580
  material: vec4<f32>,
3581
+ materialResponse: vec4<f32>,
3582
+ materialExtension: vec4<f32>,
3583
+ specularColor: vec4<f32>,
2146
3584
  };
2147
3585
 
2148
3586
  struct TriangleRecord {
@@ -2152,7 +3590,7 @@ struct TriangleRecord {
2152
3590
  flags: u32,
2153
3591
  materialRefId: u32,
2154
3592
  mediumRefId: u32,
2155
- pad0: u32,
3593
+ materialSlot: u32,
2156
3594
  pad1: u32,
2157
3595
  v0: vec4<f32>,
2158
3596
  v1: vec4<f32>,
@@ -2165,6 +3603,15 @@ struct TriangleRecord {
2165
3603
  color: vec4<f32>,
2166
3604
  emission: vec4<f32>,
2167
3605
  material: vec4<f32>,
3606
+ materialResponse: vec4<f32>,
3607
+ materialExtension: vec4<f32>,
3608
+ specularColor: vec4<f32>,
3609
+ baseColorAtlas: vec4<f32>,
3610
+ metallicRoughnessAtlas: vec4<f32>,
3611
+ normalAtlas: vec4<f32>,
3612
+ occlusionAtlas: vec4<f32>,
3613
+ emissiveAtlas: vec4<f32>,
3614
+ textureSettings: vec4<f32>,
2168
3615
  };
2169
3616
 
2170
3617
  struct BvhNode {
@@ -2185,10 +3632,10 @@ struct BvhLeafRef {
2185
3632
 
2186
3633
  struct ScatterResult {
2187
3634
  direction: vec4<f32>,
3635
+ pdf: f32,
3636
+ mediumRefId: u32,
2188
3637
  flags: u32,
2189
3638
  pad0: u32,
2190
- pad1: u32,
2191
- pad2: u32,
2192
3639
  };
2193
3640
 
2194
3641
  struct MeshVertex {
@@ -2209,10 +3656,19 @@ struct MeshRange {
2209
3656
  triangleCount: u32,
2210
3657
  firstVertex: u32,
2211
3658
  vertexCount: u32,
2212
- pad0: u32,
3659
+ materialSlot: u32,
2213
3660
  color: vec4<f32>,
2214
3661
  emission: vec4<f32>,
2215
3662
  material: vec4<f32>,
3663
+ materialResponse: vec4<f32>,
3664
+ materialExtension: vec4<f32>,
3665
+ specularColor: vec4<f32>,
3666
+ baseColorAtlas: vec4<f32>,
3667
+ metallicRoughnessAtlas: vec4<f32>,
3668
+ normalAtlas: vec4<f32>,
3669
+ occlusionAtlas: vec4<f32>,
3670
+ emissiveAtlas: vec4<f32>,
3671
+ textureSettings: vec4<f32>,
2216
3672
  };
2217
3673
 
2218
3674
  struct FrameConfig {
@@ -2253,6 +3709,7 @@ struct FrameConfig {
2253
3709
  _portalPad1: u32,
2254
3710
  environmentMapSettings: vec4<f32>,
2255
3711
  pathResolveSettings: vec4<f32>,
3712
+ environmentMapMeta: vec4<f32>,
2256
3713
  };
2257
3714
 
2258
3715
  struct Counters {
@@ -2315,6 +3772,16 @@ struct EnvironmentPortal {
2315
3772
  @group(0) @binding(20) var environmentMapTexture: texture_2d<f32>;
2316
3773
  @group(0) @binding(21) var environmentMapSampler: sampler;
2317
3774
  @group(0) @binding(22) var<storage, read_write> pathVertices: array<vec4<f32>>;
3775
+ @group(0) @binding(23) var baseColorAtlasTexture: texture_2d<f32>;
3776
+ @group(0) @binding(24) var metallicRoughnessAtlasTexture: texture_2d<f32>;
3777
+ @group(0) @binding(25) var normalAtlasTexture: texture_2d<f32>;
3778
+ @group(0) @binding(26) var occlusionAtlasTexture: texture_2d<f32>;
3779
+ @group(0) @binding(27) var emissiveAtlasTexture: texture_2d<f32>;
3780
+ @group(0) @binding(28) var materialAtlasSampler: sampler;
3781
+ @group(0) @binding(29) var brdfLutTexture: texture_2d<f32>;
3782
+ @group(0) @binding(30) var brdfLutSampler: sampler;
3783
+ @group(0) @binding(31) var environmentSamplingTexture: texture_2d<f32>;
3784
+ @group(0) @binding(32) var mediumTableTexture: texture_2d<f32>;
2318
3785
 
2319
3786
  fn hash_u32(value: u32) -> u32 {
2320
3787
  var x = value;
@@ -2351,6 +3818,146 @@ fn safe_normalize(value: vec3<f32>, fallback: vec3<f32>) -> vec3<f32> {
2351
3818
  return value / len;
2352
3819
  }
2353
3820
 
3821
+ struct TangentBasis {
3822
+ tangent: vec3<f32>,
3823
+ bitangent: vec3<f32>,
3824
+ };
3825
+
3826
+ struct SurfaceMaterialSample {
3827
+ color: vec4<f32>,
3828
+ emission: vec4<f32>,
3829
+ material: vec4<f32>,
3830
+ materialResponse: vec4<f32>,
3831
+ materialExtension: vec4<f32>,
3832
+ specularColor: vec4<f32>,
3833
+ shadingNormal: vec3<f32>,
3834
+ occlusion: f32,
3835
+ };
3836
+
3837
+ fn srgb_to_linear_channel(value: f32) -> f32 {
3838
+ if (value <= 0.04045) {
3839
+ return value / 12.92;
3840
+ }
3841
+ return pow((value + 0.055) / 1.055, 2.4);
3842
+ }
3843
+
3844
+ fn srgb_to_linear_vec3(value: vec3<f32>) -> vec3<f32> {
3845
+ return vec3<f32>(
3846
+ srgb_to_linear_channel(value.x),
3847
+ srgb_to_linear_channel(value.y),
3848
+ srgb_to_linear_channel(value.z)
3849
+ );
3850
+ }
3851
+
3852
+ fn wrap_uv(uv: vec2<f32>) -> vec2<f32> {
3853
+ return fract(fract(uv) + vec2<f32>(1.0));
3854
+ }
3855
+
3856
+ fn atlas_sample_uv(rect: vec4<f32>, uv: vec2<f32>) -> vec2<f32> {
3857
+ let local = wrap_uv(uv);
3858
+ let clamped = clamp(local, vec2<f32>(0.001), vec2<f32>(0.999));
3859
+ return rect.xy + clamped * rect.zw;
3860
+ }
3861
+
3862
+ fn sample_atlas(textureRef: texture_2d<f32>, rect: vec4<f32>, uv: vec2<f32>) -> vec4<f32> {
3863
+ return textureSampleLevel(textureRef, materialAtlasSampler, atlas_sample_uv(rect, uv), 0.0);
3864
+ }
3865
+
3866
+ fn build_triangle_tangent_basis(
3867
+ triangle: TriangleRecord,
3868
+ fallbackNormal: vec3<f32>
3869
+ ) -> TangentBasis {
3870
+ let edge1 = triangle.v1.xyz - triangle.v0.xyz;
3871
+ let edge2 = triangle.v2.xyz - triangle.v0.xyz;
3872
+ let uv0 = triangle.uv0uv1.xy;
3873
+ let uv1 = triangle.uv0uv1.zw;
3874
+ let uv2 = triangle.uv2Pad.xy;
3875
+ let deltaUv1 = uv1 - uv0;
3876
+ let deltaUv2 = uv2 - uv0;
3877
+ let determinant = deltaUv1.x * deltaUv2.y - deltaUv1.y * deltaUv2.x;
3878
+ if (abs(determinant) <= 0.000001) {
3879
+ let tangentFallback = select(vec3<f32>(0.0, 1.0, 0.0), vec3<f32>(1.0, 0.0, 0.0), abs(fallbackNormal.y) >= 0.999);
3880
+ let tangent = safe_normalize(cross(tangentFallback, fallbackNormal), vec3<f32>(1.0, 0.0, 0.0));
3881
+ let bitangent = safe_normalize(cross(fallbackNormal, tangent), vec3<f32>(0.0, 0.0, 1.0));
3882
+ return TangentBasis(tangent, bitangent);
3883
+ }
3884
+ let inverse = 1.0 / determinant;
3885
+ let tangent = safe_normalize(
3886
+ inverse * (edge1 * deltaUv2.y - edge2 * deltaUv1.y),
3887
+ vec3<f32>(1.0, 0.0, 0.0)
3888
+ );
3889
+ let bitangent = safe_normalize(
3890
+ inverse * (-edge1 * deltaUv2.x + edge2 * deltaUv1.x),
3891
+ vec3<f32>(0.0, 0.0, 1.0)
3892
+ );
3893
+ return TangentBasis(tangent, bitangent);
3894
+ }
3895
+
3896
+ fn sample_surface_material(
3897
+ triangle: TriangleRecord,
3898
+ uv: vec2<f32>,
3899
+ geometricNormal: vec3<f32>,
3900
+ shadingNormal: vec3<f32>
3901
+ ) -> SurfaceMaterialSample {
3902
+ let baseColorTexel = sample_atlas(baseColorAtlasTexture, triangle.baseColorAtlas, uv);
3903
+ let baseColor = vec4<f32>(
3904
+ clamp(triangle.color.rgb * srgb_to_linear_vec3(baseColorTexel.rgb), vec3<f32>(0.0), vec3<f32>(1.0)),
3905
+ clamp(triangle.color.a * baseColorTexel.a, 0.0, 1.0)
3906
+ );
3907
+ let metallicRoughnessTexel = sample_atlas(
3908
+ metallicRoughnessAtlasTexture,
3909
+ triangle.metallicRoughnessAtlas,
3910
+ uv
3911
+ );
3912
+ let normalTexel = sample_atlas(normalAtlasTexture, triangle.normalAtlas, uv);
3913
+ let occlusionTexel = sample_atlas(occlusionAtlasTexture, triangle.occlusionAtlas, uv);
3914
+ let emissiveTexel = sample_atlas(emissiveAtlasTexture, triangle.emissiveAtlas, uv);
3915
+ let normalScale = clamp(triangle.textureSettings.x, 0.0, 1.0);
3916
+ let tangentBasis = build_triangle_tangent_basis(triangle, geometricNormal);
3917
+ let tangentNormal = safe_normalize(
3918
+ vec3<f32>(
3919
+ (normalTexel.x * 2.0 - 1.0) * normalScale,
3920
+ (normalTexel.y * 2.0 - 1.0) * normalScale,
3921
+ 1.0 + ((normalTexel.z * 2.0 - 1.0) - 1.0) * normalScale
3922
+ ),
3923
+ vec3<f32>(0.0, 0.0, 1.0)
3924
+ );
3925
+ let mappedNormal = safe_normalize(
3926
+ tangentBasis.tangent * tangentNormal.x +
3927
+ tangentBasis.bitangent * tangentNormal.y +
3928
+ shadingNormal * tangentNormal.z,
3929
+ shadingNormal
3930
+ );
3931
+ let emission = vec4<f32>(
3932
+ max(
3933
+ triangle.emission.rgb *
3934
+ srgb_to_linear_vec3(emissiveTexel.rgb) *
3935
+ max(triangle.textureSettings.z, 0.0),
3936
+ vec3<f32>(0.0)
3937
+ ),
3938
+ clamp(triangle.emission.a * emissiveTexel.a, 0.0, 1.0)
3939
+ );
3940
+ return SurfaceMaterialSample(
3941
+ baseColor,
3942
+ emission,
3943
+ vec4<f32>(
3944
+ clamp(triangle.material.x * metallicRoughnessTexel.y, 0.0, 1.0),
3945
+ clamp(triangle.material.y * metallicRoughnessTexel.z, 0.0, 1.0),
3946
+ clamp(triangle.material.z * baseColor.a, 0.0, 1.0),
3947
+ clamp(triangle.material.w, 1.0, 3.0)
3948
+ ),
3949
+ triangle.materialResponse,
3950
+ triangle.materialExtension,
3951
+ triangle.specularColor,
3952
+ repair_shading_normal(geometricNormal, mappedNormal),
3953
+ clamp(
3954
+ mix(1.0, occlusionTexel.x, clamp(triangle.textureSettings.y, 0.0, 1.0)),
3955
+ 0.0,
3956
+ 1.0
3957
+ )
3958
+ );
3959
+ }
3960
+
2354
3961
  fn saturate(value: f32) -> f32 {
2355
3962
  return clamp(value, 0.0, 1.0);
2356
3963
  }
@@ -2359,6 +3966,10 @@ fn max_component(value: vec3<f32>) -> f32 {
2359
3966
  return max(max(value.x, value.y), value.z);
2360
3967
  }
2361
3968
 
3969
+ fn radiance_luminance(value: vec3<f32>) -> f32 {
3970
+ return dot(value, vec3<f32>(0.2126, 0.7152, 0.0722));
3971
+ }
3972
+
2362
3973
  fn environment_map_enabled() -> bool {
2363
3974
  return config.environmentMapSettings.x > 0.5;
2364
3975
  }
@@ -2388,103 +3999,440 @@ fn clear_deferred_path(rayId: u32) {
2388
3999
  }
2389
4000
  }
2390
4001
 
2391
- fn record_deferred_path_response(ray: RayRecord, response: vec3<f32>) {
2392
- if (!deferred_path_resolve_enabled() || ray.rayId >= config.tilePixelCount || ray.bounce >= config.maxDepth) {
2393
- return;
2394
- }
2395
- pathVertices[path_vertex_index(ray.rayId, ray.bounce)] =
2396
- vec4<f32>(max(response, vec3<f32>(0.0)), 1.0);
4002
+ fn record_deferred_path_response(ray: RayRecord, response: vec3<f32>) {
4003
+ if (!deferred_path_resolve_enabled() || ray.rayId >= config.tilePixelCount || ray.bounce >= config.maxDepth) {
4004
+ return;
4005
+ }
4006
+ pathVertices[path_vertex_index(ray.rayId, ray.bounce)] =
4007
+ vec4<f32>(max(response, vec3<f32>(0.0)), 1.0);
4008
+ }
4009
+
4010
+ fn record_deferred_terminal_source(ray: RayRecord, sourceRadiance: vec3<f32>) {
4011
+ if (!deferred_path_resolve_enabled() || ray.rayId >= config.tilePixelCount) {
4012
+ return;
4013
+ }
4014
+ pathVertices[path_vertex_index(ray.rayId, config.maxDepth)] =
4015
+ vec4<f32>(clamp_sample_radiance(sourceRadiance), 1.0);
4016
+ }
4017
+
4018
+ fn environment_map_uv(direction: vec3<f32>) -> vec2<f32> {
4019
+ let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
4020
+ let rotationTurns = config.environmentMapSettings.z / 6.28318530718;
4021
+ let u = fract(atan2(rayDirection.z, rayDirection.x) / 6.28318530718 + 0.5 + rotationTurns);
4022
+ let v = acos(clamp(rayDirection.y, -1.0, 1.0)) / 3.14159265359;
4023
+ return vec2<f32>(u, clamp(v, 0.0, 1.0));
4024
+ }
4025
+
4026
+ fn environment_map_radiance(direction: vec3<f32>) -> vec3<f32> {
4027
+ let uv = environment_map_uv(direction);
4028
+ let texel = max(textureSampleLevel(environmentMapTexture, environmentMapSampler, uv, 0.0).rgb, vec3<f32>(0.0));
4029
+ return texel * max(config.environmentMapSettings.y, 0.0);
4030
+ }
4031
+
4032
+ fn procedural_environment_radiance(direction: vec3<f32>) -> vec3<f32> {
4033
+ let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
4034
+ let upFactor = saturate(rayDirection.y * 0.5 + 0.5);
4035
+ let sunDirection = safe_normalize(
4036
+ config.environmentSunDirectionIntensity.xyz,
4037
+ vec3<f32>(0.0, 1.0, 0.0)
4038
+ );
4039
+ let sunGlow = pow(saturate(dot(rayDirection, sunDirection)), 192.0);
4040
+ let gradient =
4041
+ config.environmentHorizonColor.xyz * (1.0 - upFactor) +
4042
+ config.environmentZenithColor.xyz * upFactor;
4043
+ return (
4044
+ gradient +
4045
+ config.environmentSunColor.xyz * sunGlow
4046
+ ) * max(config.environmentSunDirectionIntensity.w, 0.0001);
4047
+ }
4048
+
4049
+ fn base_environment_radiance(direction: vec3<f32>) -> vec3<f32> {
4050
+ if (environment_map_enabled()) {
4051
+ return environment_map_radiance(direction);
4052
+ }
4053
+ return procedural_environment_radiance(direction);
4054
+ }
4055
+
4056
+ fn environment_portal_radiance_scale(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
4057
+ if (config.environmentPortalCount == 0u || config.environmentPortalMode == 0u) {
4058
+ return vec3<f32>(1.0);
4059
+ }
4060
+ var scale = vec3<f32>(0.0);
4061
+ for (var portalIndex = 0u; portalIndex < config.environmentPortalCount; portalIndex = portalIndex + 1u) {
4062
+ let portal = environmentPortals[portalIndex];
4063
+ if (portal.kind == 1u) {
4064
+ let portalNormal = safe_normalize(portal.normal.xyz, vec3<f32>(0.0, 0.0, 1.0));
4065
+ let denominator = dot(direction, portalNormal);
4066
+ let twoSided = (portal.flags & 1u) != 0u;
4067
+ var facing = abs(denominator) > 0.0001;
4068
+ if (!twoSided && denominator <= 0.0001) {
4069
+ facing = false;
4070
+ }
4071
+ if (facing) {
4072
+ let distance = dot(portal.position.xyz - origin, portalNormal) / denominator;
4073
+ if (distance > 0.001) {
4074
+ let hitPosition = origin + direction * distance;
4075
+ let local = hitPosition - portal.position.xyz;
4076
+ let tangent = safe_normalize(portal.tangent.xyz, vec3<f32>(1.0, 0.0, 0.0));
4077
+ let bitangent = safe_normalize(portal.bitangent.xyz, vec3<f32>(0.0, 1.0, 0.0));
4078
+ let u = dot(local, tangent);
4079
+ let v = dot(local, bitangent);
4080
+ if (abs(u) <= portal.tangent.w && abs(v) <= portal.bitangent.w) {
4081
+ let areaWeight = clamp(sqrt(max(portal.position.w, 0.0001)), 0.25, 4.0);
4082
+ let angleWeight = max(abs(denominator), 0.08);
4083
+ let portalScale = portal.color.rgb * portal.normal.w * portal.color.a * areaWeight * angleWeight;
4084
+ scale = max(scale, portalScale);
4085
+ }
4086
+ }
4087
+ }
4088
+ }
4089
+ }
4090
+ return scale;
4091
+ }
4092
+
4093
+ fn environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
4094
+ let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
4095
+ let portalScale = environment_portal_radiance_scale(origin, rayDirection);
4096
+ let portalHit = max_component(portalScale) > 0.0001;
4097
+ return base_environment_radiance(rayDirection) *
4098
+ select(vec3<f32>(1.0), portalScale, portalHit);
4099
+ }
4100
+
4101
+ fn direct_environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
4102
+ let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
4103
+ let portalScale = environment_portal_radiance_scale(origin, rayDirection);
4104
+ let portalHit = max_component(portalScale) > 0.0001;
4105
+ if (
4106
+ config.environmentPortalCount > 0u &&
4107
+ config.environmentPortalMode == 2u &&
4108
+ !portalHit
4109
+ ) {
4110
+ return vec3<f32>(0.0);
4111
+ }
4112
+ return base_environment_radiance(rayDirection) *
4113
+ select(vec3<f32>(1.0), portalScale, portalHit);
4114
+ }
4115
+
4116
+ fn radical_inverse_vdc(bitsValue: u32) -> f32 {
4117
+ var bits = bitsValue;
4118
+ bits = (bits << 16u) | (bits >> 16u);
4119
+ bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xaaaaaaaau) >> 1u);
4120
+ bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xccccccccu) >> 2u);
4121
+ bits = ((bits & 0x0f0f0f0fu) << 4u) | ((bits & 0xf0f0f0f0u) >> 4u);
4122
+ bits = ((bits & 0x00ff00ffu) << 8u) | ((bits & 0xff00ff00u) >> 8u);
4123
+ return f32(bits) * 2.3283064365386963e-10;
4124
+ }
4125
+
4126
+ fn hammersley_2d(index: u32, count: u32) -> vec2<f32> {
4127
+ return vec2<f32>(f32(index) / max(f32(count), 1.0), radical_inverse_vdc(index));
4128
+ }
4129
+
4130
+ fn build_basis_tangent(normal: vec3<f32>) -> vec3<f32> {
4131
+ let tangentFallback = select(vec3<f32>(0.0, 1.0, 0.0), vec3<f32>(1.0, 0.0, 0.0), abs(normal.y) >= 0.999);
4132
+ return safe_normalize(cross(tangentFallback, normal), vec3<f32>(1.0, 0.0, 0.0));
4133
+ }
4134
+
4135
+ fn local_to_world(local: vec3<f32>, normal: vec3<f32>) -> vec3<f32> {
4136
+ let tangent = build_basis_tangent(normal);
4137
+ let bitangent = safe_normalize(cross(normal, tangent), vec3<f32>(0.0, 0.0, 1.0));
4138
+ return safe_normalize(tangent * local.x + bitangent * local.y + normal * local.z, normal);
4139
+ }
4140
+
4141
+ fn cosine_sample_hemisphere(sample: vec2<f32>, normal: vec3<f32>) -> vec3<f32> {
4142
+ let phi = 6.28318530718 * sample.x;
4143
+ let radius = sqrt(sample.y);
4144
+ let x = cos(phi) * radius;
4145
+ let y = sin(phi) * radius;
4146
+ let z = sqrt(max(0.0, 1.0 - sample.y));
4147
+ return local_to_world(vec3<f32>(x, y, z), normal);
4148
+ }
4149
+
4150
+ fn importance_sample_ggx(sample: vec2<f32>, roughness: f32, normal: vec3<f32>) -> vec3<f32> {
4151
+ let alpha = max(roughness * roughness, 0.0001);
4152
+ let phi = 6.28318530718 * sample.x;
4153
+ let cosTheta = sqrt((1.0 - sample.y) / max(1.0 + (alpha * alpha - 1.0) * sample.y, 0.0001));
4154
+ let sinTheta = sqrt(max(0.0, 1.0 - cosTheta * cosTheta));
4155
+ let localHalf = vec3<f32>(cos(phi) * sinTheta, sin(phi) * sinTheta, cosTheta);
4156
+ return local_to_world(localHalf, normal);
4157
+ }
4158
+
4159
+ fn distribution_ggx(normal: vec3<f32>, halfVector: vec3<f32>, roughness: f32) -> f32 {
4160
+ let alpha = max(roughness * roughness, 0.0001);
4161
+ let alpha2 = alpha * alpha;
4162
+ let nDotH = saturate(dot(normal, halfVector));
4163
+ let denominator = nDotH * nDotH * (alpha2 - 1.0) + 1.0;
4164
+ return alpha2 / max(3.14159265359 * denominator * denominator, 0.000001);
4165
+ }
4166
+
4167
+ fn geometry_schlick_ggx(nDotValue: f32, roughness: f32) -> f32 {
4168
+ let k = ((roughness + 1.0) * (roughness + 1.0)) / 8.0;
4169
+ return nDotValue / max(nDotValue * (1.0 - k) + k, 0.000001);
4170
+ }
4171
+
4172
+ fn geometry_smith(normal: vec3<f32>, viewDirection: vec3<f32>, lightDirection: vec3<f32>, roughness: f32) -> f32 {
4173
+ let nDotV = saturate(dot(normal, viewDirection));
4174
+ let nDotL = saturate(dot(normal, lightDirection));
4175
+ return geometry_schlick_ggx(nDotV, roughness) * geometry_schlick_ggx(nDotL, roughness);
4176
+ }
4177
+
4178
+ fn fresnel_schlick(cosine: f32, f0: vec3<f32>) -> vec3<f32> {
4179
+ return f0 + (vec3<f32>(1.0) - f0) * pow(1.0 - cosine, 5.0);
4180
+ }
4181
+
4182
+ fn sample_brdf_lut(nDotV: f32, roughness: f32) -> vec2<f32> {
4183
+ let uv = vec2<f32>(clamp(nDotV, 0.0, 1.0), clamp(roughness, 0.0, 1.0));
4184
+ return textureSampleLevel(brdfLutTexture, brdfLutSampler, uv, 0.0).xy;
4185
+ }
4186
+
4187
+ fn prefiltered_environment_radiance(direction: vec3<f32>, roughness: f32) -> vec3<f32> {
4188
+ let uv = environment_map_uv(direction);
4189
+ let maxLevel = max(config.environmentMapMeta.z - 1.0, 0.0);
4190
+ let lod = clamp(roughness, 0.0, 1.0) * maxLevel;
4191
+ let texel = max(textureSampleLevel(environmentMapTexture, environmentMapSampler, uv, lod).rgb, vec3<f32>(0.0));
4192
+ return texel * max(config.environmentMapSettings.y, 0.0);
4193
+ }
4194
+
4195
+ fn environment_pdf_dimensions() -> vec2<u32> {
4196
+ return vec2<u32>(
4197
+ max(u32(config.environmentMapMeta.x), 1u),
4198
+ max(u32(config.environmentMapMeta.y), 1u)
4199
+ );
4200
+ }
4201
+
4202
+ fn environment_importance_sampling_enabled() -> bool {
4203
+ return config.environmentMapMeta.w > 0.5;
4204
+ }
4205
+
4206
+ fn uniform_sphere_pdf() -> f32 {
4207
+ return 1.0 / (4.0 * 3.14159265359);
4208
+ }
4209
+
4210
+ fn sample_uniform_sphere_direction(sample: vec2<f32>) -> vec3<f32> {
4211
+ let z = 1.0 - 2.0 * sample.y;
4212
+ let radial = sqrt(max(1.0 - z * z, 0.0));
4213
+ let phi = sample.x * 6.28318530718;
4214
+ return vec3<f32>(cos(phi) * radial, z, sin(phi) * radial);
4215
+ }
4216
+
4217
+ fn environment_sampling_texel(x: u32, y: u32) -> vec4<f32> {
4218
+ return textureLoad(environmentSamplingTexture, vec2<i32>(i32(x), i32(y)), 0);
4219
+ }
4220
+
4221
+ fn environment_pdf_texel(x: u32, y: u32) -> f32 {
4222
+ return environment_sampling_texel(x, y).x;
4223
+ }
4224
+
4225
+ fn environment_row_cdf_texel(y: u32) -> f32 {
4226
+ return environment_sampling_texel(0u, y).z;
4227
+ }
4228
+
4229
+ fn environment_column_cdf_texel(x: u32, y: u32) -> f32 {
4230
+ return environment_sampling_texel(x, y).y;
4231
+ }
4232
+
4233
+ fn environment_direction_pdf(direction: vec3<f32>) -> f32 {
4234
+ if (!environment_importance_sampling_enabled()) {
4235
+ return uniform_sphere_pdf();
4236
+ }
4237
+ let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
4238
+ let uv = environment_map_uv(rayDirection);
4239
+ let dimensions = environment_pdf_dimensions();
4240
+ let width = max(f32(dimensions.x), 1.0);
4241
+ let height = max(f32(dimensions.y), 1.0);
4242
+ let x = min(u32(uv.x * width), dimensions.x - 1u);
4243
+ let y = min(u32(uv.y * height), dimensions.y - 1u);
4244
+ let discretePdf = max(environment_pdf_texel(x, y), 0.0);
4245
+ let sinTheta = sqrt(max(1.0 - rayDirection.y * rayDirection.y, 0.0));
4246
+ let solidAngle = max((2.0 * 3.14159265359 * 3.14159265359 * sinTheta) / (width * height), 0.000001);
4247
+ return discretePdf / solidAngle;
4248
+ }
4249
+
4250
+ fn sample_row_cdf(count: u32, sampleValue: f32) -> u32 {
4251
+ if (count == 0u) {
4252
+ return 0u;
4253
+ }
4254
+ var low = 0u;
4255
+ var high = count - 1u;
4256
+ loop {
4257
+ if (low >= high) {
4258
+ break;
4259
+ }
4260
+ let mid = (low + high) / 2u;
4261
+ let cdfValue = environment_row_cdf_texel(mid);
4262
+ if (sampleValue <= cdfValue) {
4263
+ high = mid;
4264
+ } else {
4265
+ low = mid + 1u;
4266
+ }
4267
+ }
4268
+ return min(low, count - 1u);
4269
+ }
4270
+
4271
+ fn sample_column_cdf(row: u32, count: u32, sampleValue: f32) -> u32 {
4272
+ if (count == 0u) {
4273
+ return 0u;
4274
+ }
4275
+ var low = 0u;
4276
+ var high = count - 1u;
4277
+ loop {
4278
+ if (low >= high) {
4279
+ break;
4280
+ }
4281
+ let mid = (low + high) / 2u;
4282
+ let cdfValue = environment_column_cdf_texel(mid, row);
4283
+ if (sampleValue <= cdfValue) {
4284
+ high = mid;
4285
+ } else {
4286
+ low = mid + 1u;
4287
+ }
4288
+ }
4289
+ return min(low, count - 1u);
4290
+ }
4291
+
4292
+ struct EnvironmentSample {
4293
+ direction: vec3<f32>,
4294
+ radiance: vec3<f32>,
4295
+ pdf: f32,
4296
+ };
4297
+
4298
+ fn sample_environment_importance(sample: vec2<f32>) -> EnvironmentSample {
4299
+ if (!environment_importance_sampling_enabled()) {
4300
+ let direction = sample_uniform_sphere_direction(sample);
4301
+ return EnvironmentSample(direction, base_environment_radiance(direction), uniform_sphere_pdf());
4302
+ }
4303
+ let dimensions = environment_pdf_dimensions();
4304
+ let row = sample_row_cdf(dimensions.y, sample.y);
4305
+ let column = sample_column_cdf(row, dimensions.x, sample.x);
4306
+ let uv = vec2<f32>(
4307
+ (f32(column) + 0.5) / max(f32(dimensions.x), 1.0),
4308
+ (f32(row) + 0.5) / max(f32(dimensions.y), 1.0)
4309
+ );
4310
+ let theta = uv.y * 3.14159265359;
4311
+ let phi = (uv.x - 0.5 - config.environmentMapSettings.z / 6.28318530718) * 6.28318530718;
4312
+ let sinTheta = sin(theta);
4313
+ let direction = vec3<f32>(cos(phi) * sinTheta, cos(theta), sin(phi) * sinTheta);
4314
+ let pdf = environment_direction_pdf(direction);
4315
+ return EnvironmentSample(direction, base_environment_radiance(direction), pdf);
2397
4316
  }
2398
4317
 
2399
- fn record_deferred_terminal_source(ray: RayRecord, sourceRadiance: vec3<f32>) {
2400
- if (!deferred_path_resolve_enabled() || ray.rayId >= config.tilePixelCount) {
2401
- return;
2402
- }
2403
- pathVertices[path_vertex_index(ray.rayId, config.maxDepth)] =
2404
- vec4<f32>(clamp_sample_radiance(sourceRadiance), 1.0);
4318
+ fn power_heuristic(pdfA: f32, pdfB: f32) -> f32 {
4319
+ let a2 = pdfA * pdfA;
4320
+ let b2 = pdfB * pdfB;
4321
+ return a2 / max(a2 + b2, 0.000001);
2405
4322
  }
2406
4323
 
2407
- fn environment_map_uv(direction: vec3<f32>) -> vec2<f32> {
4324
+ fn visible_environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
2408
4325
  let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
2409
- let rotationTurns = config.environmentMapSettings.z / 6.28318530718;
2410
- let u = fract(atan2(rayDirection.z, rayDirection.x) / 6.28318530718 + 0.5 + rotationTurns);
2411
- let v = acos(clamp(rayDirection.y, -1.0, 1.0)) / 3.14159265359;
2412
- return vec2<f32>(u, clamp(v, 0.0, 1.0));
4326
+ let visible = !scene_visibility_blocked(origin, rayDirection, 1000000.0);
4327
+ return select(vec3<f32>(0.0), direct_environment_radiance(origin, rayDirection), visible);
2413
4328
  }
2414
4329
 
2415
- fn environment_map_radiance(direction: vec3<f32>) -> vec3<f32> {
2416
- let uv = environment_map_uv(direction);
2417
- let texel = max(textureSampleLevel(environmentMapTexture, environmentMapSampler, uv, 0.0).rgb, vec3<f32>(0.0));
2418
- return texel * max(config.environmentMapSettings.y, 0.0);
4330
+ fn glossy_environment_direction(
4331
+ incidentDirection: vec3<f32>,
4332
+ normal: vec3<f32>,
4333
+ roughness: f32,
4334
+ normalBlendScale: f32
4335
+ ) -> vec3<f32> {
4336
+ let reflectionDirection = reflect(incidentDirection, normal);
4337
+ let blend = clamp(roughness * roughness * normalBlendScale, 0.0, 0.92);
4338
+ return safe_normalize(mix(reflectionDirection, normal, blend), normal);
2419
4339
  }
2420
4340
 
2421
- fn procedural_environment_radiance(direction: vec3<f32>) -> vec3<f32> {
2422
- let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
2423
- let upFactor = saturate(rayDirection.y * 0.5 + 0.5);
2424
- let sunDirection = safe_normalize(
2425
- config.environmentSunDirectionIntensity.xyz,
2426
- vec3<f32>(0.0, 1.0, 0.0)
2427
- );
2428
- let sunGlow = pow(saturate(dot(rayDirection, sunDirection)), 192.0);
2429
- let gradient =
2430
- config.environmentHorizonColor.xyz * (1.0 - upFactor) +
2431
- config.environmentZenithColor.xyz * upFactor;
2432
- return (
2433
- gradient +
2434
- config.environmentSunColor.xyz * sunGlow
2435
- ) * max(config.environmentSunDirectionIntensity.w, 0.0001);
4341
+ fn surface_glossiness(hit: HitRecord) -> f32 {
4342
+ let roughness = clamp(hit.material.x, 0.0, 1.0);
4343
+ let metallic = clamp(hit.material.y, 0.0, 1.0);
4344
+ let sheen = clamp(max_component(hit.materialResponse.xyz), 0.0, 1.0);
4345
+ let clearcoat = clamp(hit.materialResponse.w, 0.0, 1.0);
4346
+ let specularWeight = clamp(hit.materialExtension.y, 0.0, 1.0);
4347
+ let transmission = clamp(hit.materialExtension.z, 0.0, 1.0);
4348
+ let baseGloss =
4349
+ max(
4350
+ clearcoat,
4351
+ max(sheen * 0.72, max(specularWeight * (0.38 + metallic * 0.62), transmission))
4352
+ );
4353
+ return clamp(baseGloss * (1.0 - roughness * 0.72) + metallic * (1.0 - roughness) * 0.35, 0.0, 1.0);
2436
4354
  }
2437
4355
 
2438
- fn base_environment_radiance(direction: vec3<f32>) -> vec3<f32> {
2439
- if (environment_map_enabled()) {
2440
- return environment_map_radiance(direction);
2441
- }
2442
- return procedural_environment_radiance(direction);
4356
+ fn surface_specular_f0(hit: HitRecord, surfaceColor: vec3<f32>) -> vec3<f32> {
4357
+ let metallic = clamp(hit.material.y, 0.0, 1.0);
4358
+ let specularWeight = clamp(hit.materialExtension.y, 0.0, 1.0);
4359
+ let specularColor = clamp(hit.specularColor.xyz, vec3<f32>(0.0), vec3<f32>(1.0));
4360
+ let dielectricF0 = vec3<f32>(0.04) * specularWeight * specularColor;
4361
+ return mix(dielectricF0, surfaceColor, metallic);
2443
4362
  }
2444
4363
 
2445
- fn environment_portal_radiance_scale(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
2446
- if (config.environmentPortalCount == 0u || config.environmentPortalMode == 0u) {
2447
- return vec3<f32>(1.0);
2448
- }
2449
- var scale = vec3<f32>(0.0);
2450
- for (var portalIndex = 0u; portalIndex < config.environmentPortalCount; portalIndex = portalIndex + 1u) {
2451
- let portal = environmentPortals[portalIndex];
2452
- if (portal.kind == 1u) {
2453
- let portalNormal = safe_normalize(portal.normal.xyz, vec3<f32>(0.0, 0.0, 1.0));
2454
- let denominator = dot(direction, portalNormal);
2455
- let twoSided = (portal.flags & 1u) != 0u;
2456
- var facing = abs(denominator) > 0.0001;
2457
- if (!twoSided && denominator <= 0.0001) {
2458
- facing = false;
2459
- }
2460
- if (facing) {
2461
- let distance = dot(portal.position.xyz - origin, portalNormal) / denominator;
2462
- if (distance > 0.001) {
2463
- let hitPosition = origin + direction * distance;
2464
- let local = hitPosition - portal.position.xyz;
2465
- let tangent = safe_normalize(portal.tangent.xyz, vec3<f32>(1.0, 0.0, 0.0));
2466
- let bitangent = safe_normalize(portal.bitangent.xyz, vec3<f32>(0.0, 1.0, 0.0));
2467
- let u = dot(local, tangent);
2468
- let v = dot(local, bitangent);
2469
- if (abs(u) <= portal.tangent.w && abs(v) <= portal.bitangent.w) {
2470
- let areaWeight = clamp(sqrt(max(portal.position.w, 0.0001)), 0.25, 4.0);
2471
- let angleWeight = max(abs(denominator), 0.08);
2472
- let portalScale = portal.color.rgb * portal.normal.w * portal.color.a * areaWeight * angleWeight;
2473
- scale = max(scale, portalScale);
2474
- }
2475
- }
2476
- }
2477
- }
2478
- }
2479
- return scale;
4364
+ fn surface_bsdf_sampling_weights(hit: HitRecord) -> vec3<f32> {
4365
+ let metallic = clamp(hit.material.y, 0.0, 1.0);
4366
+ let clearcoat = clamp(hit.materialResponse.w, 0.0, 1.0);
4367
+ let specularWeight = clamp(hit.materialExtension.y, 0.0, 1.0);
4368
+ let diffuseWeight = clamp(
4369
+ (1.0 - metallic) * max(1.0 - specularWeight * 0.5 - clearcoat * 0.25, 0.15),
4370
+ 0.0,
4371
+ 1.0
4372
+ );
4373
+ let specWeight = clamp(max(metallic, specularWeight * 0.75) * (1.0 - clearcoat * 0.5), 0.0, 1.0);
4374
+ let clearcoatWeight = clamp(clearcoat, 0.0, 1.0);
4375
+ let totalWeight = max(diffuseWeight + specWeight + clearcoatWeight, 0.000001);
4376
+ return vec3<f32>(
4377
+ diffuseWeight / totalWeight,
4378
+ specWeight / totalWeight,
4379
+ clearcoatWeight / totalWeight
4380
+ );
2480
4381
  }
2481
4382
 
2482
- fn environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
2483
- let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
2484
- let portalScale = environment_portal_radiance_scale(origin, rayDirection);
2485
- let portalHit = max_component(portalScale) > 0.0001;
2486
- return base_environment_radiance(rayDirection) *
2487
- select(vec3<f32>(1.0), portalScale, portalHit);
4383
+ fn evaluate_surface_bsdf(hit: HitRecord, viewDirection: vec3<f32>, lightDirection: vec3<f32>) -> vec3<f32> {
4384
+ let normal = safe_normalize(hit.shadingNormal.xyz, vec3<f32>(0.0, 1.0, 0.0));
4385
+ let surfaceColor = clamp(max(hit.color.xyz, config.ambientColor.xyz * 0.35), vec3<f32>(0.0), vec3<f32>(1.0));
4386
+ let roughness = clamp(hit.material.x, 0.0, 1.0);
4387
+ let metallic = clamp(hit.material.y, 0.0, 1.0);
4388
+ let clearcoat = clamp(hit.materialResponse.w, 0.0, 1.0);
4389
+ let clearcoatRoughness = clamp(hit.materialExtension.x, 0.0, 1.0);
4390
+ let occlusion = clamp(hit.occlusion, 0.0, 1.0);
4391
+ let nDotV = saturate(dot(normal, viewDirection));
4392
+ let nDotL = saturate(dot(normal, lightDirection));
4393
+ if (nDotV <= 0.0 || nDotL <= 0.0) {
4394
+ return vec3<f32>(0.0);
4395
+ }
4396
+ let halfVector = safe_normalize(viewDirection + lightDirection, normal);
4397
+ let vDotH = saturate(dot(viewDirection, halfVector));
4398
+ let f0 = surface_specular_f0(hit, surfaceColor);
4399
+ let fresnel = fresnel_schlick(vDotH, f0);
4400
+ let distribution = distribution_ggx(normal, halfVector, roughness);
4401
+ let geometry = geometry_smith(normal, viewDirection, lightDirection, roughness);
4402
+ let specular = (distribution * geometry * fresnel) / max(4.0 * nDotV * nDotL, 0.000001);
4403
+ let diffuseWeight = (1.0 - metallic) * (1.0 - clearcoat * 0.24) * (1.0 - clamp(max_component(fresnel), 0.0, 0.98));
4404
+ let diffuse = surfaceColor * diffuseWeight / 3.14159265359;
4405
+ let clearcoatHalf = safe_normalize(viewDirection + lightDirection, normal);
4406
+ let clearcoatDistribution = distribution_ggx(normal, clearcoatHalf, max(clearcoatRoughness, 0.02));
4407
+ let clearcoatGeometry = geometry_smith(normal, viewDirection, lightDirection, max(clearcoatRoughness, 0.02));
4408
+ let clearcoatFresnel = fresnel_schlick(saturate(dot(viewDirection, clearcoatHalf)), vec3<f32>(0.04));
4409
+ let clearcoatTerm =
4410
+ (clearcoatDistribution * clearcoatGeometry * clearcoatFresnel) /
4411
+ max(4.0 * nDotV * nDotL, 0.000001) *
4412
+ clearcoat;
4413
+ return (diffuse + specular + clearcoatTerm) * mix(0.42, 1.0, occlusion);
4414
+ }
4415
+
4416
+ fn diffuse_pdf(normal: vec3<f32>, lightDirection: vec3<f32>) -> f32 {
4417
+ return saturate(dot(normal, lightDirection)) / 3.14159265359;
4418
+ }
4419
+
4420
+ fn ggx_pdf(normal: vec3<f32>, viewDirection: vec3<f32>, lightDirection: vec3<f32>, roughness: f32) -> f32 {
4421
+ let halfVector = safe_normalize(viewDirection + lightDirection, normal);
4422
+ let nDotH = saturate(dot(normal, halfVector));
4423
+ let vDotH = saturate(dot(viewDirection, halfVector));
4424
+ let distribution = distribution_ggx(normal, halfVector, roughness);
4425
+ return (distribution * nDotH) / max(4.0 * vDotH, 0.000001);
4426
+ }
4427
+
4428
+ fn evaluate_surface_bsdf_pdf(hit: HitRecord, viewDirection: vec3<f32>, lightDirection: vec3<f32>) -> f32 {
4429
+ let normal = safe_normalize(hit.shadingNormal.xyz, vec3<f32>(0.0, 1.0, 0.0));
4430
+ let roughness = clamp(hit.material.x, 0.0, 1.0);
4431
+ let weights = surface_bsdf_sampling_weights(hit);
4432
+ let diffuseTerm = diffuse_pdf(normal, lightDirection);
4433
+ let specTerm = ggx_pdf(normal, viewDirection, lightDirection, max(roughness, 0.02));
4434
+ let clearcoatTerm = ggx_pdf(normal, viewDirection, lightDirection, max(clamp(hit.materialExtension.x, 0.0, 1.0), 0.02));
4435
+ return weights.x * diffuseTerm + weights.y * specTerm + weights.z * clearcoatTerm;
2488
4436
  }
2489
4437
 
2490
4438
  fn gated_environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
@@ -2499,12 +4447,102 @@ fn gated_environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f
2499
4447
  return environment_radiance(origin, direction);
2500
4448
  }
2501
4449
 
4450
+ fn medium_dimensions() -> vec2<u32> {
4451
+ return textureDimensions(mediumTableTexture);
4452
+ }
4453
+
4454
+ fn medium_valid(mediumRefId: u32) -> bool {
4455
+ let dimensions = medium_dimensions();
4456
+ return mediumRefId > 0u && mediumRefId < dimensions.x;
4457
+ }
4458
+
4459
+ fn medium_absorption(mediumRefId: u32) -> vec3<f32> {
4460
+ if (!medium_valid(mediumRefId)) {
4461
+ return vec3<f32>(0.0);
4462
+ }
4463
+ return max(
4464
+ textureLoad(mediumTableTexture, vec2<i32>(i32(mediumRefId), 0), 0).xyz,
4465
+ vec3<f32>(0.0)
4466
+ );
4467
+ }
4468
+
4469
+ fn medium_scattering(mediumRefId: u32) -> vec3<f32> {
4470
+ if (!medium_valid(mediumRefId)) {
4471
+ return vec3<f32>(0.0);
4472
+ }
4473
+ return max(
4474
+ textureLoad(mediumTableTexture, vec2<i32>(i32(mediumRefId), 1), 0).xyz,
4475
+ vec3<f32>(0.0)
4476
+ );
4477
+ }
4478
+
4479
+ fn medium_transmittance(mediumRefId: u32, distance: f32) -> vec3<f32> {
4480
+ if (!medium_valid(mediumRefId) || distance <= 0.000001) {
4481
+ return vec3<f32>(1.0);
4482
+ }
4483
+ let extinction = medium_absorption(mediumRefId) + medium_scattering(mediumRefId);
4484
+ return vec3<f32>(
4485
+ exp(-extinction.x * distance),
4486
+ exp(-extinction.y * distance),
4487
+ exp(-extinction.z * distance)
4488
+ );
4489
+ }
4490
+
4491
+ fn transmitted_medium_ref_id(ray: RayRecord, hit: HitRecord) -> u32 {
4492
+ if (hit.mediumRefId == 0u) {
4493
+ return ray.mediumRefId;
4494
+ }
4495
+ if (hit.frontFace == 1u) {
4496
+ return hit.mediumRefId;
4497
+ }
4498
+ if (ray.mediumRefId == hit.mediumRefId) {
4499
+ return 0u;
4500
+ }
4501
+ return ray.mediumRefId;
4502
+ }
4503
+
2502
4504
  fn surface_path_response(hit: HitRecord) -> vec3<f32> {
2503
4505
  let color = clamp(hit.color.xyz, vec3<f32>(0.0), vec3<f32>(1.0));
2504
4506
  let opacity = clamp(hit.material.z, 0.0, 1.0);
4507
+ let occlusion = clamp(hit.occlusion, 0.0, 1.0);
2505
4508
  let materialEnergy = select(0.68, 0.92, hit.materialKind == 1u || hit.materialKind == 2u);
2506
4509
  let transparentEnergy = select(materialEnergy, 0.9, hit.hitType == 3u);
2507
- return mix(vec3<f32>(1.0), color, max(opacity, 0.18)) * transparentEnergy;
4510
+ return mix(vec3<f32>(1.0), color, max(opacity, 0.18)) * transparentEnergy * mix(0.55, 1.0, occlusion);
4511
+ }
4512
+
4513
+ fn bounded_path_response_luminance(ray: RayRecord, hit: HitRecord) -> f32 {
4514
+ let daylightFloor = max(config.pathResolveSettings.y, 0.0) * 0.08;
4515
+ let hdriFloor = max(config.environmentMapSettings.w, 0.0) * 0.02;
4516
+ let sceneFloor = max(daylightFloor, hdriFloor);
4517
+ if (sceneFloor <= 0.000001) {
4518
+ return 0.0;
4519
+ }
4520
+ let bounceRatio = select(
4521
+ 0.0,
4522
+ f32(ray.bounce) / max(f32(config.maxDepth - 1u), 1.0),
4523
+ config.maxDepth > 1u
4524
+ );
4525
+ let bounceScale = 1.0 - bounceRatio * 0.55;
4526
+ let materialScale = select(1.0, 0.34, hit.materialKind == 1u || hit.materialKind == 2u);
4527
+ let transparentScale = select(materialScale, 0.58, hit.hitType == 3u);
4528
+ let opacityScale = mix(0.55, 1.0, clamp(hit.material.z, 0.0, 1.0));
4529
+ return sceneFloor * bounceScale * transparentScale * opacityScale;
4530
+ }
4531
+
4532
+ fn stabilize_surface_path_response(ray: RayRecord, hit: HitRecord, response: vec3<f32>) -> vec3<f32> {
4533
+ let minimumLuminance = bounded_path_response_luminance(ray, hit);
4534
+ let responseLuminance = radiance_luminance(response);
4535
+ if (minimumLuminance <= 0.000001 || responseLuminance >= minimumLuminance) {
4536
+ return response;
4537
+ }
4538
+ let tintBase = max(response, max(hit.color.xyz * 0.65, config.ambientColor.xyz * 0.35));
4539
+ let tint = tintBase / max(max_component(tintBase), 0.0001);
4540
+ let lifted = select(
4541
+ tint * minimumLuminance,
4542
+ response * (minimumLuminance / max(responseLuminance, 0.0001)),
4543
+ responseLuminance > 0.0001
4544
+ );
4545
+ return clamp(lifted, vec3<f32>(0.0), vec3<f32>(0.98));
2508
4546
  }
2509
4547
 
2510
4548
  fn sunlit_baseline_radiance(normal: vec3<f32>) -> vec3<f32> {
@@ -2523,12 +4561,24 @@ fn sunlit_baseline_radiance(normal: vec3<f32>) -> vec3<f32> {
2523
4561
  return clamp_sample_radiance(sunTint * baseline * skyFacing * directionalWeight * 0.04);
2524
4562
  }
2525
4563
 
2526
- fn terminal_surface_environment_source(hit: HitRecord) -> vec3<f32> {
4564
+ fn terminal_surface_environment_source(ray: RayRecord, hit: HitRecord) -> vec3<f32> {
2527
4565
  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
4566
+ let origin = hit.position.xyz + normal * 0.003;
4567
+ let roughness = clamp(hit.material.x, 0.0, 1.0);
4568
+ let glossiness = surface_glossiness(hit);
4569
+ let normalEnvironment = gated_environment_radiance(origin, normal);
4570
+ let viewDirection = safe_normalize(-ray.direction.xyz, normal);
4571
+ let reflectionDirection = glossy_environment_direction(
4572
+ ray.direction.xyz,
4573
+ normal,
4574
+ roughness,
4575
+ mix(0.88, 0.38, glossiness)
2531
4576
  );
4577
+ let reflectionEnvironment = prefiltered_environment_radiance(reflectionDirection, roughness);
4578
+ let surfaceColor = clamp(max(hit.color.xyz, config.ambientColor.xyz * 0.35), vec3<f32>(0.0), vec3<f32>(1.0));
4579
+ let f0 = surface_specular_f0(hit, surfaceColor);
4580
+ let brdfTerm = sample_brdf_lut(saturate(dot(normal, viewDirection)), roughness);
4581
+ let specularEnvironment = reflectionEnvironment * (f0 * brdfTerm.x + vec3<f32>(brdfTerm.y));
2532
4582
  let sunlitFloor = sunlit_baseline_radiance(normal);
2533
4583
  let ambientFloor = select(
2534
4584
  max(config.ambientColor.xyz, sunlitFloor * 0.82),
@@ -2540,17 +4590,27 @@ fn terminal_surface_environment_source(hit: HitRecord) -> vec3<f32> {
2540
4590
  max(config.environmentMapSettings.w, max(0.12, config.pathResolveSettings.y * 0.42)),
2541
4591
  environment_map_enabled()
2542
4592
  );
2543
- let environmentFloor = max(ambientFloor, max(sunlitFloor, normalEnvironment * environmentInfluence));
4593
+ let glossyEnvironment = max(
4594
+ normalEnvironment,
4595
+ max(reflectionEnvironment * mix(0.24, 0.92, glossiness), specularEnvironment)
4596
+ );
4597
+ let environmentFloor = max(ambientFloor, max(sunlitFloor, glossyEnvironment * environmentInfluence));
2544
4598
  let materialFloor = select(0.7, 1.0, hit.materialKind == 0u || hit.materialKind == 3u);
2545
4599
  return clamp_sample_radiance(environmentFloor * materialFloor);
2546
4600
  }
2547
4601
 
2548
- fn terminal_surface_environment_contribution(ray: RayRecord, hit: HitRecord) -> vec3<f32> {
4602
+ fn terminal_surface_environment_contribution(
4603
+ ray: RayRecord,
4604
+ throughput: vec3<f32>,
4605
+ hit: HitRecord
4606
+ ) -> vec3<f32> {
2549
4607
  let surfaceColor = max(hit.color.xyz, config.ambientColor.xyz);
4608
+ let occlusion = mix(0.75, 1.0, clamp(hit.occlusion, 0.0, 1.0));
2550
4609
  return clamp_sample_radiance(
2551
- ray.throughput.xyz *
4610
+ throughput *
2552
4611
  surfaceColor *
2553
- terminal_surface_environment_source(hit)
4612
+ terminal_surface_environment_source(ray, hit) *
4613
+ occlusion
2554
4614
  );
2555
4615
  }
2556
4616
 
@@ -2583,6 +4643,10 @@ fn direct_environment_portal_irradiance(origin: vec3<f32>, normal: vec3<f32>) ->
2583
4643
  );
2584
4644
  let area = max(portal.position.w, 0.0001);
2585
4645
  let distanceFalloff = clamp(area / max(distanceSquared, area * 0.25), 0.0, 2.5);
4646
+ let traceDistance = max(sqrt(distanceSquared) - 0.01, 0.01);
4647
+ if (scene_visibility_blocked(origin, direction, traceDistance)) {
4648
+ continue;
4649
+ }
2586
4650
  irradiance = irradiance +
2587
4651
  portal.color.rgb *
2588
4652
  portal.normal.w *
@@ -2594,48 +4658,79 @@ fn direct_environment_portal_irradiance(origin: vec3<f32>, normal: vec3<f32>) ->
2594
4658
  return irradiance;
2595
4659
  }
2596
4660
 
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()
4661
+ fn visibility_test_ray(origin: vec3<f32>, direction: vec3<f32>) -> RayRecord {
4662
+ let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
4663
+ return RayRecord(
4664
+ 0u,
4665
+ 0u,
4666
+ 0u,
4667
+ 0u,
4668
+ 0u,
4669
+ 0u,
4670
+ 0u,
4671
+ 0u,
4672
+ vec4<f32>(origin, 1.0),
4673
+ vec4<f32>(rayDirection, 0.0),
4674
+ vec4<f32>(1.0, 1.0, 1.0, 1.0)
2616
4675
  );
2617
- let skyIrradiance = max(ambientIrradiance, normalEnvironment * skyVisibility * environmentIrradianceScale);
4676
+ }
2618
4677
 
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);
4678
+ fn scene_visibility_blocked(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> bool {
4679
+ let testRay = visibility_test_ray(origin, direction);
4680
+ let nearest = max(maxDistance, 0.001);
2627
4681
 
2628
- let diffuseWeight = select(1.0 - metallic * 0.65, 0.22, hit.materialKind == 1u);
2629
- let diffuse = surfaceColor * (skyIrradiance + sunIrradiance + portalIrradiance) * diffuseWeight;
4682
+ for (var objectIndex = 0u; objectIndex < config.sceneObjectCount; objectIndex = objectIndex + 1u) {
4683
+ let object = sceneObjects[objectIndex];
4684
+ var current = no_candidate();
4685
+ if (object.kind == 1u) {
4686
+ current = intersect_sphere(testRay, object);
4687
+ } else if (object.kind == 2u) {
4688
+ current = intersect_box(testRay, object);
4689
+ }
4690
+ if (current.hit == 1u && current.distance < nearest) {
4691
+ return true;
4692
+ }
4693
+ }
2630
4694
 
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);
4695
+ let meshCandidate = intersect_bvh(testRay, nearest);
4696
+ return meshCandidate.hit == 1u && meshCandidate.distance < nearest;
4697
+ }
2636
4698
 
2637
- let bounceWeight = select(1.0, 0.38, ray.bounce > 0u);
2638
- return clamp_sample_radiance(ray.throughput.xyz * (diffuse + specular) * bounceWeight);
4699
+ fn surface_direct_environment_contribution(ray: RayRecord, hit: HitRecord) -> vec3<f32> {
4700
+ let normal = safe_normalize(hit.shadingNormal.xyz, vec3<f32>(0.0, 1.0, 0.0));
4701
+ let origin = hit.position.xyz + normal * 0.003;
4702
+ let viewDirection = safe_normalize(-ray.direction.xyz, normal);
4703
+ let lightSample = sample_environment_importance(vec2<f32>(
4704
+ random01(mix_seed(ray.sourcePixelId, ray.sampleId, ray.bounce, config.frameIndex, 41u)),
4705
+ random01(mix_seed(ray.sourcePixelId, ray.sampleId, ray.bounce, config.frameIndex, 43u))
4706
+ ));
4707
+ if (lightSample.pdf <= 0.000001) {
4708
+ return vec3<f32>(0.0);
4709
+ }
4710
+ let lightDirection = safe_normalize(lightSample.direction, normal);
4711
+ let nDotL = saturate(dot(normal, lightDirection));
4712
+ if (nDotL <= 0.000001) {
4713
+ return vec3<f32>(0.0);
4714
+ }
4715
+ if (scene_visibility_blocked(origin, lightDirection, 1000000.0)) {
4716
+ return vec3<f32>(0.0);
4717
+ }
4718
+ let incidentRadiance = direct_environment_radiance(origin, lightDirection);
4719
+ if (max_component(incidentRadiance) <= 0.000001) {
4720
+ return vec3<f32>(0.0);
4721
+ }
4722
+ let bsdf = evaluate_surface_bsdf(hit, viewDirection, lightDirection);
4723
+ if (max_component(bsdf) <= 0.000001) {
4724
+ return vec3<f32>(0.0);
4725
+ }
4726
+ let bsdfPdf = evaluate_surface_bsdf_pdf(hit, viewDirection, lightDirection);
4727
+ let misWeight = power_heuristic(lightSample.pdf, bsdfPdf);
4728
+ let contribution =
4729
+ ray.throughput.xyz *
4730
+ bsdf *
4731
+ incidentRadiance *
4732
+ (nDotL * misWeight / max(lightSample.pdf, 0.000001));
4733
+ return clamp_sample_radiance(contribution);
2639
4734
  }
2640
4735
 
2641
4736
  fn default_mesh_range() -> MeshRange {
@@ -2654,7 +4749,16 @@ fn default_mesh_range() -> MeshRange {
2654
4749
  0u,
2655
4750
  vec4<f32>(0.72, 0.72, 0.68, 1.0),
2656
4751
  vec4<f32>(0.0),
2657
- vec4<f32>(0.72, 0.0, 1.0, 1.45)
4752
+ vec4<f32>(0.72, 0.0, 1.0, 1.45),
4753
+ vec4<f32>(0.0, 0.0, 0.0, 0.08),
4754
+ vec4<f32>(1.0, 1.0, 1.0, 1.0),
4755
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
4756
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
4757
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
4758
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
4759
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
4760
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
4761
+ vec4<f32>(1.0, 1.0, 1.0, 0.0)
2658
4762
  );
2659
4763
  }
2660
4764
 
@@ -2750,7 +4854,7 @@ fn prepareMeshTrianglesAndLeaves(@builtin(global_invocation_id) globalId: vec3<u
2750
4854
  mesh.flags,
2751
4855
  mesh.materialRefId,
2752
4856
  mesh.mediumRefId,
2753
- 0u,
4857
+ mesh.materialSlot,
2754
4858
  0u,
2755
4859
  vec4<f32>(vertex0.position.xyz, 0.0),
2756
4860
  vec4<f32>(vertex1.position.xyz, 0.0),
@@ -2762,7 +4866,16 @@ fn prepareMeshTrianglesAndLeaves(@builtin(global_invocation_id) globalId: vec3<u
2762
4866
  vec4<f32>(uv2, 0.0, 0.0),
2763
4867
  mesh.color,
2764
4868
  mesh.emission,
2765
- mesh.material
4869
+ mesh.material,
4870
+ mesh.materialResponse,
4871
+ mesh.materialExtension,
4872
+ mesh.specularColor,
4873
+ mesh.baseColorAtlas,
4874
+ mesh.metallicRoughnessAtlas,
4875
+ mesh.normalAtlas,
4876
+ mesh.occlusionAtlas,
4877
+ mesh.emissiveAtlas,
4878
+ mesh.textureSettings
2766
4879
  );
2767
4880
 
2768
4881
  let leafBase = config.triangleCount - 1u;
@@ -2921,7 +5034,8 @@ fn make_miss(ray: RayRecord) -> HitRecord {
2921
5034
  0u,
2922
5035
  0u,
2923
5036
  -1.0,
2924
- vec3<f32>(0.0),
5037
+ 1.0,
5038
+ vec2<f32>(0.0),
2925
5039
  vec4<f32>(ray.origin.xyz + ray.direction.xyz * 1000.0, 1.0),
2926
5040
  vec4<f32>(-ray.direction.xyz, 0.0),
2927
5041
  vec4<f32>(-ray.direction.xyz, 0.0),
@@ -2929,7 +5043,10 @@ fn make_miss(ray: RayRecord) -> HitRecord {
2929
5043
  vec4<f32>(0.0),
2930
5044
  vec4<f32>(radiance, 1.0),
2931
5045
  vec4<f32>(0.0),
2932
- 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)
2933
5050
  );
2934
5051
  }
2935
5052
 
@@ -2964,7 +5081,7 @@ fn intersect_sphere(ray: RayRecord, object: SceneObject) -> Candidate {
2964
5081
  0xffffffffu,
2965
5082
  object.objectId,
2966
5083
  object.objectId,
2967
- 0u
5084
+ object.mediumRefId
2968
5085
  );
2969
5086
  }
2970
5087
 
@@ -3016,7 +5133,7 @@ fn intersect_box(ray: RayRecord, object: SceneObject) -> Candidate {
3016
5133
  0xffffffffu,
3017
5134
  object.objectId,
3018
5135
  object.objectId,
3019
- 0u
5136
+ object.mediumRefId
3020
5137
  );
3021
5138
  }
3022
5139
 
@@ -3224,6 +5341,19 @@ fn denoise_range_space(value: vec3<f32>) -> vec3<f32> {
3224
5341
  return value / (vec3<f32>(1.0) + value);
3225
5342
  }
3226
5343
 
5344
+ fn denoise_sample_count() -> f32 {
5345
+ return clamp(1.0 / max(config.projectionAndSampling.z, 0.000001), 1.0, 256.0);
5346
+ }
5347
+
5348
+ fn denoise_strength() -> f32 {
5349
+ let spp = denoise_sample_count();
5350
+ return clamp(0.44 / sqrt(spp), 0.08, 0.44);
5351
+ }
5352
+
5353
+ fn denoise_kernel_radius() -> i32 {
5354
+ return select(1i, 2i, denoise_sample_count() < 2.5);
5355
+ }
5356
+
3227
5357
  @compute @workgroup_size(64)
3228
5358
  fn generatePrimaryRays(@builtin(global_invocation_id) globalId: vec3<u32>) {
3229
5359
  let index = globalId.x;
@@ -3254,6 +5384,10 @@ fn intersectActiveQueue(@builtin(global_invocation_id) globalId: vec3<u32>) {
3254
5384
  let ray = activeQueue[index];
3255
5385
  var nearest = 1000000.0;
3256
5386
  var hitObject = SceneObject(
5387
+ 0u,
5388
+ 0u,
5389
+ 0u,
5390
+ 0u,
3257
5391
  0u,
3258
5392
  0u,
3259
5393
  0u,
@@ -3262,7 +5396,10 @@ fn intersectActiveQueue(@builtin(global_invocation_id) globalId: vec3<u32>) {
3262
5396
  vec4<f32>(0.0),
3263
5397
  vec4<f32>(0.0),
3264
5398
  vec4<f32>(0.0),
3265
- vec4<f32>(1.0, 0.0, 1.0, 1.0)
5399
+ vec4<f32>(1.0, 0.0, 1.0, 1.0),
5400
+ vec4<f32>(0.0, 0.0, 0.0, 0.08),
5401
+ vec4<f32>(0.08, 1.0, 0.0, 0.0),
5402
+ vec4<f32>(1.0, 1.0, 1.0, 1.0)
3266
5403
  );
3267
5404
  var candidate = no_candidate();
3268
5405
  var hitTriangle = TriangleRecord(
@@ -3284,7 +5421,16 @@ fn intersectActiveQueue(@builtin(global_invocation_id) globalId: vec3<u32>) {
3284
5421
  vec4<f32>(0.0),
3285
5422
  vec4<f32>(0.0),
3286
5423
  vec4<f32>(0.0),
3287
- vec4<f32>(1.0, 0.0, 1.0, 1.0)
5424
+ vec4<f32>(1.0, 0.0, 1.0, 1.0),
5425
+ vec4<f32>(0.0, 0.0, 0.0, 0.08),
5426
+ vec4<f32>(0.08, 1.0, 0.0, 0.0),
5427
+ vec4<f32>(1.0, 1.0, 1.0, 1.0),
5428
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
5429
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
5430
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
5431
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
5432
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
5433
+ vec4<f32>(1.0, 1.0, 1.0, 0.0)
3288
5434
  );
3289
5435
 
3290
5436
  for (var objectIndex = 0u; objectIndex < config.sceneObjectCount; objectIndex = objectIndex + 1u) {
@@ -3317,16 +5463,28 @@ fn intersectActiveQueue(@builtin(global_invocation_id) globalId: vec3<u32>) {
3317
5463
  let position = ray.origin.xyz + ray.direction.xyz * candidate.distance;
3318
5464
  let hitMaterialKind = select(hitObject.materialKind, hitTriangle.materialKind, candidate.triangleIndex != 0xffffffffu);
3319
5465
  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);
5466
+ let meshSurface = sample_surface_material(
5467
+ hitTriangle,
5468
+ candidate.uv,
5469
+ candidate.geometricNormal,
5470
+ candidate.shadingNormal
5471
+ );
5472
+ let hitColor = select(hitObject.color, meshSurface.color, candidate.triangleIndex != 0xffffffffu);
5473
+ let hitEmission = select(hitObject.emission, meshSurface.emission, candidate.triangleIndex != 0xffffffffu);
5474
+ let hitMaterial = select(hitObject.material, meshSurface.material, candidate.triangleIndex != 0xffffffffu);
5475
+ let hitMaterialResponse = select(hitObject.materialResponse, meshSurface.materialResponse, candidate.triangleIndex != 0xffffffffu);
5476
+ let hitMaterialExtension = select(hitObject.materialExtension, meshSurface.materialExtension, candidate.triangleIndex != 0xffffffffu);
5477
+ let hitSpecularColor = select(hitObject.specularColor, meshSurface.specularColor, candidate.triangleIndex != 0xffffffffu);
5478
+ let hitShadingNormal = select(candidate.shadingNormal, meshSurface.shadingNormal, candidate.triangleIndex != 0xffffffffu);
3323
5479
  let hitPrimitiveId = select(candidate.primitiveId, hitTriangle.triangleId, candidate.triangleIndex != 0xffffffffu);
3324
5480
  let hitMaterialRefId = select(candidate.materialRefId, hitTriangle.materialRefId, candidate.triangleIndex != 0xffffffffu);
3325
5481
  let hitMediumRefId = select(candidate.mediumRefId, hitTriangle.mediumRefId, candidate.triangleIndex != 0xffffffffu);
5482
+ let hitMaterialSlot = select(0u, hitTriangle.materialSlot, candidate.triangleIndex != 0xffffffffu);
5483
+ let hitOcclusion = select(1.0, meshSurface.occlusion, candidate.triangleIndex != 0xffffffffu);
3326
5484
  var hitType = 0u;
3327
5485
  if (hitMaterialKind == 4u || emission_power(hitEmission) > 0.0001) {
3328
5486
  hitType = 1u;
3329
- } else if (hitMaterialKind == 3u || hitMaterial.z < 0.999) {
5487
+ } else if (hitMaterialKind == 3u || hitMaterial.z < 0.999 || hitMaterialExtension.z > 0.001) {
3330
5488
  hitType = 3u;
3331
5489
  }
3332
5490
  atomicAdd(&counters.hitCount, 1u);
@@ -3340,19 +5498,23 @@ fn intersectActiveQueue(@builtin(global_invocation_id) globalId: vec3<u32>) {
3340
5498
  hitPrimitiveId,
3341
5499
  hitMaterialRefId,
3342
5500
  hitMediumRefId,
3343
- 0u,
5501
+ hitMaterialSlot,
3344
5502
  0u,
3345
5503
  0u,
3346
5504
  candidate.distance,
3347
- vec3<f32>(0.0),
5505
+ hitOcclusion,
5506
+ vec2<f32>(0.0),
3348
5507
  vec4<f32>(position, 1.0),
3349
5508
  vec4<f32>(candidate.geometricNormal, 0.0),
3350
- vec4<f32>(candidate.shadingNormal, 0.0),
5509
+ vec4<f32>(hitShadingNormal, 0.0),
3351
5510
  vec4<f32>(candidate.barycentric, 0.0),
3352
5511
  vec4<f32>(candidate.uv, 0.0, 0.0),
3353
5512
  hitColor,
3354
5513
  hitEmission,
3355
- hitMaterial
5514
+ hitMaterial,
5515
+ hitMaterialResponse,
5516
+ hitMaterialExtension,
5517
+ hitSpecularColor
3356
5518
  );
3357
5519
  }
3358
5520
 
@@ -3421,60 +5583,106 @@ fn sample_environment_portal_direction(hit: HitRecord, seed: u32, fallback: vec3
3421
5583
  }
3422
5584
 
3423
5585
  fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult {
5586
+ let normal = safe_normalize(hit.shadingNormal.xyz, vec3<f32>(0.0, 1.0, 0.0));
5587
+ let viewDirection = safe_normalize(-ray.direction.xyz, normal);
3424
5588
  let roughness = clamp(hit.material.x, 0.0, 1.0);
3425
- if (hit.materialKind == 1u) {
5589
+ let transmission = clamp(hit.materialExtension.z, 0.0, 1.0);
5590
+ if (hit.materialKind == 1u && roughness <= 0.02) {
3426
5591
  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,
5592
+ vec4<f32>(reflect(ray.direction.xyz, normal), 0.0),
5593
+ 1.0,
5594
+ ray.mediumRefId,
5595
+ RAY_FLAG_DELTA_SAMPLE,
3435
5596
  0u,
3436
- 0u,
3437
- 0u
3438
5597
  );
3439
5598
  }
3440
5599
 
3441
- if (hit.materialKind == 2u || hit.materialKind == 3u) {
5600
+ if (hit.materialKind == 2u || hit.materialKind == 3u || transmission > 0.001) {
3442
5601
  let ior = max(hit.material.w, 1.01);
3443
5602
  let etaRatio = select(ior, 1.0 / ior, hit.frontFace == 1u);
3444
- let cosTheta = min(dot(-ray.direction.xyz, hit.shadingNormal.xyz), 1.0);
5603
+ let cosTheta = min(dot(-ray.direction.xyz, normal), 1.0);
3445
5604
  let sinTheta = sqrt(max(0.0, 1.0 - cosTheta * cosTheta));
3446
5605
  let cannotRefract = etaRatio * sinTheta > 1.0;
3447
5606
  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);
5607
+ let transmissionReflectChance = select(
5608
+ reflectChance,
5609
+ max(reflectChance, 1.0 - transmission),
5610
+ transmission > 0.001
5611
+ );
5612
+ if (cannotRefract || random01(seed + 23u) < transmissionReflectChance) {
5613
+ return ScatterResult(
5614
+ vec4<f32>(reflect(ray.direction.xyz, normal), 0.0),
5615
+ 1.0,
5616
+ ray.mediumRefId,
5617
+ RAY_FLAG_DELTA_SAMPLE,
5618
+ 0u,
5619
+ );
3450
5620
  }
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
- );
5621
+ return ScatterResult(
5622
+ vec4<f32>(refract_direction(ray.direction.xyz, normal, etaRatio), 0.0),
5623
+ 1.0,
5624
+ transmitted_medium_ref_id(ray, hit),
5625
+ RAY_FLAG_DELTA_SAMPLE,
5626
+ 0u,
5627
+ );
5628
+ }
5629
+
5630
+ let guidedEmissiveAvailable = config.emissiveTriangleCount > 0u;
5631
+ let guidedPortalAvailable =
5632
+ config.environmentPortalCount > 0u && config.environmentPortalMode != 0u;
5633
+ let guidedSelector = random01(seed + 17u);
5634
+ if (guidedEmissiveAvailable && guidedSelector < 0.18) {
5635
+ let guidedDirection = sample_emissive_triangle_direction(hit, seed + 101u, normal);
5636
+ if (dot(normal, guidedDirection) > 0.000001) {
5637
+ let guidedPdf = max(evaluate_surface_bsdf_pdf(hit, viewDirection, guidedDirection), 0.000001);
5638
+ return ScatterResult(
5639
+ vec4<f32>(guidedDirection, 0.0),
5640
+ guidedPdf,
5641
+ ray.mediumRefId,
5642
+ RAY_FLAG_GUIDED_EMISSIVE,
5643
+ 0u,
5644
+ );
5645
+ }
5646
+ }
5647
+ if (guidedPortalAvailable && guidedSelector < 0.32) {
5648
+ let guidedDirection = sample_environment_portal_direction(hit, seed + 131u, normal);
5649
+ if (dot(normal, guidedDirection) > 0.000001) {
5650
+ let guidedPdf = max(evaluate_surface_bsdf_pdf(hit, viewDirection, guidedDirection), 0.000001);
5651
+ return ScatterResult(vec4<f32>(guidedDirection, 0.0), guidedPdf, ray.mediumRefId, 0u, 0u);
5652
+ }
5653
+ }
5654
+
5655
+ let weights = surface_bsdf_sampling_weights(hit);
5656
+ let selector = random01(seed + 31u);
5657
+ var lightDirection = normal;
5658
+ if (selector < weights.x) {
5659
+ lightDirection = cosine_sample_hemisphere(
5660
+ vec2<f32>(random01(seed + 37u), random01(seed + 41u)),
5661
+ normal
5662
+ );
5663
+ } else if (selector < weights.x + weights.y) {
5664
+ let halfVector = importance_sample_ggx(
5665
+ vec2<f32>(random01(seed + 47u), random01(seed + 53u)),
5666
+ max(roughness, 0.02),
5667
+ normal
5668
+ );
5669
+ lightDirection = safe_normalize(reflect(-viewDirection, halfVector), normal);
5670
+ } else {
5671
+ let halfVector = importance_sample_ggx(
5672
+ vec2<f32>(random01(seed + 59u), random01(seed + 61u)),
5673
+ max(clamp(hit.materialExtension.x, 0.0, 1.0), 0.02),
5674
+ normal
5675
+ );
5676
+ lightDirection = safe_normalize(reflect(-viewDirection, halfVector), normal);
5677
+ }
5678
+ if (dot(normal, lightDirection) <= 0.000001) {
5679
+ lightDirection = cosine_sample_hemisphere(
5680
+ vec2<f32>(random01(seed + 67u), random01(seed + 71u)),
5681
+ normal
5682
+ );
5683
+ }
5684
+ let pdf = max(evaluate_surface_bsdf_pdf(hit, viewDirection, lightDirection), 0.000001);
5685
+ return ScatterResult(vec4<f32>(lightDirection, 0.0), pdf, ray.mediumRefId, 0u, 0u);
3478
5686
  }
3479
5687
 
3480
5688
  @compute @workgroup_size(64)
@@ -3487,15 +5695,17 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
3487
5695
 
3488
5696
  let ray = activeQueue[index];
3489
5697
  let hit = hits[index];
5698
+ let segmentTransmittance = medium_transmittance(ray.mediumRefId, hit.distance);
5699
+ let arrivingThroughput = ray.throughput.xyz * segmentTransmittance;
3490
5700
  var contribution = vec3<f32>(0.0);
3491
5701
 
3492
5702
  if (hit.hitType == 1u) {
3493
5703
  let guidedLightWeight = select(1.0, 0.24, (ray.flags & RAY_FLAG_GUIDED_EMISSIVE) != 0u);
3494
5704
  let sourceRadiance = max(hit.emission.xyz, hit.color.xyz) * guidedLightWeight;
3495
5705
  if (deferred_path_resolve_enabled()) {
3496
- record_deferred_terminal_source(ray, sourceRadiance);
5706
+ record_deferred_terminal_source(ray, sourceRadiance * segmentTransmittance);
3497
5707
  } else {
3498
- contribution = clamp_sample_radiance(ray.throughput.xyz * sourceRadiance);
5708
+ contribution = clamp_sample_radiance(arrivingThroughput * sourceRadiance);
3499
5709
  accumulation[ray.rayId] =
3500
5710
  accumulation[ray.rayId] + vec4<f32>(contribution * sample_weight(), 1.0);
3501
5711
  }
@@ -3504,10 +5714,17 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
3504
5714
  }
3505
5715
 
3506
5716
  if (hit.hitType == 2u) {
5717
+ var sourceRadiance = hit.color.xyz;
5718
+ if ((ray.flags & RAY_FLAG_DELTA_SAMPLE) == 0u) {
5719
+ let bsdfPdf = max(ray.throughput.w, 0.000001);
5720
+ let lightPdf = environment_direction_pdf(ray.direction.xyz);
5721
+ let misWeight = power_heuristic(bsdfPdf, lightPdf);
5722
+ sourceRadiance = sourceRadiance * misWeight;
5723
+ }
3507
5724
  if (deferred_path_resolve_enabled()) {
3508
- record_deferred_terminal_source(ray, hit.color.xyz);
5725
+ record_deferred_terminal_source(ray, sourceRadiance * segmentTransmittance);
3509
5726
  } else {
3510
- contribution = clamp_sample_radiance(ray.throughput.xyz * max(hit.color.xyz, config.ambientColor.xyz));
5727
+ contribution = clamp_sample_radiance(arrivingThroughput * sourceRadiance);
3511
5728
  accumulation[ray.rayId] =
3512
5729
  accumulation[ray.rayId] + vec4<f32>(contribution * sample_weight(), 1.0);
3513
5730
  }
@@ -3515,24 +5732,47 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
3515
5732
  return;
3516
5733
  }
3517
5734
 
3518
- let response = surface_path_response(hit);
5735
+ let response = stabilize_surface_path_response(
5736
+ ray,
5737
+ hit,
5738
+ surface_path_response(hit) * segmentTransmittance
5739
+ );
3519
5740
  record_deferred_path_response(ray, response);
3520
5741
 
3521
5742
  let shouldEstimateDirectEnvironment =
3522
- !deferred_path_resolve_enabled() &&
3523
5743
  (hit.materialKind == 0u || hit.materialKind == 1u) &&
3524
- hit.material.z >= 0.95;
5744
+ hit.material.z >= 0.95 &&
5745
+ ray.bounce < 2u;
3525
5746
  if (shouldEstimateDirectEnvironment) {
3526
- let directEnvironment = surface_direct_environment_contribution(ray, hit);
5747
+ let directEnvironment = surface_direct_environment_contribution(
5748
+ RayRecord(
5749
+ ray.rayId,
5750
+ ray.parentRayId,
5751
+ ray.sourcePixelId,
5752
+ ray.sampleId,
5753
+ ray.bounce,
5754
+ ray.mediumRefId,
5755
+ ray.flags,
5756
+ 0u,
5757
+ ray.origin,
5758
+ ray.direction,
5759
+ vec4<f32>(arrivingThroughput, ray.throughput.w)
5760
+ ),
5761
+ hit
5762
+ );
3527
5763
  accumulation[ray.rayId] =
3528
5764
  accumulation[ray.rayId] + vec4<f32>(directEnvironment * sample_weight(), 0.0);
3529
5765
  }
3530
5766
 
3531
5767
  if (ray.bounce + 1u >= config.maxDepth) {
3532
5768
  if (deferred_path_resolve_enabled()) {
3533
- record_deferred_terminal_source(ray, terminal_surface_environment_source(hit));
5769
+ record_deferred_terminal_source(ray, terminal_surface_environment_source(ray, hit));
3534
5770
  } else {
3535
- let terminalEnvironment = terminal_surface_environment_contribution(ray, hit);
5771
+ let terminalEnvironment = terminal_surface_environment_contribution(
5772
+ ray,
5773
+ arrivingThroughput,
5774
+ hit
5775
+ );
3536
5776
  accumulation[ray.rayId] =
3537
5777
  accumulation[ray.rayId] + vec4<f32>(terminalEnvironment * sample_weight(), 1.0);
3538
5778
  }
@@ -3545,9 +5785,13 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
3545
5785
  let nextIndex = atomicAdd(&counters.nextCount, 1u);
3546
5786
  if (nextIndex >= config.tilePixelCount) {
3547
5787
  if (deferred_path_resolve_enabled()) {
3548
- record_deferred_terminal_source(ray, terminal_surface_environment_source(hit));
5788
+ record_deferred_terminal_source(ray, terminal_surface_environment_source(ray, hit));
3549
5789
  } else {
3550
- let overflowEnvironment = terminal_surface_environment_contribution(ray, hit);
5790
+ let overflowEnvironment = terminal_surface_environment_contribution(
5791
+ ray,
5792
+ arrivingThroughput,
5793
+ hit
5794
+ );
3551
5795
  accumulation[ray.rayId] =
3552
5796
  accumulation[ray.rayId] + vec4<f32>(overflowEnvironment * sample_weight(), 1.0);
3553
5797
  }
@@ -3561,12 +5805,12 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
3561
5805
  ray.sourcePixelId,
3562
5806
  ray.sampleId,
3563
5807
  ray.bounce + 1u,
3564
- ray.mediumRefId,
5808
+ scatter.mediumRefId,
3565
5809
  scatter.flags,
3566
5810
  0u,
3567
5811
  vec4<f32>(offset_origin(hit.position.xyz, hit.shadingNormal.xyz), 1.0),
3568
5812
  scatter.direction,
3569
- vec4<f32>(throughput, ray.throughput.w)
5813
+ vec4<f32>(throughput, scatter.pdf)
3570
5814
  );
3571
5815
  }
3572
5816
 
@@ -3635,8 +5879,11 @@ fn denoiseLinearRadiance(@builtin(global_invocation_id) globalId: vec3<u32>) {
3635
5879
 
3636
5880
  let pixel = vec2<i32>(i32(x), i32(y));
3637
5881
  let center = textureLoad(denoiseInputRadiance, pixel, 0).xyz;
3638
- var sum = center * 1.4;
3639
- var totalWeight = 1.4;
5882
+ let strength = denoise_strength();
5883
+ let kernelRadius = denoise_kernel_radius();
5884
+ let centerWeight = 1.7 - strength * 0.35;
5885
+ var sum = center * centerWeight;
5886
+ var totalWeight = centerWeight;
3640
5887
  let centerRange = denoise_range_space(center);
3641
5888
 
3642
5889
  for (var oy = -2i; oy <= 2i; oy = oy + 1i) {
@@ -3644,13 +5891,16 @@ fn denoiseLinearRadiance(@builtin(global_invocation_id) globalId: vec3<u32>) {
3644
5891
  if (ox == 0i && oy == 0i) {
3645
5892
  continue;
3646
5893
  }
5894
+ if (abs(ox) > kernelRadius || abs(oy) > kernelRadius) {
5895
+ continue;
5896
+ }
3647
5897
  let sx = clamp(i32(x) + ox, 0i, i32(config.canvasWidth) - 1i);
3648
5898
  let sy = clamp(i32(y) + oy, 0i, i32(config.canvasHeight) - 1i);
3649
5899
  let sampleColor = textureLoad(denoiseInputRadiance, vec2<i32>(sx, sy), 0).xyz;
3650
5900
  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);
5901
+ let rangeWeight = 1.0 / (1.0 + colorDistance * (11.0 + strength * 6.0));
5902
+ let distanceWeight = 1.0 / (1.0 + f32(ox * ox + oy * oy) * (0.62 + strength * 0.24));
5903
+ let diagonalWeight = select(1.0, 0.92, abs(ox) + abs(oy) > 1i);
3654
5904
  let weight = rangeWeight * diagonalWeight * distanceWeight;
3655
5905
  sum = sum + sampleColor * weight;
3656
5906
  totalWeight = totalWeight + weight;
@@ -3658,8 +5908,9 @@ fn denoiseLinearRadiance(@builtin(global_invocation_id) globalId: vec3<u32>) {
3658
5908
  }
3659
5909
 
3660
5910
  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));
5911
+ let outlier = saturate(length(denoise_range_space(center) - denoise_range_space(filtered)) * 2.1);
5912
+ let blend = min(0.3, strength * (0.62 + outlier * 0.12));
5913
+ let color = min(mix(center, filtered, blend), vec3<f32>(16.0));
3663
5914
  textureStore(denoisedRadianceImage, pixel, vec4<f32>(color, 1.0));
3664
5915
  }
3665
5916
 
@@ -3673,8 +5924,10 @@ fn resolveDenoisedOutputImage(@builtin(global_invocation_id) globalId: vec3<u32>
3673
5924
 
3674
5925
  let pixel = vec2<i32>(i32(x), i32(y));
3675
5926
  let center = textureLoad(finalDenoiseInputRadiance, pixel, 0).xyz;
3676
- var sum = center * 1.25;
3677
- var totalWeight = 1.25;
5927
+ let strength = denoise_strength();
5928
+ let centerWeight = 1.35 - strength * 0.25;
5929
+ var sum = center * centerWeight;
5930
+ var totalWeight = centerWeight;
3678
5931
  let centerRange = denoise_range_space(center);
3679
5932
 
3680
5933
  for (var oy = -1i; oy <= 1i; oy = oy + 1i) {
@@ -3686,8 +5939,8 @@ fn resolveDenoisedOutputImage(@builtin(global_invocation_id) globalId: vec3<u32>
3686
5939
  let sy = clamp(i32(y) + oy, 0i, i32(config.canvasHeight) - 1i);
3687
5940
  let sampleColor = textureLoad(finalDenoiseInputRadiance, vec2<i32>(sx, sy), 0).xyz;
3688
5941
  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);
5942
+ let rangeWeight = 1.0 / (1.0 + colorDistance * (12.0 + strength * 8.0));
5943
+ let distanceWeight = 1.0 / (1.0 + f32(ox * ox + oy * oy) * (0.82 + strength * 0.28));
3691
5944
  let weight = rangeWeight * distanceWeight;
3692
5945
  sum = sum + sampleColor * weight;
3693
5946
  totalWeight = totalWeight + weight;
@@ -3695,8 +5948,9 @@ fn resolveDenoisedOutputImage(@builtin(global_invocation_id) globalId: vec3<u32>
3695
5948
  }
3696
5949
 
3697
5950
  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));
5951
+ let outlier = saturate(length(denoise_range_space(center) - denoise_range_space(filtered)) * 2.2);
5952
+ let blend = min(0.18, strength * (0.42 + outlier * 0.08));
5953
+ let radiance = min(mix(center, filtered, blend), vec3<f32>(16.0));
3700
5954
  textureStore(denoisedOutputImage, pixel, vec4<f32>(tone_map_radiance(radiance), 1.0));
3701
5955
  }
3702
5956
  `;
@@ -3801,96 +6055,47 @@ function createGpuAdapterParallelismDiagnostics(adapter, device) {
3801
6055
  });
3802
6056
  }
3803
6057
 
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
6058
  function createEnvironmentMapSnapshot(environmentMap) {
3883
6059
  return Object.freeze({
3884
6060
  enabled: environmentMap.enabled,
3885
6061
  width: environmentMap.width,
3886
6062
  height: environmentMap.height,
6063
+ mipLevelCount: environmentMap.mipLevelCount ?? 1,
3887
6064
  projection: environmentMap.projection,
3888
6065
  intensity: environmentMap.intensity,
3889
6066
  rotationRadians: environmentMap.rotationRadians,
3890
6067
  ambientStrength: environmentMap.ambientStrength,
6068
+ hasImportanceData: environmentMap.hasImportanceData === true,
3891
6069
  });
3892
6070
  }
3893
6071
 
6072
+ function nowMs() {
6073
+ if (typeof performance !== "undefined" && typeof performance.now === "function") {
6074
+ return performance.now();
6075
+ }
6076
+ return Date.now();
6077
+ }
6078
+
6079
+ function estimateSubmittedGpuWorkTimeoutMs(config, tileCount, overrideTimeoutMs = null) {
6080
+ if (Number.isFinite(overrideTimeoutMs)) {
6081
+ return Math.max(1, Math.trunc(Number(overrideTimeoutMs)));
6082
+ }
6083
+ const samplesPerPixel = Math.max(
6084
+ 1,
6085
+ Number(config?.renderedSamplesPerPixel ?? config?.samplesPerPixel ?? 1)
6086
+ );
6087
+ const maxDepth = Math.max(1, Number(config?.maxDepth ?? 1));
6088
+ const deferredResolvePasses = config?.deferredPathResolve ? 1 : 0;
6089
+ const denoisePasses = config?.denoise ? (samplesPerPixel < 4 ? 2 : 1) : 0;
6090
+ const tiles = Math.max(1, Number(tileCount ?? 1));
6091
+ const estimatedPasses =
6092
+ tiles * (samplesPerPixel * (maxDepth + 1 + deferredResolvePasses) + denoisePasses + 1);
6093
+ return Math.min(
6094
+ GPU_MAX_SUBMITTED_WORK_TIMEOUT_MS,
6095
+ GPU_SUBMITTED_WORK_TIMEOUT_MS + estimatedPasses * 5
6096
+ );
6097
+ }
6098
+
3894
6099
  export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3895
6100
  assertAnalyticDisplayQualityPolicy(options);
3896
6101
  const constants = getGpuUsageConstants();
@@ -4116,6 +6321,65 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4116
6321
  config.environmentMap,
4117
6322
  config.environmentColor
4118
6323
  );
6324
+ const environmentSamplingResource = createEnvironmentSamplingTextureResource(
6325
+ device,
6326
+ constants,
6327
+ config.environmentMap,
6328
+ config.environmentColor
6329
+ );
6330
+ let mediumTextureResource = createMediumTextureResource(
6331
+ device,
6332
+ constants,
6333
+ config.mediums
6334
+ );
6335
+ config = Object.freeze({
6336
+ ...config,
6337
+ environmentMap: Object.freeze({
6338
+ ...config.environmentMap,
6339
+ width: environmentMapResource.width,
6340
+ height: environmentMapResource.height,
6341
+ mipLevelCount: environmentMapResource.mipLevelCount,
6342
+ hasImportanceData: environmentSamplingResource.hasImportanceData,
6343
+ }),
6344
+ });
6345
+ const brdfLutResource = createBrdfLutResource(device, constants);
6346
+ const baseColorAtlasResource = createAtlasTextureResource(
6347
+ device,
6348
+ constants,
6349
+ config.gpuMaterialSource.baseColorAtlas,
6350
+ "plasius.wavefront.materialAtlas.baseColor"
6351
+ );
6352
+ const metallicRoughnessAtlasResource = createAtlasTextureResource(
6353
+ device,
6354
+ constants,
6355
+ config.gpuMaterialSource.metallicRoughnessAtlas,
6356
+ "plasius.wavefront.materialAtlas.metallicRoughness"
6357
+ );
6358
+ const normalAtlasResource = createAtlasTextureResource(
6359
+ device,
6360
+ constants,
6361
+ config.gpuMaterialSource.normalAtlas,
6362
+ "plasius.wavefront.materialAtlas.normal"
6363
+ );
6364
+ const occlusionAtlasResource = createAtlasTextureResource(
6365
+ device,
6366
+ constants,
6367
+ config.gpuMaterialSource.occlusionAtlas,
6368
+ "plasius.wavefront.materialAtlas.occlusion"
6369
+ );
6370
+ const emissiveAtlasResource = createAtlasTextureResource(
6371
+ device,
6372
+ constants,
6373
+ config.gpuMaterialSource.emissiveAtlas,
6374
+ "plasius.wavefront.materialAtlas.emissive"
6375
+ );
6376
+ const materialAtlasSampler = device.createSampler({
6377
+ label: "plasius.wavefront.materialAtlasSampler",
6378
+ addressModeU: "clamp-to-edge",
6379
+ addressModeV: "clamp-to-edge",
6380
+ magFilter: "linear",
6381
+ minFilter: "linear",
6382
+ });
4119
6383
 
4120
6384
  const traceBindGroupLayout = device.createBindGroupLayout({
4121
6385
  label: "plasius.wavefront.traceBindGroupLayout",
@@ -4147,6 +6411,16 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4147
6411
  { binding: 20, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
4148
6412
  { binding: 21, visibility: constants.shader.COMPUTE, sampler: { type: "filtering" } },
4149
6413
  { binding: 22, visibility: constants.shader.COMPUTE, buffer: { type: "storage" } },
6414
+ { binding: 23, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
6415
+ { binding: 24, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
6416
+ { binding: 25, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
6417
+ { binding: 26, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
6418
+ { binding: 27, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
6419
+ { binding: 28, visibility: constants.shader.COMPUTE, sampler: { type: "filtering" } },
6420
+ { binding: 29, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
6421
+ { binding: 30, visibility: constants.shader.COMPUTE, sampler: { type: "filtering" } },
6422
+ { binding: 31, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
6423
+ { binding: 32, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
4150
6424
  ],
4151
6425
  });
4152
6426
  const accelerationBindGroupLayout = device.createBindGroupLayout({
@@ -4225,6 +6499,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4225
6499
  label: "plasius.wavefront.computeShader",
4226
6500
  code: WAVEFRONT_COMPUTE_WGSL,
4227
6501
  });
6502
+ await assertShaderModuleCompiles(computeShader, "plasius.wavefront.computeShader");
4228
6503
 
4229
6504
  const pipelines = {
4230
6505
  prepareMeshTrianglesAndLeaves: await createComputePipeline(
@@ -4326,14 +6601,28 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4326
6601
  { binding: 20, resource: environmentMapResource.view },
4327
6602
  { binding: 21, resource: environmentMapResource.sampler },
4328
6603
  { binding: 22, resource: { buffer: pathVertexBuffer } },
6604
+ { binding: 23, resource: baseColorAtlasResource.view },
6605
+ { binding: 24, resource: metallicRoughnessAtlasResource.view },
6606
+ { binding: 25, resource: normalAtlasResource.view },
6607
+ { binding: 26, resource: occlusionAtlasResource.view },
6608
+ { binding: 27, resource: emissiveAtlasResource.view },
6609
+ { binding: 28, resource: materialAtlasSampler },
6610
+ { binding: 29, resource: brdfLutResource.view },
6611
+ { binding: 30, resource: brdfLutResource.sampler },
6612
+ { binding: 31, resource: environmentSamplingResource.view },
6613
+ { binding: 32, resource: mediumTextureResource.view },
4329
6614
  ],
4330
6615
  });
4331
6616
  }
4332
6617
 
4333
- const bindGroups = [
4334
- createTraceBindGroup(activeQueue, nextQueue, "plasius.wavefront.bind.activeNext"),
4335
- createTraceBindGroup(nextQueue, activeQueue, "plasius.wavefront.bind.nextActive"),
4336
- ];
6618
+ function createTraceBindGroups() {
6619
+ return [
6620
+ createTraceBindGroup(activeQueue, nextQueue, "plasius.wavefront.bind.activeNext"),
6621
+ createTraceBindGroup(nextQueue, activeQueue, "plasius.wavefront.bind.nextActive"),
6622
+ ];
6623
+ }
6624
+
6625
+ let bindGroups = createTraceBindGroups();
4337
6626
  const bvhBuildBindGroup = device.createBindGroup({
4338
6627
  label: "plasius.wavefront.bind.bvhBuild",
4339
6628
  layout: accelerationBindGroupLayout,
@@ -4381,6 +6670,11 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4381
6670
  outputView,
4382
6671
  "plasius.wavefront.bind.denoise.scratchToOutput"
4383
6672
  );
6673
+ const denoiseDirectResolveBindGroup = createDenoiseResolveBindGroup(
6674
+ radianceView,
6675
+ outputView,
6676
+ "plasius.wavefront.bind.denoise.radianceToOutput"
6677
+ );
4384
6678
 
4385
6679
  const presentBindGroupLayout = device.createBindGroupLayout({
4386
6680
  label: "plasius.wavefront.presentBindGroupLayout",
@@ -4420,24 +6714,138 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4420
6714
  let accelerationBuilt = !config.gpuAccelerationBuildRequired;
4421
6715
  let accelerationBuildCount = 0;
4422
6716
  let activeCameraOptions = options.camera ?? DEFAULT_CAMERA;
6717
+ let lastCompletedFrameTimeMs = null;
6718
+ let lastCompletedSamplesPerPixel = Math.max(1, config.samplesPerPixel);
4423
6719
  let lastGpuParallelism = createGpuParallelismDiagnostics(
4424
6720
  gpuAdapterParallelism,
4425
6721
  createGpuParallelismCounters()
4426
6722
  );
4427
6723
 
6724
+ function resolveRenderedSamplesPerPixel(renderOptions = {}, awaitGPUCompletion = true) {
6725
+ const targetSamplesPerPixel = clamp(
6726
+ readPositiveInteger(
6727
+ "samplesPerPixel",
6728
+ renderOptions.samplesPerPixel,
6729
+ config.samplesPerPixel
6730
+ ),
6731
+ 1,
6732
+ config.samplesPerPixel
6733
+ );
6734
+ const frameTimeBudgetMs = Number.isFinite(renderOptions.frameTimeBudgetMs)
6735
+ ? Math.max(0, Number(renderOptions.frameTimeBudgetMs))
6736
+ : null;
6737
+ const minimumSamplesPerPixel = clamp(
6738
+ readPositiveInteger(
6739
+ "minimumSamplesPerPixel",
6740
+ renderOptions.minimumSamplesPerPixel,
6741
+ frameTimeBudgetMs !== null && targetSamplesPerPixel > 1 ? 1 : targetSamplesPerPixel
6742
+ ),
6743
+ 1,
6744
+ targetSamplesPerPixel
6745
+ );
6746
+ if (frameTimeBudgetMs === null || !awaitGPUCompletion || targetSamplesPerPixel <= minimumSamplesPerPixel) {
6747
+ return Object.freeze({
6748
+ renderedSamplesPerPixel: targetSamplesPerPixel,
6749
+ targetSamplesPerPixel,
6750
+ minimumSamplesPerPixel,
6751
+ frameTimeBudgetMs,
6752
+ budgetConstrained: false,
6753
+ });
6754
+ }
6755
+ const estimatedSampleTimeMs =
6756
+ Number.isFinite(lastCompletedFrameTimeMs) && lastCompletedFrameTimeMs > 0
6757
+ ? lastCompletedFrameTimeMs / Math.max(1, lastCompletedSamplesPerPixel)
6758
+ : null;
6759
+ if (!Number.isFinite(estimatedSampleTimeMs) || estimatedSampleTimeMs <= 0) {
6760
+ return Object.freeze({
6761
+ renderedSamplesPerPixel: minimumSamplesPerPixel,
6762
+ targetSamplesPerPixel,
6763
+ minimumSamplesPerPixel,
6764
+ frameTimeBudgetMs,
6765
+ budgetConstrained: minimumSamplesPerPixel < targetSamplesPerPixel,
6766
+ });
6767
+ }
6768
+ const budgetLimitedSamples = clamp(
6769
+ Math.floor(frameTimeBudgetMs / estimatedSampleTimeMs),
6770
+ minimumSamplesPerPixel,
6771
+ targetSamplesPerPixel
6772
+ );
6773
+ return Object.freeze({
6774
+ renderedSamplesPerPixel: budgetLimitedSamples,
6775
+ targetSamplesPerPixel,
6776
+ minimumSamplesPerPixel,
6777
+ frameTimeBudgetMs,
6778
+ budgetConstrained: budgetLimitedSamples < targetSamplesPerPixel,
6779
+ });
6780
+ }
6781
+
6782
+ function createFrameStats({
6783
+ frameIndex,
6784
+ accelerationBuildSubmitted,
6785
+ frameSubmissionCount,
6786
+ parallelismCounters,
6787
+ renderedSamplesPerPixel,
6788
+ targetSamplesPerPixel,
6789
+ frameTimeBudgetMs,
6790
+ budgetConstrained,
6791
+ }) {
6792
+ lastGpuParallelism = createGpuParallelismDiagnostics(gpuAdapterParallelism, parallelismCounters);
6793
+ const commandSubmissions = frameSubmissionCount + (accelerationBuildSubmitted ? 1 : 0);
6794
+ return Object.freeze({
6795
+ frame,
6796
+ frameIndex,
6797
+ width: config.width,
6798
+ height: config.height,
6799
+ maxDepth: config.maxDepth,
6800
+ tiles: tiles.length,
6801
+ tileSize: config.tileSize,
6802
+ samplesPerPixel: targetSamplesPerPixel,
6803
+ renderedSamplesPerPixel,
6804
+ frameTimeBudgetMs,
6805
+ budgetConstrained,
6806
+ maxFramePassesPerSubmission: config.maxFramePassesPerSubmission,
6807
+ screenRays: config.width * config.height,
6808
+ primaryRays: config.width * config.height * renderedSamplesPerPixel,
6809
+ sceneObjectCount: config.sceneObjectCount,
6810
+ triangleCount: config.triangleCount,
6811
+ emissiveTriangleCount: config.emissiveTriangleCount,
6812
+ environmentPortalCount: config.environmentPortalCount,
6813
+ environmentPortalMode: config.environmentPortalMode,
6814
+ mediumCount: config.mediumCount,
6815
+ environmentMap: createEnvironmentMapSnapshot(config.environmentMap),
6816
+ deferredPathResolve: config.deferredPathResolve,
6817
+ bvhNodeCount: config.bvhNodeCount,
6818
+ displayQuality: config.displayQuality,
6819
+ accelerationBuildMode: config.accelerationBuildMode,
6820
+ gpuAccelerationBuildRequired: config.gpuAccelerationBuildRequired,
6821
+ accelerationBuildSubmitted,
6822
+ accelerationBuilt,
6823
+ accelerationBuildCount,
6824
+ commandSubmissions,
6825
+ frameConfigSlots: frameConfigSlotCount,
6826
+ gpuParallelism: lastGpuParallelism,
6827
+ memory: config.memory,
6828
+ });
6829
+ }
6830
+
6831
+ function writeFrameConfigSlot(slot, tile, frameIndex, buildRange = {}) {
6832
+ if (slot >= frameConfigSlotCount) {
6833
+ throw new Error("Wavefront frame config slot capacity exceeded.");
6834
+ }
6835
+ const offset = slot * configBufferStride;
6836
+ device.queue.writeBuffer(
6837
+ configBuffer,
6838
+ offset,
6839
+ createConfigPayload(config, tile, frameIndex, buildRange)
6840
+ );
6841
+ return offset;
6842
+ }
6843
+
4428
6844
  function createFrameConfigWriter(frameIndex) {
4429
6845
  let slot = 0;
4430
6846
  return (tile, buildRange = {}) => {
4431
- if (slot >= frameConfigSlotCount) {
4432
- throw new Error("Wavefront frame config slot capacity exceeded.");
4433
- }
4434
- const offset = slot * configBufferStride;
6847
+ const offset = writeFrameConfigSlot(slot, tile, frameIndex, buildRange);
4435
6848
  slot += 1;
4436
- device.queue.writeBuffer(
4437
- configBuffer,
4438
- offset,
4439
- createConfigPayload(config, tile, frameIndex, buildRange)
4440
- );
4441
6849
  return offset;
4442
6850
  };
4443
6851
  }
@@ -4483,7 +6891,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4483
6891
  passEncoder.setPipeline(pipelines.prepareMeshTrianglesAndLeaves);
4484
6892
  const prepareWorkgroups = Math.ceil(config.bvhLeafSortCapacity / WORKGROUP_SIZE);
4485
6893
  passEncoder.dispatchWorkgroups(prepareWorkgroups);
4486
- recordDirectDispatch(parallelism, [prepareWorkgroups]);
6894
+ recordDirectDispatch(parallelism, [prepareWorkgroups], WORKGROUP_SIZE);
4487
6895
  passEncoder.setPipeline(pipelines.sortBvhLeafRefs);
4488
6896
  for (let stageIndex = 0; stageIndex < config.bvhSortStages.length; stageIndex += 1) {
4489
6897
  passEncoder.setBindGroup(0, bvhBuildBindGroup, [
@@ -4491,13 +6899,13 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4491
6899
  ]);
4492
6900
  const sortWorkgroups = Math.ceil(config.bvhLeafSortCapacity / WORKGROUP_SIZE);
4493
6901
  passEncoder.dispatchWorkgroups(sortWorkgroups);
4494
- recordDirectDispatch(parallelism, [sortWorkgroups]);
6902
+ recordDirectDispatch(parallelism, [sortWorkgroups], WORKGROUP_SIZE);
4495
6903
  }
4496
6904
  passEncoder.setBindGroup(0, bvhBuildBindGroup, [0]);
4497
6905
  passEncoder.setPipeline(pipelines.writeSortedBvhLeaves);
4498
6906
  const leafWriteWorkgroups = Math.ceil(config.triangleCount / WORKGROUP_SIZE);
4499
6907
  passEncoder.dispatchWorkgroups(leafWriteWorkgroups);
4500
- recordDirectDispatch(parallelism, [leafWriteWorkgroups]);
6908
+ recordDirectDispatch(parallelism, [leafWriteWorkgroups], WORKGROUP_SIZE);
4501
6909
  passEncoder.setPipeline(pipelines.buildBvhInternalLevel);
4502
6910
  for (let levelIndex = 0; levelIndex < config.bvhBuildLevels.length; levelIndex += 1) {
4503
6911
  const buildLevel = config.bvhBuildLevels[levelIndex];
@@ -4506,7 +6914,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4506
6914
  ]);
4507
6915
  const levelWorkgroups = Math.ceil(buildLevel.count / WORKGROUP_SIZE);
4508
6916
  passEncoder.dispatchWorkgroups(levelWorkgroups);
4509
- recordDirectDispatch(parallelism, [levelWorkgroups]);
6917
+ recordDirectDispatch(parallelism, [levelWorkgroups], WORKGROUP_SIZE);
4510
6918
  }
4511
6919
  passEncoder.end();
4512
6920
  device.queue.submit([encoder.finish()]);
@@ -4524,7 +6932,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4524
6932
  generatePass.setBindGroup(0, bindGroups[0], [configOffset]);
4525
6933
  generatePass.setPipeline(pipelines.generatePrimaryRays);
4526
6934
  generatePass.dispatchWorkgroups(tileWorkgroups);
4527
- recordDirectDispatch(parallelism, [tileWorkgroups]);
6935
+ recordDirectDispatch(parallelism, [tileWorkgroups], WORKGROUP_SIZE);
4528
6936
  generatePass.end();
4529
6937
 
4530
6938
  for (let bounceIndex = 0; bounceIndex < config.maxDepth; bounceIndex += 1) {
@@ -4541,10 +6949,10 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4541
6949
  passEncoder.setBindGroup(0, bindGroups[bounceIndex % 2], [configOffset]);
4542
6950
  passEncoder.setPipeline(pipelines.intersectActiveQueue);
4543
6951
  passEncoder.dispatchWorkgroupsIndirect(activeDispatchBuffer, 0);
4544
- recordIndirectDispatch(parallelism, tileWorkgroups);
6952
+ recordIndirectDispatch(parallelism, tileWorkgroups, WORKGROUP_SIZE);
4545
6953
  passEncoder.setPipeline(pipelines.resolveSurfaceRecords);
4546
6954
  passEncoder.dispatchWorkgroupsIndirect(activeDispatchBuffer, 0);
4547
- recordIndirectDispatch(parallelism, tileWorkgroups);
6955
+ recordIndirectDispatch(parallelism, tileWorkgroups, WORKGROUP_SIZE);
4548
6956
  passEncoder.setPipeline(pipelines.compactAndSwapQueues);
4549
6957
  passEncoder.dispatchWorkgroups(1);
4550
6958
  recordDirectDispatch(parallelism, [1], 1);
@@ -4561,32 +6969,47 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4561
6969
  passEncoder.setBindGroup(0, bindGroups[0], [configOffset]);
4562
6970
  passEncoder.setPipeline(pipelines.accumulateTerminalRadiance);
4563
6971
  passEncoder.dispatchWorkgroups(tileWorkgroups);
4564
- recordDirectDispatch(parallelism, [tileWorkgroups]);
6972
+ recordDirectDispatch(parallelism, [tileWorkgroups], WORKGROUP_SIZE);
4565
6973
  passEncoder.end();
4566
6974
  }
4567
6975
 
4568
- function encodeDenoise(encoder, configOffset, parallelism) {
6976
+ function encodeDenoise(encoder, configOffset, parallelism, renderedSamplesPerPixel = config.samplesPerPixel) {
4569
6977
  if (!config.denoise) {
4570
6978
  return;
4571
6979
  }
4572
6980
  const denoiseWorkgroupsX = Math.ceil(config.width / 8);
4573
6981
  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();
6982
+ const useTwoPassDenoise = renderedSamplesPerPixel < 4;
6983
+ if (useTwoPassDenoise) {
6984
+ const radiancePass = encoder.beginComputePass({
6985
+ label: "plasius.wavefront.denoiseRadiancePass",
6986
+ });
6987
+ radiancePass.setBindGroup(0, denoiseRadianceBindGroup, [configOffset]);
6988
+ radiancePass.setPipeline(pipelines.denoiseLinearRadiance);
6989
+ radiancePass.dispatchWorkgroups(denoiseWorkgroupsX, denoiseWorkgroupsY);
6990
+ recordDirectDispatch(
6991
+ parallelism,
6992
+ [denoiseWorkgroupsX, denoiseWorkgroupsY],
6993
+ WORKGROUP_SIZE
6994
+ );
6995
+ radiancePass.end();
6996
+ }
4582
6997
 
4583
6998
  const resolvePass = encoder.beginComputePass({
4584
6999
  label: "plasius.wavefront.denoiseResolvePass",
4585
7000
  });
4586
- resolvePass.setBindGroup(0, denoiseResolveBindGroup, [configOffset]);
7001
+ resolvePass.setBindGroup(
7002
+ 0,
7003
+ useTwoPassDenoise ? denoiseResolveBindGroup : denoiseDirectResolveBindGroup,
7004
+ [configOffset]
7005
+ );
4587
7006
  resolvePass.setPipeline(pipelines.resolveDenoisedOutputImage);
4588
7007
  resolvePass.dispatchWorkgroups(denoiseWorkgroupsX, denoiseWorkgroupsY);
4589
- recordDirectDispatch(parallelism, [denoiseWorkgroupsX, denoiseWorkgroupsY]);
7008
+ recordDirectDispatch(
7009
+ parallelism,
7010
+ [denoiseWorkgroupsX, denoiseWorkgroupsY],
7011
+ WORKGROUP_SIZE
7012
+ );
4590
7013
  resolvePass.end();
4591
7014
  }
4592
7015
 
@@ -4609,105 +7032,233 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4609
7032
  passEncoder.end();
4610
7033
  }
4611
7034
 
4612
- function dispatchFrame(frameIndex, parallelism) {
7035
+ function dispatchFrame(frameIndex, parallelism, renderedSamplesPerPixel = config.samplesPerPixel) {
4613
7036
  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}`,
7037
+ const batch = createGpuSubmissionBatcher({
7038
+ device,
7039
+ frameIndex,
7040
+ maxFramePassesPerSubmission: config.maxFramePassesPerSubmission,
4618
7041
  });
4619
7042
 
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
7043
  for (const tile of tiles) {
4644
- for (let sampleIndex = 0; sampleIndex < config.samplesPerPixel; sampleIndex += 1) {
7044
+ for (let sampleIndex = 0; sampleIndex < renderedSamplesPerPixel; sampleIndex += 1) {
4645
7045
  const configOffset = writeFrameConfig(tile, {
4646
7046
  sampleIndex,
4647
- sampleWeight: 1 / config.samplesPerPixel,
7047
+ sampleWeight: 1 / renderedSamplesPerPixel,
4648
7048
  });
4649
- encodeTileSample(reserveEncoder(), tile, configOffset, parallelism);
7049
+ encodeTileSample(
7050
+ batch.reserve(config.maxDepth + 1),
7051
+ tile,
7052
+ configOffset,
7053
+ parallelism
7054
+ );
4650
7055
  if (config.deferredPathResolve) {
4651
- encodeTileOutput(reserveEncoder(), tile, configOffset, parallelism);
7056
+ encodeTileOutput(batch.reserve(1), tile, configOffset, parallelism);
4652
7057
  }
4653
7058
  }
4654
7059
  if (!config.deferredPathResolve) {
4655
7060
  const outputConfigOffset = writeFrameConfig(tile, {
4656
7061
  sampleIndex: 0,
4657
- sampleWeight: 1 / config.samplesPerPixel,
7062
+ sampleWeight: 1 / renderedSamplesPerPixel,
4658
7063
  });
4659
- encodeTileOutput(reserveEncoder(), tile, outputConfigOffset, parallelism);
7064
+ encodeTileOutput(batch.reserve(1), tile, outputConfigOffset, parallelism);
4660
7065
  }
4661
7066
  }
4662
7067
  if (config.denoise) {
4663
7068
  const denoiseConfigOffset = writeFrameConfig(
4664
7069
  { x: 0, y: 0, width: config.width, height: config.height },
4665
- { sampleIndex: 0, sampleWeight: 1 / config.samplesPerPixel }
7070
+ { sampleIndex: 0, sampleWeight: 1 / renderedSamplesPerPixel }
7071
+ );
7072
+ const denoisePassCount = renderedSamplesPerPixel < 4 ? 2 : 1;
7073
+ encodeDenoise(
7074
+ batch.reserve(denoisePassCount),
7075
+ denoiseConfigOffset,
7076
+ parallelism,
7077
+ renderedSamplesPerPixel
4666
7078
  );
4667
- encodeDenoise(reserveEncoder(), denoiseConfigOffset, parallelism);
4668
7079
  }
4669
- encodePresent(reserveEncoder());
4670
- submitCurrentEncoder();
4671
- return submissionCount;
7080
+ encodePresent(batch.reserve(1));
7081
+ return batch.flush();
4672
7082
  }
4673
7083
 
4674
- function renderOnce() {
7084
+ function renderOnce(renderOptions = {}, resolvedSamplingPlan = null) {
7085
+ const frameStartTimeMs = nowMs();
4675
7086
  frame += 1;
4676
7087
  const frameIndex = frame + config.frameIndex;
7088
+ const samplingPlan = resolvedSamplingPlan ?? resolveRenderedSamplesPerPixel(renderOptions, false);
4677
7089
  const parallelismCounters = createGpuParallelismCounters();
4678
7090
  const accelerationBuildSubmitted = dispatchGpuAccelerationBuild(frameIndex, parallelismCounters);
4679
- const frameSubmissionCount = dispatchFrame(frameIndex, parallelismCounters);
4680
- lastGpuParallelism = createGpuParallelismDiagnostics(gpuAdapterParallelism, parallelismCounters);
7091
+ const frameSubmissionCount = dispatchFrame(
7092
+ frameIndex,
7093
+ parallelismCounters,
7094
+ samplingPlan.renderedSamplesPerPixel
7095
+ );
7096
+ const frameTimeMs = Math.max(0, nowMs() - frameStartTimeMs);
4681
7097
  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,
7098
+ ...createFrameStats({
7099
+ frameIndex,
7100
+ accelerationBuildSubmitted,
7101
+ frameSubmissionCount,
7102
+ parallelismCounters,
7103
+ renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel,
7104
+ targetSamplesPerPixel: samplingPlan.targetSamplesPerPixel,
7105
+ frameTimeBudgetMs: samplingPlan.frameTimeBudgetMs,
7106
+ budgetConstrained: samplingPlan.budgetConstrained,
7107
+ }),
7108
+ gpuWorkerJobs: createGpuWorkerJobDiagnostics(
7109
+ lastGpuParallelism,
7110
+ frameSubmissionCount + (accelerationBuildSubmitted ? 1 : 0),
7111
+ frameTimeMs,
7112
+ false
7113
+ ),
7114
+ });
7115
+ }
7116
+
7117
+ async function waitForSubmittedGpuWork(options = {}) {
7118
+ if (typeof device.queue.onSubmittedWorkDone !== "function") {
7119
+ return true;
7120
+ }
7121
+ const timeoutMs = Math.max(
7122
+ 1,
7123
+ Number.isFinite(options.timeoutMs)
7124
+ ? Number(options.timeoutMs)
7125
+ : GPU_SUBMITTED_WORK_TIMEOUT_MS
7126
+ );
7127
+ const allowTimeout = options.allowTimeout !== false;
7128
+ const completionPromise = device.queue.onSubmittedWorkDone().then(
7129
+ () => ({ status: "done" }),
7130
+ (error) => {
7131
+ throw error;
7132
+ }
7133
+ );
7134
+ const lossPromise =
7135
+ typeof device.lost?.then === "function"
7136
+ ? device.lost.then((info) => {
7137
+ throw new Error(
7138
+ `WebGPU device lost while waiting for submitted work (${info?.reason ?? "unknown"}).`
7139
+ );
7140
+ })
7141
+ : null;
7142
+ let timeoutHandle = null;
7143
+ let resolveTimeoutPromise = null;
7144
+ let timeoutSettled = false;
7145
+ const settleTimeoutPromise = (value) => {
7146
+ if (timeoutSettled) {
7147
+ return;
7148
+ }
7149
+ timeoutSettled = true;
7150
+ resolveTimeoutPromise?.(value);
7151
+ };
7152
+ const timeoutPromise = new Promise((resolve) => {
7153
+ resolveTimeoutPromise = resolve;
7154
+ timeoutHandle = setTimeout(() => settleTimeoutPromise({ status: "timeout" }), timeoutMs);
7155
+ });
7156
+ let result;
7157
+ try {
7158
+ result = await Promise.race(
7159
+ [completionPromise, timeoutPromise, lossPromise].filter(Boolean)
7160
+ );
7161
+ } finally {
7162
+ if (timeoutHandle !== null) {
7163
+ clearTimeout(timeoutHandle);
7164
+ settleTimeoutPromise({ status: "cancelled" });
7165
+ }
7166
+ }
7167
+ if (result?.status === "timeout") {
7168
+ if (!allowTimeout) {
7169
+ throw new Error(`Timed out after ${timeoutMs} ms waiting for submitted GPU work.`);
7170
+ }
7171
+ console.warn(
7172
+ `[plasius.wavefront] Submitted GPU work did not report completion within ${timeoutMs} ms; continuing.`
7173
+ );
7174
+ return false;
7175
+ }
7176
+ return true;
7177
+ }
7178
+
7179
+ function dispatchFrameAwaitingGpu(
7180
+ frameIndex,
7181
+ parallelism,
7182
+ renderedSamplesPerPixel = config.samplesPerPixel
7183
+ ) {
7184
+ const samplePassesPerSample = config.maxDepth + 1 + (config.deferredPathResolve ? 1 : 0);
7185
+ const denoisePassCount = config.denoise ? (renderedSamplesPerPixel < 4 ? 2 : 1) : 0;
7186
+ const tailPassCount = denoisePassCount + 1;
7187
+ const sampleBatchSize = Math.max(
7188
+ 1,
7189
+ Math.floor(
7190
+ Math.max(config.maxFramePassesPerSubmission - tailPassCount, 1) /
7191
+ Math.max(samplePassesPerSample, 1)
7192
+ )
7193
+ );
7194
+ let submissionCount = 0;
7195
+
7196
+ for (const tile of tiles) {
7197
+ for (
7198
+ let sampleStart = 0;
7199
+ sampleStart < renderedSamplesPerPixel;
7200
+ sampleStart += sampleBatchSize
7201
+ ) {
7202
+ const sampleEnd = Math.min(renderedSamplesPerPixel, sampleStart + sampleBatchSize);
7203
+ const batch = createGpuSubmissionBatcher({
7204
+ device,
7205
+ frameIndex,
7206
+ maxFramePassesPerSubmission: config.maxFramePassesPerSubmission,
7207
+ startingSubmissionCount: submissionCount,
7208
+ });
7209
+ let slot = 0;
7210
+ for (let sampleIndex = sampleStart; sampleIndex < sampleEnd; sampleIndex += 1) {
7211
+ const configOffset = writeFrameConfigSlot(slot, tile, frameIndex, {
7212
+ sampleIndex,
7213
+ sampleWeight: 1 / renderedSamplesPerPixel,
7214
+ });
7215
+ slot += 1;
7216
+ encodeTileSample(
7217
+ batch.reserve(config.maxDepth + 1),
7218
+ tile,
7219
+ configOffset,
7220
+ parallelism
7221
+ );
7222
+ if (config.deferredPathResolve) {
7223
+ encodeTileOutput(batch.reserve(1), tile, configOffset, parallelism);
7224
+ }
7225
+ }
7226
+ if (!config.deferredPathResolve && sampleEnd >= renderedSamplesPerPixel) {
7227
+ const outputConfigOffset = writeFrameConfigSlot(slot, tile, frameIndex, {
7228
+ sampleIndex: 0,
7229
+ sampleWeight: 1 / renderedSamplesPerPixel,
7230
+ });
7231
+ encodeTileOutput(batch.reserve(1), tile, outputConfigOffset, parallelism);
7232
+ }
7233
+ batch.flush();
7234
+ submissionCount += batch.getSubmissionCount();
7235
+ }
7236
+ }
7237
+
7238
+ const tail = createGpuSubmissionBatcher({
7239
+ device,
7240
+ frameIndex,
4689
7241
  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,
7242
+ startingSubmissionCount: submissionCount,
4710
7243
  });
7244
+ if (config.denoise) {
7245
+ const denoiseConfigOffset = writeFrameConfigSlot(
7246
+ 0,
7247
+ { x: 0, y: 0, width: config.width, height: config.height },
7248
+ frameIndex,
7249
+ { sampleIndex: 0, sampleWeight: 1 / renderedSamplesPerPixel }
7250
+ );
7251
+ encodeDenoise(
7252
+ tail.reserve(denoisePassCount),
7253
+ denoiseConfigOffset,
7254
+ parallelism,
7255
+ renderedSamplesPerPixel
7256
+ );
7257
+ }
7258
+ encodePresent(tail.reserve(1));
7259
+ tail.flush();
7260
+ submissionCount += tail.getSubmissionCount();
7261
+ return submissionCount;
4711
7262
  }
4712
7263
 
4713
7264
  async function readOutputProbe(optionsForProbe = {}) {
@@ -4722,6 +7273,10 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4722
7273
  size: 256,
4723
7274
  usage: constants.buffer.COPY_DST | constants.buffer.MAP_READ,
4724
7275
  });
7276
+ await waitForSubmittedGpuWork({
7277
+ timeoutMs: GPU_READBACK_COMPLETION_TIMEOUT_MS,
7278
+ allowTimeout: false,
7279
+ });
4725
7280
  const encoder = device.createCommandEncoder({
4726
7281
  label: "plasius.wavefront.outputProbe.copy",
4727
7282
  });
@@ -4731,6 +7286,10 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4731
7286
  { width: 1, height: 1, depthOrArrayLayers: 1 }
4732
7287
  );
4733
7288
  device.queue.submit([encoder.finish()]);
7289
+ await waitForSubmittedGpuWork({
7290
+ timeoutMs: GPU_READBACK_COMPLETION_TIMEOUT_MS,
7291
+ allowTimeout: false,
7292
+ });
4734
7293
  await readback.mapAsync(mapMode.READ);
4735
7294
  const bytes = new Uint8Array(readback.getMappedRange()).slice(0, 4);
4736
7295
  readback.unmap();
@@ -4744,7 +7303,60 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4744
7303
  }
4745
7304
 
4746
7305
  async function renderFrame(renderOptions = {}) {
4747
- const frameStats = renderOnce();
7306
+ const awaitGPUCompletion = renderOptions.awaitGPUCompletion !== false;
7307
+ const samplingPlan = resolveRenderedSamplesPerPixel(renderOptions, awaitGPUCompletion);
7308
+ const useThrottledHighSamplePath =
7309
+ awaitGPUCompletion && samplingPlan.renderedSamplesPerPixel >= 8;
7310
+ const submittedWorkTimeoutMs = estimateSubmittedGpuWorkTimeoutMs(
7311
+ { ...config, renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel },
7312
+ tiles.length,
7313
+ renderOptions.submittedWorkTimeoutMs
7314
+ );
7315
+ const frameStartTimeMs = nowMs();
7316
+ const submissionWaitOptions = awaitGPUCompletion
7317
+ ? { timeoutMs: submittedWorkTimeoutMs, allowTimeout: false }
7318
+ : { timeoutMs: submittedWorkTimeoutMs };
7319
+ let frameStats;
7320
+ if (useThrottledHighSamplePath) {
7321
+ frame += 1;
7322
+ const frameIndex = frame + config.frameIndex;
7323
+ const parallelismCounters = createGpuParallelismCounters();
7324
+ const accelerationBuildSubmitted = dispatchGpuAccelerationBuild(frameIndex, parallelismCounters);
7325
+ const frameSubmissionCount = dispatchFrameAwaitingGpu(
7326
+ frameIndex,
7327
+ parallelismCounters,
7328
+ samplingPlan.renderedSamplesPerPixel
7329
+ );
7330
+ frameStats = createFrameStats({
7331
+ frameIndex,
7332
+ accelerationBuildSubmitted,
7333
+ frameSubmissionCount,
7334
+ parallelismCounters,
7335
+ renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel,
7336
+ targetSamplesPerPixel: samplingPlan.targetSamplesPerPixel,
7337
+ frameTimeBudgetMs: samplingPlan.frameTimeBudgetMs,
7338
+ budgetConstrained: samplingPlan.budgetConstrained,
7339
+ });
7340
+ } else {
7341
+ frameStats = renderOnce(renderOptions, samplingPlan);
7342
+ }
7343
+ if (awaitGPUCompletion) {
7344
+ await waitForSubmittedGpuWork(submissionWaitOptions);
7345
+ }
7346
+ const frameTimeMs = Math.max(0, nowMs() - frameStartTimeMs);
7347
+ if (awaitGPUCompletion) {
7348
+ lastCompletedFrameTimeMs = frameTimeMs;
7349
+ lastCompletedSamplesPerPixel = frameStats.renderedSamplesPerPixel ?? frameStats.samplesPerPixel;
7350
+ }
7351
+ frameStats = Object.freeze({
7352
+ ...frameStats,
7353
+ gpuWorkerJobs: createGpuWorkerJobDiagnostics(
7354
+ frameStats.gpuParallelism,
7355
+ frameStats.commandSubmissions,
7356
+ frameTimeMs,
7357
+ awaitGPUCompletion
7358
+ ),
7359
+ });
4748
7360
  const probe =
4749
7361
  renderOptions.readOutputProbe === false ? null : await readOutputProbe(renderOptions.probe);
4750
7362
  const maxChannel = probe ? Math.max(...probe.rgba.slice(0, 3)) : 0;
@@ -4769,10 +7381,8 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4769
7381
  });
4770
7382
  }
4771
7383
 
4772
- function updateSceneObjects(sceneObjects) {
4773
- const nextPackedScene = packWavefrontSceneObjects(sceneObjects, config.sceneObjectCapacity);
4774
- packedScene = nextPackedScene;
4775
- config = createWavefrontPathTracingComputeConfig({
7384
+ function rebuildLiveConfig(overrides = {}) {
7385
+ return createWavefrontPathTracingComputeConfig({
4776
7386
  ...options,
4777
7387
  canvas,
4778
7388
  width: config.width,
@@ -4783,27 +7393,38 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4783
7393
  sceneObjectCapacity: config.sceneObjectCapacity,
4784
7394
  sceneObjects: packedScene.objects,
4785
7395
  camera: activeCameraOptions,
7396
+ environmentMap: {
7397
+ ...config.environmentMap,
7398
+ },
4786
7399
  frameIndex: config.frameIndex,
7400
+ ...overrides,
4787
7401
  });
7402
+ }
7403
+
7404
+ function rebuildMediumResources(nextConfig) {
7405
+ const previousMediumTextureResource = mediumTextureResource;
7406
+ mediumTextureResource = createMediumTextureResource(device, constants, nextConfig.mediums);
7407
+ bindGroups = createTraceBindGroups();
7408
+ if (previousMediumTextureResource?.ownsTexture) {
7409
+ previousMediumTextureResource.texture?.destroy?.();
7410
+ }
7411
+ }
7412
+
7413
+ function updateSceneObjects(sceneObjects) {
7414
+ const nextPackedScene = packWavefrontSceneObjects(sceneObjects, config.sceneObjectCapacity);
7415
+ packedScene = nextPackedScene;
7416
+ const nextConfig = rebuildLiveConfig();
7417
+ if (!mediumTablesEqual(config.mediums, nextConfig.mediums)) {
7418
+ rebuildMediumResources(nextConfig);
7419
+ }
7420
+ config = nextConfig;
4788
7421
  device.queue.writeBuffer(sceneObjectBuffer, 0, packedScene.buffer);
4789
7422
  return config;
4790
7423
  }
4791
7424
 
4792
7425
  function updateCamera(cameraOptions = {}) {
4793
7426
  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
- });
7427
+ config = rebuildLiveConfig();
4807
7428
  return config;
4808
7429
  }
4809
7430
 
@@ -4822,6 +7443,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4822
7443
  emissiveTriangleCount: config.emissiveTriangleCount,
4823
7444
  environmentPortalCount: config.environmentPortalCount,
4824
7445
  environmentPortalMode: config.environmentPortalMode,
7446
+ mediumCount: config.mediumCount,
4825
7447
  environmentMap: createEnvironmentMapSnapshot(config.environmentMap),
4826
7448
  deferredPathResolve: config.deferredPathResolve,
4827
7449
  bvhNodeCount: config.bvhNodeCount,
@@ -4860,6 +7482,28 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4860
7482
  if (environmentMapResource.ownsTexture) {
4861
7483
  environmentMapResource.texture?.destroy?.();
4862
7484
  }
7485
+ if (environmentSamplingResource.ownsTexture) {
7486
+ environmentSamplingResource.texture?.destroy?.();
7487
+ }
7488
+ if (mediumTextureResource.ownsTexture) {
7489
+ mediumTextureResource.texture?.destroy?.();
7490
+ }
7491
+ brdfLutResource.texture?.destroy?.();
7492
+ if (baseColorAtlasResource.ownsTexture) {
7493
+ baseColorAtlasResource.texture?.destroy?.();
7494
+ }
7495
+ if (metallicRoughnessAtlasResource.ownsTexture) {
7496
+ metallicRoughnessAtlasResource.texture?.destroy?.();
7497
+ }
7498
+ if (normalAtlasResource.ownsTexture) {
7499
+ normalAtlasResource.texture?.destroy?.();
7500
+ }
7501
+ if (occlusionAtlasResource.ownsTexture) {
7502
+ occlusionAtlasResource.texture?.destroy?.();
7503
+ }
7504
+ if (emissiveAtlasResource.ownsTexture) {
7505
+ emissiveAtlasResource.texture?.destroy?.();
7506
+ }
4863
7507
  context.unconfigure?.();
4864
7508
  }
4865
7509