@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.
package/dist/index.js CHANGED
@@ -1,33 +1,176 @@
1
+ // src/wavefront-frame-runtime.js
2
+ function createGpuParallelismCounters() {
3
+ return {
4
+ directDispatches: 0,
5
+ directWorkgroups: 0,
6
+ directShaderInvocations: 0,
7
+ multiWorkgroupDispatches: 0,
8
+ largestDirectWorkgroupsPerDispatch: 0,
9
+ indirectDispatches: 0,
10
+ estimatedIndirectWorkgroupsUpperBound: 0,
11
+ estimatedIndirectShaderInvocationsUpperBound: 0,
12
+ indirectDispatchesWithMultiWorkgroupCapacity: 0,
13
+ largestEstimatedIndirectWorkgroupsPerDispatch: 0
14
+ };
15
+ }
16
+ function countDispatchWorkgroups(groups) {
17
+ return groups.reduce((product, value) => {
18
+ const numeric = Number(value ?? 1);
19
+ const count = Number.isFinite(numeric) ? Math.max(1, Math.trunc(numeric)) : 1;
20
+ return product * count;
21
+ }, 1);
22
+ }
23
+ function recordDirectDispatch(parallelism, groups, invocationsPerWorkgroup = 1) {
24
+ const workgroups = countDispatchWorkgroups(groups);
25
+ parallelism.directDispatches += 1;
26
+ parallelism.directWorkgroups += workgroups;
27
+ parallelism.directShaderInvocations += workgroups * invocationsPerWorkgroup;
28
+ parallelism.largestDirectWorkgroupsPerDispatch = Math.max(
29
+ parallelism.largestDirectWorkgroupsPerDispatch,
30
+ workgroups
31
+ );
32
+ if (workgroups > 1) {
33
+ parallelism.multiWorkgroupDispatches += 1;
34
+ }
35
+ }
36
+ function recordIndirectDispatch(parallelism, estimatedWorkgroupsUpperBound, invocationsPerWorkgroup = 1) {
37
+ const workgroups = Math.max(1, Math.trunc(Number(estimatedWorkgroupsUpperBound) || 1));
38
+ parallelism.indirectDispatches += 1;
39
+ parallelism.estimatedIndirectWorkgroupsUpperBound += workgroups;
40
+ parallelism.estimatedIndirectShaderInvocationsUpperBound += workgroups * invocationsPerWorkgroup;
41
+ parallelism.largestEstimatedIndirectWorkgroupsPerDispatch = Math.max(
42
+ parallelism.largestEstimatedIndirectWorkgroupsPerDispatch,
43
+ workgroups
44
+ );
45
+ if (workgroups > 1) {
46
+ parallelism.indirectDispatchesWithMultiWorkgroupCapacity += 1;
47
+ }
48
+ }
49
+ function createGpuParallelismDiagnostics(adapterDiagnostics, counters) {
50
+ const totalEstimatedWorkgroupsUpperBound = counters.directWorkgroups + counters.estimatedIndirectWorkgroupsUpperBound;
51
+ const totalEstimatedShaderInvocationsUpperBound = counters.directShaderInvocations + counters.estimatedIndirectShaderInvocationsUpperBound;
52
+ const exposesMultiWorkgroupParallelism = counters.multiWorkgroupDispatches > 0 || counters.indirectDispatchesWithMultiWorkgroupCapacity > 0;
53
+ return Object.freeze({
54
+ ...adapterDiagnostics,
55
+ directDispatches: counters.directDispatches,
56
+ directWorkgroups: counters.directWorkgroups,
57
+ directShaderInvocations: counters.directShaderInvocations,
58
+ multiWorkgroupDispatches: counters.multiWorkgroupDispatches,
59
+ largestDirectWorkgroupsPerDispatch: counters.largestDirectWorkgroupsPerDispatch,
60
+ indirectDispatches: counters.indirectDispatches,
61
+ estimatedIndirectWorkgroupsUpperBound: counters.estimatedIndirectWorkgroupsUpperBound,
62
+ estimatedIndirectShaderInvocationsUpperBound: counters.estimatedIndirectShaderInvocationsUpperBound,
63
+ indirectDispatchesWithMultiWorkgroupCapacity: counters.indirectDispatchesWithMultiWorkgroupCapacity,
64
+ largestEstimatedIndirectWorkgroupsPerDispatch: counters.largestEstimatedIndirectWorkgroupsPerDispatch,
65
+ totalEstimatedWorkgroupsUpperBound,
66
+ totalEstimatedShaderInvocationsUpperBound,
67
+ exposesMultiWorkgroupParallelism,
68
+ likelyUsesMoreThanOnePhysicalGpuCore: null,
69
+ coreUtilizationStatus: "not-exposed-by-webgpu"
70
+ });
71
+ }
72
+ function createGpuWorkerJobDiagnostics(parallelism, commandSubmissions, frameTimeMs, awaitedGpuCompletion) {
73
+ const directDispatchesCompleted = Math.max(0, Number(parallelism?.directDispatches ?? 0));
74
+ const indirectDispatchesCompleted = Math.max(
75
+ 0,
76
+ Number(parallelism?.indirectDispatches ?? 0)
77
+ );
78
+ const completedPerFrame = directDispatchesCompleted + indirectDispatchesCompleted;
79
+ const completedPerSubmission = commandSubmissions > 0 ? completedPerFrame / commandSubmissions : completedPerFrame;
80
+ const completedPerSecond = awaitedGpuCompletion && frameTimeMs > 0 ? completedPerFrame * 1e3 / frameTimeMs : null;
81
+ return Object.freeze({
82
+ completedPerFrame,
83
+ completedPerSecond,
84
+ completedPerSubmission,
85
+ directDispatchesCompleted,
86
+ indirectDispatchesCompleted,
87
+ frameTimeMs,
88
+ awaitedGpuCompletion
89
+ });
90
+ }
91
+ function createGpuSubmissionBatcher({
92
+ device,
93
+ frameIndex,
94
+ maxFramePassesPerSubmission,
95
+ startingSubmissionCount = 0,
96
+ labelPrefix = "plasius.wavefront.frame"
97
+ }) {
98
+ let encodedFramePasses = 0;
99
+ let submissionCount = 0;
100
+ let encoder = createCommandEncoder();
101
+ function createCommandEncoder() {
102
+ return device.createCommandEncoder({
103
+ label: `${labelPrefix}.${frameIndex}.batched.${startingSubmissionCount + submissionCount + 1}`
104
+ });
105
+ }
106
+ function submitCurrentEncoder() {
107
+ if (encodedFramePasses <= 0) {
108
+ return false;
109
+ }
110
+ device.queue.submit([encoder.finish()]);
111
+ submissionCount += 1;
112
+ encodedFramePasses = 0;
113
+ encoder = createCommandEncoder();
114
+ return true;
115
+ }
116
+ return Object.freeze({
117
+ reserve(passCount = 1) {
118
+ if (encodedFramePasses > 0 && encodedFramePasses + passCount > maxFramePassesPerSubmission) {
119
+ submitCurrentEncoder();
120
+ }
121
+ encodedFramePasses += passCount;
122
+ return encoder;
123
+ },
124
+ flush() {
125
+ submitCurrentEncoder();
126
+ return submissionCount;
127
+ },
128
+ getSubmissionCount() {
129
+ return submissionCount;
130
+ }
131
+ });
132
+ }
133
+
1
134
  // src/wavefront-compute.js
2
135
  var DEFAULT_WIDTH = 1280;
3
136
  var DEFAULT_HEIGHT = 720;
4
137
  var DEFAULT_MAX_DEPTH = 6;
5
138
  var DEFAULT_TILE_SIZE = 128;
6
139
  var DEFAULT_SAMPLES_PER_PIXEL = 1;
140
+ var MAX_SAMPLES_PER_PIXEL = 256;
141
+ var DEFAULT_BRDF_LUT_SIZE = 128;
142
+ var DEFAULT_BRDF_LUT_SAMPLE_COUNT = 256;
7
143
  var DEFAULT_MAX_FRAME_PASSES_PER_SUBMISSION = 256;
8
144
  var DEFAULT_SCENE_OBJECT_CAPACITY = 128;
9
145
  var DEFAULT_ENVIRONMENT_PORTAL_CAPACITY = 32;
146
+ var DEFAULT_MEDIUM_PHASE_MODEL = 0;
10
147
  var WORKGROUP_SIZE = 64;
11
148
  var rendererWavefrontComputeMode = "webgpu-compute";
12
149
  var rendererWavefrontComputeWorkgroupSize = WORKGROUP_SIZE;
13
150
  var rendererWavefrontComputeStatsStride = 8;
14
151
  var RAY_RECORD_BYTES = 80;
15
- var HIT_RECORD_BYTES = 208;
16
- var SCENE_OBJECT_RECORD_BYTES = 96;
152
+ var HIT_RECORD_BYTES = 256;
153
+ var SCENE_OBJECT_RECORD_BYTES = 160;
17
154
  var MESH_VERTEX_RECORD_BYTES = 48;
18
- var MESH_RANGE_RECORD_BYTES = 96;
19
- var TRIANGLE_RECORD_BYTES = 208;
155
+ var MESH_RANGE_RECORD_BYTES = 240;
156
+ var TRIANGLE_RECORD_BYTES = 352;
157
+ var GPU_MATERIAL_RECORD_BYTES = 192;
20
158
  var BVH_NODE_RECORD_BYTES = 48;
21
159
  var BVH_LEAF_REF_RECORD_BYTES = 16;
22
160
  var EMISSIVE_TRIANGLE_INDEX_BYTES = 4;
23
161
  var ENVIRONMENT_PORTAL_RECORD_BYTES = 96;
162
+ var MEDIUM_TABLE_ROWS = 2;
24
163
  var ACCUMULATION_RECORD_BYTES = 16;
25
164
  var PATH_VERTEX_RECORD_BYTES = 16;
26
- var CONFIG_BUFFER_BYTES = 304;
165
+ var GPU_SUBMITTED_WORK_TIMEOUT_MS = 5e3;
166
+ var GPU_READBACK_COMPLETION_TIMEOUT_MS = 6e4;
167
+ var GPU_MAX_SUBMITTED_WORK_TIMEOUT_MS = 6e4;
168
+ var CONFIG_BUFFER_BYTES = 320;
27
169
  var COUNTER_DISPATCH_ARGS_OFFSET = 16;
28
170
  var INDIRECT_DISPATCH_ARGS_BYTES = 12;
29
171
  var COUNTER_BUFFER_BYTES = 32;
30
172
  var TRACE_STORAGE_BUFFER_BINDINGS = 10;
173
+ var BRDF_LUT_UPLOAD_CACHE = /* @__PURE__ */ new Map();
31
174
  var MATERIAL_DIFFUSE = 0;
32
175
  var MATERIAL_METAL = 1;
33
176
  var MATERIAL_DIELECTRIC = 2;
@@ -62,6 +205,7 @@ var wavefrontPathTracingComputeLimits = Object.freeze({
62
205
  meshVertexRecordBytes: MESH_VERTEX_RECORD_BYTES,
63
206
  meshRangeRecordBytes: MESH_RANGE_RECORD_BYTES,
64
207
  triangleRecordBytes: TRIANGLE_RECORD_BYTES,
208
+ materialRecordBytes: GPU_MATERIAL_RECORD_BYTES,
65
209
  bvhNodeRecordBytes: BVH_NODE_RECORD_BYTES,
66
210
  bvhLeafReferenceRecordBytes: BVH_LEAF_REF_RECORD_BYTES,
67
211
  emissiveTriangleIndexBytes: EMISSIVE_TRIANGLE_INDEX_BYTES,
@@ -145,6 +289,32 @@ function asColor(value, fallback = [1, 1, 1, 1]) {
145
289
  clamp(readFiniteNumber("color[3]", value[3], fallback[3] ?? 1), 0, 1)
146
290
  ];
147
291
  }
292
+ function deriveLegacySheenColor(baseColor, sheen, sheenTint) {
293
+ const sheenStrength = clamp(Number(sheen) || 0, 0, 1);
294
+ if (sheenStrength <= 0) {
295
+ return [0, 0, 0, 1];
296
+ }
297
+ const tint = clamp(Number(sheenTint) || 0, 0, 1);
298
+ const base = asColor(baseColor, [1, 1, 1, 1]);
299
+ return [
300
+ clamp((1 - tint) * sheenStrength + base[0] * tint * sheenStrength, 0, 1),
301
+ clamp((1 - tint) * sheenStrength + base[1] * tint * sheenStrength, 0, 1),
302
+ clamp((1 - tint) * sheenStrength + base[2] * tint * sheenStrength, 0, 1),
303
+ 1
304
+ ];
305
+ }
306
+ function resolveSheenColor(input, fallbackBaseColor) {
307
+ if (input?.sheenColor || input?.material?.sheenColor) {
308
+ return asColor(input.sheenColor ?? input.material?.sheenColor, [0, 0, 0, 1]).map(
309
+ (value, index) => index < 3 ? clamp(value, 0, 1) : 1
310
+ );
311
+ }
312
+ return deriveLegacySheenColor(
313
+ fallbackBaseColor,
314
+ input?.sheen ?? input?.material?.sheen,
315
+ input?.sheenTint ?? input?.material?.sheenTint
316
+ );
317
+ }
148
318
  function resolveEnvironmentMap(input = null) {
149
319
  const source = input && typeof input === "object" ? input : null;
150
320
  const hasTexture = Boolean(source?.view || source?.texture || source?.data);
@@ -154,6 +324,11 @@ function resolveEnvironmentMap(input = null) {
154
324
  enabled: hasTexture && source?.enabled !== false,
155
325
  width,
156
326
  height,
327
+ mipLevelCount: readPositiveInteger(
328
+ "environmentMap.mipLevelCount",
329
+ source?.mipLevelCount,
330
+ 1
331
+ ),
157
332
  format: typeof source?.format === "string" ? source.format : "rgba16float",
158
333
  projection: typeof source?.projection === "string" ? source.projection : "equirectangular",
159
334
  texture: source?.texture ?? null,
@@ -165,7 +340,8 @@ function resolveEnvironmentMap(input = null) {
165
340
  ambientStrength: Math.max(
166
341
  0,
167
342
  readFiniteNumber("environmentMap.ambientStrength", source?.ambientStrength, 0.32)
168
- )
343
+ ),
344
+ hasImportanceData: source?.hasImportanceData === true
169
345
  });
170
346
  }
171
347
  function resolveDeferredPathResolve(options = {}) {
@@ -330,6 +506,156 @@ function deriveBounds(input) {
330
506
  }
331
507
  return null;
332
508
  }
509
+ function deriveBeerLambertAbsorptionFromAttenuationColor(attenuationColor, attenuationDistance, density = 1) {
510
+ const distance = Number(attenuationDistance);
511
+ const densityScale = Math.max(0, Number(density) || 0);
512
+ if (!Number.isFinite(distance) || distance <= 0 || densityScale <= 0) {
513
+ return [0, 0, 0];
514
+ }
515
+ return attenuationColor.slice(0, 3).map((channel) => {
516
+ const clamped = clamp(Number(channel) || 0, 1e-4, 1);
517
+ return Math.max(0, -Math.log(clamped) / distance * densityScale);
518
+ });
519
+ }
520
+ function readMediumPhaseModel(value) {
521
+ if (typeof value === "number" && Number.isFinite(value)) {
522
+ return Math.max(0, Math.trunc(value));
523
+ }
524
+ switch (String(value ?? "").trim().toLowerCase()) {
525
+ case "isotropic":
526
+ default:
527
+ return DEFAULT_MEDIUM_PHASE_MODEL;
528
+ }
529
+ }
530
+ function resolveWavefrontVolumeInput(input) {
531
+ return input?.volume ?? input?.material?.volume ?? null;
532
+ }
533
+ function normalizeWavefrontThickness(input, label) {
534
+ const volume = resolveWavefrontVolumeInput(input);
535
+ return Math.max(
536
+ 0,
537
+ readFiniteNumber(
538
+ label,
539
+ input?.thickness ?? volume?.thickness ?? input?.material?.thickness,
540
+ 0
541
+ )
542
+ );
543
+ }
544
+ function resolveWavefrontMediumId(input, fallbackId = 1) {
545
+ return input?.mediumRefId ?? input?.mediumId ?? input?.material?.mediumId ?? input?.materialRefId ?? input?.material?.id ?? input?.materialId ?? input?.id ?? fallbackId;
546
+ }
547
+ function deriveWavefrontTransportMedium(input, fallbackId = 1) {
548
+ const resolvedId = resolveWavefrontMediumId(input, fallbackId);
549
+ if (input?.medium) {
550
+ return normalizeWavefrontMedium(
551
+ {
552
+ ...input.medium,
553
+ id: input.medium.id ?? input.medium.mediumId ?? resolvedId
554
+ },
555
+ fallbackId
556
+ );
557
+ }
558
+ const volume = resolveWavefrontVolumeInput(input);
559
+ if (!volume) {
560
+ return null;
561
+ }
562
+ return normalizeWavefrontMedium(
563
+ {
564
+ id: resolvedId,
565
+ phaseModel: volume.phaseModel,
566
+ density: volume.density,
567
+ attenuationColor: volume.attenuationColor,
568
+ attenuationDistance: volume.attenuationDistance,
569
+ absorption: volume.absorption,
570
+ scattering: volume.scattering
571
+ },
572
+ fallbackId
573
+ );
574
+ }
575
+ function normalizeWavefrontMedium(input = {}, index = 0) {
576
+ const id = readNonNegativeInteger("medium id", input.id ?? input.mediumId, index);
577
+ const density = Math.max(0, readFiniteNumber("medium density", input.density, 1));
578
+ const attenuationColor = asColor(
579
+ input.attenuationColor ?? input.color ?? input.medium?.attenuationColor,
580
+ [1, 1, 1, 1]
581
+ );
582
+ const attenuationDistance = readFiniteNumber(
583
+ "medium attenuationDistance",
584
+ input.attenuationDistance ?? input.distance ?? input.medium?.attenuationDistance,
585
+ 0
586
+ );
587
+ const absorption = Array.isArray(input.absorption) || Array.isArray(input.medium?.absorption) ? asVec3(input.absorption ?? input.medium?.absorption, [0, 0, 0]).map(
588
+ (value) => Math.max(0, Number(value) || 0)
589
+ ) : deriveBeerLambertAbsorptionFromAttenuationColor(
590
+ attenuationColor,
591
+ attenuationDistance,
592
+ density
593
+ );
594
+ const scattering = asVec3(
595
+ input.scattering ?? input.medium?.scattering,
596
+ [0, 0, 0]
597
+ ).map((value) => Math.max(0, Number(value) || 0));
598
+ return Object.freeze({
599
+ id,
600
+ phaseModel: readMediumPhaseModel(input.phaseModel ?? input.medium?.phaseModel),
601
+ density,
602
+ attenuationColor: Object.freeze(attenuationColor),
603
+ attenuationDistance,
604
+ absorption: Object.freeze(absorption),
605
+ scattering: Object.freeze(scattering)
606
+ });
607
+ }
608
+ function collectWavefrontMediums(options, meshes, sceneObjects = []) {
609
+ const mediumsById = /* @__PURE__ */ new Map();
610
+ mediumsById.set(
611
+ 0,
612
+ Object.freeze({
613
+ id: 0,
614
+ phaseModel: DEFAULT_MEDIUM_PHASE_MODEL,
615
+ density: 0,
616
+ attenuationColor: Object.freeze([1, 1, 1, 1]),
617
+ attenuationDistance: 0,
618
+ absorption: Object.freeze([0, 0, 0]),
619
+ scattering: Object.freeze([0, 0, 0])
620
+ })
621
+ );
622
+ const register = (input, fallbackId = mediumsById.size) => {
623
+ if (!input) {
624
+ return;
625
+ }
626
+ const normalized = normalizeWavefrontMedium(
627
+ typeof input === "object" ? { id: fallbackId, ...input } : { id: fallbackId },
628
+ fallbackId
629
+ );
630
+ const existing = mediumsById.get(normalized.id);
631
+ if (existing && JSON.stringify(existing) !== JSON.stringify(normalized)) {
632
+ throw new Error(`Medium id ${normalized.id} is defined more than once with different values.`);
633
+ }
634
+ mediumsById.set(normalized.id, normalized);
635
+ };
636
+ for (const medium of options.mediums ?? []) {
637
+ register(medium);
638
+ }
639
+ for (const mesh of meshes) {
640
+ register(mesh.medium, mesh.mediumRefId ?? mesh.medium?.id ?? 0);
641
+ }
642
+ for (const mesh of meshes) {
643
+ if ((mesh.mediumRefId ?? 0) > 0 && !mediumsById.has(mesh.mediumRefId)) {
644
+ register({ id: mesh.mediumRefId });
645
+ }
646
+ }
647
+ for (const object of sceneObjects) {
648
+ register(object.medium, object.mediumRefId ?? object.medium?.id ?? 0);
649
+ }
650
+ for (const object of sceneObjects) {
651
+ if ((object.mediumRefId ?? 0) > 0 && !mediumsById.has(object.mediumRefId)) {
652
+ register({ id: object.mediumRefId });
653
+ }
654
+ }
655
+ return Object.freeze(
656
+ Array.from(mediumsById.values()).sort((left, right) => left.id - right.id)
657
+ );
658
+ }
333
659
  function normalizeWavefrontSceneObject(input = {}, index = 0) {
334
660
  const bounds = deriveBounds(input);
335
661
  const kind = readObjectKind(input.kind ?? input.type ?? (bounds ? "box" : "sphere"));
@@ -339,7 +665,8 @@ function normalizeWavefrontSceneObject(input = {}, index = 0) {
339
665
  input.halfExtent ?? input.halfExtents ?? input.extents ?? bounds?.halfExtent,
340
666
  [0.5, 0.5, 0.5]
341
667
  ).map((value) => Math.max(value, 1e-3));
342
- const materialKind = readMaterialKind(input.materialKind ?? input.material?.kind);
668
+ const materialKindInput = input.materialKind ?? input.material?.kind;
669
+ const materialKind = readMaterialKind(materialKindInput);
343
670
  const color = asColor(
344
671
  input.color ?? input.baseColor ?? input.albedo ?? input.material?.color ?? input.material?.baseColor,
345
672
  [0.72, 0.72, 0.68, 1]
@@ -348,19 +675,55 @@ function normalizeWavefrontSceneObject(input = {}, index = 0) {
348
675
  input.emission ?? input.emissive ?? input.material?.emission ?? input.material?.emissive,
349
676
  [0, 0, 0, 1]
350
677
  );
678
+ const opacity = clamp(readFiniteNumber("opacity", input.opacity ?? input.material?.opacity, color[3] ?? 1), 0, 1);
679
+ const transmission = clamp(
680
+ readFiniteNumber("transmission", input.transmission ?? input.material?.transmission, 0),
681
+ 0,
682
+ 1
683
+ );
684
+ const sheenColor = resolveSheenColor(input, color);
685
+ const specularColor = asColor(
686
+ input.specularColor ?? input.material?.specularColor,
687
+ [1, 1, 1, 1]
688
+ ).map((value, componentIndex) => componentIndex < 3 ? clamp(value, 0, 1) : 1);
689
+ const medium = deriveWavefrontTransportMedium(input, index + 1);
690
+ const resolvedMaterialKind = emission[0] > 0 || emission[1] > 0 || emission[2] > 0 ? MATERIAL_EMISSIVE : materialKindInput === void 0 || materialKindInput === null ? transmission > 1e-3 || opacity < 0.999 ? MATERIAL_TRANSPARENT : materialKind : materialKind;
351
691
  return Object.freeze({
352
692
  id: readNonNegativeInteger("id", input.id, index + 1),
353
693
  kind,
354
- materialKind: emission[0] > 0 || emission[1] > 0 || emission[2] > 0 ? MATERIAL_EMISSIVE : materialKind,
694
+ materialKind: resolvedMaterialKind,
355
695
  flags: readNonNegativeInteger("flags", input.flags, 0),
696
+ mediumRefId: readNonNegativeInteger(
697
+ "mediumRefId",
698
+ input.mediumRefId ?? medium?.id ?? input.medium?.id ?? input.mediumId,
699
+ 0
700
+ ),
701
+ medium,
356
702
  center: Object.freeze(center),
357
703
  halfExtent: Object.freeze(halfExtent),
358
704
  color: Object.freeze(color),
359
705
  emission: Object.freeze(emission),
360
706
  roughness: clamp(readFiniteNumber("roughness", input.roughness ?? input.material?.roughness, 0.72), 0, 1),
361
707
  metallic: clamp(readFiniteNumber("metallic", input.metallic ?? input.material?.metallic, 0), 0, 1),
362
- opacity: clamp(readFiniteNumber("opacity", input.opacity ?? input.material?.opacity, color[3] ?? 1), 0, 1),
363
- ior: clamp(readFiniteNumber("ior", input.ior ?? input.material?.ior, 1.45), 1, 3)
708
+ opacity,
709
+ ior: clamp(readFiniteNumber("ior", input.ior ?? input.material?.ior, 1.45), 1, 3),
710
+ sheen: clamp(readFiniteNumber("sheen", input.sheen ?? input.material?.sheen, 0), 0, 1),
711
+ sheenTint: clamp(readFiniteNumber("sheenTint", input.sheenTint ?? input.material?.sheenTint, 0), 0, 1),
712
+ sheenColor: Object.freeze(sheenColor),
713
+ clearcoat: clamp(readFiniteNumber("clearcoat", input.clearcoat ?? input.material?.clearcoat, 0), 0, 1),
714
+ clearcoatRoughness: clamp(
715
+ readFiniteNumber(
716
+ "clearcoatRoughness",
717
+ input.clearcoatRoughness ?? input.material?.clearcoatRoughness,
718
+ 0.08
719
+ ),
720
+ 0,
721
+ 1
722
+ ),
723
+ specular: clamp(readFiniteNumber("specular", input.specular ?? input.material?.specular, 1), 0, 1),
724
+ specularColor: Object.freeze(specularColor),
725
+ thickness: normalizeWavefrontThickness(input, "thickness"),
726
+ transmission
364
727
  });
365
728
  }
366
729
  function createDefaultWavefrontSceneObjects() {
@@ -432,7 +795,8 @@ function normalizeWavefrontMesh(input = {}, meshIndex = 0) {
432
795
  input.uvs ?? input.texcoords ?? input.uv,
433
796
  (value) => readFiniteNumber("mesh uv", value, 0)
434
797
  ) : null;
435
- const materialKind = readMaterialKind(input.materialKind ?? input.material?.kind);
798
+ const materialKindInput = input.materialKind ?? input.material?.kind;
799
+ const materialKind = readMaterialKind(materialKindInput);
436
800
  const color = asColor(
437
801
  input.color ?? input.baseColor ?? input.albedo ?? input.material?.color ?? input.material?.baseColor,
438
802
  [0.72, 0.72, 0.68, 1]
@@ -441,13 +805,26 @@ function normalizeWavefrontMesh(input = {}, meshIndex = 0) {
441
805
  input.emission ?? input.emissive ?? input.material?.emission ?? input.material?.emissive,
442
806
  [0, 0, 0, 1]
443
807
  );
808
+ const opacity = clamp(readFiniteNumber("opacity", input.opacity ?? input.material?.opacity, color[3] ?? 1), 0, 1);
809
+ const transmission = clamp(
810
+ readFiniteNumber("transmission", input.transmission ?? input.material?.transmission, 0),
811
+ 0,
812
+ 1
813
+ );
814
+ const sheenColor = resolveSheenColor(input, color);
815
+ const specularColor = asColor(
816
+ input.specularColor ?? input.material?.specularColor,
817
+ [1, 1, 1, 1]
818
+ ).map((value, componentIndex) => componentIndex < 3 ? clamp(value, 0, 1) : 1);
819
+ const medium = deriveWavefrontTransportMedium(input, meshIndex + 1);
820
+ const resolvedMaterialKind = emission[0] > 0 || emission[1] > 0 || emission[2] > 0 ? MATERIAL_EMISSIVE : materialKindInput === void 0 || materialKindInput === null ? transmission > 1e-3 || opacity < 0.999 ? MATERIAL_TRANSPARENT : materialKind : materialKind;
444
821
  return Object.freeze({
445
822
  id: readNonNegativeInteger("mesh id", input.id, meshIndex + 1),
446
823
  positions: Object.freeze(Array.from(positions, (value) => readFiniteNumber("mesh position", value, 0))),
447
824
  indices: Object.freeze(indices),
448
825
  normals: normals ? Object.freeze(normals) : null,
449
826
  uvs: uvs ? Object.freeze(uvs) : null,
450
- materialKind: emission[0] > 0 || emission[1] > 0 || emission[2] > 0 ? MATERIAL_EMISSIVE : materialKind,
827
+ materialKind: resolvedMaterialKind,
451
828
  flags: readNonNegativeInteger("mesh flags", input.flags, 0),
452
829
  materialRefId: readNonNegativeInteger(
453
830
  "mesh materialRefId",
@@ -456,17 +833,176 @@ function normalizeWavefrontMesh(input = {}, meshIndex = 0) {
456
833
  ),
457
834
  mediumRefId: readNonNegativeInteger(
458
835
  "mesh mediumRefId",
459
- input.mediumRefId ?? input.medium?.id ?? input.mediumId,
836
+ input.mediumRefId ?? medium?.id ?? input.medium?.id ?? input.mediumId ?? input.material?.mediumId,
460
837
  0
461
838
  ),
839
+ medium,
462
840
  color: Object.freeze(color),
463
841
  emission: Object.freeze(emission),
464
842
  roughness: clamp(readFiniteNumber("roughness", input.roughness ?? input.material?.roughness, 0.72), 0, 1),
465
843
  metallic: clamp(readFiniteNumber("metallic", input.metallic ?? input.material?.metallic, 0), 0, 1),
466
- opacity: clamp(readFiniteNumber("opacity", input.opacity ?? input.material?.opacity, color[3] ?? 1), 0, 1),
467
- ior: clamp(readFiniteNumber("ior", input.ior ?? input.material?.ior, 1.45), 1, 3)
844
+ opacity,
845
+ ior: clamp(readFiniteNumber("ior", input.ior ?? input.material?.ior, 1.45), 1, 3),
846
+ sheen: clamp(readFiniteNumber("sheen", input.sheen ?? input.material?.sheen, 0), 0, 1),
847
+ sheenTint: clamp(readFiniteNumber("sheenTint", input.sheenTint ?? input.material?.sheenTint, 0), 0, 1),
848
+ sheenColor: Object.freeze(sheenColor),
849
+ clearcoat: clamp(readFiniteNumber("clearcoat", input.clearcoat ?? input.material?.clearcoat, 0), 0, 1),
850
+ clearcoatRoughness: clamp(
851
+ readFiniteNumber(
852
+ "clearcoatRoughness",
853
+ input.clearcoatRoughness ?? input.material?.clearcoatRoughness,
854
+ 0.08
855
+ ),
856
+ 0,
857
+ 1
858
+ ),
859
+ specular: clamp(readFiniteNumber("specular", input.specular ?? input.material?.specular, 1), 0, 1),
860
+ specularColor: Object.freeze(specularColor),
861
+ thickness: normalizeWavefrontThickness(input, "mesh thickness"),
862
+ transmission,
863
+ baseColorTexture: input.baseColorTexture ?? input.material?.baseColorTexture ?? null,
864
+ metallicRoughnessTexture: input.metallicRoughnessTexture ?? input.material?.metallicRoughnessTexture ?? null,
865
+ normalTexture: input.normalTexture ?? input.material?.normalTexture ?? null,
866
+ occlusionTexture: input.occlusionTexture ?? input.material?.occlusionTexture ?? null,
867
+ emissiveTexture: input.emissiveTexture ?? input.material?.emissiveTexture ?? null
468
868
  });
469
869
  }
870
+ function clampUnit(value) {
871
+ return clamp(Number(value) || 0, 0, 1);
872
+ }
873
+ function srgbToLinear(value) {
874
+ const channel = clampUnit(value);
875
+ if (channel <= 0.04045) {
876
+ return channel / 12.92;
877
+ }
878
+ return ((channel + 0.055) / 1.055) ** 2.4;
879
+ }
880
+ function sampleTextureRgba(texture, uv = [0, 0], colorSpace = "linear") {
881
+ if (!texture || !Number.isFinite(texture.width) || !Number.isFinite(texture.height) || !texture.data || texture.width <= 0 || texture.height <= 0) {
882
+ return [1, 1, 1, 1];
883
+ }
884
+ const u = (uv[0] % 1 + 1) % 1;
885
+ const v = (uv[1] % 1 + 1) % 1;
886
+ const x = Math.min(texture.width - 1, Math.max(0, Math.round(u * (texture.width - 1))));
887
+ const y = Math.min(texture.height - 1, Math.max(0, Math.round((1 - v) * (texture.height - 1))));
888
+ const offset = (y * texture.width + x) * 4;
889
+ const data = texture.data;
890
+ const scale2 = resolveTextureSampleScale(data);
891
+ const defaultChannel = scale2 === 1 ? 1 : Math.round(1 / scale2);
892
+ const color = [
893
+ (data[offset] ?? defaultChannel) * scale2,
894
+ (data[offset + 1] ?? defaultChannel) * scale2,
895
+ (data[offset + 2] ?? defaultChannel) * scale2,
896
+ (data[offset + 3] ?? defaultChannel) * scale2
897
+ ];
898
+ if (colorSpace === "srgb") {
899
+ return [srgbToLinear(color[0]), srgbToLinear(color[1]), srgbToLinear(color[2]), color[3]];
900
+ }
901
+ return color;
902
+ }
903
+ function resolveTextureSampleScale(data) {
904
+ if (data instanceof Uint8Array || data instanceof Uint8ClampedArray) {
905
+ return 1 / 255;
906
+ }
907
+ if (data instanceof Uint16Array) {
908
+ return 1 / 65535;
909
+ }
910
+ if (Array.isArray(data) && data.some((value) => Number(value) > 1)) {
911
+ return 1 / 255;
912
+ }
913
+ return 1;
914
+ }
915
+ function normalizeVectorOrFallback(vector, fallback) {
916
+ return normalize(Array.isArray(vector) ? vector : fallback, fallback);
917
+ }
918
+ function buildTriangleTangentBasis(v0, v1, v2, uv0, uv1, uv2, fallbackNormal) {
919
+ const edge1 = subtract(v1, v0);
920
+ const edge2 = subtract(v2, v0);
921
+ const deltaUv1 = [uv1[0] - uv0[0], uv1[1] - uv0[1]];
922
+ const deltaUv2 = [uv2[0] - uv0[0], uv2[1] - uv0[1]];
923
+ const determinant = deltaUv1[0] * deltaUv2[1] - deltaUv1[1] * deltaUv2[0];
924
+ if (Math.abs(determinant) < 1e-6) {
925
+ const tangentFallback = Math.abs(fallbackNormal[1]) < 0.999 ? [0, 1, 0] : [1, 0, 0];
926
+ const tangent2 = normalize(cross(tangentFallback, fallbackNormal), [1, 0, 0]);
927
+ const bitangent2 = normalize(cross(fallbackNormal, tangent2), [0, 0, 1]);
928
+ return { tangent: tangent2, bitangent: bitangent2 };
929
+ }
930
+ const inverse = 1 / determinant;
931
+ const tangent = normalize(
932
+ [
933
+ inverse * (edge1[0] * deltaUv2[1] - edge2[0] * deltaUv1[1]),
934
+ inverse * (edge1[1] * deltaUv2[1] - edge2[1] * deltaUv1[1]),
935
+ inverse * (edge1[2] * deltaUv2[1] - edge2[2] * deltaUv1[1])
936
+ ],
937
+ [1, 0, 0]
938
+ );
939
+ const bitangent = normalize(
940
+ [
941
+ inverse * (-edge1[0] * deltaUv2[0] + edge2[0] * deltaUv1[0]),
942
+ inverse * (-edge1[1] * deltaUv2[0] + edge2[1] * deltaUv1[0]),
943
+ inverse * (-edge1[2] * deltaUv2[0] + edge2[2] * deltaUv1[0])
944
+ ],
945
+ [0, 0, 1]
946
+ );
947
+ return { tangent, bitangent };
948
+ }
949
+ function applyNormalMap(normal, tangent, bitangent, normalTexture, uv) {
950
+ if (!normalTexture) {
951
+ return normalizeVectorOrFallback(normal, [0, 1, 0]);
952
+ }
953
+ const sample = sampleTextureRgba(normalTexture, uv, "linear");
954
+ const strength = clampUnit(normalTexture.scale ?? 1);
955
+ const tangentNormal = normalize(
956
+ [
957
+ (sample[0] * 2 - 1) * strength,
958
+ (sample[1] * 2 - 1) * strength,
959
+ 1 + (sample[2] * 2 - 1 - 1) * strength
960
+ ],
961
+ [0, 0, 1]
962
+ );
963
+ return normalize(
964
+ [
965
+ tangent[0] * tangentNormal[0] + bitangent[0] * tangentNormal[1] + normal[0] * tangentNormal[2],
966
+ tangent[1] * tangentNormal[0] + bitangent[1] * tangentNormal[1] + normal[1] * tangentNormal[2],
967
+ tangent[2] * tangentNormal[0] + bitangent[2] * tangentNormal[1] + normal[2] * tangentNormal[2]
968
+ ],
969
+ normal
970
+ );
971
+ }
972
+ function sampleBaseColor(mesh, uv) {
973
+ const sample = mesh.baseColorTexture ? sampleTextureRgba(mesh.baseColorTexture, uv, "srgb") : [1, 1, 1, 1];
974
+ return [
975
+ clampUnit(mesh.color[0] * sample[0]),
976
+ clampUnit(mesh.color[1] * sample[1]),
977
+ clampUnit(mesh.color[2] * sample[2]),
978
+ clampUnit((mesh.color[3] ?? 1) * sample[3])
979
+ ];
980
+ }
981
+ function sampleSurfaceMaterial(mesh, uv) {
982
+ const textureSample = mesh.metallicRoughnessTexture ? sampleTextureRgba(mesh.metallicRoughnessTexture, uv, "linear") : [1, 1, 1, 1];
983
+ return {
984
+ roughness: clamp(mesh.roughness * textureSample[1], 0, 1),
985
+ metallic: clamp(mesh.metallic * textureSample[2], 0, 1)
986
+ };
987
+ }
988
+ function averageColors(colors) {
989
+ const count = Math.max(colors.length, 1);
990
+ return colors.reduce(
991
+ (accumulator, color) => [
992
+ accumulator[0] + color[0] / count,
993
+ accumulator[1] + color[1] / count,
994
+ accumulator[2] + color[2] / count,
995
+ accumulator[3] + color[3] / count
996
+ ],
997
+ [0, 0, 0, 0]
998
+ );
999
+ }
1000
+ function averageNumbers(values, fallback = 0) {
1001
+ if (!Array.isArray(values) || values.length === 0) {
1002
+ return fallback;
1003
+ }
1004
+ return values.reduce((sum, value) => sum + value, 0) / values.length;
1005
+ }
470
1006
  function createMeshTriangleRecords(meshes) {
471
1007
  const source = Array.isArray(meshes) ? meshes : [];
472
1008
  let nextTriangleId = 0;
@@ -487,6 +1023,16 @@ function createMeshTriangleRecords(meshes) {
487
1023
  const uv0 = mesh.uvs ? readVector2(mesh.uvs, a) : [0, 0];
488
1024
  const uv1 = mesh.uvs ? readVector2(mesh.uvs, b) : [0, 0];
489
1025
  const uv2 = mesh.uvs ? readVector2(mesh.uvs, c) : [0, 0];
1026
+ const tangentBasis = buildTriangleTangentBasis(v0, v1, v2, uv0, uv1, uv2, faceNormal);
1027
+ const shadedN0 = applyNormalMap(n0, tangentBasis.tangent, tangentBasis.bitangent, mesh.normalTexture, uv0);
1028
+ const shadedN1 = applyNormalMap(n1, tangentBasis.tangent, tangentBasis.bitangent, mesh.normalTexture, uv1);
1029
+ const shadedN2 = applyNormalMap(n2, tangentBasis.tangent, tangentBasis.bitangent, mesh.normalTexture, uv2);
1030
+ const sampledColors = [sampleBaseColor(mesh, uv0), sampleBaseColor(mesh, uv1), sampleBaseColor(mesh, uv2)];
1031
+ const sampledMaterials = [
1032
+ sampleSurfaceMaterial(mesh, uv0),
1033
+ sampleSurfaceMaterial(mesh, uv1),
1034
+ sampleSurfaceMaterial(mesh, uv2)
1035
+ ];
490
1036
  const bounds = triangleBounds(v0, v1, v2);
491
1037
  triangles.push(
492
1038
  Object.freeze({
@@ -496,18 +1042,42 @@ function createMeshTriangleRecords(meshes) {
496
1042
  flags: mesh.flags,
497
1043
  materialRefId: mesh.materialRefId,
498
1044
  mediumRefId: mesh.mediumRefId,
1045
+ materialSlot: meshIndex,
499
1046
  v0: Object.freeze(v0),
500
1047
  v1: Object.freeze(v1),
501
1048
  v2: Object.freeze(v2),
502
- n0: Object.freeze(n0),
503
- n1: Object.freeze(n1),
504
- n2: Object.freeze(n2),
1049
+ n0: Object.freeze(shadedN0),
1050
+ n1: Object.freeze(shadedN1),
1051
+ n2: Object.freeze(shadedN2),
505
1052
  uv0: Object.freeze(uv0),
506
1053
  uv1: Object.freeze(uv1),
507
1054
  uv2: Object.freeze(uv2),
508
- color: mesh.color,
1055
+ color: Object.freeze(averageColors(sampledColors)),
509
1056
  emission: mesh.emission,
510
- material: Object.freeze([mesh.roughness, mesh.metallic, mesh.opacity, mesh.ior]),
1057
+ material: Object.freeze([
1058
+ averageNumbers(sampledMaterials.map((sample) => sample.roughness), mesh.roughness),
1059
+ averageNumbers(sampledMaterials.map((sample) => sample.metallic), mesh.metallic),
1060
+ mesh.opacity,
1061
+ mesh.ior
1062
+ ]),
1063
+ materialResponse: Object.freeze([
1064
+ mesh.sheenColor[0] ?? 0,
1065
+ mesh.sheenColor[1] ?? 0,
1066
+ mesh.sheenColor[2] ?? 0,
1067
+ mesh.clearcoat
1068
+ ]),
1069
+ materialExtension: Object.freeze([
1070
+ mesh.clearcoatRoughness,
1071
+ mesh.specular,
1072
+ mesh.transmission,
1073
+ mesh.thickness
1074
+ ]),
1075
+ specularColor: Object.freeze([
1076
+ mesh.specularColor[0] ?? 1,
1077
+ mesh.specularColor[1] ?? 1,
1078
+ mesh.specularColor[2] ?? 1,
1079
+ 1
1080
+ ]),
511
1081
  bounds: Object.freeze({
512
1082
  min: Object.freeze(bounds.min),
513
1083
  max: Object.freeze(bounds.max)
@@ -616,50 +1186,264 @@ function nextPowerOfTwo(value) {
616
1186
  }
617
1187
  return 2 ** Math.ceil(Math.log2(value));
618
1188
  }
619
- function estimateBvhLeafSortCapacity(triangleCount) {
620
- return triangleCount <= 0 ? 0 : nextPowerOfTwo(triangleCount);
621
- }
622
- function createWavefrontBvhSortStages(itemCountInput) {
623
- const itemCount = readNonNegativeInteger("itemCount", itemCountInput, 0);
624
- const sortCount = estimateBvhLeafSortCapacity(itemCount);
625
- if (sortCount <= 1) {
626
- return Object.freeze([]);
1189
+ function textureComponentToByte(value, fallback) {
1190
+ const numeric = Number(value);
1191
+ if (!Number.isFinite(numeric)) {
1192
+ return fallback;
627
1193
  }
628
- const stages = [];
629
- for (let sequenceSize = 2; sequenceSize <= sortCount; sequenceSize *= 2) {
630
- for (let compareDistance = sequenceSize / 2; compareDistance >= 1; compareDistance /= 2) {
631
- stages.push(
632
- Object.freeze({
633
- compareDistance,
634
- sequenceSize
635
- })
636
- );
637
- }
1194
+ if (numeric >= 0 && numeric <= 1) {
1195
+ return Math.max(0, Math.min(255, Math.round(numeric * 255)));
638
1196
  }
639
- return Object.freeze(stages);
1197
+ return Math.max(0, Math.min(255, Math.round(numeric)));
640
1198
  }
641
- function createWavefrontBvhBuildLevels(triangleCountInput) {
642
- const triangleCount = readNonNegativeInteger("triangleCount", triangleCountInput, 0);
643
- const internalCount = Math.max(0, triangleCount - 1);
644
- if (internalCount === 0) {
645
- return Object.freeze([]);
1199
+ function createSolidTextureSample(width, height, rgba) {
1200
+ const data = new Uint8Array(width * height * 4);
1201
+ for (let offset = 0; offset < data.length; offset += 4) {
1202
+ data[offset] = rgba[0];
1203
+ data[offset + 1] = rgba[1];
1204
+ data[offset + 2] = rgba[2];
1205
+ data[offset + 3] = rgba[3];
646
1206
  }
647
- const levels = [];
648
- let depth = 0;
649
- while (Math.pow(2, depth) - 1 < internalCount) {
650
- depth += 1;
1207
+ return Object.freeze({
1208
+ width,
1209
+ height,
1210
+ data
1211
+ });
1212
+ }
1213
+ function normalizeTextureSampleInput(texture, fallbackColor) {
1214
+ if (!texture || !Number.isFinite(texture.width) || !Number.isFinite(texture.height) || texture.width <= 0 || texture.height <= 0) {
1215
+ return createSolidTextureSample(1, 1, fallbackColor);
651
1216
  }
652
- for (let level = depth - 1; level >= 0; level -= 1) {
653
- const start = Math.pow(2, level) - 1;
654
- const end = Math.min(Math.pow(2, level + 1) - 2, internalCount - 1);
655
- if (end >= start) {
656
- levels.push(
657
- Object.freeze({
658
- start,
659
- count: end - start + 1
660
- })
661
- );
662
- }
1217
+ const pixelCount = Math.trunc(texture.width) * Math.trunc(texture.height) * 4;
1218
+ const source = ArrayBuffer.isView(texture.data) || Array.isArray(texture.data) ? texture.data : null;
1219
+ if (!source || source.length < pixelCount) {
1220
+ return createSolidTextureSample(1, 1, fallbackColor);
1221
+ }
1222
+ const data = new Uint8Array(pixelCount);
1223
+ for (let index = 0; index < pixelCount; index += 1) {
1224
+ data[index] = textureComponentToByte(source[index], fallbackColor[index % 4]);
1225
+ }
1226
+ return Object.freeze({
1227
+ width: Math.trunc(texture.width),
1228
+ height: Math.trunc(texture.height),
1229
+ data
1230
+ });
1231
+ }
1232
+ function buildTextureAtlas(textures, fallbackColor) {
1233
+ const padding = 1;
1234
+ const defaultTexture = createSolidTextureSample(1, 1, fallbackColor);
1235
+ const uniqueEntries = [{ source: null, texture: defaultTexture }];
1236
+ const bySource = /* @__PURE__ */ new Map();
1237
+ for (const texture of Array.isArray(textures) ? textures : []) {
1238
+ if (!texture || bySource.has(texture)) {
1239
+ continue;
1240
+ }
1241
+ const normalized = normalizeTextureSampleInput(texture, fallbackColor);
1242
+ bySource.set(texture, uniqueEntries.length);
1243
+ uniqueEntries.push({ source: texture, texture: normalized });
1244
+ }
1245
+ const totalArea = uniqueEntries.reduce((sum, entry) => {
1246
+ return sum + (entry.texture.width + padding * 2) * (entry.texture.height + padding * 2);
1247
+ }, 0);
1248
+ const maxTileWidth = uniqueEntries.reduce((maxWidth, entry) => {
1249
+ return Math.max(maxWidth, entry.texture.width + padding * 2);
1250
+ }, 1);
1251
+ const targetWidth = Math.max(
1252
+ maxTileWidth,
1253
+ nextPowerOfTwo(Math.max(maxTileWidth, Math.ceil(Math.sqrt(totalArea))))
1254
+ );
1255
+ let cursorX = 0;
1256
+ let cursorY = 0;
1257
+ let rowHeight = 0;
1258
+ let atlasWidth = 0;
1259
+ const placements = uniqueEntries.map((entry) => {
1260
+ const tileWidth = entry.texture.width + padding * 2;
1261
+ const tileHeight = entry.texture.height + padding * 2;
1262
+ if (cursorX > 0 && cursorX + tileWidth > targetWidth) {
1263
+ cursorX = 0;
1264
+ cursorY += rowHeight;
1265
+ rowHeight = 0;
1266
+ }
1267
+ const placement = Object.freeze({
1268
+ x: cursorX,
1269
+ y: cursorY,
1270
+ tileWidth,
1271
+ tileHeight,
1272
+ width: entry.texture.width,
1273
+ height: entry.texture.height
1274
+ });
1275
+ cursorX += tileWidth;
1276
+ atlasWidth = Math.max(atlasWidth, cursorX);
1277
+ rowHeight = Math.max(rowHeight, tileHeight);
1278
+ return placement;
1279
+ });
1280
+ const atlasHeight = Math.max(1, cursorY + rowHeight);
1281
+ const atlasData = new Uint8Array(Math.max(1, atlasWidth * atlasHeight * 4));
1282
+ const writePixel = (x, y, rgba) => {
1283
+ const offset = (y * atlasWidth + x) * 4;
1284
+ atlasData[offset] = rgba[0];
1285
+ atlasData[offset + 1] = rgba[1];
1286
+ atlasData[offset + 2] = rgba[2];
1287
+ atlasData[offset + 3] = rgba[3];
1288
+ };
1289
+ const rects = placements.map((placement, entryIndex) => {
1290
+ const { texture } = uniqueEntries[entryIndex];
1291
+ for (let y = 0; y < placement.tileHeight; y += 1) {
1292
+ for (let x = 0; x < placement.tileWidth; x += 1) {
1293
+ const sampleX = Math.max(0, Math.min(texture.width - 1, x - padding));
1294
+ const sampleY = Math.max(0, Math.min(texture.height - 1, y - padding));
1295
+ const sourceOffset = (sampleY * texture.width + sampleX) * 4;
1296
+ writePixel(placement.x + x, placement.y + y, texture.data.slice(sourceOffset, sourceOffset + 4));
1297
+ }
1298
+ }
1299
+ return Object.freeze([
1300
+ (placement.x + padding) / Math.max(1, atlasWidth),
1301
+ (placement.y + padding) / Math.max(1, atlasHeight),
1302
+ placement.width / Math.max(1, atlasWidth),
1303
+ placement.height / Math.max(1, atlasHeight)
1304
+ ]);
1305
+ });
1306
+ const rectBySource = /* @__PURE__ */ new Map();
1307
+ uniqueEntries.forEach((entry, index) => {
1308
+ if (entry.source) {
1309
+ rectBySource.set(entry.source, rects[index]);
1310
+ }
1311
+ });
1312
+ return Object.freeze({
1313
+ width: Math.max(1, atlasWidth),
1314
+ height: Math.max(1, atlasHeight),
1315
+ data: atlasData,
1316
+ defaultRect: rects[0],
1317
+ resolveRect(texture) {
1318
+ return rectBySource.get(texture) ?? rects[0];
1319
+ }
1320
+ });
1321
+ }
1322
+ function createWavefrontGpuMaterialSource(meshes = []) {
1323
+ const source = Array.isArray(meshes) ? meshes : [meshes];
1324
+ const normalized = source.map((meshInput, meshIndex) => normalizeWavefrontMesh(meshInput, meshIndex));
1325
+ const baseColorAtlas = buildTextureAtlas(
1326
+ normalized.map((mesh) => mesh.baseColorTexture),
1327
+ [255, 255, 255, 255]
1328
+ );
1329
+ const metallicRoughnessAtlas = buildTextureAtlas(
1330
+ normalized.map((mesh) => mesh.metallicRoughnessTexture),
1331
+ [255, 255, 255, 255]
1332
+ );
1333
+ const normalAtlas = buildTextureAtlas(
1334
+ normalized.map((mesh) => mesh.normalTexture),
1335
+ [128, 128, 255, 255]
1336
+ );
1337
+ const occlusionAtlas = buildTextureAtlas(
1338
+ normalized.map((mesh) => mesh.occlusionTexture),
1339
+ [255, 255, 255, 255]
1340
+ );
1341
+ const emissiveAtlas = buildTextureAtlas(
1342
+ normalized.map((mesh) => mesh.emissiveTexture),
1343
+ [255, 255, 255, 255]
1344
+ );
1345
+ const bytes = new ArrayBuffer(Math.max(1, normalized.length) * GPU_MATERIAL_RECORD_BYTES);
1346
+ const floatView = new Float32Array(bytes);
1347
+ normalized.forEach((mesh, meshIndex) => {
1348
+ const byteOffset = meshIndex * GPU_MATERIAL_RECORD_BYTES;
1349
+ writeVec4(floatView, byteOffset, mesh.color);
1350
+ writeVec4(floatView, byteOffset + 16, mesh.emission);
1351
+ writeVec4(floatView, byteOffset + 32, [
1352
+ mesh.roughness,
1353
+ mesh.metallic,
1354
+ mesh.opacity,
1355
+ mesh.ior
1356
+ ]);
1357
+ writeVec4(floatView, byteOffset + 48, [
1358
+ mesh.sheenColor[0] ?? 0,
1359
+ mesh.sheenColor[1] ?? 0,
1360
+ mesh.sheenColor[2] ?? 0,
1361
+ mesh.clearcoat
1362
+ ]);
1363
+ writeVec4(floatView, byteOffset + 64, [
1364
+ mesh.clearcoatRoughness,
1365
+ mesh.specular,
1366
+ mesh.transmission,
1367
+ mesh.thickness
1368
+ ]);
1369
+ writeVec4(floatView, byteOffset + 80, [
1370
+ mesh.specularColor[0] ?? 1,
1371
+ mesh.specularColor[1] ?? 1,
1372
+ mesh.specularColor[2] ?? 1,
1373
+ 1
1374
+ ]);
1375
+ writeVec4(floatView, byteOffset + 96, baseColorAtlas.resolveRect(mesh.baseColorTexture));
1376
+ writeVec4(
1377
+ floatView,
1378
+ byteOffset + 112,
1379
+ metallicRoughnessAtlas.resolveRect(mesh.metallicRoughnessTexture)
1380
+ );
1381
+ writeVec4(floatView, byteOffset + 128, normalAtlas.resolveRect(mesh.normalTexture));
1382
+ writeVec4(floatView, byteOffset + 144, occlusionAtlas.resolveRect(mesh.occlusionTexture));
1383
+ writeVec4(floatView, byteOffset + 160, emissiveAtlas.resolveRect(mesh.emissiveTexture));
1384
+ writeVec4(floatView, byteOffset + 176, [
1385
+ clampUnit(mesh.normalTexture?.scale ?? mesh.normalTexture?.strength ?? 1),
1386
+ clampUnit(mesh.occlusionTexture?.strength ?? 1),
1387
+ clampUnit(mesh.emissiveTexture?.strength ?? 1),
1388
+ 0
1389
+ ]);
1390
+ });
1391
+ return Object.freeze({
1392
+ buffer: bytes,
1393
+ count: normalized.length,
1394
+ recordBytes: GPU_MATERIAL_RECORD_BYTES,
1395
+ records: Object.freeze(normalized),
1396
+ baseColorAtlas,
1397
+ metallicRoughnessAtlas,
1398
+ normalAtlas,
1399
+ occlusionAtlas,
1400
+ emissiveAtlas
1401
+ });
1402
+ }
1403
+ function estimateBvhLeafSortCapacity(triangleCount) {
1404
+ return triangleCount <= 0 ? 0 : nextPowerOfTwo(triangleCount);
1405
+ }
1406
+ function createWavefrontBvhSortStages(itemCountInput) {
1407
+ const itemCount = readNonNegativeInteger("itemCount", itemCountInput, 0);
1408
+ const sortCount = estimateBvhLeafSortCapacity(itemCount);
1409
+ if (sortCount <= 1) {
1410
+ return Object.freeze([]);
1411
+ }
1412
+ const stages = [];
1413
+ for (let sequenceSize = 2; sequenceSize <= sortCount; sequenceSize *= 2) {
1414
+ for (let compareDistance = sequenceSize / 2; compareDistance >= 1; compareDistance /= 2) {
1415
+ stages.push(
1416
+ Object.freeze({
1417
+ compareDistance,
1418
+ sequenceSize
1419
+ })
1420
+ );
1421
+ }
1422
+ }
1423
+ return Object.freeze(stages);
1424
+ }
1425
+ function createWavefrontBvhBuildLevels(triangleCountInput) {
1426
+ const triangleCount = readNonNegativeInteger("triangleCount", triangleCountInput, 0);
1427
+ const internalCount = Math.max(0, triangleCount - 1);
1428
+ if (internalCount === 0) {
1429
+ return Object.freeze([]);
1430
+ }
1431
+ const levels = [];
1432
+ let depth = 0;
1433
+ while (Math.pow(2, depth) - 1 < internalCount) {
1434
+ depth += 1;
1435
+ }
1436
+ for (let level = depth - 1; level >= 0; level -= 1) {
1437
+ const start = Math.pow(2, level) - 1;
1438
+ const end = Math.min(Math.pow(2, level + 1) - 2, internalCount - 1);
1439
+ if (end >= start) {
1440
+ levels.push(
1441
+ Object.freeze({
1442
+ start,
1443
+ count: end - start + 1
1444
+ })
1445
+ );
1446
+ }
663
1447
  }
664
1448
  return Object.freeze(levels);
665
1449
  }
@@ -673,9 +1457,10 @@ function resolveAccelerationBuildMode(options = {}) {
673
1457
  }
674
1458
  return mode;
675
1459
  }
676
- function createWavefrontGpuMeshSource(meshes = []) {
1460
+ function createWavefrontGpuMeshSource(meshes = [], gpuMaterialSourceInput = null) {
677
1461
  const source = Array.isArray(meshes) ? meshes : [meshes];
678
1462
  const normalized = source.map((meshInput, meshIndex) => normalizeWavefrontMesh(meshInput, meshIndex));
1463
+ const gpuMaterialSource = gpuMaterialSourceInput ?? createWavefrontGpuMaterialSource(normalized);
679
1464
  const vertexCount = normalized.reduce((count, mesh) => count + mesh.positions.length / 3, 0);
680
1465
  const indexCount = normalized.reduce((count, mesh) => count + mesh.indices.length, 0);
681
1466
  const triangleCount = Math.floor(indexCount / 3);
@@ -727,7 +1512,7 @@ function createWavefrontGpuMeshSource(meshes = []) {
727
1512
  meshUints[meshOffset + 8] = mesh.indices.length / 3;
728
1513
  meshUints[meshOffset + 9] = meshVertexBase;
729
1514
  meshUints[meshOffset + 10] = meshVertexCount;
730
- meshUints[meshOffset + 11] = 0;
1515
+ meshUints[meshOffset + 11] = meshIndex;
731
1516
  const floatOffset = meshOffset;
732
1517
  writeVec4(meshFloats, floatOffset * 4 + 48, mesh.color);
733
1518
  writeVec4(meshFloats, floatOffset * 4 + 64, mesh.emission);
@@ -737,6 +1522,55 @@ function createWavefrontGpuMeshSource(meshes = []) {
737
1522
  mesh.opacity,
738
1523
  mesh.ior
739
1524
  ]);
1525
+ writeVec4(meshFloats, floatOffset * 4 + 96, [
1526
+ mesh.sheenColor[0] ?? 0,
1527
+ mesh.sheenColor[1] ?? 0,
1528
+ mesh.sheenColor[2] ?? 0,
1529
+ mesh.clearcoat
1530
+ ]);
1531
+ writeVec4(meshFloats, floatOffset * 4 + 112, [
1532
+ mesh.clearcoatRoughness,
1533
+ mesh.specular,
1534
+ mesh.transmission,
1535
+ mesh.thickness
1536
+ ]);
1537
+ writeVec4(meshFloats, floatOffset * 4 + 128, [
1538
+ mesh.specularColor[0] ?? 1,
1539
+ mesh.specularColor[1] ?? 1,
1540
+ mesh.specularColor[2] ?? 1,
1541
+ 1
1542
+ ]);
1543
+ writeVec4(
1544
+ meshFloats,
1545
+ floatOffset * 4 + 144,
1546
+ gpuMaterialSource.baseColorAtlas.resolveRect(mesh.baseColorTexture)
1547
+ );
1548
+ writeVec4(
1549
+ meshFloats,
1550
+ floatOffset * 4 + 160,
1551
+ gpuMaterialSource.metallicRoughnessAtlas.resolveRect(mesh.metallicRoughnessTexture)
1552
+ );
1553
+ writeVec4(
1554
+ meshFloats,
1555
+ floatOffset * 4 + 176,
1556
+ gpuMaterialSource.normalAtlas.resolveRect(mesh.normalTexture)
1557
+ );
1558
+ writeVec4(
1559
+ meshFloats,
1560
+ floatOffset * 4 + 192,
1561
+ gpuMaterialSource.occlusionAtlas.resolveRect(mesh.occlusionTexture)
1562
+ );
1563
+ writeVec4(
1564
+ meshFloats,
1565
+ floatOffset * 4 + 208,
1566
+ gpuMaterialSource.emissiveAtlas.resolveRect(mesh.emissiveTexture)
1567
+ );
1568
+ writeVec4(meshFloats, floatOffset * 4 + 224, [
1569
+ clampUnit(mesh.normalTexture?.scale ?? mesh.normalTexture?.strength ?? 1),
1570
+ clampUnit(mesh.occlusionTexture?.strength ?? 1),
1571
+ clampUnit(mesh.emissiveTexture?.strength ?? 1),
1572
+ 0
1573
+ ]);
740
1574
  vertexCursor += meshVertexCount;
741
1575
  indexCursor += mesh.indices.length;
742
1576
  triangleCursor += mesh.indices.length / 3;
@@ -799,12 +1633,16 @@ function normalizeSceneObjects(sceneObjects, useDefaultScene = true) {
799
1633
  const source = Array.isArray(sceneObjects) && sceneObjects.length > 0 ? sceneObjects : useDefaultScene ? createDefaultWavefrontSceneObjects() : [];
800
1634
  return source.map((object, index) => normalizeWavefrontSceneObject(object, index));
801
1635
  }
1636
+ function normalizeWavefrontMeshes(meshes) {
1637
+ const source = Array.isArray(meshes) ? meshes : [];
1638
+ return source.map((mesh, index) => normalizeWavefrontMesh(mesh, index));
1639
+ }
802
1640
  function normalizeMeshes(options = {}) {
803
1641
  if (Array.isArray(options.meshes)) {
804
- return options.meshes;
1642
+ return normalizeWavefrontMeshes(options.meshes);
805
1643
  }
806
1644
  if (options.mesh) {
807
- return [options.mesh];
1645
+ return normalizeWavefrontMeshes([options.mesh]);
808
1646
  }
809
1647
  return [];
810
1648
  }
@@ -1039,12 +1877,14 @@ function estimateWavefrontPathTracingMemory(options = {}) {
1039
1877
  options.environmentPortalCapacity,
1040
1878
  0
1041
1879
  );
1880
+ const materialCapacity = readNonNegativeInteger("materialCapacity", options.materialCapacity, 0);
1042
1881
  const queueBytes = tilePixelCapacity * RAY_RECORD_BYTES;
1043
1882
  const hitBytes = tilePixelCapacity * HIT_RECORD_BYTES;
1044
1883
  const accumulationBytes = tilePixelCapacity * ACCUMULATION_RECORD_BYTES;
1045
1884
  const pathVertexBytes = tilePixelCapacity * (maxDepth + 1) * PATH_VERTEX_RECORD_BYTES;
1046
1885
  const sceneObjectBytes = sceneObjectCapacity * SCENE_OBJECT_RECORD_BYTES;
1047
1886
  const triangleBytes = triangleCapacity * TRIANGLE_RECORD_BYTES;
1887
+ const materialTableBytes = materialCapacity * GPU_MATERIAL_RECORD_BYTES;
1048
1888
  const bvhNodeBytes = bvhNodeCapacity * BVH_NODE_RECORD_BYTES;
1049
1889
  const bvhLeafReferenceBytes = bvhLeafSortCapacity * BVH_LEAF_REF_RECORD_BYTES;
1050
1890
  const emissiveTriangleMetadataBytes = emissiveTriangleCapacity * BVH_NODE_RECORD_BYTES;
@@ -1057,6 +1897,7 @@ function estimateWavefrontPathTracingMemory(options = {}) {
1057
1897
  pathVertexBytes,
1058
1898
  sceneObjectBytes,
1059
1899
  triangleBytes,
1900
+ materialTableBytes,
1060
1901
  bvhNodeBytes,
1061
1902
  bvhLeafReferenceBytes,
1062
1903
  emissiveTriangleMetadataBytes,
@@ -1064,7 +1905,7 @@ function estimateWavefrontPathTracingMemory(options = {}) {
1064
1905
  configBytes: CONFIG_BUFFER_BYTES,
1065
1906
  counterBytes: COUNTER_BUFFER_BYTES,
1066
1907
  indirectDispatchBytes: INDIRECT_DISPATCH_ARGS_BYTES,
1067
- totalHotBufferBytes: queueBytes * 2 + hitBytes + accumulationBytes + pathVertexBytes + sceneObjectBytes + triangleBytes + bvhNodeBytes + bvhLeafReferenceBytes + emissiveTriangleMetadataBytes + environmentPortalBytes + CONFIG_BUFFER_BYTES + COUNTER_BUFFER_BYTES + INDIRECT_DISPATCH_ARGS_BYTES
1908
+ totalHotBufferBytes: queueBytes * 2 + hitBytes + accumulationBytes + pathVertexBytes + sceneObjectBytes + triangleBytes + materialTableBytes + bvhNodeBytes + bvhLeafReferenceBytes + emissiveTriangleMetadataBytes + environmentPortalBytes + CONFIG_BUFFER_BYTES + COUNTER_BUFFER_BYTES + INDIRECT_DISPATCH_ARGS_BYTES
1068
1909
  });
1069
1910
  }
1070
1911
  function createWavefrontPathTracingComputeConfig(options = {}) {
@@ -1078,7 +1919,7 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
1078
1919
  const samplesPerPixel = clamp(
1079
1920
  readPositiveInteger("samplesPerPixel", options.samplesPerPixel, DEFAULT_SAMPLES_PER_PIXEL),
1080
1921
  1,
1081
- 64
1922
+ MAX_SAMPLES_PER_PIXEL
1082
1923
  );
1083
1924
  const maxFramePassesPerSubmission = clamp(
1084
1925
  readPositiveInteger(
@@ -1096,7 +1937,8 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
1096
1937
  );
1097
1938
  const meshes = normalizeMeshes(options);
1098
1939
  const meshSourceShape = estimateMeshSourceShape(meshes);
1099
- const gpuMeshSource = meshes.length > 0 ? createWavefrontGpuMeshSource(meshes) : createWavefrontGpuMeshSource([]);
1940
+ const gpuMaterialSource = meshes.length > 0 ? createWavefrontGpuMaterialSource(meshes) : createWavefrontGpuMaterialSource([]);
1941
+ const gpuMeshSource = meshes.length > 0 ? createWavefrontGpuMeshSource(meshes, gpuMaterialSource) : createWavefrontGpuMeshSource([]);
1100
1942
  const meshAcceleration = accelerationBuildMode === "cpu-debug" ? createWavefrontMeshAcceleration(meshes) : Object.freeze({ nodes: Object.freeze([]), triangles: Object.freeze([]) });
1101
1943
  const emissiveTriangleIndices = createWavefrontEmissiveTriangleIndexSource(
1102
1944
  meshes,
@@ -1107,6 +1949,7 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
1107
1949
  const sceneObjects = Object.freeze(
1108
1950
  normalizeSceneObjects(options.sceneObjects, meshes.length === 0)
1109
1951
  );
1952
+ const mediums = collectWavefrontMediums(options, meshes, sceneObjects);
1110
1953
  const sceneObjectCapacity = Math.max(
1111
1954
  sceneObjects.length,
1112
1955
  readPositiveInteger("sceneObjectCapacity", options.sceneObjectCapacity, DEFAULT_SCENE_OBJECT_CAPACITY)
@@ -1165,9 +2008,12 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
1165
2008
  sceneObjects,
1166
2009
  sceneObjectCount: sceneObjects.length,
1167
2010
  sceneObjectCapacity,
2011
+ mediums,
2012
+ mediumCount: mediums.length,
1168
2013
  accelerationBuildMode,
1169
2014
  gpuAccelerationBuildRequired: accelerationBuildMode === "gpu" && triangleCount > 0,
1170
2015
  gpuMeshSource,
2016
+ gpuMaterialSource,
1171
2017
  meshAcceleration,
1172
2018
  emissiveTriangleIndices,
1173
2019
  emissiveTriangleCount: emissiveTriangleIndices.count,
@@ -1198,6 +2044,7 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
1198
2044
  maxDepth,
1199
2045
  sceneObjectCapacity,
1200
2046
  triangleCapacity,
2047
+ materialCapacity: gpuMaterialSource.count,
1201
2048
  bvhNodeCapacity,
1202
2049
  bvhLeafSortCapacity,
1203
2050
  emissiveTriangleCapacity: emissiveTriangleIndices.capacity,
@@ -1264,16 +2111,35 @@ function packWavefrontSceneObjects(sceneObjects, capacity = sceneObjects.length)
1264
2111
  uintView[u32 + 1] = object.id;
1265
2112
  uintView[u32 + 2] = object.materialKind;
1266
2113
  uintView[u32 + 3] = object.flags;
1267
- writeVec4(floatView, byteOffset + 16, [...object.center, 0]);
1268
- writeVec4(floatView, byteOffset + 32, [...object.halfExtent, 0]);
1269
- writeVec4(floatView, byteOffset + 48, object.color);
1270
- writeVec4(floatView, byteOffset + 64, object.emission);
1271
- writeVec4(floatView, byteOffset + 80, [
2114
+ uintView[u32 + 4] = object.mediumRefId;
2115
+ writeVec4(floatView, byteOffset + 32, [...object.center, 0]);
2116
+ writeVec4(floatView, byteOffset + 48, [...object.halfExtent, 0]);
2117
+ writeVec4(floatView, byteOffset + 64, object.color);
2118
+ writeVec4(floatView, byteOffset + 80, object.emission);
2119
+ writeVec4(floatView, byteOffset + 96, [
1272
2120
  object.roughness,
1273
2121
  object.metallic,
1274
2122
  object.opacity,
1275
2123
  object.ior
1276
2124
  ]);
2125
+ writeVec4(floatView, byteOffset + 112, [
2126
+ object.sheenColor[0] ?? 0,
2127
+ object.sheenColor[1] ?? 0,
2128
+ object.sheenColor[2] ?? 0,
2129
+ object.clearcoat
2130
+ ]);
2131
+ writeVec4(floatView, byteOffset + 128, [
2132
+ object.clearcoatRoughness,
2133
+ object.specular,
2134
+ object.transmission,
2135
+ object.thickness
2136
+ ]);
2137
+ writeVec4(floatView, byteOffset + 144, [
2138
+ object.specularColor[0] ?? 1,
2139
+ object.specularColor[1] ?? 1,
2140
+ object.specularColor[2] ?? 1,
2141
+ 1
2142
+ ]);
1277
2143
  });
1278
2144
  return Object.freeze({
1279
2145
  buffer: bytes,
@@ -1298,7 +2164,7 @@ function packWavefrontTriangles(triangles, capacity = triangles.length) {
1298
2164
  uintView[u32 + 3] = triangle.flags;
1299
2165
  uintView[u32 + 4] = triangle.materialRefId;
1300
2166
  uintView[u32 + 5] = triangle.mediumRefId;
1301
- uintView[u32 + 6] = 0;
2167
+ uintView[u32 + 6] = triangle.materialSlot ?? 0;
1302
2168
  uintView[u32 + 7] = 0;
1303
2169
  writeVec4(floatView, byteOffset + 32, [...triangle.v0, 0]);
1304
2170
  writeVec4(floatView, byteOffset + 48, [...triangle.v1, 0]);
@@ -1311,6 +2177,15 @@ function packWavefrontTriangles(triangles, capacity = triangles.length) {
1311
2177
  writeVec4(floatView, byteOffset + 160, triangle.color);
1312
2178
  writeVec4(floatView, byteOffset + 176, triangle.emission);
1313
2179
  writeVec4(floatView, byteOffset + 192, triangle.material);
2180
+ writeVec4(floatView, byteOffset + 208, triangle.materialResponse);
2181
+ writeVec4(floatView, byteOffset + 224, triangle.materialExtension ?? [0.08, 1, 0, 0]);
2182
+ writeVec4(floatView, byteOffset + 240, triangle.specularColor ?? [1, 1, 1, 1]);
2183
+ writeVec4(floatView, byteOffset + 256, triangle.baseColorAtlas ?? [0, 0, 1, 1]);
2184
+ writeVec4(floatView, byteOffset + 272, triangle.metallicRoughnessAtlas ?? [0, 0, 1, 1]);
2185
+ writeVec4(floatView, byteOffset + 288, triangle.normalAtlas ?? [0, 0, 1, 1]);
2186
+ writeVec4(floatView, byteOffset + 304, triangle.occlusionAtlas ?? [0, 0, 1, 1]);
2187
+ writeVec4(floatView, byteOffset + 320, triangle.emissiveAtlas ?? [0, 0, 1, 1]);
2188
+ writeVec4(floatView, byteOffset + 336, triangle.textureSettings ?? [1, 1, 1, 0]);
1314
2189
  });
1315
2190
  return Object.freeze({
1316
2191
  buffer: bytes,
@@ -1404,6 +2279,12 @@ function createConfigPayload(config, tile, frameIndex, buildRange = {}) {
1404
2279
  0,
1405
2280
  0
1406
2281
  ]);
2282
+ writeVec4(floatView, 304, [
2283
+ config.environmentMap.width ?? 1,
2284
+ config.environmentMap.height ?? 1,
2285
+ config.environmentMap.mipLevelCount ?? 1,
2286
+ config.environmentMap.hasImportanceData ? 1 : 0
2287
+ ]);
1407
2288
  return bytes;
1408
2289
  }
1409
2290
  function createTiles(width, height, tileSize) {
@@ -1577,7 +2458,8 @@ function intersectWavefrontReferenceTriangle(ray, triangle, options = {}) {
1577
2458
  position: Object.freeze(position),
1578
2459
  color: triangle.color,
1579
2460
  emission: triangle.emission,
1580
- material: triangle.material
2461
+ material: triangle.material,
2462
+ materialResponse: triangle.materialResponse
1581
2463
  });
1582
2464
  }
1583
2465
  function createWavefrontReferenceEnvironmentHit(config, ray) {
@@ -1603,7 +2485,8 @@ function createWavefrontReferenceEnvironmentHit(config, ray) {
1603
2485
  position: Object.freeze(add(ray.origin, scale(ray.direction, 1e3))),
1604
2486
  color: Object.freeze([0, 0, 0, 0]),
1605
2487
  emission: radiance,
1606
- material: Object.freeze([1, 0, 1, 1])
2488
+ material: Object.freeze([1, 0, 1, 1]),
2489
+ materialResponse: Object.freeze([0, 0, 0, 0])
1607
2490
  });
1608
2491
  }
1609
2492
  function traceWavefrontReferenceTriangles(config, ray, triangles, options = {}) {
@@ -1682,6 +2565,32 @@ function environmentMapIntegerScale(data) {
1682
2565
  }
1683
2566
  return 1;
1684
2567
  }
2568
+ function environmentMapHasSamplingData(environmentMap) {
2569
+ if (!environmentMap || !environmentMap.data) {
2570
+ return false;
2571
+ }
2572
+ const width = Math.max(1, environmentMap.width ?? 1);
2573
+ const height = Math.max(1, environmentMap.height ?? 1);
2574
+ return environmentMap.data.length >= width * height * 4;
2575
+ }
2576
+ function createRgba8TextureUpload(source) {
2577
+ const width = Math.max(1, Math.trunc(source.width));
2578
+ const height = Math.max(1, Math.trunc(source.height));
2579
+ const bytesPerRow = alignTo(width * 4, 256);
2580
+ const bytes = new Uint8Array(bytesPerRow * height);
2581
+ const data = source.data instanceof Uint8Array ? source.data : new Uint8Array(source.data);
2582
+ for (let y = 0; y < height; y += 1) {
2583
+ const sourceOffset = y * width * 4;
2584
+ const targetOffset = y * bytesPerRow;
2585
+ bytes.set(data.subarray(sourceOffset, sourceOffset + width * 4), targetOffset);
2586
+ }
2587
+ return Object.freeze({
2588
+ bytes,
2589
+ bytesPerRow,
2590
+ width,
2591
+ height
2592
+ });
2593
+ }
1685
2594
  function readEnvironmentMapComponent(data, index, fallback, integerScale = 1) {
1686
2595
  if (!data || index >= data.length) {
1687
2596
  return fallback;
@@ -1689,39 +2598,311 @@ function readEnvironmentMapComponent(data, index, fallback, integerScale = 1) {
1689
2598
  const value = Number(data[index]);
1690
2599
  return Number.isFinite(value) ? Math.max(0, value) * integerScale : fallback;
1691
2600
  }
1692
- function createEnvironmentMapUploadBytes(environmentMap, fallbackColor) {
1693
- const width = Math.max(1, environmentMap.width);
1694
- const height = Math.max(1, environmentMap.height);
2601
+ function buildOrthonormalBasis(normal) {
2602
+ const tangentFallback = Math.abs(normal[1]) < 0.999 ? [0, 1, 0] : [1, 0, 0];
2603
+ const tangent = normalize(cross(tangentFallback, normal), [1, 0, 0]);
2604
+ const bitangent = normalize(cross(normal, tangent), [0, 0, 1]);
2605
+ return { tangent, bitangent };
2606
+ }
2607
+ function localToWorld(local, normal) {
2608
+ const basis = buildOrthonormalBasis(normal);
2609
+ return normalize(
2610
+ add(
2611
+ add(scale(basis.tangent, local[0]), scale(basis.bitangent, local[1])),
2612
+ scale(normal, local[2])
2613
+ ),
2614
+ normal
2615
+ );
2616
+ }
2617
+ function radicalInverseVdc(bits) {
2618
+ let value = bits >>> 0;
2619
+ value = (value << 16 | value >>> 16) >>> 0;
2620
+ value = ((value & 1431655765) << 1 | (value & 2863311530) >>> 1) >>> 0;
2621
+ value = ((value & 858993459) << 2 | (value & 3435973836) >>> 2) >>> 0;
2622
+ value = ((value & 252645135) << 4 | (value & 4042322160) >>> 4) >>> 0;
2623
+ value = ((value & 16711935) << 8 | (value & 4278255360) >>> 8) >>> 0;
2624
+ return value * 23283064365386963e-26;
2625
+ }
2626
+ function hammersley(index, count) {
2627
+ return [index / Math.max(count, 1), radicalInverseVdc(index)];
2628
+ }
2629
+ function importanceSampleGgx(sample, roughness, normal) {
2630
+ const alpha = Math.max(roughness * roughness, 1e-4);
2631
+ const phi = 2 * Math.PI * sample[0];
2632
+ const cosTheta = Math.sqrt((1 - sample[1]) / (1 + (alpha * alpha - 1) * sample[1]));
2633
+ const sinTheta = Math.sqrt(Math.max(0, 1 - cosTheta * cosTheta));
2634
+ const halfVector = localToWorld(
2635
+ [Math.cos(phi) * sinTheta, Math.sin(phi) * sinTheta, cosTheta],
2636
+ normal
2637
+ );
2638
+ return normalize(halfVector, normal);
2639
+ }
2640
+ function geometrySchlickGgx(nDotV, roughness) {
2641
+ const k = (roughness + 1) * (roughness + 1) / 8;
2642
+ return nDotV / Math.max(nDotV * (1 - k) + k, 1e-6);
2643
+ }
2644
+ function geometrySmith(nDotV, nDotL, roughness) {
2645
+ return geometrySchlickGgx(nDotV, roughness) * geometrySchlickGgx(nDotL, roughness);
2646
+ }
2647
+ function integrateBrdfSample(nDotV, roughness, sampleCount) {
2648
+ const viewDirection = [Math.sqrt(Math.max(0, 1 - nDotV * nDotV)), 0, nDotV];
2649
+ const normal = [0, 0, 1];
2650
+ let scaleTerm = 0;
2651
+ let biasTerm = 0;
2652
+ for (let index = 0; index < sampleCount; index += 1) {
2653
+ const xi = hammersley(index, sampleCount);
2654
+ const halfVector = importanceSampleGgx(xi, roughness, normal);
2655
+ const vDotH = Math.max(dot(viewDirection, halfVector), 0);
2656
+ const lightDirection = normalize(
2657
+ subtract(scale(halfVector, 2 * vDotH), viewDirection),
2658
+ normal
2659
+ );
2660
+ const nDotL = Math.max(lightDirection[2], 0);
2661
+ const nDotH = Math.max(halfVector[2], 0);
2662
+ if (nDotL <= 0 || nDotH <= 0 || vDotH <= 0) {
2663
+ continue;
2664
+ }
2665
+ const geometry = geometrySmith(nDotV, nDotL, roughness);
2666
+ const visibility = geometry * vDotH / Math.max(nDotH * nDotV, 1e-6);
2667
+ const fresnel = (1 - vDotH) ** 5;
2668
+ scaleTerm += (1 - fresnel) * visibility;
2669
+ biasTerm += fresnel * visibility;
2670
+ }
2671
+ return [scaleTerm / sampleCount, biasTerm / sampleCount];
2672
+ }
2673
+ function createBrdfLutUploadBytes(size = DEFAULT_BRDF_LUT_SIZE, sampleCount = DEFAULT_BRDF_LUT_SAMPLE_COUNT) {
2674
+ const cacheKey = `${Math.max(1, Math.trunc(size))}:${Math.max(1, Math.trunc(sampleCount))}`;
2675
+ const cached = BRDF_LUT_UPLOAD_CACHE.get(cacheKey);
2676
+ if (cached) {
2677
+ return cached;
2678
+ }
2679
+ const width = Math.max(1, Math.trunc(size));
2680
+ const height = Math.max(1, Math.trunc(size));
1695
2681
  const rowBytes = width * 8;
1696
2682
  const bytesPerRow = alignTo(rowBytes, 256);
1697
2683
  const bytes = new Uint8Array(bytesPerRow * height);
2684
+ const view = new DataView(bytes.buffer);
2685
+ for (let y = 0; y < height; y += 1) {
2686
+ const roughness = (y + 0.5) / height;
2687
+ for (let x = 0; x < width; x += 1) {
2688
+ const nDotV = Math.max((x + 0.5) / width, 1e-4);
2689
+ const [scaleTerm, biasTerm] = integrateBrdfSample(nDotV, roughness, sampleCount);
2690
+ const offset = y * bytesPerRow + x * 8;
2691
+ view.setUint16(offset, float32ToFloat16Bits(scaleTerm), true);
2692
+ view.setUint16(offset + 2, float32ToFloat16Bits(biasTerm), true);
2693
+ view.setUint16(offset + 4, float32ToFloat16Bits(0), true);
2694
+ view.setUint16(offset + 6, float32ToFloat16Bits(1), true);
2695
+ }
2696
+ }
2697
+ const upload = Object.freeze({ bytes, bytesPerRow, width, height });
2698
+ BRDF_LUT_UPLOAD_CACHE.set(cacheKey, upload);
2699
+ return upload;
2700
+ }
2701
+ function createLinearEnvironmentPixels(environmentMap, fallbackColor) {
2702
+ const width = Math.max(1, environmentMap.width);
2703
+ const height = Math.max(1, environmentMap.height);
2704
+ const pixels = new Float32Array(width * height * 4);
1698
2705
  const data = environmentMap.data;
1699
2706
  const integerScale = environmentMapIntegerScale(data);
1700
- const view = new DataView(bytes.buffer);
1701
- const writeComponent = (targetOffset, sourceOffset, fallback) => {
1702
- view.setUint16(
1703
- targetOffset,
1704
- float32ToFloat16Bits(
1705
- readEnvironmentMapComponent(data, sourceOffset, fallback, integerScale)
1706
- ),
1707
- true
1708
- );
2707
+ for (let index = 0; index < width * height; index += 1) {
2708
+ const sourceOffset = index * 4;
2709
+ const targetOffset = index * 4;
2710
+ pixels[targetOffset] = readEnvironmentMapComponent(data, sourceOffset, fallbackColor[0], integerScale);
2711
+ pixels[targetOffset + 1] = readEnvironmentMapComponent(data, sourceOffset + 1, fallbackColor[1], integerScale);
2712
+ pixels[targetOffset + 2] = readEnvironmentMapComponent(data, sourceOffset + 2, fallbackColor[2], integerScale);
2713
+ pixels[targetOffset + 3] = readEnvironmentMapComponent(data, sourceOffset + 3, fallbackColor[3] ?? 1, integerScale);
2714
+ }
2715
+ return pixels;
2716
+ }
2717
+ function environmentUvToDirection(u, v, rotationRadians = 0) {
2718
+ const angle = (u - rotationRadians / (2 * Math.PI) - 0.5) * 2 * Math.PI;
2719
+ const theta = v * Math.PI;
2720
+ const sinTheta = Math.sin(theta);
2721
+ return [
2722
+ Math.cos(angle) * sinTheta,
2723
+ Math.cos(theta),
2724
+ Math.sin(angle) * sinTheta
2725
+ ];
2726
+ }
2727
+ function sampleEnvironmentPixelsBilinear(pixels, width, height, u, v) {
2728
+ const wrappedU = (u % 1 + 1) % 1;
2729
+ const clampedV = clamp(v, 0, 1);
2730
+ const x = wrappedU * width - 0.5;
2731
+ const y = clampedV * height - 0.5;
2732
+ const x0 = (Math.floor(x) % width + width) % width;
2733
+ const y0 = clamp(Math.floor(y), 0, height - 1);
2734
+ const x1 = (x0 + 1) % width;
2735
+ const y1 = clamp(y0 + 1, 0, height - 1);
2736
+ const tx = x - Math.floor(x);
2737
+ const ty = y - Math.floor(y);
2738
+ const read = (px, py) => {
2739
+ const offset = (py * width + px) * 4;
2740
+ return [pixels[offset], pixels[offset + 1], pixels[offset + 2], pixels[offset + 3]];
1709
2741
  };
2742
+ const a = read(x0, y0);
2743
+ const b = read(x1, y0);
2744
+ const c = read(x0, y1);
2745
+ const d = read(x1, y1);
2746
+ const mixPair = (first, second, factor) => first * (1 - factor) + second * factor;
2747
+ return [
2748
+ mixPair(mixPair(a[0], b[0], tx), mixPair(c[0], d[0], tx), ty),
2749
+ mixPair(mixPair(a[1], b[1], tx), mixPair(c[1], d[1], tx), ty),
2750
+ mixPair(mixPair(a[2], b[2], tx), mixPair(c[2], d[2], tx), ty),
2751
+ mixPair(mixPair(a[3], b[3], tx), mixPair(c[3], d[3], tx), ty)
2752
+ ];
2753
+ }
2754
+ function directionToEnvironmentUv(direction, rotationRadians = 0) {
2755
+ const unitDirection = normalize(direction, [0, 1, 0]);
2756
+ const rotationTurns = rotationRadians / (2 * Math.PI);
2757
+ const u = ((Math.atan2(unitDirection[2], unitDirection[0]) / (2 * Math.PI) + 0.5 + rotationTurns) % 1 + 1) % 1;
2758
+ const v = Math.acos(clamp(unitDirection[1], -1, 1)) / Math.PI;
2759
+ return [u, clamp(v, 0, 1)];
2760
+ }
2761
+ function sampleEnvironmentRadiance(pixels, width, height, direction, rotationRadians = 0) {
2762
+ const [u, v] = directionToEnvironmentUv(direction, rotationRadians);
2763
+ return sampleEnvironmentPixelsBilinear(pixels, width, height, u, v);
2764
+ }
2765
+ function createFloat16RgbaUploadFromLevels(levels) {
2766
+ return levels.map((level) => {
2767
+ const rowBytes = level.width * 8;
2768
+ const bytesPerRow = alignTo(rowBytes, 256);
2769
+ const bytes = new Uint8Array(bytesPerRow * level.height);
2770
+ const view = new DataView(bytes.buffer);
2771
+ for (let y = 0; y < level.height; y += 1) {
2772
+ for (let x = 0; x < level.width; x += 1) {
2773
+ const sourceOffset = (y * level.width + x) * 4;
2774
+ const targetOffset = y * bytesPerRow + x * 8;
2775
+ view.setUint16(targetOffset, float32ToFloat16Bits(level.data[sourceOffset]), true);
2776
+ view.setUint16(targetOffset + 2, float32ToFloat16Bits(level.data[sourceOffset + 1]), true);
2777
+ view.setUint16(targetOffset + 4, float32ToFloat16Bits(level.data[sourceOffset + 2]), true);
2778
+ view.setUint16(targetOffset + 6, float32ToFloat16Bits(level.data[sourceOffset + 3]), true);
2779
+ }
2780
+ }
2781
+ return Object.freeze({ bytes, bytesPerRow, width: level.width, height: level.height });
2782
+ });
2783
+ }
2784
+ function createPrefilteredEnvironmentLevels(environmentMap, fallbackColor) {
2785
+ const sourcePixels = createLinearEnvironmentPixels(environmentMap, fallbackColor);
2786
+ const sourceWidth = Math.max(1, environmentMap.width);
2787
+ const sourceHeight = Math.max(1, environmentMap.height);
2788
+ const mipLevelCount = Math.max(1, Math.floor(Math.log2(Math.max(sourceWidth, sourceHeight))) + 1);
2789
+ const levels = [
2790
+ Object.freeze({
2791
+ width: sourceWidth,
2792
+ height: sourceHeight,
2793
+ data: sourcePixels
2794
+ })
2795
+ ];
2796
+ for (let mipLevel = 1; mipLevel < mipLevelCount; mipLevel += 1) {
2797
+ const width = Math.max(1, sourceWidth >> mipLevel);
2798
+ const height = Math.max(1, sourceHeight >> mipLevel);
2799
+ const roughness = mipLevelCount <= 1 ? 0 : mipLevel / (mipLevelCount - 1);
2800
+ const data = new Float32Array(width * height * 4);
2801
+ const sampleCount = roughness < 0.25 ? 64 : roughness < 0.6 ? 96 : 128;
2802
+ for (let y = 0; y < height; y += 1) {
2803
+ for (let x = 0; x < width; x += 1) {
2804
+ const direction = environmentUvToDirection((x + 0.5) / width, (y + 0.5) / height, environmentMap.rotationRadians);
2805
+ const normal = normalize(direction, [0, 1, 0]);
2806
+ const viewDirection = normal;
2807
+ let totalWeight = 0;
2808
+ const accum = [0, 0, 0];
2809
+ for (let sampleIndex = 0; sampleIndex < sampleCount; sampleIndex += 1) {
2810
+ const xi = hammersley(sampleIndex, sampleCount);
2811
+ const halfVector = importanceSampleGgx(xi, roughness, normal);
2812
+ const viewDotHalf = Math.max(dot(viewDirection, halfVector), 0);
2813
+ const lightDirection = normalize(
2814
+ subtract(scale(halfVector, 2 * viewDotHalf), viewDirection),
2815
+ normal
2816
+ );
2817
+ const nDotL = Math.max(dot(normal, lightDirection), 0);
2818
+ if (nDotL <= 1e-6) {
2819
+ continue;
2820
+ }
2821
+ const radiance = sampleEnvironmentRadiance(
2822
+ sourcePixels,
2823
+ sourceWidth,
2824
+ sourceHeight,
2825
+ lightDirection,
2826
+ environmentMap.rotationRadians
2827
+ );
2828
+ accum[0] += radiance[0] * nDotL;
2829
+ accum[1] += radiance[1] * nDotL;
2830
+ accum[2] += radiance[2] * nDotL;
2831
+ totalWeight += nDotL;
2832
+ }
2833
+ const offset = (y * width + x) * 4;
2834
+ data[offset] = accum[0] / Math.max(totalWeight, 1e-6);
2835
+ data[offset + 1] = accum[1] / Math.max(totalWeight, 1e-6);
2836
+ data[offset + 2] = accum[2] / Math.max(totalWeight, 1e-6);
2837
+ data[offset + 3] = 1;
2838
+ }
2839
+ }
2840
+ levels.push(Object.freeze({ width, height, data }));
2841
+ }
2842
+ return Object.freeze({
2843
+ levels,
2844
+ mipLevelCount,
2845
+ width: sourceWidth,
2846
+ height: sourceHeight
2847
+ });
2848
+ }
2849
+ function createEnvironmentSamplingTables(environmentMap, fallbackColor) {
2850
+ if (!environmentMapHasSamplingData(environmentMap)) {
2851
+ return Object.freeze({
2852
+ width: 1,
2853
+ height: 1,
2854
+ pdf: new Float32Array([1]),
2855
+ marginalCdf: new Float32Array([1]),
2856
+ conditionalCdf: new Float32Array([1]),
2857
+ hasImportanceData: false
2858
+ });
2859
+ }
2860
+ const pixels = createLinearEnvironmentPixels(environmentMap, fallbackColor);
2861
+ const width = Math.max(1, environmentMap.width);
2862
+ const height = Math.max(1, environmentMap.height);
2863
+ const pdf = new Float32Array(width * height);
2864
+ const marginalCdf = new Float32Array(height);
2865
+ const conditionalCdf = new Float32Array(width * height);
2866
+ const rowSums = new Float32Array(height);
2867
+ let totalWeight = 0;
1710
2868
  for (let y = 0; y < height; y += 1) {
2869
+ const theta = (y + 0.5) / height * Math.PI;
2870
+ const sinTheta = Math.max(Math.sin(theta), 1e-4);
2871
+ let rowWeight = 0;
1711
2872
  for (let x = 0; x < width; x += 1) {
1712
- const sourceOffset = (y * width + x) * 4;
1713
- const targetOffset = y * bytesPerRow + x * 8;
1714
- writeComponent(targetOffset, sourceOffset, fallbackColor[0]);
1715
- writeComponent(targetOffset + 2, sourceOffset + 1, fallbackColor[1]);
1716
- writeComponent(targetOffset + 4, sourceOffset + 2, fallbackColor[2]);
1717
- writeComponent(targetOffset + 6, sourceOffset + 3, fallbackColor[3] ?? 1);
2873
+ const offset = (y * width + x) * 4;
2874
+ const luminance = pixels[offset] * 0.2126 + pixels[offset + 1] * 0.7152 + pixels[offset + 2] * 0.0722;
2875
+ const weight = Math.max(luminance * sinTheta, 1e-6);
2876
+ pdf[y * width + x] = weight;
2877
+ rowWeight += weight;
2878
+ conditionalCdf[y * width + x] = rowWeight;
2879
+ }
2880
+ rowSums[y] = rowWeight;
2881
+ totalWeight += rowWeight;
2882
+ if (rowWeight > 0) {
2883
+ for (let x = 0; x < width; x += 1) {
2884
+ conditionalCdf[y * width + x] /= rowWeight;
2885
+ }
2886
+ } else {
2887
+ for (let x = 0; x < width; x += 1) {
2888
+ conditionalCdf[y * width + x] = (x + 1) / width;
2889
+ }
1718
2890
  }
2891
+ marginalCdf[y] = totalWeight;
2892
+ }
2893
+ for (let y = 0; y < height; y += 1) {
2894
+ marginalCdf[y] /= Math.max(totalWeight, 1e-6);
2895
+ }
2896
+ for (let index = 0; index < pdf.length; index += 1) {
2897
+ pdf[index] /= Math.max(totalWeight, 1e-6);
1719
2898
  }
1720
2899
  return Object.freeze({
1721
- bytes,
1722
- bytesPerRow,
1723
2900
  width,
1724
- height
2901
+ height,
2902
+ pdf,
2903
+ marginalCdf,
2904
+ conditionalCdf,
2905
+ hasImportanceData: true
1725
2906
  });
1726
2907
  }
1727
2908
  function createEnvironmentMapResource(device, constants, environmentMap, fallbackColor) {
@@ -1733,10 +2914,14 @@ function createEnvironmentMapResource(device, constants, environmentMap, fallbac
1733
2914
  addressModeU: "repeat",
1734
2915
  addressModeV: "clamp-to-edge",
1735
2916
  magFilter: "linear",
1736
- minFilter: "linear"
2917
+ minFilter: "linear",
2918
+ mipmapFilter: "linear"
1737
2919
  }),
1738
2920
  texture: null,
1739
- ownsTexture: false
2921
+ ownsTexture: false,
2922
+ width: Math.max(1, environmentMap.width),
2923
+ height: Math.max(1, environmentMap.height),
2924
+ mipLevelCount: Math.max(1, environmentMap.mipLevelCount ?? 1)
1740
2925
  });
1741
2926
  }
1742
2927
  if (environmentMap.texture && typeof environmentMap.texture.createView === "function") {
@@ -1747,15 +2932,91 @@ function createEnvironmentMapResource(device, constants, environmentMap, fallbac
1747
2932
  addressModeU: "repeat",
1748
2933
  addressModeV: "clamp-to-edge",
1749
2934
  magFilter: "linear",
1750
- minFilter: "linear"
2935
+ minFilter: "linear",
2936
+ mipmapFilter: "linear"
1751
2937
  }),
1752
2938
  texture: environmentMap.texture,
1753
- ownsTexture: false
2939
+ ownsTexture: false,
2940
+ width: Math.max(1, environmentMap.width),
2941
+ height: Math.max(1, environmentMap.height),
2942
+ mipLevelCount: Math.max(1, environmentMap.mipLevelCount ?? 1)
1754
2943
  });
1755
2944
  }
1756
- const upload = createEnvironmentMapUploadBytes(environmentMap, fallbackColor);
2945
+ const prefiltered = createPrefilteredEnvironmentLevels(environmentMap, fallbackColor);
2946
+ const uploads = createFloat16RgbaUploadFromLevels(prefiltered.levels);
1757
2947
  const texture = device.createTexture({
1758
2948
  label: environmentMap.enabled ? "plasius.wavefront.environmentMap" : "plasius.wavefront.environmentMapFallback",
2949
+ size: { width: prefiltered.width, height: prefiltered.height },
2950
+ format: "rgba16float",
2951
+ mipLevelCount: prefiltered.mipLevelCount,
2952
+ usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
2953
+ });
2954
+ uploads.forEach((upload, mipLevel) => {
2955
+ device.queue.writeTexture(
2956
+ { texture, mipLevel },
2957
+ upload.bytes,
2958
+ { bytesPerRow: upload.bytesPerRow, rowsPerImage: upload.height },
2959
+ { width: upload.width, height: upload.height, depthOrArrayLayers: 1 }
2960
+ );
2961
+ });
2962
+ return Object.freeze({
2963
+ view: texture.createView(),
2964
+ sampler: environmentMap.sampler ?? device.createSampler({
2965
+ label: "plasius.wavefront.environmentMapSampler",
2966
+ addressModeU: "repeat",
2967
+ addressModeV: "clamp-to-edge",
2968
+ magFilter: "linear",
2969
+ minFilter: "linear",
2970
+ mipmapFilter: "linear"
2971
+ }),
2972
+ texture,
2973
+ ownsTexture: true,
2974
+ width: prefiltered.width,
2975
+ height: prefiltered.height,
2976
+ mipLevelCount: prefiltered.mipLevelCount
2977
+ });
2978
+ }
2979
+ function createEnvironmentSamplingTextureResource(device, constants, environmentMap, fallbackColor) {
2980
+ const tables = createEnvironmentSamplingTables(environmentMap, fallbackColor);
2981
+ const rowBytes = tables.width * 8;
2982
+ const bytesPerRow = alignTo(rowBytes, 256);
2983
+ const bytes = new Uint8Array(bytesPerRow * tables.height);
2984
+ const view = new DataView(bytes.buffer);
2985
+ for (let y = 0; y < tables.height; y += 1) {
2986
+ for (let x = 0; x < tables.width; x += 1) {
2987
+ const probability = tables.pdf[y * tables.width + x];
2988
+ const conditional = tables.conditionalCdf[y * tables.width + x];
2989
+ const marginal = tables.marginalCdf[y];
2990
+ const offset = y * bytesPerRow + x * 8;
2991
+ view.setUint16(offset, float32ToFloat16Bits(probability), true);
2992
+ view.setUint16(offset + 2, float32ToFloat16Bits(conditional), true);
2993
+ view.setUint16(offset + 4, float32ToFloat16Bits(marginal), true);
2994
+ view.setUint16(offset + 6, float32ToFloat16Bits(1), true);
2995
+ }
2996
+ }
2997
+ const texture = device.createTexture({
2998
+ label: "plasius.wavefront.environmentSampling",
2999
+ size: { width: tables.width, height: tables.height },
3000
+ format: "rgba16float",
3001
+ usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
3002
+ });
3003
+ device.queue.writeTexture(
3004
+ { texture },
3005
+ bytes,
3006
+ { bytesPerRow, rowsPerImage: tables.height },
3007
+ { width: tables.width, height: tables.height, depthOrArrayLayers: 1 }
3008
+ );
3009
+ return Object.freeze({
3010
+ view: texture.createView(),
3011
+ texture,
3012
+ ownsTexture: true,
3013
+ hasImportanceData: tables.hasImportanceData
3014
+ });
3015
+ }
3016
+ function createBrdfLutResource(device, constants, size = DEFAULT_BRDF_LUT_SIZE) {
3017
+ const upload = createBrdfLutUploadBytes(size);
3018
+ const texture = device.createTexture({
3019
+ label: "plasius.wavefront.brdfLut",
1759
3020
  size: { width: upload.width, height: upload.height },
1760
3021
  format: "rgba16float",
1761
3022
  usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
@@ -1768,14 +3029,110 @@ function createEnvironmentMapResource(device, constants, environmentMap, fallbac
1768
3029
  );
1769
3030
  return Object.freeze({
1770
3031
  view: texture.createView(),
1771
- sampler: environmentMap.sampler ?? device.createSampler({
1772
- label: "plasius.wavefront.environmentMapSampler",
1773
- addressModeU: "repeat",
3032
+ sampler: device.createSampler({
3033
+ label: "plasius.wavefront.brdfLutSampler",
3034
+ addressModeU: "clamp-to-edge",
1774
3035
  addressModeV: "clamp-to-edge",
1775
3036
  magFilter: "linear",
1776
3037
  minFilter: "linear"
1777
3038
  }),
1778
3039
  texture,
3040
+ ownsTexture: true,
3041
+ width: upload.width,
3042
+ height: upload.height
3043
+ });
3044
+ }
3045
+ function createMediumTextureResource(device, constants, mediums) {
3046
+ const normalized = Array.isArray(mediums) && mediums.length > 0 ? mediums : [{ id: 0 }];
3047
+ const width = Math.max(
3048
+ 1,
3049
+ normalized.reduce((maximum, medium) => Math.max(maximum, medium.id ?? 0), 0) + 1
3050
+ );
3051
+ const level = {
3052
+ width,
3053
+ height: MEDIUM_TABLE_ROWS,
3054
+ data: new Float32Array(width * MEDIUM_TABLE_ROWS * 4)
3055
+ };
3056
+ for (const medium of normalized) {
3057
+ const mediumId = Math.max(0, Math.trunc(Number(medium.id) || 0));
3058
+ const absorptionOffset = mediumId * 4;
3059
+ level.data[absorptionOffset] = Math.max(0, medium.absorption?.[0] ?? 0);
3060
+ level.data[absorptionOffset + 1] = Math.max(0, medium.absorption?.[1] ?? 0);
3061
+ level.data[absorptionOffset + 2] = Math.max(0, medium.absorption?.[2] ?? 0);
3062
+ level.data[absorptionOffset + 3] = Math.max(0, medium.phaseModel ?? 0);
3063
+ const scatteringOffset = (width + mediumId) * 4;
3064
+ level.data[scatteringOffset] = Math.max(0, medium.scattering?.[0] ?? 0);
3065
+ level.data[scatteringOffset + 1] = Math.max(0, medium.scattering?.[1] ?? 0);
3066
+ level.data[scatteringOffset + 2] = Math.max(0, medium.scattering?.[2] ?? 0);
3067
+ level.data[scatteringOffset + 3] = Math.max(0, medium.density ?? 0);
3068
+ }
3069
+ const upload = createFloat16RgbaUploadFromLevels([level])[0];
3070
+ const texture = device.createTexture({
3071
+ label: "plasius.wavefront.mediumTable",
3072
+ size: { width, height: MEDIUM_TABLE_ROWS },
3073
+ format: "rgba16float",
3074
+ usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
3075
+ });
3076
+ device.queue.writeTexture(
3077
+ { texture },
3078
+ upload.bytes,
3079
+ { bytesPerRow: upload.bytesPerRow, rowsPerImage: upload.height },
3080
+ { width, height: MEDIUM_TABLE_ROWS, depthOrArrayLayers: 1 }
3081
+ );
3082
+ return Object.freeze({
3083
+ texture,
3084
+ view: texture.createView(),
3085
+ ownsTexture: true,
3086
+ count: normalized.length,
3087
+ width
3088
+ });
3089
+ }
3090
+ function mediumTablesEqual(left, right) {
3091
+ const leftMediums = Array.isArray(left) ? left : [];
3092
+ const rightMediums = Array.isArray(right) ? right : [];
3093
+ if (leftMediums.length !== rightMediums.length) {
3094
+ return false;
3095
+ }
3096
+ for (let index = 0; index < leftMediums.length; index += 1) {
3097
+ const leftMedium = leftMediums[index];
3098
+ const rightMedium = rightMediums[index];
3099
+ if ((leftMedium?.id ?? 0) !== (rightMedium?.id ?? 0)) {
3100
+ return false;
3101
+ }
3102
+ if ((leftMedium?.phaseModel ?? 0) !== (rightMedium?.phaseModel ?? 0)) {
3103
+ return false;
3104
+ }
3105
+ if ((leftMedium?.density ?? 0) !== (rightMedium?.density ?? 0)) {
3106
+ return false;
3107
+ }
3108
+ for (let component = 0; component < 3; component += 1) {
3109
+ if ((leftMedium?.absorption?.[component] ?? 0) !== (rightMedium?.absorption?.[component] ?? 0)) {
3110
+ return false;
3111
+ }
3112
+ if ((leftMedium?.scattering?.[component] ?? 0) !== (rightMedium?.scattering?.[component] ?? 0)) {
3113
+ return false;
3114
+ }
3115
+ }
3116
+ }
3117
+ return true;
3118
+ }
3119
+ function createAtlasTextureResource(device, constants, atlas, label) {
3120
+ const upload = createRgba8TextureUpload(atlas);
3121
+ const texture = device.createTexture({
3122
+ label,
3123
+ size: { width: upload.width, height: upload.height },
3124
+ format: "rgba8unorm",
3125
+ usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
3126
+ });
3127
+ device.queue.writeTexture(
3128
+ { texture },
3129
+ upload.bytes,
3130
+ { bytesPerRow: upload.bytesPerRow, rowsPerImage: upload.height },
3131
+ { width: upload.width, height: upload.height, depthOrArrayLayers: 1 }
3132
+ );
3133
+ return Object.freeze({
3134
+ texture,
3135
+ view: texture.createView(),
1779
3136
  ownsTexture: true
1780
3137
  });
1781
3138
  }
@@ -1821,6 +3178,24 @@ ${diagnostics}` : "";
1821
3178
  });
1822
3179
  }
1823
3180
  }
3181
+ async function assertShaderModuleCompiles(shaderModule, label) {
3182
+ if (typeof shaderModule?.compilationInfo !== "function") {
3183
+ return;
3184
+ }
3185
+ const info = await shaderModule.compilationInfo();
3186
+ const messages = Array.isArray(info?.messages) ? info.messages : [];
3187
+ const errors = messages.filter((message) => message?.type === "error");
3188
+ if (errors.length <= 0) {
3189
+ return;
3190
+ }
3191
+ const diagnostics = errors.map((message) => {
3192
+ const line = Number.isFinite(message.lineNum) ? message.lineNum : "?";
3193
+ const column = Number.isFinite(message.linePos) ? message.linePos : "?";
3194
+ return `line ${line}:${column} ${message.message}`;
3195
+ }).join("\n");
3196
+ throw new Error(`WGSL compilation preflight failed for ${label}:
3197
+ ${diagnostics}`);
3198
+ }
1824
3199
  async function createRenderPipeline(device, descriptor) {
1825
3200
  if (typeof device.createRenderPipelineAsync === "function") {
1826
3201
  return device.createRenderPipelineAsync(descriptor);
@@ -1829,6 +3204,7 @@ async function createRenderPipeline(device, descriptor) {
1829
3204
  }
1830
3205
  var WAVEFRONT_COMPUTE_WGSL = `
1831
3206
  const RAY_FLAG_GUIDED_EMISSIVE: u32 = 1u;
3207
+ const RAY_FLAG_DELTA_SAMPLE: u32 = 2u;
1832
3208
 
1833
3209
  struct RayRecord {
1834
3210
  rayId: u32,
@@ -1854,11 +3230,12 @@ struct HitRecord {
1854
3230
  primitiveId: u32,
1855
3231
  materialRefId: u32,
1856
3232
  mediumRefId: u32,
3233
+ materialSlot: u32,
1857
3234
  pad0: u32,
1858
3235
  pad1: u32,
1859
- pad2: u32,
1860
3236
  distance: f32,
1861
- pad3: vec3<f32>,
3237
+ occlusion: f32,
3238
+ pad2: vec2<f32>,
1862
3239
  position: vec4<f32>,
1863
3240
  geometricNormal: vec4<f32>,
1864
3241
  shadingNormal: vec4<f32>,
@@ -1867,6 +3244,9 @@ struct HitRecord {
1867
3244
  color: vec4<f32>,
1868
3245
  emission: vec4<f32>,
1869
3246
  material: vec4<f32>,
3247
+ materialResponse: vec4<f32>,
3248
+ materialExtension: vec4<f32>,
3249
+ specularColor: vec4<f32>,
1870
3250
  };
1871
3251
 
1872
3252
  struct SceneObject {
@@ -1874,11 +3254,18 @@ struct SceneObject {
1874
3254
  objectId: u32,
1875
3255
  materialKind: u32,
1876
3256
  flags: u32,
3257
+ mediumRefId: u32,
3258
+ pad0: u32,
3259
+ pad1: u32,
3260
+ pad2: u32,
1877
3261
  center: vec4<f32>,
1878
3262
  halfExtent: vec4<f32>,
1879
3263
  color: vec4<f32>,
1880
3264
  emission: vec4<f32>,
1881
3265
  material: vec4<f32>,
3266
+ materialResponse: vec4<f32>,
3267
+ materialExtension: vec4<f32>,
3268
+ specularColor: vec4<f32>,
1882
3269
  };
1883
3270
 
1884
3271
  struct TriangleRecord {
@@ -1888,7 +3275,7 @@ struct TriangleRecord {
1888
3275
  flags: u32,
1889
3276
  materialRefId: u32,
1890
3277
  mediumRefId: u32,
1891
- pad0: u32,
3278
+ materialSlot: u32,
1892
3279
  pad1: u32,
1893
3280
  v0: vec4<f32>,
1894
3281
  v1: vec4<f32>,
@@ -1901,6 +3288,15 @@ struct TriangleRecord {
1901
3288
  color: vec4<f32>,
1902
3289
  emission: vec4<f32>,
1903
3290
  material: vec4<f32>,
3291
+ materialResponse: vec4<f32>,
3292
+ materialExtension: vec4<f32>,
3293
+ specularColor: vec4<f32>,
3294
+ baseColorAtlas: vec4<f32>,
3295
+ metallicRoughnessAtlas: vec4<f32>,
3296
+ normalAtlas: vec4<f32>,
3297
+ occlusionAtlas: vec4<f32>,
3298
+ emissiveAtlas: vec4<f32>,
3299
+ textureSettings: vec4<f32>,
1904
3300
  };
1905
3301
 
1906
3302
  struct BvhNode {
@@ -1921,10 +3317,10 @@ struct BvhLeafRef {
1921
3317
 
1922
3318
  struct ScatterResult {
1923
3319
  direction: vec4<f32>,
3320
+ pdf: f32,
3321
+ mediumRefId: u32,
1924
3322
  flags: u32,
1925
3323
  pad0: u32,
1926
- pad1: u32,
1927
- pad2: u32,
1928
3324
  };
1929
3325
 
1930
3326
  struct MeshVertex {
@@ -1945,10 +3341,19 @@ struct MeshRange {
1945
3341
  triangleCount: u32,
1946
3342
  firstVertex: u32,
1947
3343
  vertexCount: u32,
1948
- pad0: u32,
3344
+ materialSlot: u32,
1949
3345
  color: vec4<f32>,
1950
3346
  emission: vec4<f32>,
1951
3347
  material: vec4<f32>,
3348
+ materialResponse: vec4<f32>,
3349
+ materialExtension: vec4<f32>,
3350
+ specularColor: vec4<f32>,
3351
+ baseColorAtlas: vec4<f32>,
3352
+ metallicRoughnessAtlas: vec4<f32>,
3353
+ normalAtlas: vec4<f32>,
3354
+ occlusionAtlas: vec4<f32>,
3355
+ emissiveAtlas: vec4<f32>,
3356
+ textureSettings: vec4<f32>,
1952
3357
  };
1953
3358
 
1954
3359
  struct FrameConfig {
@@ -1989,6 +3394,7 @@ struct FrameConfig {
1989
3394
  _portalPad1: u32,
1990
3395
  environmentMapSettings: vec4<f32>,
1991
3396
  pathResolveSettings: vec4<f32>,
3397
+ environmentMapMeta: vec4<f32>,
1992
3398
  };
1993
3399
 
1994
3400
  struct Counters {
@@ -2051,6 +3457,16 @@ struct EnvironmentPortal {
2051
3457
  @group(0) @binding(20) var environmentMapTexture: texture_2d<f32>;
2052
3458
  @group(0) @binding(21) var environmentMapSampler: sampler;
2053
3459
  @group(0) @binding(22) var<storage, read_write> pathVertices: array<vec4<f32>>;
3460
+ @group(0) @binding(23) var baseColorAtlasTexture: texture_2d<f32>;
3461
+ @group(0) @binding(24) var metallicRoughnessAtlasTexture: texture_2d<f32>;
3462
+ @group(0) @binding(25) var normalAtlasTexture: texture_2d<f32>;
3463
+ @group(0) @binding(26) var occlusionAtlasTexture: texture_2d<f32>;
3464
+ @group(0) @binding(27) var emissiveAtlasTexture: texture_2d<f32>;
3465
+ @group(0) @binding(28) var materialAtlasSampler: sampler;
3466
+ @group(0) @binding(29) var brdfLutTexture: texture_2d<f32>;
3467
+ @group(0) @binding(30) var brdfLutSampler: sampler;
3468
+ @group(0) @binding(31) var environmentSamplingTexture: texture_2d<f32>;
3469
+ @group(0) @binding(32) var mediumTableTexture: texture_2d<f32>;
2054
3470
 
2055
3471
  fn hash_u32(value: u32) -> u32 {
2056
3472
  var x = value;
@@ -2087,6 +3503,146 @@ fn safe_normalize(value: vec3<f32>, fallback: vec3<f32>) -> vec3<f32> {
2087
3503
  return value / len;
2088
3504
  }
2089
3505
 
3506
+ struct TangentBasis {
3507
+ tangent: vec3<f32>,
3508
+ bitangent: vec3<f32>,
3509
+ };
3510
+
3511
+ struct SurfaceMaterialSample {
3512
+ color: vec4<f32>,
3513
+ emission: vec4<f32>,
3514
+ material: vec4<f32>,
3515
+ materialResponse: vec4<f32>,
3516
+ materialExtension: vec4<f32>,
3517
+ specularColor: vec4<f32>,
3518
+ shadingNormal: vec3<f32>,
3519
+ occlusion: f32,
3520
+ };
3521
+
3522
+ fn srgb_to_linear_channel(value: f32) -> f32 {
3523
+ if (value <= 0.04045) {
3524
+ return value / 12.92;
3525
+ }
3526
+ return pow((value + 0.055) / 1.055, 2.4);
3527
+ }
3528
+
3529
+ fn srgb_to_linear_vec3(value: vec3<f32>) -> vec3<f32> {
3530
+ return vec3<f32>(
3531
+ srgb_to_linear_channel(value.x),
3532
+ srgb_to_linear_channel(value.y),
3533
+ srgb_to_linear_channel(value.z)
3534
+ );
3535
+ }
3536
+
3537
+ fn wrap_uv(uv: vec2<f32>) -> vec2<f32> {
3538
+ return fract(fract(uv) + vec2<f32>(1.0));
3539
+ }
3540
+
3541
+ fn atlas_sample_uv(rect: vec4<f32>, uv: vec2<f32>) -> vec2<f32> {
3542
+ let local = wrap_uv(uv);
3543
+ let clamped = clamp(local, vec2<f32>(0.001), vec2<f32>(0.999));
3544
+ return rect.xy + clamped * rect.zw;
3545
+ }
3546
+
3547
+ fn sample_atlas(textureRef: texture_2d<f32>, rect: vec4<f32>, uv: vec2<f32>) -> vec4<f32> {
3548
+ return textureSampleLevel(textureRef, materialAtlasSampler, atlas_sample_uv(rect, uv), 0.0);
3549
+ }
3550
+
3551
+ fn build_triangle_tangent_basis(
3552
+ triangle: TriangleRecord,
3553
+ fallbackNormal: vec3<f32>
3554
+ ) -> TangentBasis {
3555
+ let edge1 = triangle.v1.xyz - triangle.v0.xyz;
3556
+ let edge2 = triangle.v2.xyz - triangle.v0.xyz;
3557
+ let uv0 = triangle.uv0uv1.xy;
3558
+ let uv1 = triangle.uv0uv1.zw;
3559
+ let uv2 = triangle.uv2Pad.xy;
3560
+ let deltaUv1 = uv1 - uv0;
3561
+ let deltaUv2 = uv2 - uv0;
3562
+ let determinant = deltaUv1.x * deltaUv2.y - deltaUv1.y * deltaUv2.x;
3563
+ if (abs(determinant) <= 0.000001) {
3564
+ let tangentFallback = select(vec3<f32>(0.0, 1.0, 0.0), vec3<f32>(1.0, 0.0, 0.0), abs(fallbackNormal.y) >= 0.999);
3565
+ let tangent = safe_normalize(cross(tangentFallback, fallbackNormal), vec3<f32>(1.0, 0.0, 0.0));
3566
+ let bitangent = safe_normalize(cross(fallbackNormal, tangent), vec3<f32>(0.0, 0.0, 1.0));
3567
+ return TangentBasis(tangent, bitangent);
3568
+ }
3569
+ let inverse = 1.0 / determinant;
3570
+ let tangent = safe_normalize(
3571
+ inverse * (edge1 * deltaUv2.y - edge2 * deltaUv1.y),
3572
+ vec3<f32>(1.0, 0.0, 0.0)
3573
+ );
3574
+ let bitangent = safe_normalize(
3575
+ inverse * (-edge1 * deltaUv2.x + edge2 * deltaUv1.x),
3576
+ vec3<f32>(0.0, 0.0, 1.0)
3577
+ );
3578
+ return TangentBasis(tangent, bitangent);
3579
+ }
3580
+
3581
+ fn sample_surface_material(
3582
+ triangle: TriangleRecord,
3583
+ uv: vec2<f32>,
3584
+ geometricNormal: vec3<f32>,
3585
+ shadingNormal: vec3<f32>
3586
+ ) -> SurfaceMaterialSample {
3587
+ let baseColorTexel = sample_atlas(baseColorAtlasTexture, triangle.baseColorAtlas, uv);
3588
+ let baseColor = vec4<f32>(
3589
+ clamp(triangle.color.rgb * srgb_to_linear_vec3(baseColorTexel.rgb), vec3<f32>(0.0), vec3<f32>(1.0)),
3590
+ clamp(triangle.color.a * baseColorTexel.a, 0.0, 1.0)
3591
+ );
3592
+ let metallicRoughnessTexel = sample_atlas(
3593
+ metallicRoughnessAtlasTexture,
3594
+ triangle.metallicRoughnessAtlas,
3595
+ uv
3596
+ );
3597
+ let normalTexel = sample_atlas(normalAtlasTexture, triangle.normalAtlas, uv);
3598
+ let occlusionTexel = sample_atlas(occlusionAtlasTexture, triangle.occlusionAtlas, uv);
3599
+ let emissiveTexel = sample_atlas(emissiveAtlasTexture, triangle.emissiveAtlas, uv);
3600
+ let normalScale = clamp(triangle.textureSettings.x, 0.0, 1.0);
3601
+ let tangentBasis = build_triangle_tangent_basis(triangle, geometricNormal);
3602
+ let tangentNormal = safe_normalize(
3603
+ vec3<f32>(
3604
+ (normalTexel.x * 2.0 - 1.0) * normalScale,
3605
+ (normalTexel.y * 2.0 - 1.0) * normalScale,
3606
+ 1.0 + ((normalTexel.z * 2.0 - 1.0) - 1.0) * normalScale
3607
+ ),
3608
+ vec3<f32>(0.0, 0.0, 1.0)
3609
+ );
3610
+ let mappedNormal = safe_normalize(
3611
+ tangentBasis.tangent * tangentNormal.x +
3612
+ tangentBasis.bitangent * tangentNormal.y +
3613
+ shadingNormal * tangentNormal.z,
3614
+ shadingNormal
3615
+ );
3616
+ let emission = vec4<f32>(
3617
+ max(
3618
+ triangle.emission.rgb *
3619
+ srgb_to_linear_vec3(emissiveTexel.rgb) *
3620
+ max(triangle.textureSettings.z, 0.0),
3621
+ vec3<f32>(0.0)
3622
+ ),
3623
+ clamp(triangle.emission.a * emissiveTexel.a, 0.0, 1.0)
3624
+ );
3625
+ return SurfaceMaterialSample(
3626
+ baseColor,
3627
+ emission,
3628
+ vec4<f32>(
3629
+ clamp(triangle.material.x * metallicRoughnessTexel.y, 0.0, 1.0),
3630
+ clamp(triangle.material.y * metallicRoughnessTexel.z, 0.0, 1.0),
3631
+ clamp(triangle.material.z * baseColor.a, 0.0, 1.0),
3632
+ clamp(triangle.material.w, 1.0, 3.0)
3633
+ ),
3634
+ triangle.materialResponse,
3635
+ triangle.materialExtension,
3636
+ triangle.specularColor,
3637
+ repair_shading_normal(geometricNormal, mappedNormal),
3638
+ clamp(
3639
+ mix(1.0, occlusionTexel.x, clamp(triangle.textureSettings.y, 0.0, 1.0)),
3640
+ 0.0,
3641
+ 1.0
3642
+ )
3643
+ );
3644
+ }
3645
+
2090
3646
  fn saturate(value: f32) -> f32 {
2091
3647
  return clamp(value, 0.0, 1.0);
2092
3648
  }
@@ -2095,6 +3651,10 @@ fn max_component(value: vec3<f32>) -> f32 {
2095
3651
  return max(max(value.x, value.y), value.z);
2096
3652
  }
2097
3653
 
3654
+ fn radiance_luminance(value: vec3<f32>) -> f32 {
3655
+ return dot(value, vec3<f32>(0.2126, 0.7152, 0.0722));
3656
+ }
3657
+
2098
3658
  fn environment_map_enabled() -> bool {
2099
3659
  return config.environmentMapSettings.x > 0.5;
2100
3660
  }
@@ -2215,12 +3775,349 @@ fn environment_portal_radiance_scale(origin: vec3<f32>, direction: vec3<f32>) ->
2215
3775
  return scale;
2216
3776
  }
2217
3777
 
2218
- fn environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
2219
- let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
2220
- let portalScale = environment_portal_radiance_scale(origin, rayDirection);
2221
- let portalHit = max_component(portalScale) > 0.0001;
2222
- return base_environment_radiance(rayDirection) *
2223
- select(vec3<f32>(1.0), portalScale, portalHit);
3778
+ fn environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
3779
+ let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
3780
+ let portalScale = environment_portal_radiance_scale(origin, rayDirection);
3781
+ let portalHit = max_component(portalScale) > 0.0001;
3782
+ return base_environment_radiance(rayDirection) *
3783
+ select(vec3<f32>(1.0), portalScale, portalHit);
3784
+ }
3785
+
3786
+ fn direct_environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
3787
+ let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
3788
+ let portalScale = environment_portal_radiance_scale(origin, rayDirection);
3789
+ let portalHit = max_component(portalScale) > 0.0001;
3790
+ if (
3791
+ config.environmentPortalCount > 0u &&
3792
+ config.environmentPortalMode == 2u &&
3793
+ !portalHit
3794
+ ) {
3795
+ return vec3<f32>(0.0);
3796
+ }
3797
+ return base_environment_radiance(rayDirection) *
3798
+ select(vec3<f32>(1.0), portalScale, portalHit);
3799
+ }
3800
+
3801
+ fn radical_inverse_vdc(bitsValue: u32) -> f32 {
3802
+ var bits = bitsValue;
3803
+ bits = (bits << 16u) | (bits >> 16u);
3804
+ bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xaaaaaaaau) >> 1u);
3805
+ bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xccccccccu) >> 2u);
3806
+ bits = ((bits & 0x0f0f0f0fu) << 4u) | ((bits & 0xf0f0f0f0u) >> 4u);
3807
+ bits = ((bits & 0x00ff00ffu) << 8u) | ((bits & 0xff00ff00u) >> 8u);
3808
+ return f32(bits) * 2.3283064365386963e-10;
3809
+ }
3810
+
3811
+ fn hammersley_2d(index: u32, count: u32) -> vec2<f32> {
3812
+ return vec2<f32>(f32(index) / max(f32(count), 1.0), radical_inverse_vdc(index));
3813
+ }
3814
+
3815
+ fn build_basis_tangent(normal: vec3<f32>) -> vec3<f32> {
3816
+ let tangentFallback = select(vec3<f32>(0.0, 1.0, 0.0), vec3<f32>(1.0, 0.0, 0.0), abs(normal.y) >= 0.999);
3817
+ return safe_normalize(cross(tangentFallback, normal), vec3<f32>(1.0, 0.0, 0.0));
3818
+ }
3819
+
3820
+ fn local_to_world(local: vec3<f32>, normal: vec3<f32>) -> vec3<f32> {
3821
+ let tangent = build_basis_tangent(normal);
3822
+ let bitangent = safe_normalize(cross(normal, tangent), vec3<f32>(0.0, 0.0, 1.0));
3823
+ return safe_normalize(tangent * local.x + bitangent * local.y + normal * local.z, normal);
3824
+ }
3825
+
3826
+ fn cosine_sample_hemisphere(sample: vec2<f32>, normal: vec3<f32>) -> vec3<f32> {
3827
+ let phi = 6.28318530718 * sample.x;
3828
+ let radius = sqrt(sample.y);
3829
+ let x = cos(phi) * radius;
3830
+ let y = sin(phi) * radius;
3831
+ let z = sqrt(max(0.0, 1.0 - sample.y));
3832
+ return local_to_world(vec3<f32>(x, y, z), normal);
3833
+ }
3834
+
3835
+ fn importance_sample_ggx(sample: vec2<f32>, roughness: f32, normal: vec3<f32>) -> vec3<f32> {
3836
+ let alpha = max(roughness * roughness, 0.0001);
3837
+ let phi = 6.28318530718 * sample.x;
3838
+ let cosTheta = sqrt((1.0 - sample.y) / max(1.0 + (alpha * alpha - 1.0) * sample.y, 0.0001));
3839
+ let sinTheta = sqrt(max(0.0, 1.0 - cosTheta * cosTheta));
3840
+ let localHalf = vec3<f32>(cos(phi) * sinTheta, sin(phi) * sinTheta, cosTheta);
3841
+ return local_to_world(localHalf, normal);
3842
+ }
3843
+
3844
+ fn distribution_ggx(normal: vec3<f32>, halfVector: vec3<f32>, roughness: f32) -> f32 {
3845
+ let alpha = max(roughness * roughness, 0.0001);
3846
+ let alpha2 = alpha * alpha;
3847
+ let nDotH = saturate(dot(normal, halfVector));
3848
+ let denominator = nDotH * nDotH * (alpha2 - 1.0) + 1.0;
3849
+ return alpha2 / max(3.14159265359 * denominator * denominator, 0.000001);
3850
+ }
3851
+
3852
+ fn geometry_schlick_ggx(nDotValue: f32, roughness: f32) -> f32 {
3853
+ let k = ((roughness + 1.0) * (roughness + 1.0)) / 8.0;
3854
+ return nDotValue / max(nDotValue * (1.0 - k) + k, 0.000001);
3855
+ }
3856
+
3857
+ fn geometry_smith(normal: vec3<f32>, viewDirection: vec3<f32>, lightDirection: vec3<f32>, roughness: f32) -> f32 {
3858
+ let nDotV = saturate(dot(normal, viewDirection));
3859
+ let nDotL = saturate(dot(normal, lightDirection));
3860
+ return geometry_schlick_ggx(nDotV, roughness) * geometry_schlick_ggx(nDotL, roughness);
3861
+ }
3862
+
3863
+ fn fresnel_schlick(cosine: f32, f0: vec3<f32>) -> vec3<f32> {
3864
+ return f0 + (vec3<f32>(1.0) - f0) * pow(1.0 - cosine, 5.0);
3865
+ }
3866
+
3867
+ fn sample_brdf_lut(nDotV: f32, roughness: f32) -> vec2<f32> {
3868
+ let uv = vec2<f32>(clamp(nDotV, 0.0, 1.0), clamp(roughness, 0.0, 1.0));
3869
+ return textureSampleLevel(brdfLutTexture, brdfLutSampler, uv, 0.0).xy;
3870
+ }
3871
+
3872
+ fn prefiltered_environment_radiance(direction: vec3<f32>, roughness: f32) -> vec3<f32> {
3873
+ let uv = environment_map_uv(direction);
3874
+ let maxLevel = max(config.environmentMapMeta.z - 1.0, 0.0);
3875
+ let lod = clamp(roughness, 0.0, 1.0) * maxLevel;
3876
+ let texel = max(textureSampleLevel(environmentMapTexture, environmentMapSampler, uv, lod).rgb, vec3<f32>(0.0));
3877
+ return texel * max(config.environmentMapSettings.y, 0.0);
3878
+ }
3879
+
3880
+ fn environment_pdf_dimensions() -> vec2<u32> {
3881
+ return vec2<u32>(
3882
+ max(u32(config.environmentMapMeta.x), 1u),
3883
+ max(u32(config.environmentMapMeta.y), 1u)
3884
+ );
3885
+ }
3886
+
3887
+ fn environment_importance_sampling_enabled() -> bool {
3888
+ return config.environmentMapMeta.w > 0.5;
3889
+ }
3890
+
3891
+ fn uniform_sphere_pdf() -> f32 {
3892
+ return 1.0 / (4.0 * 3.14159265359);
3893
+ }
3894
+
3895
+ fn sample_uniform_sphere_direction(sample: vec2<f32>) -> vec3<f32> {
3896
+ let z = 1.0 - 2.0 * sample.y;
3897
+ let radial = sqrt(max(1.0 - z * z, 0.0));
3898
+ let phi = sample.x * 6.28318530718;
3899
+ return vec3<f32>(cos(phi) * radial, z, sin(phi) * radial);
3900
+ }
3901
+
3902
+ fn environment_sampling_texel(x: u32, y: u32) -> vec4<f32> {
3903
+ return textureLoad(environmentSamplingTexture, vec2<i32>(i32(x), i32(y)), 0);
3904
+ }
3905
+
3906
+ fn environment_pdf_texel(x: u32, y: u32) -> f32 {
3907
+ return environment_sampling_texel(x, y).x;
3908
+ }
3909
+
3910
+ fn environment_row_cdf_texel(y: u32) -> f32 {
3911
+ return environment_sampling_texel(0u, y).z;
3912
+ }
3913
+
3914
+ fn environment_column_cdf_texel(x: u32, y: u32) -> f32 {
3915
+ return environment_sampling_texel(x, y).y;
3916
+ }
3917
+
3918
+ fn environment_direction_pdf(direction: vec3<f32>) -> f32 {
3919
+ if (!environment_importance_sampling_enabled()) {
3920
+ return uniform_sphere_pdf();
3921
+ }
3922
+ let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
3923
+ let uv = environment_map_uv(rayDirection);
3924
+ let dimensions = environment_pdf_dimensions();
3925
+ let width = max(f32(dimensions.x), 1.0);
3926
+ let height = max(f32(dimensions.y), 1.0);
3927
+ let x = min(u32(uv.x * width), dimensions.x - 1u);
3928
+ let y = min(u32(uv.y * height), dimensions.y - 1u);
3929
+ let discretePdf = max(environment_pdf_texel(x, y), 0.0);
3930
+ let sinTheta = sqrt(max(1.0 - rayDirection.y * rayDirection.y, 0.0));
3931
+ let solidAngle = max((2.0 * 3.14159265359 * 3.14159265359 * sinTheta) / (width * height), 0.000001);
3932
+ return discretePdf / solidAngle;
3933
+ }
3934
+
3935
+ fn sample_row_cdf(count: u32, sampleValue: f32) -> u32 {
3936
+ if (count == 0u) {
3937
+ return 0u;
3938
+ }
3939
+ var low = 0u;
3940
+ var high = count - 1u;
3941
+ loop {
3942
+ if (low >= high) {
3943
+ break;
3944
+ }
3945
+ let mid = (low + high) / 2u;
3946
+ let cdfValue = environment_row_cdf_texel(mid);
3947
+ if (sampleValue <= cdfValue) {
3948
+ high = mid;
3949
+ } else {
3950
+ low = mid + 1u;
3951
+ }
3952
+ }
3953
+ return min(low, count - 1u);
3954
+ }
3955
+
3956
+ fn sample_column_cdf(row: u32, count: u32, sampleValue: f32) -> u32 {
3957
+ if (count == 0u) {
3958
+ return 0u;
3959
+ }
3960
+ var low = 0u;
3961
+ var high = count - 1u;
3962
+ loop {
3963
+ if (low >= high) {
3964
+ break;
3965
+ }
3966
+ let mid = (low + high) / 2u;
3967
+ let cdfValue = environment_column_cdf_texel(mid, row);
3968
+ if (sampleValue <= cdfValue) {
3969
+ high = mid;
3970
+ } else {
3971
+ low = mid + 1u;
3972
+ }
3973
+ }
3974
+ return min(low, count - 1u);
3975
+ }
3976
+
3977
+ struct EnvironmentSample {
3978
+ direction: vec3<f32>,
3979
+ radiance: vec3<f32>,
3980
+ pdf: f32,
3981
+ };
3982
+
3983
+ fn sample_environment_importance(sample: vec2<f32>) -> EnvironmentSample {
3984
+ if (!environment_importance_sampling_enabled()) {
3985
+ let direction = sample_uniform_sphere_direction(sample);
3986
+ return EnvironmentSample(direction, base_environment_radiance(direction), uniform_sphere_pdf());
3987
+ }
3988
+ let dimensions = environment_pdf_dimensions();
3989
+ let row = sample_row_cdf(dimensions.y, sample.y);
3990
+ let column = sample_column_cdf(row, dimensions.x, sample.x);
3991
+ let uv = vec2<f32>(
3992
+ (f32(column) + 0.5) / max(f32(dimensions.x), 1.0),
3993
+ (f32(row) + 0.5) / max(f32(dimensions.y), 1.0)
3994
+ );
3995
+ let theta = uv.y * 3.14159265359;
3996
+ let phi = (uv.x - 0.5 - config.environmentMapSettings.z / 6.28318530718) * 6.28318530718;
3997
+ let sinTheta = sin(theta);
3998
+ let direction = vec3<f32>(cos(phi) * sinTheta, cos(theta), sin(phi) * sinTheta);
3999
+ let pdf = environment_direction_pdf(direction);
4000
+ return EnvironmentSample(direction, base_environment_radiance(direction), pdf);
4001
+ }
4002
+
4003
+ fn power_heuristic(pdfA: f32, pdfB: f32) -> f32 {
4004
+ let a2 = pdfA * pdfA;
4005
+ let b2 = pdfB * pdfB;
4006
+ return a2 / max(a2 + b2, 0.000001);
4007
+ }
4008
+
4009
+ fn visible_environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
4010
+ let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
4011
+ let visible = !scene_visibility_blocked(origin, rayDirection, 1000000.0);
4012
+ return select(vec3<f32>(0.0), direct_environment_radiance(origin, rayDirection), visible);
4013
+ }
4014
+
4015
+ fn glossy_environment_direction(
4016
+ incidentDirection: vec3<f32>,
4017
+ normal: vec3<f32>,
4018
+ roughness: f32,
4019
+ normalBlendScale: f32
4020
+ ) -> vec3<f32> {
4021
+ let reflectionDirection = reflect(incidentDirection, normal);
4022
+ let blend = clamp(roughness * roughness * normalBlendScale, 0.0, 0.92);
4023
+ return safe_normalize(mix(reflectionDirection, normal, blend), normal);
4024
+ }
4025
+
4026
+ fn surface_glossiness(hit: HitRecord) -> f32 {
4027
+ let roughness = clamp(hit.material.x, 0.0, 1.0);
4028
+ let metallic = clamp(hit.material.y, 0.0, 1.0);
4029
+ let sheen = clamp(max_component(hit.materialResponse.xyz), 0.0, 1.0);
4030
+ let clearcoat = clamp(hit.materialResponse.w, 0.0, 1.0);
4031
+ let specularWeight = clamp(hit.materialExtension.y, 0.0, 1.0);
4032
+ let transmission = clamp(hit.materialExtension.z, 0.0, 1.0);
4033
+ let baseGloss =
4034
+ max(
4035
+ clearcoat,
4036
+ max(sheen * 0.72, max(specularWeight * (0.38 + metallic * 0.62), transmission))
4037
+ );
4038
+ return clamp(baseGloss * (1.0 - roughness * 0.72) + metallic * (1.0 - roughness) * 0.35, 0.0, 1.0);
4039
+ }
4040
+
4041
+ fn surface_specular_f0(hit: HitRecord, surfaceColor: vec3<f32>) -> vec3<f32> {
4042
+ let metallic = clamp(hit.material.y, 0.0, 1.0);
4043
+ let specularWeight = clamp(hit.materialExtension.y, 0.0, 1.0);
4044
+ let specularColor = clamp(hit.specularColor.xyz, vec3<f32>(0.0), vec3<f32>(1.0));
4045
+ let dielectricF0 = vec3<f32>(0.04) * specularWeight * specularColor;
4046
+ return mix(dielectricF0, surfaceColor, metallic);
4047
+ }
4048
+
4049
+ fn surface_bsdf_sampling_weights(hit: HitRecord) -> vec3<f32> {
4050
+ let metallic = clamp(hit.material.y, 0.0, 1.0);
4051
+ let clearcoat = clamp(hit.materialResponse.w, 0.0, 1.0);
4052
+ let specularWeight = clamp(hit.materialExtension.y, 0.0, 1.0);
4053
+ let diffuseWeight = clamp(
4054
+ (1.0 - metallic) * max(1.0 - specularWeight * 0.5 - clearcoat * 0.25, 0.15),
4055
+ 0.0,
4056
+ 1.0
4057
+ );
4058
+ let specWeight = clamp(max(metallic, specularWeight * 0.75) * (1.0 - clearcoat * 0.5), 0.0, 1.0);
4059
+ let clearcoatWeight = clamp(clearcoat, 0.0, 1.0);
4060
+ let totalWeight = max(diffuseWeight + specWeight + clearcoatWeight, 0.000001);
4061
+ return vec3<f32>(
4062
+ diffuseWeight / totalWeight,
4063
+ specWeight / totalWeight,
4064
+ clearcoatWeight / totalWeight
4065
+ );
4066
+ }
4067
+
4068
+ fn evaluate_surface_bsdf(hit: HitRecord, viewDirection: vec3<f32>, lightDirection: vec3<f32>) -> vec3<f32> {
4069
+ let normal = safe_normalize(hit.shadingNormal.xyz, vec3<f32>(0.0, 1.0, 0.0));
4070
+ let surfaceColor = clamp(max(hit.color.xyz, config.ambientColor.xyz * 0.35), vec3<f32>(0.0), vec3<f32>(1.0));
4071
+ let roughness = clamp(hit.material.x, 0.0, 1.0);
4072
+ let metallic = clamp(hit.material.y, 0.0, 1.0);
4073
+ let clearcoat = clamp(hit.materialResponse.w, 0.0, 1.0);
4074
+ let clearcoatRoughness = clamp(hit.materialExtension.x, 0.0, 1.0);
4075
+ let occlusion = clamp(hit.occlusion, 0.0, 1.0);
4076
+ let nDotV = saturate(dot(normal, viewDirection));
4077
+ let nDotL = saturate(dot(normal, lightDirection));
4078
+ if (nDotV <= 0.0 || nDotL <= 0.0) {
4079
+ return vec3<f32>(0.0);
4080
+ }
4081
+ let halfVector = safe_normalize(viewDirection + lightDirection, normal);
4082
+ let vDotH = saturate(dot(viewDirection, halfVector));
4083
+ let f0 = surface_specular_f0(hit, surfaceColor);
4084
+ let fresnel = fresnel_schlick(vDotH, f0);
4085
+ let distribution = distribution_ggx(normal, halfVector, roughness);
4086
+ let geometry = geometry_smith(normal, viewDirection, lightDirection, roughness);
4087
+ let specular = (distribution * geometry * fresnel) / max(4.0 * nDotV * nDotL, 0.000001);
4088
+ let diffuseWeight = (1.0 - metallic) * (1.0 - clearcoat * 0.24) * (1.0 - clamp(max_component(fresnel), 0.0, 0.98));
4089
+ let diffuse = surfaceColor * diffuseWeight / 3.14159265359;
4090
+ let clearcoatHalf = safe_normalize(viewDirection + lightDirection, normal);
4091
+ let clearcoatDistribution = distribution_ggx(normal, clearcoatHalf, max(clearcoatRoughness, 0.02));
4092
+ let clearcoatGeometry = geometry_smith(normal, viewDirection, lightDirection, max(clearcoatRoughness, 0.02));
4093
+ let clearcoatFresnel = fresnel_schlick(saturate(dot(viewDirection, clearcoatHalf)), vec3<f32>(0.04));
4094
+ let clearcoatTerm =
4095
+ (clearcoatDistribution * clearcoatGeometry * clearcoatFresnel) /
4096
+ max(4.0 * nDotV * nDotL, 0.000001) *
4097
+ clearcoat;
4098
+ return (diffuse + specular + clearcoatTerm) * mix(0.42, 1.0, occlusion);
4099
+ }
4100
+
4101
+ fn diffuse_pdf(normal: vec3<f32>, lightDirection: vec3<f32>) -> f32 {
4102
+ return saturate(dot(normal, lightDirection)) / 3.14159265359;
4103
+ }
4104
+
4105
+ fn ggx_pdf(normal: vec3<f32>, viewDirection: vec3<f32>, lightDirection: vec3<f32>, roughness: f32) -> f32 {
4106
+ let halfVector = safe_normalize(viewDirection + lightDirection, normal);
4107
+ let nDotH = saturate(dot(normal, halfVector));
4108
+ let vDotH = saturate(dot(viewDirection, halfVector));
4109
+ let distribution = distribution_ggx(normal, halfVector, roughness);
4110
+ return (distribution * nDotH) / max(4.0 * vDotH, 0.000001);
4111
+ }
4112
+
4113
+ fn evaluate_surface_bsdf_pdf(hit: HitRecord, viewDirection: vec3<f32>, lightDirection: vec3<f32>) -> f32 {
4114
+ let normal = safe_normalize(hit.shadingNormal.xyz, vec3<f32>(0.0, 1.0, 0.0));
4115
+ let roughness = clamp(hit.material.x, 0.0, 1.0);
4116
+ let weights = surface_bsdf_sampling_weights(hit);
4117
+ let diffuseTerm = diffuse_pdf(normal, lightDirection);
4118
+ let specTerm = ggx_pdf(normal, viewDirection, lightDirection, max(roughness, 0.02));
4119
+ let clearcoatTerm = ggx_pdf(normal, viewDirection, lightDirection, max(clamp(hit.materialExtension.x, 0.0, 1.0), 0.02));
4120
+ return weights.x * diffuseTerm + weights.y * specTerm + weights.z * clearcoatTerm;
2224
4121
  }
2225
4122
 
2226
4123
  fn gated_environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
@@ -2235,12 +4132,102 @@ fn gated_environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f
2235
4132
  return environment_radiance(origin, direction);
2236
4133
  }
2237
4134
 
4135
+ fn medium_dimensions() -> vec2<u32> {
4136
+ return textureDimensions(mediumTableTexture);
4137
+ }
4138
+
4139
+ fn medium_valid(mediumRefId: u32) -> bool {
4140
+ let dimensions = medium_dimensions();
4141
+ return mediumRefId > 0u && mediumRefId < dimensions.x;
4142
+ }
4143
+
4144
+ fn medium_absorption(mediumRefId: u32) -> vec3<f32> {
4145
+ if (!medium_valid(mediumRefId)) {
4146
+ return vec3<f32>(0.0);
4147
+ }
4148
+ return max(
4149
+ textureLoad(mediumTableTexture, vec2<i32>(i32(mediumRefId), 0), 0).xyz,
4150
+ vec3<f32>(0.0)
4151
+ );
4152
+ }
4153
+
4154
+ fn medium_scattering(mediumRefId: u32) -> vec3<f32> {
4155
+ if (!medium_valid(mediumRefId)) {
4156
+ return vec3<f32>(0.0);
4157
+ }
4158
+ return max(
4159
+ textureLoad(mediumTableTexture, vec2<i32>(i32(mediumRefId), 1), 0).xyz,
4160
+ vec3<f32>(0.0)
4161
+ );
4162
+ }
4163
+
4164
+ fn medium_transmittance(mediumRefId: u32, distance: f32) -> vec3<f32> {
4165
+ if (!medium_valid(mediumRefId) || distance <= 0.000001) {
4166
+ return vec3<f32>(1.0);
4167
+ }
4168
+ let extinction = medium_absorption(mediumRefId) + medium_scattering(mediumRefId);
4169
+ return vec3<f32>(
4170
+ exp(-extinction.x * distance),
4171
+ exp(-extinction.y * distance),
4172
+ exp(-extinction.z * distance)
4173
+ );
4174
+ }
4175
+
4176
+ fn transmitted_medium_ref_id(ray: RayRecord, hit: HitRecord) -> u32 {
4177
+ if (hit.mediumRefId == 0u) {
4178
+ return ray.mediumRefId;
4179
+ }
4180
+ if (hit.frontFace == 1u) {
4181
+ return hit.mediumRefId;
4182
+ }
4183
+ if (ray.mediumRefId == hit.mediumRefId) {
4184
+ return 0u;
4185
+ }
4186
+ return ray.mediumRefId;
4187
+ }
4188
+
2238
4189
  fn surface_path_response(hit: HitRecord) -> vec3<f32> {
2239
4190
  let color = clamp(hit.color.xyz, vec3<f32>(0.0), vec3<f32>(1.0));
2240
4191
  let opacity = clamp(hit.material.z, 0.0, 1.0);
4192
+ let occlusion = clamp(hit.occlusion, 0.0, 1.0);
2241
4193
  let materialEnergy = select(0.68, 0.92, hit.materialKind == 1u || hit.materialKind == 2u);
2242
4194
  let transparentEnergy = select(materialEnergy, 0.9, hit.hitType == 3u);
2243
- return mix(vec3<f32>(1.0), color, max(opacity, 0.18)) * transparentEnergy;
4195
+ return mix(vec3<f32>(1.0), color, max(opacity, 0.18)) * transparentEnergy * mix(0.55, 1.0, occlusion);
4196
+ }
4197
+
4198
+ fn bounded_path_response_luminance(ray: RayRecord, hit: HitRecord) -> f32 {
4199
+ let daylightFloor = max(config.pathResolveSettings.y, 0.0) * 0.08;
4200
+ let hdriFloor = max(config.environmentMapSettings.w, 0.0) * 0.02;
4201
+ let sceneFloor = max(daylightFloor, hdriFloor);
4202
+ if (sceneFloor <= 0.000001) {
4203
+ return 0.0;
4204
+ }
4205
+ let bounceRatio = select(
4206
+ 0.0,
4207
+ f32(ray.bounce) / max(f32(config.maxDepth - 1u), 1.0),
4208
+ config.maxDepth > 1u
4209
+ );
4210
+ let bounceScale = 1.0 - bounceRatio * 0.55;
4211
+ let materialScale = select(1.0, 0.34, hit.materialKind == 1u || hit.materialKind == 2u);
4212
+ let transparentScale = select(materialScale, 0.58, hit.hitType == 3u);
4213
+ let opacityScale = mix(0.55, 1.0, clamp(hit.material.z, 0.0, 1.0));
4214
+ return sceneFloor * bounceScale * transparentScale * opacityScale;
4215
+ }
4216
+
4217
+ fn stabilize_surface_path_response(ray: RayRecord, hit: HitRecord, response: vec3<f32>) -> vec3<f32> {
4218
+ let minimumLuminance = bounded_path_response_luminance(ray, hit);
4219
+ let responseLuminance = radiance_luminance(response);
4220
+ if (minimumLuminance <= 0.000001 || responseLuminance >= minimumLuminance) {
4221
+ return response;
4222
+ }
4223
+ let tintBase = max(response, max(hit.color.xyz * 0.65, config.ambientColor.xyz * 0.35));
4224
+ let tint = tintBase / max(max_component(tintBase), 0.0001);
4225
+ let lifted = select(
4226
+ tint * minimumLuminance,
4227
+ response * (minimumLuminance / max(responseLuminance, 0.0001)),
4228
+ responseLuminance > 0.0001
4229
+ );
4230
+ return clamp(lifted, vec3<f32>(0.0), vec3<f32>(0.98));
2244
4231
  }
2245
4232
 
2246
4233
  fn sunlit_baseline_radiance(normal: vec3<f32>) -> vec3<f32> {
@@ -2259,12 +4246,24 @@ fn sunlit_baseline_radiance(normal: vec3<f32>) -> vec3<f32> {
2259
4246
  return clamp_sample_radiance(sunTint * baseline * skyFacing * directionalWeight * 0.04);
2260
4247
  }
2261
4248
 
2262
- fn terminal_surface_environment_source(hit: HitRecord) -> vec3<f32> {
4249
+ fn terminal_surface_environment_source(ray: RayRecord, hit: HitRecord) -> vec3<f32> {
2263
4250
  let normal = safe_normalize(hit.shadingNormal.xyz, vec3<f32>(0.0, 1.0, 0.0));
2264
- let normalEnvironment = gated_environment_radiance(
2265
- hit.position.xyz + normal * 0.003,
2266
- normal
4251
+ let origin = hit.position.xyz + normal * 0.003;
4252
+ let roughness = clamp(hit.material.x, 0.0, 1.0);
4253
+ let glossiness = surface_glossiness(hit);
4254
+ let normalEnvironment = gated_environment_radiance(origin, normal);
4255
+ let viewDirection = safe_normalize(-ray.direction.xyz, normal);
4256
+ let reflectionDirection = glossy_environment_direction(
4257
+ ray.direction.xyz,
4258
+ normal,
4259
+ roughness,
4260
+ mix(0.88, 0.38, glossiness)
2267
4261
  );
4262
+ let reflectionEnvironment = prefiltered_environment_radiance(reflectionDirection, roughness);
4263
+ let surfaceColor = clamp(max(hit.color.xyz, config.ambientColor.xyz * 0.35), vec3<f32>(0.0), vec3<f32>(1.0));
4264
+ let f0 = surface_specular_f0(hit, surfaceColor);
4265
+ let brdfTerm = sample_brdf_lut(saturate(dot(normal, viewDirection)), roughness);
4266
+ let specularEnvironment = reflectionEnvironment * (f0 * brdfTerm.x + vec3<f32>(brdfTerm.y));
2268
4267
  let sunlitFloor = sunlit_baseline_radiance(normal);
2269
4268
  let ambientFloor = select(
2270
4269
  max(config.ambientColor.xyz, sunlitFloor * 0.82),
@@ -2276,17 +4275,27 @@ fn terminal_surface_environment_source(hit: HitRecord) -> vec3<f32> {
2276
4275
  max(config.environmentMapSettings.w, max(0.12, config.pathResolveSettings.y * 0.42)),
2277
4276
  environment_map_enabled()
2278
4277
  );
2279
- let environmentFloor = max(ambientFloor, max(sunlitFloor, normalEnvironment * environmentInfluence));
4278
+ let glossyEnvironment = max(
4279
+ normalEnvironment,
4280
+ max(reflectionEnvironment * mix(0.24, 0.92, glossiness), specularEnvironment)
4281
+ );
4282
+ let environmentFloor = max(ambientFloor, max(sunlitFloor, glossyEnvironment * environmentInfluence));
2280
4283
  let materialFloor = select(0.7, 1.0, hit.materialKind == 0u || hit.materialKind == 3u);
2281
4284
  return clamp_sample_radiance(environmentFloor * materialFloor);
2282
4285
  }
2283
4286
 
2284
- fn terminal_surface_environment_contribution(ray: RayRecord, hit: HitRecord) -> vec3<f32> {
4287
+ fn terminal_surface_environment_contribution(
4288
+ ray: RayRecord,
4289
+ throughput: vec3<f32>,
4290
+ hit: HitRecord
4291
+ ) -> vec3<f32> {
2285
4292
  let surfaceColor = max(hit.color.xyz, config.ambientColor.xyz);
4293
+ let occlusion = mix(0.75, 1.0, clamp(hit.occlusion, 0.0, 1.0));
2286
4294
  return clamp_sample_radiance(
2287
- ray.throughput.xyz *
4295
+ throughput *
2288
4296
  surfaceColor *
2289
- terminal_surface_environment_source(hit)
4297
+ terminal_surface_environment_source(ray, hit) *
4298
+ occlusion
2290
4299
  );
2291
4300
  }
2292
4301
 
@@ -2319,6 +4328,10 @@ fn direct_environment_portal_irradiance(origin: vec3<f32>, normal: vec3<f32>) ->
2319
4328
  );
2320
4329
  let area = max(portal.position.w, 0.0001);
2321
4330
  let distanceFalloff = clamp(area / max(distanceSquared, area * 0.25), 0.0, 2.5);
4331
+ let traceDistance = max(sqrt(distanceSquared) - 0.01, 0.01);
4332
+ if (scene_visibility_blocked(origin, direction, traceDistance)) {
4333
+ continue;
4334
+ }
2322
4335
  irradiance = irradiance +
2323
4336
  portal.color.rgb *
2324
4337
  portal.normal.w *
@@ -2330,48 +4343,79 @@ fn direct_environment_portal_irradiance(origin: vec3<f32>, normal: vec3<f32>) ->
2330
4343
  return irradiance;
2331
4344
  }
2332
4345
 
2333
- fn surface_direct_environment_contribution(ray: RayRecord, hit: HitRecord) -> vec3<f32> {
2334
- let normal = safe_normalize(hit.shadingNormal.xyz, vec3<f32>(0.0, 1.0, 0.0));
2335
- let origin = hit.position.xyz + normal * 0.003;
2336
- let viewDirection = safe_normalize(-ray.direction.xyz, normal);
2337
- let surfaceColor = clamp(max(hit.color.xyz, config.ambientColor.xyz * 0.35), vec3<f32>(0.0), vec3<f32>(1.0));
2338
- let roughness = clamp(hit.material.x, 0.0, 1.0);
2339
- let metallic = clamp(hit.material.y, 0.0, 1.0);
2340
-
2341
- let normalEnvironment = gated_environment_radiance(origin, normal);
2342
- let skyVisibility = 0.35 + saturate(normal.y * 0.5 + 0.5) * 0.45;
2343
- let sunlitFloor = sunlit_baseline_radiance(normal);
2344
- let ambientIrradiance = max(
2345
- select(config.ambientColor.xyz * 0.72, config.ambientColor.xyz * 0.28, environment_map_enabled()),
2346
- sunlitFloor * select(0.72, 0.45, environment_map_enabled())
2347
- );
2348
- let environmentIrradianceScale = select(
2349
- max(0.16, config.pathResolveSettings.y * 0.45),
2350
- max(config.environmentMapSettings.w, max(0.16, config.pathResolveSettings.y * 0.45)),
2351
- environment_map_enabled()
4346
+ fn visibility_test_ray(origin: vec3<f32>, direction: vec3<f32>) -> RayRecord {
4347
+ let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
4348
+ return RayRecord(
4349
+ 0u,
4350
+ 0u,
4351
+ 0u,
4352
+ 0u,
4353
+ 0u,
4354
+ 0u,
4355
+ 0u,
4356
+ 0u,
4357
+ vec4<f32>(origin, 1.0),
4358
+ vec4<f32>(rayDirection, 0.0),
4359
+ vec4<f32>(1.0, 1.0, 1.0, 1.0)
2352
4360
  );
2353
- let skyIrradiance = max(ambientIrradiance, normalEnvironment * skyVisibility * environmentIrradianceScale);
4361
+ }
2354
4362
 
2355
- let sunDirection = safe_normalize(
2356
- config.environmentSunDirectionIntensity.xyz,
2357
- vec3<f32>(0.0, 1.0, 0.0)
2358
- );
2359
- let sunFacing = saturate(dot(normal, sunDirection));
2360
- let sunRadiance = gated_environment_radiance(origin, sunDirection);
2361
- let sunIrradiance = sunRadiance * sunFacing * 0.2;
2362
- let portalIrradiance = direct_environment_portal_irradiance(origin, normal);
4363
+ fn scene_visibility_blocked(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> bool {
4364
+ let testRay = visibility_test_ray(origin, direction);
4365
+ let nearest = max(maxDistance, 0.001);
2363
4366
 
2364
- let diffuseWeight = select(1.0 - metallic * 0.65, 0.22, hit.materialKind == 1u);
2365
- let diffuse = surfaceColor * (skyIrradiance + sunIrradiance + portalIrradiance) * diffuseWeight;
4367
+ for (var objectIndex = 0u; objectIndex < config.sceneObjectCount; objectIndex = objectIndex + 1u) {
4368
+ let object = sceneObjects[objectIndex];
4369
+ var current = no_candidate();
4370
+ if (object.kind == 1u) {
4371
+ current = intersect_sphere(testRay, object);
4372
+ } else if (object.kind == 2u) {
4373
+ current = intersect_box(testRay, object);
4374
+ }
4375
+ if (current.hit == 1u && current.distance < nearest) {
4376
+ return true;
4377
+ }
4378
+ }
2366
4379
 
2367
- let halfVector = safe_normalize(sunDirection + viewDirection, normal);
2368
- let specularPower = 8.0 + (1.0 - roughness) * 96.0;
2369
- let specularFacing = pow(saturate(dot(normal, halfVector)), specularPower) * sunFacing;
2370
- let specularTint = mix(vec3<f32>(0.04), surfaceColor, metallic);
2371
- let specular = specularTint * sunRadiance * specularFacing * select(0.16, 0.48, hit.materialKind == 1u || hit.materialKind == 2u);
4380
+ let meshCandidate = intersect_bvh(testRay, nearest);
4381
+ return meshCandidate.hit == 1u && meshCandidate.distance < nearest;
4382
+ }
2372
4383
 
2373
- let bounceWeight = select(1.0, 0.38, ray.bounce > 0u);
2374
- return clamp_sample_radiance(ray.throughput.xyz * (diffuse + specular) * bounceWeight);
4384
+ fn surface_direct_environment_contribution(ray: RayRecord, hit: HitRecord) -> vec3<f32> {
4385
+ let normal = safe_normalize(hit.shadingNormal.xyz, vec3<f32>(0.0, 1.0, 0.0));
4386
+ let origin = hit.position.xyz + normal * 0.003;
4387
+ let viewDirection = safe_normalize(-ray.direction.xyz, normal);
4388
+ let lightSample = sample_environment_importance(vec2<f32>(
4389
+ random01(mix_seed(ray.sourcePixelId, ray.sampleId, ray.bounce, config.frameIndex, 41u)),
4390
+ random01(mix_seed(ray.sourcePixelId, ray.sampleId, ray.bounce, config.frameIndex, 43u))
4391
+ ));
4392
+ if (lightSample.pdf <= 0.000001) {
4393
+ return vec3<f32>(0.0);
4394
+ }
4395
+ let lightDirection = safe_normalize(lightSample.direction, normal);
4396
+ let nDotL = saturate(dot(normal, lightDirection));
4397
+ if (nDotL <= 0.000001) {
4398
+ return vec3<f32>(0.0);
4399
+ }
4400
+ if (scene_visibility_blocked(origin, lightDirection, 1000000.0)) {
4401
+ return vec3<f32>(0.0);
4402
+ }
4403
+ let incidentRadiance = direct_environment_radiance(origin, lightDirection);
4404
+ if (max_component(incidentRadiance) <= 0.000001) {
4405
+ return vec3<f32>(0.0);
4406
+ }
4407
+ let bsdf = evaluate_surface_bsdf(hit, viewDirection, lightDirection);
4408
+ if (max_component(bsdf) <= 0.000001) {
4409
+ return vec3<f32>(0.0);
4410
+ }
4411
+ let bsdfPdf = evaluate_surface_bsdf_pdf(hit, viewDirection, lightDirection);
4412
+ let misWeight = power_heuristic(lightSample.pdf, bsdfPdf);
4413
+ let contribution =
4414
+ ray.throughput.xyz *
4415
+ bsdf *
4416
+ incidentRadiance *
4417
+ (nDotL * misWeight / max(lightSample.pdf, 0.000001));
4418
+ return clamp_sample_radiance(contribution);
2375
4419
  }
2376
4420
 
2377
4421
  fn default_mesh_range() -> MeshRange {
@@ -2390,7 +4434,16 @@ fn default_mesh_range() -> MeshRange {
2390
4434
  0u,
2391
4435
  vec4<f32>(0.72, 0.72, 0.68, 1.0),
2392
4436
  vec4<f32>(0.0),
2393
- vec4<f32>(0.72, 0.0, 1.0, 1.45)
4437
+ vec4<f32>(0.72, 0.0, 1.0, 1.45),
4438
+ vec4<f32>(0.0, 0.0, 0.0, 0.08),
4439
+ vec4<f32>(1.0, 1.0, 1.0, 1.0),
4440
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
4441
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
4442
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
4443
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
4444
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
4445
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
4446
+ vec4<f32>(1.0, 1.0, 1.0, 0.0)
2394
4447
  );
2395
4448
  }
2396
4449
 
@@ -2486,7 +4539,7 @@ fn prepareMeshTrianglesAndLeaves(@builtin(global_invocation_id) globalId: vec3<u
2486
4539
  mesh.flags,
2487
4540
  mesh.materialRefId,
2488
4541
  mesh.mediumRefId,
2489
- 0u,
4542
+ mesh.materialSlot,
2490
4543
  0u,
2491
4544
  vec4<f32>(vertex0.position.xyz, 0.0),
2492
4545
  vec4<f32>(vertex1.position.xyz, 0.0),
@@ -2498,7 +4551,16 @@ fn prepareMeshTrianglesAndLeaves(@builtin(global_invocation_id) globalId: vec3<u
2498
4551
  vec4<f32>(uv2, 0.0, 0.0),
2499
4552
  mesh.color,
2500
4553
  mesh.emission,
2501
- mesh.material
4554
+ mesh.material,
4555
+ mesh.materialResponse,
4556
+ mesh.materialExtension,
4557
+ mesh.specularColor,
4558
+ mesh.baseColorAtlas,
4559
+ mesh.metallicRoughnessAtlas,
4560
+ mesh.normalAtlas,
4561
+ mesh.occlusionAtlas,
4562
+ mesh.emissiveAtlas,
4563
+ mesh.textureSettings
2502
4564
  );
2503
4565
 
2504
4566
  let leafBase = config.triangleCount - 1u;
@@ -2657,7 +4719,8 @@ fn make_miss(ray: RayRecord) -> HitRecord {
2657
4719
  0u,
2658
4720
  0u,
2659
4721
  -1.0,
2660
- vec3<f32>(0.0),
4722
+ 1.0,
4723
+ vec2<f32>(0.0),
2661
4724
  vec4<f32>(ray.origin.xyz + ray.direction.xyz * 1000.0, 1.0),
2662
4725
  vec4<f32>(-ray.direction.xyz, 0.0),
2663
4726
  vec4<f32>(-ray.direction.xyz, 0.0),
@@ -2665,7 +4728,10 @@ fn make_miss(ray: RayRecord) -> HitRecord {
2665
4728
  vec4<f32>(0.0),
2666
4729
  vec4<f32>(radiance, 1.0),
2667
4730
  vec4<f32>(0.0),
2668
- vec4<f32>(1.0, 0.0, 1.0, 1.0)
4731
+ vec4<f32>(1.0, 0.0, 1.0, 1.0),
4732
+ vec4<f32>(0.0, 0.0, 0.0, 0.08),
4733
+ vec4<f32>(0.08, 1.0, 0.0, 0.0),
4734
+ vec4<f32>(1.0, 1.0, 1.0, 1.0)
2669
4735
  );
2670
4736
  }
2671
4737
 
@@ -2700,7 +4766,7 @@ fn intersect_sphere(ray: RayRecord, object: SceneObject) -> Candidate {
2700
4766
  0xffffffffu,
2701
4767
  object.objectId,
2702
4768
  object.objectId,
2703
- 0u
4769
+ object.mediumRefId
2704
4770
  );
2705
4771
  }
2706
4772
 
@@ -2752,7 +4818,7 @@ fn intersect_box(ray: RayRecord, object: SceneObject) -> Candidate {
2752
4818
  0xffffffffu,
2753
4819
  object.objectId,
2754
4820
  object.objectId,
2755
- 0u
4821
+ object.mediumRefId
2756
4822
  );
2757
4823
  }
2758
4824
 
@@ -2960,6 +5026,19 @@ fn denoise_range_space(value: vec3<f32>) -> vec3<f32> {
2960
5026
  return value / (vec3<f32>(1.0) + value);
2961
5027
  }
2962
5028
 
5029
+ fn denoise_sample_count() -> f32 {
5030
+ return clamp(1.0 / max(config.projectionAndSampling.z, 0.000001), 1.0, 256.0);
5031
+ }
5032
+
5033
+ fn denoise_strength() -> f32 {
5034
+ let spp = denoise_sample_count();
5035
+ return clamp(0.44 / sqrt(spp), 0.08, 0.44);
5036
+ }
5037
+
5038
+ fn denoise_kernel_radius() -> i32 {
5039
+ return select(1i, 2i, denoise_sample_count() < 2.5);
5040
+ }
5041
+
2963
5042
  @compute @workgroup_size(64)
2964
5043
  fn generatePrimaryRays(@builtin(global_invocation_id) globalId: vec3<u32>) {
2965
5044
  let index = globalId.x;
@@ -2990,6 +5069,10 @@ fn intersectActiveQueue(@builtin(global_invocation_id) globalId: vec3<u32>) {
2990
5069
  let ray = activeQueue[index];
2991
5070
  var nearest = 1000000.0;
2992
5071
  var hitObject = SceneObject(
5072
+ 0u,
5073
+ 0u,
5074
+ 0u,
5075
+ 0u,
2993
5076
  0u,
2994
5077
  0u,
2995
5078
  0u,
@@ -2998,7 +5081,10 @@ fn intersectActiveQueue(@builtin(global_invocation_id) globalId: vec3<u32>) {
2998
5081
  vec4<f32>(0.0),
2999
5082
  vec4<f32>(0.0),
3000
5083
  vec4<f32>(0.0),
3001
- vec4<f32>(1.0, 0.0, 1.0, 1.0)
5084
+ vec4<f32>(1.0, 0.0, 1.0, 1.0),
5085
+ vec4<f32>(0.0, 0.0, 0.0, 0.08),
5086
+ vec4<f32>(0.08, 1.0, 0.0, 0.0),
5087
+ vec4<f32>(1.0, 1.0, 1.0, 1.0)
3002
5088
  );
3003
5089
  var candidate = no_candidate();
3004
5090
  var hitTriangle = TriangleRecord(
@@ -3020,7 +5106,16 @@ fn intersectActiveQueue(@builtin(global_invocation_id) globalId: vec3<u32>) {
3020
5106
  vec4<f32>(0.0),
3021
5107
  vec4<f32>(0.0),
3022
5108
  vec4<f32>(0.0),
3023
- vec4<f32>(1.0, 0.0, 1.0, 1.0)
5109
+ vec4<f32>(1.0, 0.0, 1.0, 1.0),
5110
+ vec4<f32>(0.0, 0.0, 0.0, 0.08),
5111
+ vec4<f32>(0.08, 1.0, 0.0, 0.0),
5112
+ vec4<f32>(1.0, 1.0, 1.0, 1.0),
5113
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
5114
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
5115
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
5116
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
5117
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
5118
+ vec4<f32>(1.0, 1.0, 1.0, 0.0)
3024
5119
  );
3025
5120
 
3026
5121
  for (var objectIndex = 0u; objectIndex < config.sceneObjectCount; objectIndex = objectIndex + 1u) {
@@ -3053,16 +5148,28 @@ fn intersectActiveQueue(@builtin(global_invocation_id) globalId: vec3<u32>) {
3053
5148
  let position = ray.origin.xyz + ray.direction.xyz * candidate.distance;
3054
5149
  let hitMaterialKind = select(hitObject.materialKind, hitTriangle.materialKind, candidate.triangleIndex != 0xffffffffu);
3055
5150
  let hitObjectId = select(hitObject.objectId, hitTriangle.meshId, candidate.triangleIndex != 0xffffffffu);
3056
- let hitColor = select(hitObject.color, hitTriangle.color, candidate.triangleIndex != 0xffffffffu);
3057
- let hitEmission = select(hitObject.emission, hitTriangle.emission, candidate.triangleIndex != 0xffffffffu);
3058
- let hitMaterial = select(hitObject.material, hitTriangle.material, candidate.triangleIndex != 0xffffffffu);
5151
+ let meshSurface = sample_surface_material(
5152
+ hitTriangle,
5153
+ candidate.uv,
5154
+ candidate.geometricNormal,
5155
+ candidate.shadingNormal
5156
+ );
5157
+ let hitColor = select(hitObject.color, meshSurface.color, candidate.triangleIndex != 0xffffffffu);
5158
+ let hitEmission = select(hitObject.emission, meshSurface.emission, candidate.triangleIndex != 0xffffffffu);
5159
+ let hitMaterial = select(hitObject.material, meshSurface.material, candidate.triangleIndex != 0xffffffffu);
5160
+ let hitMaterialResponse = select(hitObject.materialResponse, meshSurface.materialResponse, candidate.triangleIndex != 0xffffffffu);
5161
+ let hitMaterialExtension = select(hitObject.materialExtension, meshSurface.materialExtension, candidate.triangleIndex != 0xffffffffu);
5162
+ let hitSpecularColor = select(hitObject.specularColor, meshSurface.specularColor, candidate.triangleIndex != 0xffffffffu);
5163
+ let hitShadingNormal = select(candidate.shadingNormal, meshSurface.shadingNormal, candidate.triangleIndex != 0xffffffffu);
3059
5164
  let hitPrimitiveId = select(candidate.primitiveId, hitTriangle.triangleId, candidate.triangleIndex != 0xffffffffu);
3060
5165
  let hitMaterialRefId = select(candidate.materialRefId, hitTriangle.materialRefId, candidate.triangleIndex != 0xffffffffu);
3061
5166
  let hitMediumRefId = select(candidate.mediumRefId, hitTriangle.mediumRefId, candidate.triangleIndex != 0xffffffffu);
5167
+ let hitMaterialSlot = select(0u, hitTriangle.materialSlot, candidate.triangleIndex != 0xffffffffu);
5168
+ let hitOcclusion = select(1.0, meshSurface.occlusion, candidate.triangleIndex != 0xffffffffu);
3062
5169
  var hitType = 0u;
3063
5170
  if (hitMaterialKind == 4u || emission_power(hitEmission) > 0.0001) {
3064
5171
  hitType = 1u;
3065
- } else if (hitMaterialKind == 3u || hitMaterial.z < 0.999) {
5172
+ } else if (hitMaterialKind == 3u || hitMaterial.z < 0.999 || hitMaterialExtension.z > 0.001) {
3066
5173
  hitType = 3u;
3067
5174
  }
3068
5175
  atomicAdd(&counters.hitCount, 1u);
@@ -3076,19 +5183,23 @@ fn intersectActiveQueue(@builtin(global_invocation_id) globalId: vec3<u32>) {
3076
5183
  hitPrimitiveId,
3077
5184
  hitMaterialRefId,
3078
5185
  hitMediumRefId,
3079
- 0u,
5186
+ hitMaterialSlot,
3080
5187
  0u,
3081
5188
  0u,
3082
5189
  candidate.distance,
3083
- vec3<f32>(0.0),
5190
+ hitOcclusion,
5191
+ vec2<f32>(0.0),
3084
5192
  vec4<f32>(position, 1.0),
3085
5193
  vec4<f32>(candidate.geometricNormal, 0.0),
3086
- vec4<f32>(candidate.shadingNormal, 0.0),
5194
+ vec4<f32>(hitShadingNormal, 0.0),
3087
5195
  vec4<f32>(candidate.barycentric, 0.0),
3088
5196
  vec4<f32>(candidate.uv, 0.0, 0.0),
3089
5197
  hitColor,
3090
5198
  hitEmission,
3091
- hitMaterial
5199
+ hitMaterial,
5200
+ hitMaterialResponse,
5201
+ hitMaterialExtension,
5202
+ hitSpecularColor
3092
5203
  );
3093
5204
  }
3094
5205
 
@@ -3157,60 +5268,106 @@ fn sample_environment_portal_direction(hit: HitRecord, seed: u32, fallback: vec3
3157
5268
  }
3158
5269
 
3159
5270
  fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult {
5271
+ let normal = safe_normalize(hit.shadingNormal.xyz, vec3<f32>(0.0, 1.0, 0.0));
5272
+ let viewDirection = safe_normalize(-ray.direction.xyz, normal);
3160
5273
  let roughness = clamp(hit.material.x, 0.0, 1.0);
3161
- if (hit.materialKind == 1u) {
5274
+ let transmission = clamp(hit.materialExtension.z, 0.0, 1.0);
5275
+ if (hit.materialKind == 1u && roughness <= 0.02) {
3162
5276
  return ScatterResult(
3163
- vec4<f32>(
3164
- safe_normalize(
3165
- reflect(ray.direction.xyz, hit.shadingNormal.xyz) + random_unit_vector(seed) * roughness,
3166
- hit.shadingNormal.xyz
3167
- ),
3168
- 0.0
3169
- ),
3170
- 0u,
3171
- 0u,
5277
+ vec4<f32>(reflect(ray.direction.xyz, normal), 0.0),
5278
+ 1.0,
5279
+ ray.mediumRefId,
5280
+ RAY_FLAG_DELTA_SAMPLE,
3172
5281
  0u,
3173
- 0u
3174
5282
  );
3175
5283
  }
3176
5284
 
3177
- if (hit.materialKind == 2u || hit.materialKind == 3u) {
5285
+ if (hit.materialKind == 2u || hit.materialKind == 3u || transmission > 0.001) {
3178
5286
  let ior = max(hit.material.w, 1.01);
3179
5287
  let etaRatio = select(ior, 1.0 / ior, hit.frontFace == 1u);
3180
- let cosTheta = min(dot(-ray.direction.xyz, hit.shadingNormal.xyz), 1.0);
5288
+ let cosTheta = min(dot(-ray.direction.xyz, normal), 1.0);
3181
5289
  let sinTheta = sqrt(max(0.0, 1.0 - cosTheta * cosTheta));
3182
5290
  let cannotRefract = etaRatio * sinTheta > 1.0;
3183
5291
  let reflectChance = schlick(cosTheta, etaRatio);
3184
- if (cannotRefract || random01(seed + 23u) < reflectChance) {
3185
- return ScatterResult(vec4<f32>(reflect(ray.direction.xyz, hit.shadingNormal.xyz), 0.0), 0u, 0u, 0u, 0u);
5292
+ let transmissionReflectChance = select(
5293
+ reflectChance,
5294
+ max(reflectChance, 1.0 - transmission),
5295
+ transmission > 0.001
5296
+ );
5297
+ if (cannotRefract || random01(seed + 23u) < transmissionReflectChance) {
5298
+ return ScatterResult(
5299
+ vec4<f32>(reflect(ray.direction.xyz, normal), 0.0),
5300
+ 1.0,
5301
+ ray.mediumRefId,
5302
+ RAY_FLAG_DELTA_SAMPLE,
5303
+ 0u,
5304
+ );
3186
5305
  }
3187
- return ScatterResult(vec4<f32>(refract_direction(ray.direction.xyz, hit.shadingNormal.xyz, etaRatio), 0.0), 0u, 0u, 0u, 0u);
3188
- }
3189
-
3190
- let randomDiffuse = safe_normalize(
3191
- hit.shadingNormal.xyz + random_unit_vector(seed),
3192
- hit.shadingNormal.xyz
3193
- );
3194
- let guidedLight = sample_emissive_triangle_direction(hit, seed, randomDiffuse);
3195
- let canSampleLight = dot(hit.shadingNormal.xyz, guidedLight) > -0.04;
3196
- let guideProbability = select(0.38, 0.72, ray.bounce == 0u);
3197
- let useGuidedLight = canSampleLight && random01(seed + 37u) < guideProbability;
3198
- let guidedPortal = sample_environment_portal_direction(hit, seed, randomDiffuse);
3199
- let canSamplePortal = dot(hit.shadingNormal.xyz, guidedPortal) > -0.04;
3200
- let useGuidedPortal =
3201
- !useGuidedLight &&
3202
- canSamplePortal &&
3203
- config.environmentPortalCount > 0u &&
3204
- config.environmentPortalMode > 0u &&
3205
- random01(seed + 89u) < 0.58;
3206
- let guidedDirection = select(randomDiffuse, guidedPortal, useGuidedPortal);
3207
- return ScatterResult(
3208
- vec4<f32>(select(guidedDirection, guidedLight, useGuidedLight), 0.0),
3209
- select(0u, RAY_FLAG_GUIDED_EMISSIVE, useGuidedLight),
3210
- 0u,
3211
- 0u,
3212
- 0u
3213
- );
5306
+ return ScatterResult(
5307
+ vec4<f32>(refract_direction(ray.direction.xyz, normal, etaRatio), 0.0),
5308
+ 1.0,
5309
+ transmitted_medium_ref_id(ray, hit),
5310
+ RAY_FLAG_DELTA_SAMPLE,
5311
+ 0u,
5312
+ );
5313
+ }
5314
+
5315
+ let guidedEmissiveAvailable = config.emissiveTriangleCount > 0u;
5316
+ let guidedPortalAvailable =
5317
+ config.environmentPortalCount > 0u && config.environmentPortalMode != 0u;
5318
+ let guidedSelector = random01(seed + 17u);
5319
+ if (guidedEmissiveAvailable && guidedSelector < 0.18) {
5320
+ let guidedDirection = sample_emissive_triangle_direction(hit, seed + 101u, normal);
5321
+ if (dot(normal, guidedDirection) > 0.000001) {
5322
+ let guidedPdf = max(evaluate_surface_bsdf_pdf(hit, viewDirection, guidedDirection), 0.000001);
5323
+ return ScatterResult(
5324
+ vec4<f32>(guidedDirection, 0.0),
5325
+ guidedPdf,
5326
+ ray.mediumRefId,
5327
+ RAY_FLAG_GUIDED_EMISSIVE,
5328
+ 0u,
5329
+ );
5330
+ }
5331
+ }
5332
+ if (guidedPortalAvailable && guidedSelector < 0.32) {
5333
+ let guidedDirection = sample_environment_portal_direction(hit, seed + 131u, normal);
5334
+ if (dot(normal, guidedDirection) > 0.000001) {
5335
+ let guidedPdf = max(evaluate_surface_bsdf_pdf(hit, viewDirection, guidedDirection), 0.000001);
5336
+ return ScatterResult(vec4<f32>(guidedDirection, 0.0), guidedPdf, ray.mediumRefId, 0u, 0u);
5337
+ }
5338
+ }
5339
+
5340
+ let weights = surface_bsdf_sampling_weights(hit);
5341
+ let selector = random01(seed + 31u);
5342
+ var lightDirection = normal;
5343
+ if (selector < weights.x) {
5344
+ lightDirection = cosine_sample_hemisphere(
5345
+ vec2<f32>(random01(seed + 37u), random01(seed + 41u)),
5346
+ normal
5347
+ );
5348
+ } else if (selector < weights.x + weights.y) {
5349
+ let halfVector = importance_sample_ggx(
5350
+ vec2<f32>(random01(seed + 47u), random01(seed + 53u)),
5351
+ max(roughness, 0.02),
5352
+ normal
5353
+ );
5354
+ lightDirection = safe_normalize(reflect(-viewDirection, halfVector), normal);
5355
+ } else {
5356
+ let halfVector = importance_sample_ggx(
5357
+ vec2<f32>(random01(seed + 59u), random01(seed + 61u)),
5358
+ max(clamp(hit.materialExtension.x, 0.0, 1.0), 0.02),
5359
+ normal
5360
+ );
5361
+ lightDirection = safe_normalize(reflect(-viewDirection, halfVector), normal);
5362
+ }
5363
+ if (dot(normal, lightDirection) <= 0.000001) {
5364
+ lightDirection = cosine_sample_hemisphere(
5365
+ vec2<f32>(random01(seed + 67u), random01(seed + 71u)),
5366
+ normal
5367
+ );
5368
+ }
5369
+ let pdf = max(evaluate_surface_bsdf_pdf(hit, viewDirection, lightDirection), 0.000001);
5370
+ return ScatterResult(vec4<f32>(lightDirection, 0.0), pdf, ray.mediumRefId, 0u, 0u);
3214
5371
  }
3215
5372
 
3216
5373
  @compute @workgroup_size(64)
@@ -3223,15 +5380,17 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
3223
5380
 
3224
5381
  let ray = activeQueue[index];
3225
5382
  let hit = hits[index];
5383
+ let segmentTransmittance = medium_transmittance(ray.mediumRefId, hit.distance);
5384
+ let arrivingThroughput = ray.throughput.xyz * segmentTransmittance;
3226
5385
  var contribution = vec3<f32>(0.0);
3227
5386
 
3228
5387
  if (hit.hitType == 1u) {
3229
5388
  let guidedLightWeight = select(1.0, 0.24, (ray.flags & RAY_FLAG_GUIDED_EMISSIVE) != 0u);
3230
5389
  let sourceRadiance = max(hit.emission.xyz, hit.color.xyz) * guidedLightWeight;
3231
5390
  if (deferred_path_resolve_enabled()) {
3232
- record_deferred_terminal_source(ray, sourceRadiance);
5391
+ record_deferred_terminal_source(ray, sourceRadiance * segmentTransmittance);
3233
5392
  } else {
3234
- contribution = clamp_sample_radiance(ray.throughput.xyz * sourceRadiance);
5393
+ contribution = clamp_sample_radiance(arrivingThroughput * sourceRadiance);
3235
5394
  accumulation[ray.rayId] =
3236
5395
  accumulation[ray.rayId] + vec4<f32>(contribution * sample_weight(), 1.0);
3237
5396
  }
@@ -3240,10 +5399,17 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
3240
5399
  }
3241
5400
 
3242
5401
  if (hit.hitType == 2u) {
5402
+ var sourceRadiance = hit.color.xyz;
5403
+ if ((ray.flags & RAY_FLAG_DELTA_SAMPLE) == 0u) {
5404
+ let bsdfPdf = max(ray.throughput.w, 0.000001);
5405
+ let lightPdf = environment_direction_pdf(ray.direction.xyz);
5406
+ let misWeight = power_heuristic(bsdfPdf, lightPdf);
5407
+ sourceRadiance = sourceRadiance * misWeight;
5408
+ }
3243
5409
  if (deferred_path_resolve_enabled()) {
3244
- record_deferred_terminal_source(ray, hit.color.xyz);
5410
+ record_deferred_terminal_source(ray, sourceRadiance * segmentTransmittance);
3245
5411
  } else {
3246
- contribution = clamp_sample_radiance(ray.throughput.xyz * max(hit.color.xyz, config.ambientColor.xyz));
5412
+ contribution = clamp_sample_radiance(arrivingThroughput * sourceRadiance);
3247
5413
  accumulation[ray.rayId] =
3248
5414
  accumulation[ray.rayId] + vec4<f32>(contribution * sample_weight(), 1.0);
3249
5415
  }
@@ -3251,24 +5417,47 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
3251
5417
  return;
3252
5418
  }
3253
5419
 
3254
- let response = surface_path_response(hit);
5420
+ let response = stabilize_surface_path_response(
5421
+ ray,
5422
+ hit,
5423
+ surface_path_response(hit) * segmentTransmittance
5424
+ );
3255
5425
  record_deferred_path_response(ray, response);
3256
5426
 
3257
5427
  let shouldEstimateDirectEnvironment =
3258
- !deferred_path_resolve_enabled() &&
3259
5428
  (hit.materialKind == 0u || hit.materialKind == 1u) &&
3260
- hit.material.z >= 0.95;
5429
+ hit.material.z >= 0.95 &&
5430
+ ray.bounce < 2u;
3261
5431
  if (shouldEstimateDirectEnvironment) {
3262
- let directEnvironment = surface_direct_environment_contribution(ray, hit);
5432
+ let directEnvironment = surface_direct_environment_contribution(
5433
+ RayRecord(
5434
+ ray.rayId,
5435
+ ray.parentRayId,
5436
+ ray.sourcePixelId,
5437
+ ray.sampleId,
5438
+ ray.bounce,
5439
+ ray.mediumRefId,
5440
+ ray.flags,
5441
+ 0u,
5442
+ ray.origin,
5443
+ ray.direction,
5444
+ vec4<f32>(arrivingThroughput, ray.throughput.w)
5445
+ ),
5446
+ hit
5447
+ );
3263
5448
  accumulation[ray.rayId] =
3264
5449
  accumulation[ray.rayId] + vec4<f32>(directEnvironment * sample_weight(), 0.0);
3265
5450
  }
3266
5451
 
3267
5452
  if (ray.bounce + 1u >= config.maxDepth) {
3268
5453
  if (deferred_path_resolve_enabled()) {
3269
- record_deferred_terminal_source(ray, terminal_surface_environment_source(hit));
5454
+ record_deferred_terminal_source(ray, terminal_surface_environment_source(ray, hit));
3270
5455
  } else {
3271
- let terminalEnvironment = terminal_surface_environment_contribution(ray, hit);
5456
+ let terminalEnvironment = terminal_surface_environment_contribution(
5457
+ ray,
5458
+ arrivingThroughput,
5459
+ hit
5460
+ );
3272
5461
  accumulation[ray.rayId] =
3273
5462
  accumulation[ray.rayId] + vec4<f32>(terminalEnvironment * sample_weight(), 1.0);
3274
5463
  }
@@ -3281,9 +5470,13 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
3281
5470
  let nextIndex = atomicAdd(&counters.nextCount, 1u);
3282
5471
  if (nextIndex >= config.tilePixelCount) {
3283
5472
  if (deferred_path_resolve_enabled()) {
3284
- record_deferred_terminal_source(ray, terminal_surface_environment_source(hit));
5473
+ record_deferred_terminal_source(ray, terminal_surface_environment_source(ray, hit));
3285
5474
  } else {
3286
- let overflowEnvironment = terminal_surface_environment_contribution(ray, hit);
5475
+ let overflowEnvironment = terminal_surface_environment_contribution(
5476
+ ray,
5477
+ arrivingThroughput,
5478
+ hit
5479
+ );
3287
5480
  accumulation[ray.rayId] =
3288
5481
  accumulation[ray.rayId] + vec4<f32>(overflowEnvironment * sample_weight(), 1.0);
3289
5482
  }
@@ -3297,12 +5490,12 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
3297
5490
  ray.sourcePixelId,
3298
5491
  ray.sampleId,
3299
5492
  ray.bounce + 1u,
3300
- ray.mediumRefId,
5493
+ scatter.mediumRefId,
3301
5494
  scatter.flags,
3302
5495
  0u,
3303
5496
  vec4<f32>(offset_origin(hit.position.xyz, hit.shadingNormal.xyz), 1.0),
3304
5497
  scatter.direction,
3305
- vec4<f32>(throughput, ray.throughput.w)
5498
+ vec4<f32>(throughput, scatter.pdf)
3306
5499
  );
3307
5500
  }
3308
5501
 
@@ -3371,8 +5564,11 @@ fn denoiseLinearRadiance(@builtin(global_invocation_id) globalId: vec3<u32>) {
3371
5564
 
3372
5565
  let pixel = vec2<i32>(i32(x), i32(y));
3373
5566
  let center = textureLoad(denoiseInputRadiance, pixel, 0).xyz;
3374
- var sum = center * 1.4;
3375
- var totalWeight = 1.4;
5567
+ let strength = denoise_strength();
5568
+ let kernelRadius = denoise_kernel_radius();
5569
+ let centerWeight = 1.7 - strength * 0.35;
5570
+ var sum = center * centerWeight;
5571
+ var totalWeight = centerWeight;
3376
5572
  let centerRange = denoise_range_space(center);
3377
5573
 
3378
5574
  for (var oy = -2i; oy <= 2i; oy = oy + 1i) {
@@ -3380,13 +5576,16 @@ fn denoiseLinearRadiance(@builtin(global_invocation_id) globalId: vec3<u32>) {
3380
5576
  if (ox == 0i && oy == 0i) {
3381
5577
  continue;
3382
5578
  }
5579
+ if (abs(ox) > kernelRadius || abs(oy) > kernelRadius) {
5580
+ continue;
5581
+ }
3383
5582
  let sx = clamp(i32(x) + ox, 0i, i32(config.canvasWidth) - 1i);
3384
5583
  let sy = clamp(i32(y) + oy, 0i, i32(config.canvasHeight) - 1i);
3385
5584
  let sampleColor = textureLoad(denoiseInputRadiance, vec2<i32>(sx, sy), 0).xyz;
3386
5585
  let colorDistance = length(denoise_range_space(sampleColor) - centerRange);
3387
- let rangeWeight = 1.0 / (1.0 + colorDistance * 7.0);
3388
- let distanceWeight = 1.0 / (1.0 + f32(ox * ox + oy * oy) * 0.24);
3389
- let diagonalWeight = select(1.0, 0.78, abs(ox) + abs(oy) > 2i);
5586
+ let rangeWeight = 1.0 / (1.0 + colorDistance * (11.0 + strength * 6.0));
5587
+ let distanceWeight = 1.0 / (1.0 + f32(ox * ox + oy * oy) * (0.62 + strength * 0.24));
5588
+ let diagonalWeight = select(1.0, 0.92, abs(ox) + abs(oy) > 1i);
3390
5589
  let weight = rangeWeight * diagonalWeight * distanceWeight;
3391
5590
  sum = sum + sampleColor * weight;
3392
5591
  totalWeight = totalWeight + weight;
@@ -3394,8 +5593,9 @@ fn denoiseLinearRadiance(@builtin(global_invocation_id) globalId: vec3<u32>) {
3394
5593
  }
3395
5594
 
3396
5595
  let filtered = sum / max(totalWeight, 0.0001);
3397
- let outlier = saturate(length(denoise_range_space(center) - denoise_range_space(filtered)) * 2.4);
3398
- let color = min(mix(center, filtered, 0.52 + outlier * 0.18), vec3<f32>(16.0));
5596
+ let outlier = saturate(length(denoise_range_space(center) - denoise_range_space(filtered)) * 2.1);
5597
+ let blend = min(0.3, strength * (0.62 + outlier * 0.12));
5598
+ let color = min(mix(center, filtered, blend), vec3<f32>(16.0));
3399
5599
  textureStore(denoisedRadianceImage, pixel, vec4<f32>(color, 1.0));
3400
5600
  }
3401
5601
 
@@ -3409,8 +5609,10 @@ fn resolveDenoisedOutputImage(@builtin(global_invocation_id) globalId: vec3<u32>
3409
5609
 
3410
5610
  let pixel = vec2<i32>(i32(x), i32(y));
3411
5611
  let center = textureLoad(finalDenoiseInputRadiance, pixel, 0).xyz;
3412
- var sum = center * 1.25;
3413
- var totalWeight = 1.25;
5612
+ let strength = denoise_strength();
5613
+ let centerWeight = 1.35 - strength * 0.25;
5614
+ var sum = center * centerWeight;
5615
+ var totalWeight = centerWeight;
3414
5616
  let centerRange = denoise_range_space(center);
3415
5617
 
3416
5618
  for (var oy = -1i; oy <= 1i; oy = oy + 1i) {
@@ -3422,8 +5624,8 @@ fn resolveDenoisedOutputImage(@builtin(global_invocation_id) globalId: vec3<u32>
3422
5624
  let sy = clamp(i32(y) + oy, 0i, i32(config.canvasHeight) - 1i);
3423
5625
  let sampleColor = textureLoad(finalDenoiseInputRadiance, vec2<i32>(sx, sy), 0).xyz;
3424
5626
  let colorDistance = length(denoise_range_space(sampleColor) - centerRange);
3425
- let rangeWeight = 1.0 / (1.0 + colorDistance * 9.0);
3426
- let distanceWeight = 1.0 / (1.0 + f32(ox * ox + oy * oy) * 0.4);
5627
+ let rangeWeight = 1.0 / (1.0 + colorDistance * (12.0 + strength * 8.0));
5628
+ let distanceWeight = 1.0 / (1.0 + f32(ox * ox + oy * oy) * (0.82 + strength * 0.28));
3427
5629
  let weight = rangeWeight * distanceWeight;
3428
5630
  sum = sum + sampleColor * weight;
3429
5631
  totalWeight = totalWeight + weight;
@@ -3431,8 +5633,9 @@ fn resolveDenoisedOutputImage(@builtin(global_invocation_id) globalId: vec3<u32>
3431
5633
  }
3432
5634
 
3433
5635
  let filtered = sum / max(totalWeight, 0.0001);
3434
- let outlier = saturate(length(denoise_range_space(center) - denoise_range_space(filtered)) * 2.8);
3435
- let radiance = min(mix(center, filtered, 0.28 + outlier * 0.12), vec3<f32>(16.0));
5636
+ let outlier = saturate(length(denoise_range_space(center) - denoise_range_space(filtered)) * 2.2);
5637
+ let blend = min(0.18, strength * (0.42 + outlier * 0.08));
5638
+ let radiance = min(mix(center, filtered, blend), vec3<f32>(16.0));
3436
5639
  textureStore(denoisedOutputImage, pixel, vec4<f32>(tone_map_radiance(radiance), 1.0));
3437
5640
  }
3438
5641
  `;
@@ -3508,95 +5711,25 @@ function createAdapterInfoSnapshot(adapter) {
3508
5711
  vendor: typeof info.vendor === "string" ? info.vendor : "",
3509
5712
  architecture: typeof info.architecture === "string" ? info.architecture : "",
3510
5713
  device: typeof info.device === "string" ? info.device : "",
3511
- description: typeof info.description === "string" ? info.description : ""
3512
- });
3513
- }
3514
- function createGpuAdapterParallelismDiagnostics(adapter, device) {
3515
- return Object.freeze({
3516
- physicalCoreCount: null,
3517
- physicalCoreCountAvailable: false,
3518
- physicalCoreCountUnavailableReason: "WebGPU does not expose physical GPU core counts.",
3519
- adapterInfo: createAdapterInfoSnapshot(adapter),
3520
- adapterLimits: Object.freeze({
3521
- maxComputeInvocationsPerWorkgroup: readGpuLimit(adapter, device, "maxComputeInvocationsPerWorkgroup"),
3522
- maxComputeWorkgroupSizeX: readGpuLimit(adapter, device, "maxComputeWorkgroupSizeX"),
3523
- maxComputeWorkgroupSizeY: readGpuLimit(adapter, device, "maxComputeWorkgroupSizeY"),
3524
- maxComputeWorkgroupSizeZ: readGpuLimit(adapter, device, "maxComputeWorkgroupSizeZ"),
3525
- maxComputeWorkgroupsPerDimension: readGpuLimit(adapter, device, "maxComputeWorkgroupsPerDimension"),
3526
- maxStorageBuffersPerShaderStage: readGpuLimit(adapter, device, "maxStorageBuffersPerShaderStage"),
3527
- maxStorageBufferBindingSize: readGpuLimit(adapter, device, "maxStorageBufferBindingSize")
3528
- }),
3529
- configuredWorkgroupSize: WORKGROUP_SIZE
3530
- });
3531
- }
3532
- function createGpuParallelismCounters() {
3533
- return {
3534
- directDispatches: 0,
3535
- directWorkgroups: 0,
3536
- directShaderInvocations: 0,
3537
- multiWorkgroupDispatches: 0,
3538
- largestDirectWorkgroupsPerDispatch: 0,
3539
- indirectDispatches: 0,
3540
- estimatedIndirectWorkgroupsUpperBound: 0,
3541
- estimatedIndirectShaderInvocationsUpperBound: 0,
3542
- indirectDispatchesWithMultiWorkgroupCapacity: 0,
3543
- largestEstimatedIndirectWorkgroupsPerDispatch: 0
3544
- };
3545
- }
3546
- function countDispatchWorkgroups(groups) {
3547
- return groups.reduce((product, value) => {
3548
- const numeric = Number(value ?? 1);
3549
- const count = Number.isFinite(numeric) ? Math.max(1, Math.trunc(numeric)) : 1;
3550
- return product * count;
3551
- }, 1);
3552
- }
3553
- function recordDirectDispatch(parallelism, groups, invocationsPerWorkgroup = WORKGROUP_SIZE) {
3554
- const workgroups = countDispatchWorkgroups(groups);
3555
- parallelism.directDispatches += 1;
3556
- parallelism.directWorkgroups += workgroups;
3557
- parallelism.directShaderInvocations += workgroups * invocationsPerWorkgroup;
3558
- parallelism.largestDirectWorkgroupsPerDispatch = Math.max(
3559
- parallelism.largestDirectWorkgroupsPerDispatch,
3560
- workgroups
3561
- );
3562
- if (workgroups > 1) {
3563
- parallelism.multiWorkgroupDispatches += 1;
3564
- }
3565
- }
3566
- function recordIndirectDispatch(parallelism, estimatedWorkgroupsUpperBound, invocationsPerWorkgroup = WORKGROUP_SIZE) {
3567
- const workgroups = Math.max(1, Math.trunc(Number(estimatedWorkgroupsUpperBound) || 1));
3568
- parallelism.indirectDispatches += 1;
3569
- parallelism.estimatedIndirectWorkgroupsUpperBound += workgroups;
3570
- parallelism.estimatedIndirectShaderInvocationsUpperBound += workgroups * invocationsPerWorkgroup;
3571
- parallelism.largestEstimatedIndirectWorkgroupsPerDispatch = Math.max(
3572
- parallelism.largestEstimatedIndirectWorkgroupsPerDispatch,
3573
- workgroups
3574
- );
3575
- if (workgroups > 1) {
3576
- parallelism.indirectDispatchesWithMultiWorkgroupCapacity += 1;
3577
- }
3578
- }
3579
- function createGpuParallelismDiagnostics(adapterDiagnostics, counters) {
3580
- const totalEstimatedWorkgroupsUpperBound = counters.directWorkgroups + counters.estimatedIndirectWorkgroupsUpperBound;
3581
- const totalEstimatedShaderInvocationsUpperBound = counters.directShaderInvocations + counters.estimatedIndirectShaderInvocationsUpperBound;
3582
- const exposesMultiWorkgroupParallelism = counters.multiWorkgroupDispatches > 0 || counters.indirectDispatchesWithMultiWorkgroupCapacity > 0;
3583
- return Object.freeze({
3584
- ...adapterDiagnostics,
3585
- directDispatches: counters.directDispatches,
3586
- directWorkgroups: counters.directWorkgroups,
3587
- directShaderInvocations: counters.directShaderInvocations,
3588
- multiWorkgroupDispatches: counters.multiWorkgroupDispatches,
3589
- largestDirectWorkgroupsPerDispatch: counters.largestDirectWorkgroupsPerDispatch,
3590
- indirectDispatches: counters.indirectDispatches,
3591
- estimatedIndirectWorkgroupsUpperBound: counters.estimatedIndirectWorkgroupsUpperBound,
3592
- estimatedIndirectShaderInvocationsUpperBound: counters.estimatedIndirectShaderInvocationsUpperBound,
3593
- indirectDispatchesWithMultiWorkgroupCapacity: counters.indirectDispatchesWithMultiWorkgroupCapacity,
3594
- largestEstimatedIndirectWorkgroupsPerDispatch: counters.largestEstimatedIndirectWorkgroupsPerDispatch,
3595
- totalEstimatedWorkgroupsUpperBound,
3596
- totalEstimatedShaderInvocationsUpperBound,
3597
- exposesMultiWorkgroupParallelism,
3598
- likelyUsesMoreThanOnePhysicalGpuCore: null,
3599
- coreUtilizationStatus: "not-exposed-by-webgpu"
5714
+ description: typeof info.description === "string" ? info.description : ""
5715
+ });
5716
+ }
5717
+ function createGpuAdapterParallelismDiagnostics(adapter, device) {
5718
+ return Object.freeze({
5719
+ physicalCoreCount: null,
5720
+ physicalCoreCountAvailable: false,
5721
+ physicalCoreCountUnavailableReason: "WebGPU does not expose physical GPU core counts.",
5722
+ adapterInfo: createAdapterInfoSnapshot(adapter),
5723
+ adapterLimits: Object.freeze({
5724
+ maxComputeInvocationsPerWorkgroup: readGpuLimit(adapter, device, "maxComputeInvocationsPerWorkgroup"),
5725
+ maxComputeWorkgroupSizeX: readGpuLimit(adapter, device, "maxComputeWorkgroupSizeX"),
5726
+ maxComputeWorkgroupSizeY: readGpuLimit(adapter, device, "maxComputeWorkgroupSizeY"),
5727
+ maxComputeWorkgroupSizeZ: readGpuLimit(adapter, device, "maxComputeWorkgroupSizeZ"),
5728
+ maxComputeWorkgroupsPerDimension: readGpuLimit(adapter, device, "maxComputeWorkgroupsPerDimension"),
5729
+ maxStorageBuffersPerShaderStage: readGpuLimit(adapter, device, "maxStorageBuffersPerShaderStage"),
5730
+ maxStorageBufferBindingSize: readGpuLimit(adapter, device, "maxStorageBufferBindingSize")
5731
+ }),
5732
+ configuredWorkgroupSize: WORKGROUP_SIZE
3600
5733
  });
3601
5734
  }
3602
5735
  function createEnvironmentMapSnapshot(environmentMap) {
@@ -3604,12 +5737,38 @@ function createEnvironmentMapSnapshot(environmentMap) {
3604
5737
  enabled: environmentMap.enabled,
3605
5738
  width: environmentMap.width,
3606
5739
  height: environmentMap.height,
5740
+ mipLevelCount: environmentMap.mipLevelCount ?? 1,
3607
5741
  projection: environmentMap.projection,
3608
5742
  intensity: environmentMap.intensity,
3609
5743
  rotationRadians: environmentMap.rotationRadians,
3610
- ambientStrength: environmentMap.ambientStrength
5744
+ ambientStrength: environmentMap.ambientStrength,
5745
+ hasImportanceData: environmentMap.hasImportanceData === true
3611
5746
  });
3612
5747
  }
5748
+ function nowMs() {
5749
+ if (typeof performance !== "undefined" && typeof performance.now === "function") {
5750
+ return performance.now();
5751
+ }
5752
+ return Date.now();
5753
+ }
5754
+ function estimateSubmittedGpuWorkTimeoutMs(config, tileCount, overrideTimeoutMs = null) {
5755
+ if (Number.isFinite(overrideTimeoutMs)) {
5756
+ return Math.max(1, Math.trunc(Number(overrideTimeoutMs)));
5757
+ }
5758
+ const samplesPerPixel = Math.max(
5759
+ 1,
5760
+ Number(config?.renderedSamplesPerPixel ?? config?.samplesPerPixel ?? 1)
5761
+ );
5762
+ const maxDepth = Math.max(1, Number(config?.maxDepth ?? 1));
5763
+ const deferredResolvePasses = config?.deferredPathResolve ? 1 : 0;
5764
+ const denoisePasses = config?.denoise ? samplesPerPixel < 4 ? 2 : 1 : 0;
5765
+ const tiles = Math.max(1, Number(tileCount ?? 1));
5766
+ const estimatedPasses = tiles * (samplesPerPixel * (maxDepth + 1 + deferredResolvePasses) + denoisePasses + 1);
5767
+ return Math.min(
5768
+ GPU_MAX_SUBMITTED_WORK_TIMEOUT_MS,
5769
+ GPU_SUBMITTED_WORK_TIMEOUT_MS + estimatedPasses * 5
5770
+ );
5771
+ }
3613
5772
  async function createWavefrontPathTracingComputeRenderer(options = {}) {
3614
5773
  assertAnalyticDisplayQualityPolicy(options);
3615
5774
  const constants = getGpuUsageConstants();
@@ -3814,6 +5973,65 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3814
5973
  config.environmentMap,
3815
5974
  config.environmentColor
3816
5975
  );
5976
+ const environmentSamplingResource = createEnvironmentSamplingTextureResource(
5977
+ device,
5978
+ constants,
5979
+ config.environmentMap,
5980
+ config.environmentColor
5981
+ );
5982
+ let mediumTextureResource = createMediumTextureResource(
5983
+ device,
5984
+ constants,
5985
+ config.mediums
5986
+ );
5987
+ config = Object.freeze({
5988
+ ...config,
5989
+ environmentMap: Object.freeze({
5990
+ ...config.environmentMap,
5991
+ width: environmentMapResource.width,
5992
+ height: environmentMapResource.height,
5993
+ mipLevelCount: environmentMapResource.mipLevelCount,
5994
+ hasImportanceData: environmentSamplingResource.hasImportanceData
5995
+ })
5996
+ });
5997
+ const brdfLutResource = createBrdfLutResource(device, constants);
5998
+ const baseColorAtlasResource = createAtlasTextureResource(
5999
+ device,
6000
+ constants,
6001
+ config.gpuMaterialSource.baseColorAtlas,
6002
+ "plasius.wavefront.materialAtlas.baseColor"
6003
+ );
6004
+ const metallicRoughnessAtlasResource = createAtlasTextureResource(
6005
+ device,
6006
+ constants,
6007
+ config.gpuMaterialSource.metallicRoughnessAtlas,
6008
+ "plasius.wavefront.materialAtlas.metallicRoughness"
6009
+ );
6010
+ const normalAtlasResource = createAtlasTextureResource(
6011
+ device,
6012
+ constants,
6013
+ config.gpuMaterialSource.normalAtlas,
6014
+ "plasius.wavefront.materialAtlas.normal"
6015
+ );
6016
+ const occlusionAtlasResource = createAtlasTextureResource(
6017
+ device,
6018
+ constants,
6019
+ config.gpuMaterialSource.occlusionAtlas,
6020
+ "plasius.wavefront.materialAtlas.occlusion"
6021
+ );
6022
+ const emissiveAtlasResource = createAtlasTextureResource(
6023
+ device,
6024
+ constants,
6025
+ config.gpuMaterialSource.emissiveAtlas,
6026
+ "plasius.wavefront.materialAtlas.emissive"
6027
+ );
6028
+ const materialAtlasSampler = device.createSampler({
6029
+ label: "plasius.wavefront.materialAtlasSampler",
6030
+ addressModeU: "clamp-to-edge",
6031
+ addressModeV: "clamp-to-edge",
6032
+ magFilter: "linear",
6033
+ minFilter: "linear"
6034
+ });
3817
6035
  const traceBindGroupLayout = device.createBindGroupLayout({
3818
6036
  label: "plasius.wavefront.traceBindGroupLayout",
3819
6037
  entries: [
@@ -3843,7 +6061,17 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3843
6061
  { binding: 19, visibility: constants.shader.COMPUTE, buffer: { type: "read-only-storage" } },
3844
6062
  { binding: 20, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
3845
6063
  { binding: 21, visibility: constants.shader.COMPUTE, sampler: { type: "filtering" } },
3846
- { binding: 22, visibility: constants.shader.COMPUTE, buffer: { type: "storage" } }
6064
+ { binding: 22, visibility: constants.shader.COMPUTE, buffer: { type: "storage" } },
6065
+ { binding: 23, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
6066
+ { binding: 24, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
6067
+ { binding: 25, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
6068
+ { binding: 26, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
6069
+ { binding: 27, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
6070
+ { binding: 28, visibility: constants.shader.COMPUTE, sampler: { type: "filtering" } },
6071
+ { binding: 29, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
6072
+ { binding: 30, visibility: constants.shader.COMPUTE, sampler: { type: "filtering" } },
6073
+ { binding: 31, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
6074
+ { binding: 32, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } }
3847
6075
  ]
3848
6076
  });
3849
6077
  const accelerationBindGroupLayout = device.createBindGroupLayout({
@@ -3922,6 +6150,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3922
6150
  label: "plasius.wavefront.computeShader",
3923
6151
  code: WAVEFRONT_COMPUTE_WGSL
3924
6152
  });
6153
+ await assertShaderModuleCompiles(computeShader, "plasius.wavefront.computeShader");
3925
6154
  const pipelines = {
3926
6155
  prepareMeshTrianglesAndLeaves: await createComputePipeline(
3927
6156
  device,
@@ -4020,14 +6249,27 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4020
6249
  { binding: 19, resource: { buffer: environmentPortalBuffer } },
4021
6250
  { binding: 20, resource: environmentMapResource.view },
4022
6251
  { binding: 21, resource: environmentMapResource.sampler },
4023
- { binding: 22, resource: { buffer: pathVertexBuffer } }
6252
+ { binding: 22, resource: { buffer: pathVertexBuffer } },
6253
+ { binding: 23, resource: baseColorAtlasResource.view },
6254
+ { binding: 24, resource: metallicRoughnessAtlasResource.view },
6255
+ { binding: 25, resource: normalAtlasResource.view },
6256
+ { binding: 26, resource: occlusionAtlasResource.view },
6257
+ { binding: 27, resource: emissiveAtlasResource.view },
6258
+ { binding: 28, resource: materialAtlasSampler },
6259
+ { binding: 29, resource: brdfLutResource.view },
6260
+ { binding: 30, resource: brdfLutResource.sampler },
6261
+ { binding: 31, resource: environmentSamplingResource.view },
6262
+ { binding: 32, resource: mediumTextureResource.view }
4024
6263
  ]
4025
6264
  });
4026
6265
  }
4027
- const bindGroups = [
4028
- createTraceBindGroup(activeQueue, nextQueue, "plasius.wavefront.bind.activeNext"),
4029
- createTraceBindGroup(nextQueue, activeQueue, "plasius.wavefront.bind.nextActive")
4030
- ];
6266
+ function createTraceBindGroups() {
6267
+ return [
6268
+ createTraceBindGroup(activeQueue, nextQueue, "plasius.wavefront.bind.activeNext"),
6269
+ createTraceBindGroup(nextQueue, activeQueue, "plasius.wavefront.bind.nextActive")
6270
+ ];
6271
+ }
6272
+ let bindGroups = createTraceBindGroups();
4031
6273
  const bvhBuildBindGroup = device.createBindGroup({
4032
6274
  label: "plasius.wavefront.bind.bvhBuild",
4033
6275
  layout: accelerationBindGroupLayout,
@@ -4073,6 +6315,11 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4073
6315
  outputView,
4074
6316
  "plasius.wavefront.bind.denoise.scratchToOutput"
4075
6317
  );
6318
+ const denoiseDirectResolveBindGroup = createDenoiseResolveBindGroup(
6319
+ radianceView,
6320
+ outputView,
6321
+ "plasius.wavefront.bind.denoise.radianceToOutput"
6322
+ );
4076
6323
  const presentBindGroupLayout = device.createBindGroupLayout({
4077
6324
  label: "plasius.wavefront.presentBindGroupLayout",
4078
6325
  entries: [
@@ -4110,23 +6357,129 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4110
6357
  let accelerationBuilt = !config.gpuAccelerationBuildRequired;
4111
6358
  let accelerationBuildCount = 0;
4112
6359
  let activeCameraOptions = options.camera ?? DEFAULT_CAMERA;
6360
+ let lastCompletedFrameTimeMs = null;
6361
+ let lastCompletedSamplesPerPixel = Math.max(1, config.samplesPerPixel);
4113
6362
  let lastGpuParallelism = createGpuParallelismDiagnostics(
4114
6363
  gpuAdapterParallelism,
4115
6364
  createGpuParallelismCounters()
4116
6365
  );
6366
+ function resolveRenderedSamplesPerPixel(renderOptions = {}, awaitGPUCompletion = true) {
6367
+ const targetSamplesPerPixel = clamp(
6368
+ readPositiveInteger(
6369
+ "samplesPerPixel",
6370
+ renderOptions.samplesPerPixel,
6371
+ config.samplesPerPixel
6372
+ ),
6373
+ 1,
6374
+ config.samplesPerPixel
6375
+ );
6376
+ const frameTimeBudgetMs = Number.isFinite(renderOptions.frameTimeBudgetMs) ? Math.max(0, Number(renderOptions.frameTimeBudgetMs)) : null;
6377
+ const minimumSamplesPerPixel = clamp(
6378
+ readPositiveInteger(
6379
+ "minimumSamplesPerPixel",
6380
+ renderOptions.minimumSamplesPerPixel,
6381
+ frameTimeBudgetMs !== null && targetSamplesPerPixel > 1 ? 1 : targetSamplesPerPixel
6382
+ ),
6383
+ 1,
6384
+ targetSamplesPerPixel
6385
+ );
6386
+ if (frameTimeBudgetMs === null || !awaitGPUCompletion || targetSamplesPerPixel <= minimumSamplesPerPixel) {
6387
+ return Object.freeze({
6388
+ renderedSamplesPerPixel: targetSamplesPerPixel,
6389
+ targetSamplesPerPixel,
6390
+ minimumSamplesPerPixel,
6391
+ frameTimeBudgetMs,
6392
+ budgetConstrained: false
6393
+ });
6394
+ }
6395
+ const estimatedSampleTimeMs = Number.isFinite(lastCompletedFrameTimeMs) && lastCompletedFrameTimeMs > 0 ? lastCompletedFrameTimeMs / Math.max(1, lastCompletedSamplesPerPixel) : null;
6396
+ if (!Number.isFinite(estimatedSampleTimeMs) || estimatedSampleTimeMs <= 0) {
6397
+ return Object.freeze({
6398
+ renderedSamplesPerPixel: minimumSamplesPerPixel,
6399
+ targetSamplesPerPixel,
6400
+ minimumSamplesPerPixel,
6401
+ frameTimeBudgetMs,
6402
+ budgetConstrained: minimumSamplesPerPixel < targetSamplesPerPixel
6403
+ });
6404
+ }
6405
+ const budgetLimitedSamples = clamp(
6406
+ Math.floor(frameTimeBudgetMs / estimatedSampleTimeMs),
6407
+ minimumSamplesPerPixel,
6408
+ targetSamplesPerPixel
6409
+ );
6410
+ return Object.freeze({
6411
+ renderedSamplesPerPixel: budgetLimitedSamples,
6412
+ targetSamplesPerPixel,
6413
+ minimumSamplesPerPixel,
6414
+ frameTimeBudgetMs,
6415
+ budgetConstrained: budgetLimitedSamples < targetSamplesPerPixel
6416
+ });
6417
+ }
6418
+ function createFrameStats({
6419
+ frameIndex,
6420
+ accelerationBuildSubmitted,
6421
+ frameSubmissionCount,
6422
+ parallelismCounters,
6423
+ renderedSamplesPerPixel,
6424
+ targetSamplesPerPixel,
6425
+ frameTimeBudgetMs,
6426
+ budgetConstrained
6427
+ }) {
6428
+ lastGpuParallelism = createGpuParallelismDiagnostics(gpuAdapterParallelism, parallelismCounters);
6429
+ const commandSubmissions = frameSubmissionCount + (accelerationBuildSubmitted ? 1 : 0);
6430
+ return Object.freeze({
6431
+ frame,
6432
+ frameIndex,
6433
+ width: config.width,
6434
+ height: config.height,
6435
+ maxDepth: config.maxDepth,
6436
+ tiles: tiles.length,
6437
+ tileSize: config.tileSize,
6438
+ samplesPerPixel: targetSamplesPerPixel,
6439
+ renderedSamplesPerPixel,
6440
+ frameTimeBudgetMs,
6441
+ budgetConstrained,
6442
+ maxFramePassesPerSubmission: config.maxFramePassesPerSubmission,
6443
+ screenRays: config.width * config.height,
6444
+ primaryRays: config.width * config.height * renderedSamplesPerPixel,
6445
+ sceneObjectCount: config.sceneObjectCount,
6446
+ triangleCount: config.triangleCount,
6447
+ emissiveTriangleCount: config.emissiveTriangleCount,
6448
+ environmentPortalCount: config.environmentPortalCount,
6449
+ environmentPortalMode: config.environmentPortalMode,
6450
+ mediumCount: config.mediumCount,
6451
+ environmentMap: createEnvironmentMapSnapshot(config.environmentMap),
6452
+ deferredPathResolve: config.deferredPathResolve,
6453
+ bvhNodeCount: config.bvhNodeCount,
6454
+ displayQuality: config.displayQuality,
6455
+ accelerationBuildMode: config.accelerationBuildMode,
6456
+ gpuAccelerationBuildRequired: config.gpuAccelerationBuildRequired,
6457
+ accelerationBuildSubmitted,
6458
+ accelerationBuilt,
6459
+ accelerationBuildCount,
6460
+ commandSubmissions,
6461
+ frameConfigSlots: frameConfigSlotCount,
6462
+ gpuParallelism: lastGpuParallelism,
6463
+ memory: config.memory
6464
+ });
6465
+ }
6466
+ function writeFrameConfigSlot(slot, tile, frameIndex, buildRange = {}) {
6467
+ if (slot >= frameConfigSlotCount) {
6468
+ throw new Error("Wavefront frame config slot capacity exceeded.");
6469
+ }
6470
+ const offset = slot * configBufferStride;
6471
+ device.queue.writeBuffer(
6472
+ configBuffer,
6473
+ offset,
6474
+ createConfigPayload(config, tile, frameIndex, buildRange)
6475
+ );
6476
+ return offset;
6477
+ }
4117
6478
  function createFrameConfigWriter(frameIndex) {
4118
6479
  let slot = 0;
4119
6480
  return (tile, buildRange = {}) => {
4120
- if (slot >= frameConfigSlotCount) {
4121
- throw new Error("Wavefront frame config slot capacity exceeded.");
4122
- }
4123
- const offset = slot * configBufferStride;
6481
+ const offset = writeFrameConfigSlot(slot, tile, frameIndex, buildRange);
4124
6482
  slot += 1;
4125
- device.queue.writeBuffer(
4126
- configBuffer,
4127
- offset,
4128
- createConfigPayload(config, tile, frameIndex, buildRange)
4129
- );
4130
6483
  return offset;
4131
6484
  };
4132
6485
  }
@@ -4171,7 +6524,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4171
6524
  passEncoder.setPipeline(pipelines.prepareMeshTrianglesAndLeaves);
4172
6525
  const prepareWorkgroups = Math.ceil(config.bvhLeafSortCapacity / WORKGROUP_SIZE);
4173
6526
  passEncoder.dispatchWorkgroups(prepareWorkgroups);
4174
- recordDirectDispatch(parallelism, [prepareWorkgroups]);
6527
+ recordDirectDispatch(parallelism, [prepareWorkgroups], WORKGROUP_SIZE);
4175
6528
  passEncoder.setPipeline(pipelines.sortBvhLeafRefs);
4176
6529
  for (let stageIndex = 0; stageIndex < config.bvhSortStages.length; stageIndex += 1) {
4177
6530
  passEncoder.setBindGroup(0, bvhBuildBindGroup, [
@@ -4179,13 +6532,13 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4179
6532
  ]);
4180
6533
  const sortWorkgroups = Math.ceil(config.bvhLeafSortCapacity / WORKGROUP_SIZE);
4181
6534
  passEncoder.dispatchWorkgroups(sortWorkgroups);
4182
- recordDirectDispatch(parallelism, [sortWorkgroups]);
6535
+ recordDirectDispatch(parallelism, [sortWorkgroups], WORKGROUP_SIZE);
4183
6536
  }
4184
6537
  passEncoder.setBindGroup(0, bvhBuildBindGroup, [0]);
4185
6538
  passEncoder.setPipeline(pipelines.writeSortedBvhLeaves);
4186
6539
  const leafWriteWorkgroups = Math.ceil(config.triangleCount / WORKGROUP_SIZE);
4187
6540
  passEncoder.dispatchWorkgroups(leafWriteWorkgroups);
4188
- recordDirectDispatch(parallelism, [leafWriteWorkgroups]);
6541
+ recordDirectDispatch(parallelism, [leafWriteWorkgroups], WORKGROUP_SIZE);
4189
6542
  passEncoder.setPipeline(pipelines.buildBvhInternalLevel);
4190
6543
  for (let levelIndex = 0; levelIndex < config.bvhBuildLevels.length; levelIndex += 1) {
4191
6544
  const buildLevel = config.bvhBuildLevels[levelIndex];
@@ -4194,7 +6547,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4194
6547
  ]);
4195
6548
  const levelWorkgroups = Math.ceil(buildLevel.count / WORKGROUP_SIZE);
4196
6549
  passEncoder.dispatchWorkgroups(levelWorkgroups);
4197
- recordDirectDispatch(parallelism, [levelWorkgroups]);
6550
+ recordDirectDispatch(parallelism, [levelWorkgroups], WORKGROUP_SIZE);
4198
6551
  }
4199
6552
  passEncoder.end();
4200
6553
  device.queue.submit([encoder.finish()]);
@@ -4210,7 +6563,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4210
6563
  generatePass.setBindGroup(0, bindGroups[0], [configOffset]);
4211
6564
  generatePass.setPipeline(pipelines.generatePrimaryRays);
4212
6565
  generatePass.dispatchWorkgroups(tileWorkgroups);
4213
- recordDirectDispatch(parallelism, [tileWorkgroups]);
6566
+ recordDirectDispatch(parallelism, [tileWorkgroups], WORKGROUP_SIZE);
4214
6567
  generatePass.end();
4215
6568
  for (let bounceIndex = 0; bounceIndex < config.maxDepth; bounceIndex += 1) {
4216
6569
  encoder.copyBufferToBuffer(
@@ -4226,10 +6579,10 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4226
6579
  passEncoder.setBindGroup(0, bindGroups[bounceIndex % 2], [configOffset]);
4227
6580
  passEncoder.setPipeline(pipelines.intersectActiveQueue);
4228
6581
  passEncoder.dispatchWorkgroupsIndirect(activeDispatchBuffer, 0);
4229
- recordIndirectDispatch(parallelism, tileWorkgroups);
6582
+ recordIndirectDispatch(parallelism, tileWorkgroups, WORKGROUP_SIZE);
4230
6583
  passEncoder.setPipeline(pipelines.resolveSurfaceRecords);
4231
6584
  passEncoder.dispatchWorkgroupsIndirect(activeDispatchBuffer, 0);
4232
- recordIndirectDispatch(parallelism, tileWorkgroups);
6585
+ recordIndirectDispatch(parallelism, tileWorkgroups, WORKGROUP_SIZE);
4233
6586
  passEncoder.setPipeline(pipelines.compactAndSwapQueues);
4234
6587
  passEncoder.dispatchWorkgroups(1);
4235
6588
  recordDirectDispatch(parallelism, [1], 1);
@@ -4244,30 +6597,45 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4244
6597
  passEncoder.setBindGroup(0, bindGroups[0], [configOffset]);
4245
6598
  passEncoder.setPipeline(pipelines.accumulateTerminalRadiance);
4246
6599
  passEncoder.dispatchWorkgroups(tileWorkgroups);
4247
- recordDirectDispatch(parallelism, [tileWorkgroups]);
6600
+ recordDirectDispatch(parallelism, [tileWorkgroups], WORKGROUP_SIZE);
4248
6601
  passEncoder.end();
4249
6602
  }
4250
- function encodeDenoise(encoder, configOffset, parallelism) {
6603
+ function encodeDenoise(encoder, configOffset, parallelism, renderedSamplesPerPixel = config.samplesPerPixel) {
4251
6604
  if (!config.denoise) {
4252
6605
  return;
4253
6606
  }
4254
6607
  const denoiseWorkgroupsX = Math.ceil(config.width / 8);
4255
6608
  const denoiseWorkgroupsY = Math.ceil(config.height / 8);
4256
- const radiancePass = encoder.beginComputePass({
4257
- label: "plasius.wavefront.denoiseRadiancePass"
4258
- });
4259
- radiancePass.setBindGroup(0, denoiseRadianceBindGroup, [configOffset]);
4260
- radiancePass.setPipeline(pipelines.denoiseLinearRadiance);
4261
- radiancePass.dispatchWorkgroups(denoiseWorkgroupsX, denoiseWorkgroupsY);
4262
- recordDirectDispatch(parallelism, [denoiseWorkgroupsX, denoiseWorkgroupsY]);
4263
- radiancePass.end();
6609
+ const useTwoPassDenoise = renderedSamplesPerPixel < 4;
6610
+ if (useTwoPassDenoise) {
6611
+ const radiancePass = encoder.beginComputePass({
6612
+ label: "plasius.wavefront.denoiseRadiancePass"
6613
+ });
6614
+ radiancePass.setBindGroup(0, denoiseRadianceBindGroup, [configOffset]);
6615
+ radiancePass.setPipeline(pipelines.denoiseLinearRadiance);
6616
+ radiancePass.dispatchWorkgroups(denoiseWorkgroupsX, denoiseWorkgroupsY);
6617
+ recordDirectDispatch(
6618
+ parallelism,
6619
+ [denoiseWorkgroupsX, denoiseWorkgroupsY],
6620
+ WORKGROUP_SIZE
6621
+ );
6622
+ radiancePass.end();
6623
+ }
4264
6624
  const resolvePass = encoder.beginComputePass({
4265
6625
  label: "plasius.wavefront.denoiseResolvePass"
4266
6626
  });
4267
- resolvePass.setBindGroup(0, denoiseResolveBindGroup, [configOffset]);
6627
+ resolvePass.setBindGroup(
6628
+ 0,
6629
+ useTwoPassDenoise ? denoiseResolveBindGroup : denoiseDirectResolveBindGroup,
6630
+ [configOffset]
6631
+ );
4268
6632
  resolvePass.setPipeline(pipelines.resolveDenoisedOutputImage);
4269
6633
  resolvePass.dispatchWorkgroups(denoiseWorkgroupsX, denoiseWorkgroupsY);
4270
- recordDirectDispatch(parallelism, [denoiseWorkgroupsX, denoiseWorkgroupsY]);
6634
+ recordDirectDispatch(
6635
+ parallelism,
6636
+ [denoiseWorkgroupsX, denoiseWorkgroupsY],
6637
+ WORKGROUP_SIZE
6638
+ );
4271
6639
  resolvePass.end();
4272
6640
  }
4273
6641
  function encodePresent(encoder) {
@@ -4288,98 +6656,213 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4288
6656
  passEncoder.draw(3);
4289
6657
  passEncoder.end();
4290
6658
  }
4291
- function dispatchFrame(frameIndex, parallelism) {
6659
+ function dispatchFrame(frameIndex, parallelism, renderedSamplesPerPixel = config.samplesPerPixel) {
4292
6660
  const writeFrameConfig = createFrameConfigWriter(frameIndex);
4293
- let submissionCount = 0;
4294
- let encodedFramePasses = 0;
4295
- let encoder = device.createCommandEncoder({
4296
- label: `plasius.wavefront.frame.${frameIndex}.batched.${submissionCount + 1}`
6661
+ const batch = createGpuSubmissionBatcher({
6662
+ device,
6663
+ frameIndex,
6664
+ maxFramePassesPerSubmission: config.maxFramePassesPerSubmission
4297
6665
  });
4298
- function submitCurrentEncoder() {
4299
- if (encodedFramePasses <= 0) {
4300
- return;
4301
- }
4302
- device.queue.submit([encoder.finish()]);
4303
- submissionCount += 1;
4304
- encodedFramePasses = 0;
4305
- encoder = device.createCommandEncoder({
4306
- label: `plasius.wavefront.frame.${frameIndex}.batched.${submissionCount + 1}`
4307
- });
4308
- }
4309
- function reserveEncoder(passCount = 1) {
4310
- if (encodedFramePasses > 0 && encodedFramePasses + passCount > config.maxFramePassesPerSubmission) {
4311
- submitCurrentEncoder();
4312
- }
4313
- encodedFramePasses += passCount;
4314
- return encoder;
4315
- }
4316
6666
  for (const tile of tiles) {
4317
- for (let sampleIndex = 0; sampleIndex < config.samplesPerPixel; sampleIndex += 1) {
6667
+ for (let sampleIndex = 0; sampleIndex < renderedSamplesPerPixel; sampleIndex += 1) {
4318
6668
  const configOffset = writeFrameConfig(tile, {
4319
6669
  sampleIndex,
4320
- sampleWeight: 1 / config.samplesPerPixel
6670
+ sampleWeight: 1 / renderedSamplesPerPixel
4321
6671
  });
4322
- encodeTileSample(reserveEncoder(), tile, configOffset, parallelism);
6672
+ encodeTileSample(
6673
+ batch.reserve(config.maxDepth + 1),
6674
+ tile,
6675
+ configOffset,
6676
+ parallelism
6677
+ );
4323
6678
  if (config.deferredPathResolve) {
4324
- encodeTileOutput(reserveEncoder(), tile, configOffset, parallelism);
6679
+ encodeTileOutput(batch.reserve(1), tile, configOffset, parallelism);
4325
6680
  }
4326
6681
  }
4327
6682
  if (!config.deferredPathResolve) {
4328
6683
  const outputConfigOffset = writeFrameConfig(tile, {
4329
6684
  sampleIndex: 0,
4330
- sampleWeight: 1 / config.samplesPerPixel
6685
+ sampleWeight: 1 / renderedSamplesPerPixel
4331
6686
  });
4332
- encodeTileOutput(reserveEncoder(), tile, outputConfigOffset, parallelism);
6687
+ encodeTileOutput(batch.reserve(1), tile, outputConfigOffset, parallelism);
4333
6688
  }
4334
6689
  }
4335
6690
  if (config.denoise) {
4336
6691
  const denoiseConfigOffset = writeFrameConfig(
4337
6692
  { x: 0, y: 0, width: config.width, height: config.height },
4338
- { sampleIndex: 0, sampleWeight: 1 / config.samplesPerPixel }
6693
+ { sampleIndex: 0, sampleWeight: 1 / renderedSamplesPerPixel }
6694
+ );
6695
+ const denoisePassCount = renderedSamplesPerPixel < 4 ? 2 : 1;
6696
+ encodeDenoise(
6697
+ batch.reserve(denoisePassCount),
6698
+ denoiseConfigOffset,
6699
+ parallelism,
6700
+ renderedSamplesPerPixel
4339
6701
  );
4340
- encodeDenoise(reserveEncoder(), denoiseConfigOffset, parallelism);
4341
6702
  }
4342
- encodePresent(reserveEncoder());
4343
- submitCurrentEncoder();
4344
- return submissionCount;
6703
+ encodePresent(batch.reserve(1));
6704
+ return batch.flush();
4345
6705
  }
4346
- function renderOnce() {
6706
+ function renderOnce(renderOptions = {}, resolvedSamplingPlan = null) {
6707
+ const frameStartTimeMs = nowMs();
4347
6708
  frame += 1;
4348
6709
  const frameIndex = frame + config.frameIndex;
6710
+ const samplingPlan = resolvedSamplingPlan ?? resolveRenderedSamplesPerPixel(renderOptions, false);
4349
6711
  const parallelismCounters = createGpuParallelismCounters();
4350
6712
  const accelerationBuildSubmitted = dispatchGpuAccelerationBuild(frameIndex, parallelismCounters);
4351
- const frameSubmissionCount = dispatchFrame(frameIndex, parallelismCounters);
4352
- lastGpuParallelism = createGpuParallelismDiagnostics(gpuAdapterParallelism, parallelismCounters);
6713
+ const frameSubmissionCount = dispatchFrame(
6714
+ frameIndex,
6715
+ parallelismCounters,
6716
+ samplingPlan.renderedSamplesPerPixel
6717
+ );
6718
+ const frameTimeMs = Math.max(0, nowMs() - frameStartTimeMs);
4353
6719
  return Object.freeze({
4354
- frame,
4355
- width: config.width,
4356
- height: config.height,
4357
- maxDepth: config.maxDepth,
4358
- tiles: tiles.length,
4359
- tileSize: config.tileSize,
4360
- samplesPerPixel: config.samplesPerPixel,
6720
+ ...createFrameStats({
6721
+ frameIndex,
6722
+ accelerationBuildSubmitted,
6723
+ frameSubmissionCount,
6724
+ parallelismCounters,
6725
+ renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel,
6726
+ targetSamplesPerPixel: samplingPlan.targetSamplesPerPixel,
6727
+ frameTimeBudgetMs: samplingPlan.frameTimeBudgetMs,
6728
+ budgetConstrained: samplingPlan.budgetConstrained
6729
+ }),
6730
+ gpuWorkerJobs: createGpuWorkerJobDiagnostics(
6731
+ lastGpuParallelism,
6732
+ frameSubmissionCount + (accelerationBuildSubmitted ? 1 : 0),
6733
+ frameTimeMs,
6734
+ false
6735
+ )
6736
+ });
6737
+ }
6738
+ async function waitForSubmittedGpuWork(options2 = {}) {
6739
+ if (typeof device.queue.onSubmittedWorkDone !== "function") {
6740
+ return true;
6741
+ }
6742
+ const timeoutMs = Math.max(
6743
+ 1,
6744
+ Number.isFinite(options2.timeoutMs) ? Number(options2.timeoutMs) : GPU_SUBMITTED_WORK_TIMEOUT_MS
6745
+ );
6746
+ const allowTimeout = options2.allowTimeout !== false;
6747
+ const completionPromise = device.queue.onSubmittedWorkDone().then(
6748
+ () => ({ status: "done" }),
6749
+ (error) => {
6750
+ throw error;
6751
+ }
6752
+ );
6753
+ const lossPromise = typeof device.lost?.then === "function" ? device.lost.then((info) => {
6754
+ throw new Error(
6755
+ `WebGPU device lost while waiting for submitted work (${info?.reason ?? "unknown"}).`
6756
+ );
6757
+ }) : null;
6758
+ let timeoutHandle = null;
6759
+ let resolveTimeoutPromise = null;
6760
+ let timeoutSettled = false;
6761
+ const settleTimeoutPromise = (value) => {
6762
+ if (timeoutSettled) {
6763
+ return;
6764
+ }
6765
+ timeoutSettled = true;
6766
+ resolveTimeoutPromise?.(value);
6767
+ };
6768
+ const timeoutPromise = new Promise((resolve) => {
6769
+ resolveTimeoutPromise = resolve;
6770
+ timeoutHandle = setTimeout(() => settleTimeoutPromise({ status: "timeout" }), timeoutMs);
6771
+ });
6772
+ let result;
6773
+ try {
6774
+ result = await Promise.race(
6775
+ [completionPromise, timeoutPromise, lossPromise].filter(Boolean)
6776
+ );
6777
+ } finally {
6778
+ if (timeoutHandle !== null) {
6779
+ clearTimeout(timeoutHandle);
6780
+ settleTimeoutPromise({ status: "cancelled" });
6781
+ }
6782
+ }
6783
+ if (result?.status === "timeout") {
6784
+ if (!allowTimeout) {
6785
+ throw new Error(`Timed out after ${timeoutMs} ms waiting for submitted GPU work.`);
6786
+ }
6787
+ console.warn(
6788
+ `[plasius.wavefront] Submitted GPU work did not report completion within ${timeoutMs} ms; continuing.`
6789
+ );
6790
+ return false;
6791
+ }
6792
+ return true;
6793
+ }
6794
+ function dispatchFrameAwaitingGpu(frameIndex, parallelism, renderedSamplesPerPixel = config.samplesPerPixel) {
6795
+ const samplePassesPerSample = config.maxDepth + 1 + (config.deferredPathResolve ? 1 : 0);
6796
+ const denoisePassCount = config.denoise ? renderedSamplesPerPixel < 4 ? 2 : 1 : 0;
6797
+ const tailPassCount = denoisePassCount + 1;
6798
+ const sampleBatchSize = Math.max(
6799
+ 1,
6800
+ Math.floor(
6801
+ Math.max(config.maxFramePassesPerSubmission - tailPassCount, 1) / Math.max(samplePassesPerSample, 1)
6802
+ )
6803
+ );
6804
+ let submissionCount = 0;
6805
+ for (const tile of tiles) {
6806
+ for (let sampleStart = 0; sampleStart < renderedSamplesPerPixel; sampleStart += sampleBatchSize) {
6807
+ const sampleEnd = Math.min(renderedSamplesPerPixel, sampleStart + sampleBatchSize);
6808
+ const batch = createGpuSubmissionBatcher({
6809
+ device,
6810
+ frameIndex,
6811
+ maxFramePassesPerSubmission: config.maxFramePassesPerSubmission,
6812
+ startingSubmissionCount: submissionCount
6813
+ });
6814
+ let slot = 0;
6815
+ for (let sampleIndex = sampleStart; sampleIndex < sampleEnd; sampleIndex += 1) {
6816
+ const configOffset = writeFrameConfigSlot(slot, tile, frameIndex, {
6817
+ sampleIndex,
6818
+ sampleWeight: 1 / renderedSamplesPerPixel
6819
+ });
6820
+ slot += 1;
6821
+ encodeTileSample(
6822
+ batch.reserve(config.maxDepth + 1),
6823
+ tile,
6824
+ configOffset,
6825
+ parallelism
6826
+ );
6827
+ if (config.deferredPathResolve) {
6828
+ encodeTileOutput(batch.reserve(1), tile, configOffset, parallelism);
6829
+ }
6830
+ }
6831
+ if (!config.deferredPathResolve && sampleEnd >= renderedSamplesPerPixel) {
6832
+ const outputConfigOffset = writeFrameConfigSlot(slot, tile, frameIndex, {
6833
+ sampleIndex: 0,
6834
+ sampleWeight: 1 / renderedSamplesPerPixel
6835
+ });
6836
+ encodeTileOutput(batch.reserve(1), tile, outputConfigOffset, parallelism);
6837
+ }
6838
+ batch.flush();
6839
+ submissionCount += batch.getSubmissionCount();
6840
+ }
6841
+ }
6842
+ const tail = createGpuSubmissionBatcher({
6843
+ device,
6844
+ frameIndex,
4361
6845
  maxFramePassesPerSubmission: config.maxFramePassesPerSubmission,
4362
- screenRays: config.width * config.height,
4363
- primaryRays: config.width * config.height * config.samplesPerPixel,
4364
- sceneObjectCount: config.sceneObjectCount,
4365
- triangleCount: config.triangleCount,
4366
- emissiveTriangleCount: config.emissiveTriangleCount,
4367
- environmentPortalCount: config.environmentPortalCount,
4368
- environmentPortalMode: config.environmentPortalMode,
4369
- environmentMap: createEnvironmentMapSnapshot(config.environmentMap),
4370
- deferredPathResolve: config.deferredPathResolve,
4371
- bvhNodeCount: config.bvhNodeCount,
4372
- displayQuality: config.displayQuality,
4373
- accelerationBuildMode: config.accelerationBuildMode,
4374
- gpuAccelerationBuildRequired: config.gpuAccelerationBuildRequired,
4375
- accelerationBuildSubmitted,
4376
- accelerationBuilt,
4377
- accelerationBuildCount,
4378
- commandSubmissions: frameSubmissionCount + (accelerationBuildSubmitted ? 1 : 0),
4379
- frameConfigSlots: frameConfigSlotCount,
4380
- gpuParallelism: lastGpuParallelism,
4381
- memory: config.memory
6846
+ startingSubmissionCount: submissionCount
4382
6847
  });
6848
+ if (config.denoise) {
6849
+ const denoiseConfigOffset = writeFrameConfigSlot(
6850
+ 0,
6851
+ { x: 0, y: 0, width: config.width, height: config.height },
6852
+ frameIndex,
6853
+ { sampleIndex: 0, sampleWeight: 1 / renderedSamplesPerPixel }
6854
+ );
6855
+ encodeDenoise(
6856
+ tail.reserve(denoisePassCount),
6857
+ denoiseConfigOffset,
6858
+ parallelism,
6859
+ renderedSamplesPerPixel
6860
+ );
6861
+ }
6862
+ encodePresent(tail.reserve(1));
6863
+ tail.flush();
6864
+ submissionCount += tail.getSubmissionCount();
6865
+ return submissionCount;
4383
6866
  }
4384
6867
  async function readOutputProbe(optionsForProbe = {}) {
4385
6868
  const mapMode = constants.map;
@@ -4393,6 +6876,10 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4393
6876
  size: 256,
4394
6877
  usage: constants.buffer.COPY_DST | constants.buffer.MAP_READ
4395
6878
  });
6879
+ await waitForSubmittedGpuWork({
6880
+ timeoutMs: GPU_READBACK_COMPLETION_TIMEOUT_MS,
6881
+ allowTimeout: false
6882
+ });
4396
6883
  const encoder = device.createCommandEncoder({
4397
6884
  label: "plasius.wavefront.outputProbe.copy"
4398
6885
  });
@@ -4402,6 +6889,10 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4402
6889
  { width: 1, height: 1, depthOrArrayLayers: 1 }
4403
6890
  );
4404
6891
  device.queue.submit([encoder.finish()]);
6892
+ await waitForSubmittedGpuWork({
6893
+ timeoutMs: GPU_READBACK_COMPLETION_TIMEOUT_MS,
6894
+ allowTimeout: false
6895
+ });
4405
6896
  await readback.mapAsync(mapMode.READ);
4406
6897
  const bytes = new Uint8Array(readback.getMappedRange()).slice(0, 4);
4407
6898
  readback.unmap();
@@ -4414,7 +6905,57 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4414
6905
  });
4415
6906
  }
4416
6907
  async function renderFrame(renderOptions = {}) {
4417
- const frameStats = renderOnce();
6908
+ const awaitGPUCompletion = renderOptions.awaitGPUCompletion !== false;
6909
+ const samplingPlan = resolveRenderedSamplesPerPixel(renderOptions, awaitGPUCompletion);
6910
+ const useThrottledHighSamplePath = awaitGPUCompletion && samplingPlan.renderedSamplesPerPixel >= 8;
6911
+ const submittedWorkTimeoutMs = estimateSubmittedGpuWorkTimeoutMs(
6912
+ { ...config, renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel },
6913
+ tiles.length,
6914
+ renderOptions.submittedWorkTimeoutMs
6915
+ );
6916
+ const frameStartTimeMs = nowMs();
6917
+ const submissionWaitOptions = awaitGPUCompletion ? { timeoutMs: submittedWorkTimeoutMs, allowTimeout: false } : { timeoutMs: submittedWorkTimeoutMs };
6918
+ let frameStats;
6919
+ if (useThrottledHighSamplePath) {
6920
+ frame += 1;
6921
+ const frameIndex = frame + config.frameIndex;
6922
+ const parallelismCounters = createGpuParallelismCounters();
6923
+ const accelerationBuildSubmitted = dispatchGpuAccelerationBuild(frameIndex, parallelismCounters);
6924
+ const frameSubmissionCount = dispatchFrameAwaitingGpu(
6925
+ frameIndex,
6926
+ parallelismCounters,
6927
+ samplingPlan.renderedSamplesPerPixel
6928
+ );
6929
+ frameStats = createFrameStats({
6930
+ frameIndex,
6931
+ accelerationBuildSubmitted,
6932
+ frameSubmissionCount,
6933
+ parallelismCounters,
6934
+ renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel,
6935
+ targetSamplesPerPixel: samplingPlan.targetSamplesPerPixel,
6936
+ frameTimeBudgetMs: samplingPlan.frameTimeBudgetMs,
6937
+ budgetConstrained: samplingPlan.budgetConstrained
6938
+ });
6939
+ } else {
6940
+ frameStats = renderOnce(renderOptions, samplingPlan);
6941
+ }
6942
+ if (awaitGPUCompletion) {
6943
+ await waitForSubmittedGpuWork(submissionWaitOptions);
6944
+ }
6945
+ const frameTimeMs = Math.max(0, nowMs() - frameStartTimeMs);
6946
+ if (awaitGPUCompletion) {
6947
+ lastCompletedFrameTimeMs = frameTimeMs;
6948
+ lastCompletedSamplesPerPixel = frameStats.renderedSamplesPerPixel ?? frameStats.samplesPerPixel;
6949
+ }
6950
+ frameStats = Object.freeze({
6951
+ ...frameStats,
6952
+ gpuWorkerJobs: createGpuWorkerJobDiagnostics(
6953
+ frameStats.gpuParallelism,
6954
+ frameStats.commandSubmissions,
6955
+ frameTimeMs,
6956
+ awaitGPUCompletion
6957
+ )
6958
+ });
4418
6959
  const probe = renderOptions.readOutputProbe === false ? null : await readOutputProbe(renderOptions.probe);
4419
6960
  const maxChannel = probe ? Math.max(...probe.rgba.slice(0, 3)) : 0;
4420
6961
  return Object.freeze({
@@ -4435,10 +6976,8 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4435
6976
  queueOverflow: 0
4436
6977
  });
4437
6978
  }
4438
- function updateSceneObjects(sceneObjects) {
4439
- const nextPackedScene = packWavefrontSceneObjects(sceneObjects, config.sceneObjectCapacity);
4440
- packedScene = nextPackedScene;
4441
- config = createWavefrontPathTracingComputeConfig({
6979
+ function rebuildLiveConfig(overrides = {}) {
6980
+ return createWavefrontPathTracingComputeConfig({
4442
6981
  ...options,
4443
6982
  canvas,
4444
6983
  width: config.width,
@@ -4449,26 +6988,35 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4449
6988
  sceneObjectCapacity: config.sceneObjectCapacity,
4450
6989
  sceneObjects: packedScene.objects,
4451
6990
  camera: activeCameraOptions,
4452
- frameIndex: config.frameIndex
6991
+ environmentMap: {
6992
+ ...config.environmentMap
6993
+ },
6994
+ frameIndex: config.frameIndex,
6995
+ ...overrides
4453
6996
  });
6997
+ }
6998
+ function rebuildMediumResources(nextConfig) {
6999
+ const previousMediumTextureResource = mediumTextureResource;
7000
+ mediumTextureResource = createMediumTextureResource(device, constants, nextConfig.mediums);
7001
+ bindGroups = createTraceBindGroups();
7002
+ if (previousMediumTextureResource?.ownsTexture) {
7003
+ previousMediumTextureResource.texture?.destroy?.();
7004
+ }
7005
+ }
7006
+ function updateSceneObjects(sceneObjects) {
7007
+ const nextPackedScene = packWavefrontSceneObjects(sceneObjects, config.sceneObjectCapacity);
7008
+ packedScene = nextPackedScene;
7009
+ const nextConfig = rebuildLiveConfig();
7010
+ if (!mediumTablesEqual(config.mediums, nextConfig.mediums)) {
7011
+ rebuildMediumResources(nextConfig);
7012
+ }
7013
+ config = nextConfig;
4454
7014
  device.queue.writeBuffer(sceneObjectBuffer, 0, packedScene.buffer);
4455
7015
  return config;
4456
7016
  }
4457
7017
  function updateCamera(cameraOptions = {}) {
4458
7018
  activeCameraOptions = cameraOptions;
4459
- config = createWavefrontPathTracingComputeConfig({
4460
- ...options,
4461
- canvas,
4462
- width: config.width,
4463
- height: config.height,
4464
- maxDepth: config.maxDepth,
4465
- tileSize: config.tileSize,
4466
- samplesPerPixel: config.samplesPerPixel,
4467
- sceneObjectCapacity: config.sceneObjectCapacity,
4468
- sceneObjects: packedScene.objects,
4469
- camera: activeCameraOptions,
4470
- frameIndex: config.frameIndex
4471
- });
7019
+ config = rebuildLiveConfig();
4472
7020
  return config;
4473
7021
  }
4474
7022
  function getSnapshot() {
@@ -4486,6 +7034,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4486
7034
  emissiveTriangleCount: config.emissiveTriangleCount,
4487
7035
  environmentPortalCount: config.environmentPortalCount,
4488
7036
  environmentPortalMode: config.environmentPortalMode,
7037
+ mediumCount: config.mediumCount,
4489
7038
  environmentMap: createEnvironmentMapSnapshot(config.environmentMap),
4490
7039
  deferredPathResolve: config.deferredPathResolve,
4491
7040
  bvhNodeCount: config.bvhNodeCount,
@@ -4523,6 +7072,28 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4523
7072
  if (environmentMapResource.ownsTexture) {
4524
7073
  environmentMapResource.texture?.destroy?.();
4525
7074
  }
7075
+ if (environmentSamplingResource.ownsTexture) {
7076
+ environmentSamplingResource.texture?.destroy?.();
7077
+ }
7078
+ if (mediumTextureResource.ownsTexture) {
7079
+ mediumTextureResource.texture?.destroy?.();
7080
+ }
7081
+ brdfLutResource.texture?.destroy?.();
7082
+ if (baseColorAtlasResource.ownsTexture) {
7083
+ baseColorAtlasResource.texture?.destroy?.();
7084
+ }
7085
+ if (metallicRoughnessAtlasResource.ownsTexture) {
7086
+ metallicRoughnessAtlasResource.texture?.destroy?.();
7087
+ }
7088
+ if (normalAtlasResource.ownsTexture) {
7089
+ normalAtlasResource.texture?.destroy?.();
7090
+ }
7091
+ if (occlusionAtlasResource.ownsTexture) {
7092
+ occlusionAtlasResource.texture?.destroy?.();
7093
+ }
7094
+ if (emissiveAtlasResource.ownsTexture) {
7095
+ emissiveAtlasResource.texture?.destroy?.();
7096
+ }
4526
7097
  context.unconfigure?.();
4527
7098
  }
4528
7099
  return Object.freeze({
@@ -4675,6 +7246,48 @@ var rendererAccelerationStructurePolicies = Object.freeze(
4675
7246
  })
4676
7247
  )
4677
7248
  );
7249
+ function clampWavefrontAdaptiveSamplesPerPixel(value) {
7250
+ if (!Number.isFinite(value)) {
7251
+ return 1;
7252
+ }
7253
+ return Math.max(1, Math.min(256, Math.round(value)));
7254
+ }
7255
+ function createWavefrontAdaptiveSamplingLevels(options = {}) {
7256
+ const requestedSamplesPerPixel = clampWavefrontAdaptiveSamplesPerPixel(
7257
+ options.samplesPerPixel ?? 1
7258
+ );
7259
+ const minimumSamplesPerPixel = Math.min(
7260
+ requestedSamplesPerPixel,
7261
+ clampWavefrontAdaptiveSamplesPerPixel(options.minimumSamplesPerPixel ?? 1)
7262
+ );
7263
+ const frameTimeBudgetMs = Number.isFinite(options.frameTimeBudgetMs) ? Math.max(0, Number(options.frameTimeBudgetMs)) : 0;
7264
+ const levels = /* @__PURE__ */ new Set([minimumSamplesPerPixel, requestedSamplesPerPixel]);
7265
+ let currentSamplesPerPixel = minimumSamplesPerPixel;
7266
+ while (currentSamplesPerPixel < requestedSamplesPerPixel) {
7267
+ levels.add(currentSamplesPerPixel);
7268
+ currentSamplesPerPixel *= 2;
7269
+ }
7270
+ levels.add(Math.min(currentSamplesPerPixel, requestedSamplesPerPixel));
7271
+ return Object.freeze({
7272
+ requestedSamplesPerPixel,
7273
+ minimumSamplesPerPixel,
7274
+ frameTimeBudgetMs,
7275
+ levels: Object.freeze(
7276
+ [...levels].sort((left, right) => left - right).map(
7277
+ (samplesPerPixel) => Object.freeze({
7278
+ id: `${samplesPerPixel}spp`,
7279
+ label: `${samplesPerPixel} spp`,
7280
+ estimatedCostMs: samplesPerPixel,
7281
+ config: Object.freeze({
7282
+ samplesPerPixel,
7283
+ frameTimeBudgetMs,
7284
+ minimumSamplesPerPixel
7285
+ })
7286
+ })
7287
+ )
7288
+ )
7289
+ });
7290
+ }
4678
7291
  function createWavefrontField(name, type, description) {
4679
7292
  return Object.freeze({
4680
7293
  name,
@@ -5851,9 +8464,11 @@ export {
5851
8464
  createGpuRenderer,
5852
8465
  createRayTracingRenderPlan,
5853
8466
  createRendererDebugHooks,
8467
+ createWavefrontAdaptiveSamplingLevels,
5854
8468
  createWavefrontBvhBuildLevels,
5855
8469
  createWavefrontBvhSortStages,
5856
8470
  createWavefrontEmissiveTriangleIndexSource,
8471
+ createWavefrontGpuMaterialSource,
5857
8472
  createWavefrontGpuMeshSource,
5858
8473
  createWavefrontMeshAcceleration,
5859
8474
  createWavefrontPathTracingComputeConfig,