@plasius/gpu-renderer 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,9 +1,144 @@
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 = 256;
7
142
  var DEFAULT_MAX_FRAME_PASSES_PER_SUBMISSION = 256;
8
143
  var DEFAULT_SCENE_OBJECT_CAPACITY = 128;
9
144
  var DEFAULT_ENVIRONMENT_PORTAL_CAPACITY = 32;
@@ -12,22 +147,27 @@ var rendererWavefrontComputeMode = "webgpu-compute";
12
147
  var rendererWavefrontComputeWorkgroupSize = WORKGROUP_SIZE;
13
148
  var rendererWavefrontComputeStatsStride = 8;
14
149
  var RAY_RECORD_BYTES = 80;
15
- var HIT_RECORD_BYTES = 208;
16
- var SCENE_OBJECT_RECORD_BYTES = 96;
150
+ var HIT_RECORD_BYTES = 256;
151
+ var SCENE_OBJECT_RECORD_BYTES = 144;
17
152
  var MESH_VERTEX_RECORD_BYTES = 48;
18
- var MESH_RANGE_RECORD_BYTES = 96;
19
- var TRIANGLE_RECORD_BYTES = 208;
153
+ var MESH_RANGE_RECORD_BYTES = 240;
154
+ var TRIANGLE_RECORD_BYTES = 352;
155
+ var GPU_MATERIAL_RECORD_BYTES = 192;
20
156
  var BVH_NODE_RECORD_BYTES = 48;
21
157
  var BVH_LEAF_REF_RECORD_BYTES = 16;
22
158
  var EMISSIVE_TRIANGLE_INDEX_BYTES = 4;
23
159
  var ENVIRONMENT_PORTAL_RECORD_BYTES = 96;
24
160
  var ACCUMULATION_RECORD_BYTES = 16;
25
161
  var PATH_VERTEX_RECORD_BYTES = 16;
26
- var CONFIG_BUFFER_BYTES = 304;
162
+ var GPU_SUBMITTED_WORK_TIMEOUT_MS = 5e3;
163
+ var GPU_READBACK_COMPLETION_TIMEOUT_MS = 6e4;
164
+ var GPU_MAX_SUBMITTED_WORK_TIMEOUT_MS = 6e4;
165
+ var CONFIG_BUFFER_BYTES = 320;
27
166
  var COUNTER_DISPATCH_ARGS_OFFSET = 16;
28
167
  var INDIRECT_DISPATCH_ARGS_BYTES = 12;
29
168
  var COUNTER_BUFFER_BYTES = 32;
30
169
  var TRACE_STORAGE_BUFFER_BINDINGS = 10;
170
+ var BRDF_LUT_UPLOAD_CACHE = /* @__PURE__ */ new Map();
31
171
  var MATERIAL_DIFFUSE = 0;
32
172
  var MATERIAL_METAL = 1;
33
173
  var MATERIAL_DIELECTRIC = 2;
@@ -62,6 +202,7 @@ var wavefrontPathTracingComputeLimits = Object.freeze({
62
202
  meshVertexRecordBytes: MESH_VERTEX_RECORD_BYTES,
63
203
  meshRangeRecordBytes: MESH_RANGE_RECORD_BYTES,
64
204
  triangleRecordBytes: TRIANGLE_RECORD_BYTES,
205
+ materialRecordBytes: GPU_MATERIAL_RECORD_BYTES,
65
206
  bvhNodeRecordBytes: BVH_NODE_RECORD_BYTES,
66
207
  bvhLeafReferenceRecordBytes: BVH_LEAF_REF_RECORD_BYTES,
67
208
  emissiveTriangleIndexBytes: EMISSIVE_TRIANGLE_INDEX_BYTES,
@@ -145,6 +286,32 @@ function asColor(value, fallback = [1, 1, 1, 1]) {
145
286
  clamp(readFiniteNumber("color[3]", value[3], fallback[3] ?? 1), 0, 1)
146
287
  ];
147
288
  }
289
+ function deriveLegacySheenColor(baseColor, sheen, sheenTint) {
290
+ const sheenStrength = clamp(Number(sheen) || 0, 0, 1);
291
+ if (sheenStrength <= 0) {
292
+ return [0, 0, 0, 1];
293
+ }
294
+ const tint = clamp(Number(sheenTint) || 0, 0, 1);
295
+ const base = asColor(baseColor, [1, 1, 1, 1]);
296
+ return [
297
+ clamp((1 - tint) * sheenStrength + base[0] * tint * sheenStrength, 0, 1),
298
+ clamp((1 - tint) * sheenStrength + base[1] * tint * sheenStrength, 0, 1),
299
+ clamp((1 - tint) * sheenStrength + base[2] * tint * sheenStrength, 0, 1),
300
+ 1
301
+ ];
302
+ }
303
+ function resolveSheenColor(input, fallbackBaseColor) {
304
+ if (input?.sheenColor || input?.material?.sheenColor) {
305
+ return asColor(input.sheenColor ?? input.material?.sheenColor, [0, 0, 0, 1]).map(
306
+ (value, index) => index < 3 ? clamp(value, 0, 1) : 1
307
+ );
308
+ }
309
+ return deriveLegacySheenColor(
310
+ fallbackBaseColor,
311
+ input?.sheen ?? input?.material?.sheen,
312
+ input?.sheenTint ?? input?.material?.sheenTint
313
+ );
314
+ }
148
315
  function resolveEnvironmentMap(input = null) {
149
316
  const source = input && typeof input === "object" ? input : null;
150
317
  const hasTexture = Boolean(source?.view || source?.texture || source?.data);
@@ -154,6 +321,11 @@ function resolveEnvironmentMap(input = null) {
154
321
  enabled: hasTexture && source?.enabled !== false,
155
322
  width,
156
323
  height,
324
+ mipLevelCount: readPositiveInteger(
325
+ "environmentMap.mipLevelCount",
326
+ source?.mipLevelCount,
327
+ 1
328
+ ),
157
329
  format: typeof source?.format === "string" ? source.format : "rgba16float",
158
330
  projection: typeof source?.projection === "string" ? source.projection : "equirectangular",
159
331
  texture: source?.texture ?? null,
@@ -165,7 +337,8 @@ function resolveEnvironmentMap(input = null) {
165
337
  ambientStrength: Math.max(
166
338
  0,
167
339
  readFiniteNumber("environmentMap.ambientStrength", source?.ambientStrength, 0.32)
168
- )
340
+ ),
341
+ hasImportanceData: source?.hasImportanceData === true
169
342
  });
170
343
  }
171
344
  function resolveDeferredPathResolve(options = {}) {
@@ -339,7 +512,8 @@ function normalizeWavefrontSceneObject(input = {}, index = 0) {
339
512
  input.halfExtent ?? input.halfExtents ?? input.extents ?? bounds?.halfExtent,
340
513
  [0.5, 0.5, 0.5]
341
514
  ).map((value) => Math.max(value, 1e-3));
342
- const materialKind = readMaterialKind(input.materialKind ?? input.material?.kind);
515
+ const materialKindInput = input.materialKind ?? input.material?.kind;
516
+ const materialKind = readMaterialKind(materialKindInput);
343
517
  const color = asColor(
344
518
  input.color ?? input.baseColor ?? input.albedo ?? input.material?.color ?? input.material?.baseColor,
345
519
  [0.72, 0.72, 0.68, 1]
@@ -348,10 +522,22 @@ function normalizeWavefrontSceneObject(input = {}, index = 0) {
348
522
  input.emission ?? input.emissive ?? input.material?.emission ?? input.material?.emissive,
349
523
  [0, 0, 0, 1]
350
524
  );
525
+ const opacity = clamp(readFiniteNumber("opacity", input.opacity ?? input.material?.opacity, color[3] ?? 1), 0, 1);
526
+ const transmission = clamp(
527
+ readFiniteNumber("transmission", input.transmission ?? input.material?.transmission, 0),
528
+ 0,
529
+ 1
530
+ );
531
+ const sheenColor = resolveSheenColor(input, color);
532
+ const specularColor = asColor(
533
+ input.specularColor ?? input.material?.specularColor,
534
+ [1, 1, 1, 1]
535
+ ).map((value, componentIndex) => componentIndex < 3 ? clamp(value, 0, 1) : 1);
536
+ 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
537
  return Object.freeze({
352
538
  id: readNonNegativeInteger("id", input.id, index + 1),
353
539
  kind,
354
- materialKind: emission[0] > 0 || emission[1] > 0 || emission[2] > 0 ? MATERIAL_EMISSIVE : materialKind,
540
+ materialKind: resolvedMaterialKind,
355
541
  flags: readNonNegativeInteger("flags", input.flags, 0),
356
542
  center: Object.freeze(center),
357
543
  halfExtent: Object.freeze(halfExtent),
@@ -359,8 +545,24 @@ function normalizeWavefrontSceneObject(input = {}, index = 0) {
359
545
  emission: Object.freeze(emission),
360
546
  roughness: clamp(readFiniteNumber("roughness", input.roughness ?? input.material?.roughness, 0.72), 0, 1),
361
547
  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)
548
+ opacity,
549
+ ior: clamp(readFiniteNumber("ior", input.ior ?? input.material?.ior, 1.45), 1, 3),
550
+ sheen: clamp(readFiniteNumber("sheen", input.sheen ?? input.material?.sheen, 0), 0, 1),
551
+ sheenTint: clamp(readFiniteNumber("sheenTint", input.sheenTint ?? input.material?.sheenTint, 0), 0, 1),
552
+ sheenColor: Object.freeze(sheenColor),
553
+ clearcoat: clamp(readFiniteNumber("clearcoat", input.clearcoat ?? input.material?.clearcoat, 0), 0, 1),
554
+ clearcoatRoughness: clamp(
555
+ readFiniteNumber(
556
+ "clearcoatRoughness",
557
+ input.clearcoatRoughness ?? input.material?.clearcoatRoughness,
558
+ 0.08
559
+ ),
560
+ 0,
561
+ 1
562
+ ),
563
+ specular: clamp(readFiniteNumber("specular", input.specular ?? input.material?.specular, 1), 0, 1),
564
+ specularColor: Object.freeze(specularColor),
565
+ transmission
364
566
  });
365
567
  }
366
568
  function createDefaultWavefrontSceneObjects() {
@@ -432,7 +634,8 @@ function normalizeWavefrontMesh(input = {}, meshIndex = 0) {
432
634
  input.uvs ?? input.texcoords ?? input.uv,
433
635
  (value) => readFiniteNumber("mesh uv", value, 0)
434
636
  ) : null;
435
- const materialKind = readMaterialKind(input.materialKind ?? input.material?.kind);
637
+ const materialKindInput = input.materialKind ?? input.material?.kind;
638
+ const materialKind = readMaterialKind(materialKindInput);
436
639
  const color = asColor(
437
640
  input.color ?? input.baseColor ?? input.albedo ?? input.material?.color ?? input.material?.baseColor,
438
641
  [0.72, 0.72, 0.68, 1]
@@ -441,13 +644,25 @@ function normalizeWavefrontMesh(input = {}, meshIndex = 0) {
441
644
  input.emission ?? input.emissive ?? input.material?.emission ?? input.material?.emissive,
442
645
  [0, 0, 0, 1]
443
646
  );
647
+ const opacity = clamp(readFiniteNumber("opacity", input.opacity ?? input.material?.opacity, color[3] ?? 1), 0, 1);
648
+ const transmission = clamp(
649
+ readFiniteNumber("transmission", input.transmission ?? input.material?.transmission, 0),
650
+ 0,
651
+ 1
652
+ );
653
+ const sheenColor = resolveSheenColor(input, color);
654
+ const specularColor = asColor(
655
+ input.specularColor ?? input.material?.specularColor,
656
+ [1, 1, 1, 1]
657
+ ).map((value, componentIndex) => componentIndex < 3 ? clamp(value, 0, 1) : 1);
658
+ 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
659
  return Object.freeze({
445
660
  id: readNonNegativeInteger("mesh id", input.id, meshIndex + 1),
446
661
  positions: Object.freeze(Array.from(positions, (value) => readFiniteNumber("mesh position", value, 0))),
447
662
  indices: Object.freeze(indices),
448
663
  normals: normals ? Object.freeze(normals) : null,
449
664
  uvs: uvs ? Object.freeze(uvs) : null,
450
- materialKind: emission[0] > 0 || emission[1] > 0 || emission[2] > 0 ? MATERIAL_EMISSIVE : materialKind,
665
+ materialKind: resolvedMaterialKind,
451
666
  flags: readNonNegativeInteger("mesh flags", input.flags, 0),
452
667
  materialRefId: readNonNegativeInteger(
453
668
  "mesh materialRefId",
@@ -463,10 +678,167 @@ function normalizeWavefrontMesh(input = {}, meshIndex = 0) {
463
678
  emission: Object.freeze(emission),
464
679
  roughness: clamp(readFiniteNumber("roughness", input.roughness ?? input.material?.roughness, 0.72), 0, 1),
465
680
  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)
681
+ opacity,
682
+ ior: clamp(readFiniteNumber("ior", input.ior ?? input.material?.ior, 1.45), 1, 3),
683
+ sheen: clamp(readFiniteNumber("sheen", input.sheen ?? input.material?.sheen, 0), 0, 1),
684
+ sheenTint: clamp(readFiniteNumber("sheenTint", input.sheenTint ?? input.material?.sheenTint, 0), 0, 1),
685
+ sheenColor: Object.freeze(sheenColor),
686
+ clearcoat: clamp(readFiniteNumber("clearcoat", input.clearcoat ?? input.material?.clearcoat, 0), 0, 1),
687
+ clearcoatRoughness: clamp(
688
+ readFiniteNumber(
689
+ "clearcoatRoughness",
690
+ input.clearcoatRoughness ?? input.material?.clearcoatRoughness,
691
+ 0.08
692
+ ),
693
+ 0,
694
+ 1
695
+ ),
696
+ specular: clamp(readFiniteNumber("specular", input.specular ?? input.material?.specular, 1), 0, 1),
697
+ specularColor: Object.freeze(specularColor),
698
+ transmission,
699
+ baseColorTexture: input.baseColorTexture ?? input.material?.baseColorTexture ?? null,
700
+ metallicRoughnessTexture: input.metallicRoughnessTexture ?? input.material?.metallicRoughnessTexture ?? null,
701
+ normalTexture: input.normalTexture ?? input.material?.normalTexture ?? null,
702
+ occlusionTexture: input.occlusionTexture ?? input.material?.occlusionTexture ?? null,
703
+ emissiveTexture: input.emissiveTexture ?? input.material?.emissiveTexture ?? null
468
704
  });
469
705
  }
706
+ function clampUnit(value) {
707
+ return clamp(Number(value) || 0, 0, 1);
708
+ }
709
+ function srgbToLinear(value) {
710
+ const channel = clampUnit(value);
711
+ if (channel <= 0.04045) {
712
+ return channel / 12.92;
713
+ }
714
+ return ((channel + 0.055) / 1.055) ** 2.4;
715
+ }
716
+ function sampleTextureRgba(texture, uv = [0, 0], colorSpace = "linear") {
717
+ if (!texture || !Number.isFinite(texture.width) || !Number.isFinite(texture.height) || !texture.data || texture.width <= 0 || texture.height <= 0) {
718
+ return [1, 1, 1, 1];
719
+ }
720
+ const u = (uv[0] % 1 + 1) % 1;
721
+ const v = (uv[1] % 1 + 1) % 1;
722
+ const x = Math.min(texture.width - 1, Math.max(0, Math.round(u * (texture.width - 1))));
723
+ const y = Math.min(texture.height - 1, Math.max(0, Math.round((1 - v) * (texture.height - 1))));
724
+ const offset = (y * texture.width + x) * 4;
725
+ const data = texture.data;
726
+ const scale2 = resolveTextureSampleScale(data);
727
+ const defaultChannel = scale2 === 1 ? 1 : Math.round(1 / scale2);
728
+ const color = [
729
+ (data[offset] ?? defaultChannel) * scale2,
730
+ (data[offset + 1] ?? defaultChannel) * scale2,
731
+ (data[offset + 2] ?? defaultChannel) * scale2,
732
+ (data[offset + 3] ?? defaultChannel) * scale2
733
+ ];
734
+ if (colorSpace === "srgb") {
735
+ return [srgbToLinear(color[0]), srgbToLinear(color[1]), srgbToLinear(color[2]), color[3]];
736
+ }
737
+ return color;
738
+ }
739
+ function resolveTextureSampleScale(data) {
740
+ if (data instanceof Uint8Array || data instanceof Uint8ClampedArray) {
741
+ return 1 / 255;
742
+ }
743
+ if (data instanceof Uint16Array) {
744
+ return 1 / 65535;
745
+ }
746
+ if (Array.isArray(data) && data.some((value) => Number(value) > 1)) {
747
+ return 1 / 255;
748
+ }
749
+ return 1;
750
+ }
751
+ function normalizeVectorOrFallback(vector, fallback) {
752
+ return normalize(Array.isArray(vector) ? vector : fallback, fallback);
753
+ }
754
+ function buildTriangleTangentBasis(v0, v1, v2, uv0, uv1, uv2, fallbackNormal) {
755
+ const edge1 = subtract(v1, v0);
756
+ const edge2 = subtract(v2, v0);
757
+ const deltaUv1 = [uv1[0] - uv0[0], uv1[1] - uv0[1]];
758
+ const deltaUv2 = [uv2[0] - uv0[0], uv2[1] - uv0[1]];
759
+ const determinant = deltaUv1[0] * deltaUv2[1] - deltaUv1[1] * deltaUv2[0];
760
+ if (Math.abs(determinant) < 1e-6) {
761
+ const tangentFallback = Math.abs(fallbackNormal[1]) < 0.999 ? [0, 1, 0] : [1, 0, 0];
762
+ const tangent2 = normalize(cross(tangentFallback, fallbackNormal), [1, 0, 0]);
763
+ const bitangent2 = normalize(cross(fallbackNormal, tangent2), [0, 0, 1]);
764
+ return { tangent: tangent2, bitangent: bitangent2 };
765
+ }
766
+ const inverse = 1 / determinant;
767
+ const tangent = normalize(
768
+ [
769
+ inverse * (edge1[0] * deltaUv2[1] - edge2[0] * deltaUv1[1]),
770
+ inverse * (edge1[1] * deltaUv2[1] - edge2[1] * deltaUv1[1]),
771
+ inverse * (edge1[2] * deltaUv2[1] - edge2[2] * deltaUv1[1])
772
+ ],
773
+ [1, 0, 0]
774
+ );
775
+ const bitangent = normalize(
776
+ [
777
+ inverse * (-edge1[0] * deltaUv2[0] + edge2[0] * deltaUv1[0]),
778
+ inverse * (-edge1[1] * deltaUv2[0] + edge2[1] * deltaUv1[0]),
779
+ inverse * (-edge1[2] * deltaUv2[0] + edge2[2] * deltaUv1[0])
780
+ ],
781
+ [0, 0, 1]
782
+ );
783
+ return { tangent, bitangent };
784
+ }
785
+ function applyNormalMap(normal, tangent, bitangent, normalTexture, uv) {
786
+ if (!normalTexture) {
787
+ return normalizeVectorOrFallback(normal, [0, 1, 0]);
788
+ }
789
+ const sample = sampleTextureRgba(normalTexture, uv, "linear");
790
+ const strength = clampUnit(normalTexture.scale ?? 1);
791
+ const tangentNormal = normalize(
792
+ [
793
+ (sample[0] * 2 - 1) * strength,
794
+ (sample[1] * 2 - 1) * strength,
795
+ 1 + (sample[2] * 2 - 1 - 1) * strength
796
+ ],
797
+ [0, 0, 1]
798
+ );
799
+ return normalize(
800
+ [
801
+ tangent[0] * tangentNormal[0] + bitangent[0] * tangentNormal[1] + normal[0] * tangentNormal[2],
802
+ tangent[1] * tangentNormal[0] + bitangent[1] * tangentNormal[1] + normal[1] * tangentNormal[2],
803
+ tangent[2] * tangentNormal[0] + bitangent[2] * tangentNormal[1] + normal[2] * tangentNormal[2]
804
+ ],
805
+ normal
806
+ );
807
+ }
808
+ function sampleBaseColor(mesh, uv) {
809
+ const sample = mesh.baseColorTexture ? sampleTextureRgba(mesh.baseColorTexture, uv, "srgb") : [1, 1, 1, 1];
810
+ return [
811
+ clampUnit(mesh.color[0] * sample[0]),
812
+ clampUnit(mesh.color[1] * sample[1]),
813
+ clampUnit(mesh.color[2] * sample[2]),
814
+ clampUnit((mesh.color[3] ?? 1) * sample[3])
815
+ ];
816
+ }
817
+ function sampleSurfaceMaterial(mesh, uv) {
818
+ const textureSample = mesh.metallicRoughnessTexture ? sampleTextureRgba(mesh.metallicRoughnessTexture, uv, "linear") : [1, 1, 1, 1];
819
+ return {
820
+ roughness: clamp(mesh.roughness * textureSample[1], 0, 1),
821
+ metallic: clamp(mesh.metallic * textureSample[2], 0, 1)
822
+ };
823
+ }
824
+ function averageColors(colors) {
825
+ const count = Math.max(colors.length, 1);
826
+ return colors.reduce(
827
+ (accumulator, color) => [
828
+ accumulator[0] + color[0] / count,
829
+ accumulator[1] + color[1] / count,
830
+ accumulator[2] + color[2] / count,
831
+ accumulator[3] + color[3] / count
832
+ ],
833
+ [0, 0, 0, 0]
834
+ );
835
+ }
836
+ function averageNumbers(values, fallback = 0) {
837
+ if (!Array.isArray(values) || values.length === 0) {
838
+ return fallback;
839
+ }
840
+ return values.reduce((sum, value) => sum + value, 0) / values.length;
841
+ }
470
842
  function createMeshTriangleRecords(meshes) {
471
843
  const source = Array.isArray(meshes) ? meshes : [];
472
844
  let nextTriangleId = 0;
@@ -487,6 +859,16 @@ function createMeshTriangleRecords(meshes) {
487
859
  const uv0 = mesh.uvs ? readVector2(mesh.uvs, a) : [0, 0];
488
860
  const uv1 = mesh.uvs ? readVector2(mesh.uvs, b) : [0, 0];
489
861
  const uv2 = mesh.uvs ? readVector2(mesh.uvs, c) : [0, 0];
862
+ const tangentBasis = buildTriangleTangentBasis(v0, v1, v2, uv0, uv1, uv2, faceNormal);
863
+ const shadedN0 = applyNormalMap(n0, tangentBasis.tangent, tangentBasis.bitangent, mesh.normalTexture, uv0);
864
+ const shadedN1 = applyNormalMap(n1, tangentBasis.tangent, tangentBasis.bitangent, mesh.normalTexture, uv1);
865
+ const shadedN2 = applyNormalMap(n2, tangentBasis.tangent, tangentBasis.bitangent, mesh.normalTexture, uv2);
866
+ const sampledColors = [sampleBaseColor(mesh, uv0), sampleBaseColor(mesh, uv1), sampleBaseColor(mesh, uv2)];
867
+ const sampledMaterials = [
868
+ sampleSurfaceMaterial(mesh, uv0),
869
+ sampleSurfaceMaterial(mesh, uv1),
870
+ sampleSurfaceMaterial(mesh, uv2)
871
+ ];
490
872
  const bounds = triangleBounds(v0, v1, v2);
491
873
  triangles.push(
492
874
  Object.freeze({
@@ -496,18 +878,42 @@ function createMeshTriangleRecords(meshes) {
496
878
  flags: mesh.flags,
497
879
  materialRefId: mesh.materialRefId,
498
880
  mediumRefId: mesh.mediumRefId,
881
+ materialSlot: meshIndex,
499
882
  v0: Object.freeze(v0),
500
883
  v1: Object.freeze(v1),
501
884
  v2: Object.freeze(v2),
502
- n0: Object.freeze(n0),
503
- n1: Object.freeze(n1),
504
- n2: Object.freeze(n2),
885
+ n0: Object.freeze(shadedN0),
886
+ n1: Object.freeze(shadedN1),
887
+ n2: Object.freeze(shadedN2),
505
888
  uv0: Object.freeze(uv0),
506
889
  uv1: Object.freeze(uv1),
507
890
  uv2: Object.freeze(uv2),
508
- color: mesh.color,
891
+ color: Object.freeze(averageColors(sampledColors)),
509
892
  emission: mesh.emission,
510
- material: Object.freeze([mesh.roughness, mesh.metallic, mesh.opacity, mesh.ior]),
893
+ material: Object.freeze([
894
+ averageNumbers(sampledMaterials.map((sample) => sample.roughness), mesh.roughness),
895
+ averageNumbers(sampledMaterials.map((sample) => sample.metallic), mesh.metallic),
896
+ mesh.opacity,
897
+ mesh.ior
898
+ ]),
899
+ materialResponse: Object.freeze([
900
+ mesh.sheenColor[0] ?? 0,
901
+ mesh.sheenColor[1] ?? 0,
902
+ mesh.sheenColor[2] ?? 0,
903
+ mesh.clearcoat
904
+ ]),
905
+ materialExtension: Object.freeze([
906
+ mesh.clearcoatRoughness,
907
+ mesh.specular,
908
+ mesh.transmission,
909
+ 0
910
+ ]),
911
+ specularColor: Object.freeze([
912
+ mesh.specularColor[0] ?? 1,
913
+ mesh.specularColor[1] ?? 1,
914
+ mesh.specularColor[2] ?? 1,
915
+ 1
916
+ ]),
511
917
  bounds: Object.freeze({
512
918
  min: Object.freeze(bounds.min),
513
919
  max: Object.freeze(bounds.max)
@@ -616,6 +1022,220 @@ function nextPowerOfTwo(value) {
616
1022
  }
617
1023
  return 2 ** Math.ceil(Math.log2(value));
618
1024
  }
1025
+ function textureComponentToByte(value, fallback) {
1026
+ const numeric = Number(value);
1027
+ if (!Number.isFinite(numeric)) {
1028
+ return fallback;
1029
+ }
1030
+ if (numeric >= 0 && numeric <= 1) {
1031
+ return Math.max(0, Math.min(255, Math.round(numeric * 255)));
1032
+ }
1033
+ return Math.max(0, Math.min(255, Math.round(numeric)));
1034
+ }
1035
+ function createSolidTextureSample(width, height, rgba) {
1036
+ const data = new Uint8Array(width * height * 4);
1037
+ for (let offset = 0; offset < data.length; offset += 4) {
1038
+ data[offset] = rgba[0];
1039
+ data[offset + 1] = rgba[1];
1040
+ data[offset + 2] = rgba[2];
1041
+ data[offset + 3] = rgba[3];
1042
+ }
1043
+ return Object.freeze({
1044
+ width,
1045
+ height,
1046
+ data
1047
+ });
1048
+ }
1049
+ function normalizeTextureSampleInput(texture, fallbackColor) {
1050
+ if (!texture || !Number.isFinite(texture.width) || !Number.isFinite(texture.height) || texture.width <= 0 || texture.height <= 0) {
1051
+ return createSolidTextureSample(1, 1, fallbackColor);
1052
+ }
1053
+ const pixelCount = Math.trunc(texture.width) * Math.trunc(texture.height) * 4;
1054
+ const source = ArrayBuffer.isView(texture.data) || Array.isArray(texture.data) ? texture.data : null;
1055
+ if (!source || source.length < pixelCount) {
1056
+ return createSolidTextureSample(1, 1, fallbackColor);
1057
+ }
1058
+ const data = new Uint8Array(pixelCount);
1059
+ for (let index = 0; index < pixelCount; index += 1) {
1060
+ data[index] = textureComponentToByte(source[index], fallbackColor[index % 4]);
1061
+ }
1062
+ return Object.freeze({
1063
+ width: Math.trunc(texture.width),
1064
+ height: Math.trunc(texture.height),
1065
+ data
1066
+ });
1067
+ }
1068
+ function buildTextureAtlas(textures, fallbackColor) {
1069
+ const padding = 1;
1070
+ const defaultTexture = createSolidTextureSample(1, 1, fallbackColor);
1071
+ const uniqueEntries = [{ source: null, texture: defaultTexture }];
1072
+ const bySource = /* @__PURE__ */ new Map();
1073
+ for (const texture of Array.isArray(textures) ? textures : []) {
1074
+ if (!texture || bySource.has(texture)) {
1075
+ continue;
1076
+ }
1077
+ const normalized = normalizeTextureSampleInput(texture, fallbackColor);
1078
+ bySource.set(texture, uniqueEntries.length);
1079
+ uniqueEntries.push({ source: texture, texture: normalized });
1080
+ }
1081
+ const totalArea = uniqueEntries.reduce((sum, entry) => {
1082
+ return sum + (entry.texture.width + padding * 2) * (entry.texture.height + padding * 2);
1083
+ }, 0);
1084
+ const maxTileWidth = uniqueEntries.reduce((maxWidth, entry) => {
1085
+ return Math.max(maxWidth, entry.texture.width + padding * 2);
1086
+ }, 1);
1087
+ const targetWidth = Math.max(
1088
+ maxTileWidth,
1089
+ nextPowerOfTwo(Math.max(maxTileWidth, Math.ceil(Math.sqrt(totalArea))))
1090
+ );
1091
+ let cursorX = 0;
1092
+ let cursorY = 0;
1093
+ let rowHeight = 0;
1094
+ let atlasWidth = 0;
1095
+ const placements = uniqueEntries.map((entry) => {
1096
+ const tileWidth = entry.texture.width + padding * 2;
1097
+ const tileHeight = entry.texture.height + padding * 2;
1098
+ if (cursorX > 0 && cursorX + tileWidth > targetWidth) {
1099
+ cursorX = 0;
1100
+ cursorY += rowHeight;
1101
+ rowHeight = 0;
1102
+ }
1103
+ const placement = Object.freeze({
1104
+ x: cursorX,
1105
+ y: cursorY,
1106
+ tileWidth,
1107
+ tileHeight,
1108
+ width: entry.texture.width,
1109
+ height: entry.texture.height
1110
+ });
1111
+ cursorX += tileWidth;
1112
+ atlasWidth = Math.max(atlasWidth, cursorX);
1113
+ rowHeight = Math.max(rowHeight, tileHeight);
1114
+ return placement;
1115
+ });
1116
+ const atlasHeight = Math.max(1, cursorY + rowHeight);
1117
+ const atlasData = new Uint8Array(Math.max(1, atlasWidth * atlasHeight * 4));
1118
+ const writePixel = (x, y, rgba) => {
1119
+ const offset = (y * atlasWidth + x) * 4;
1120
+ atlasData[offset] = rgba[0];
1121
+ atlasData[offset + 1] = rgba[1];
1122
+ atlasData[offset + 2] = rgba[2];
1123
+ atlasData[offset + 3] = rgba[3];
1124
+ };
1125
+ const rects = placements.map((placement, entryIndex) => {
1126
+ const { texture } = uniqueEntries[entryIndex];
1127
+ for (let y = 0; y < placement.tileHeight; y += 1) {
1128
+ for (let x = 0; x < placement.tileWidth; x += 1) {
1129
+ const sampleX = Math.max(0, Math.min(texture.width - 1, x - padding));
1130
+ const sampleY = Math.max(0, Math.min(texture.height - 1, y - padding));
1131
+ const sourceOffset = (sampleY * texture.width + sampleX) * 4;
1132
+ writePixel(placement.x + x, placement.y + y, texture.data.slice(sourceOffset, sourceOffset + 4));
1133
+ }
1134
+ }
1135
+ return Object.freeze([
1136
+ (placement.x + padding) / Math.max(1, atlasWidth),
1137
+ (placement.y + padding) / Math.max(1, atlasHeight),
1138
+ placement.width / Math.max(1, atlasWidth),
1139
+ placement.height / Math.max(1, atlasHeight)
1140
+ ]);
1141
+ });
1142
+ const rectBySource = /* @__PURE__ */ new Map();
1143
+ uniqueEntries.forEach((entry, index) => {
1144
+ if (entry.source) {
1145
+ rectBySource.set(entry.source, rects[index]);
1146
+ }
1147
+ });
1148
+ return Object.freeze({
1149
+ width: Math.max(1, atlasWidth),
1150
+ height: Math.max(1, atlasHeight),
1151
+ data: atlasData,
1152
+ defaultRect: rects[0],
1153
+ resolveRect(texture) {
1154
+ return rectBySource.get(texture) ?? rects[0];
1155
+ }
1156
+ });
1157
+ }
1158
+ function createWavefrontGpuMaterialSource(meshes = []) {
1159
+ const source = Array.isArray(meshes) ? meshes : [meshes];
1160
+ const normalized = source.map((meshInput, meshIndex) => normalizeWavefrontMesh(meshInput, meshIndex));
1161
+ const baseColorAtlas = buildTextureAtlas(
1162
+ normalized.map((mesh) => mesh.baseColorTexture),
1163
+ [255, 255, 255, 255]
1164
+ );
1165
+ const metallicRoughnessAtlas = buildTextureAtlas(
1166
+ normalized.map((mesh) => mesh.metallicRoughnessTexture),
1167
+ [255, 255, 255, 255]
1168
+ );
1169
+ const normalAtlas = buildTextureAtlas(
1170
+ normalized.map((mesh) => mesh.normalTexture),
1171
+ [128, 128, 255, 255]
1172
+ );
1173
+ const occlusionAtlas = buildTextureAtlas(
1174
+ normalized.map((mesh) => mesh.occlusionTexture),
1175
+ [255, 255, 255, 255]
1176
+ );
1177
+ const emissiveAtlas = buildTextureAtlas(
1178
+ normalized.map((mesh) => mesh.emissiveTexture),
1179
+ [255, 255, 255, 255]
1180
+ );
1181
+ const bytes = new ArrayBuffer(Math.max(1, normalized.length) * GPU_MATERIAL_RECORD_BYTES);
1182
+ const floatView = new Float32Array(bytes);
1183
+ normalized.forEach((mesh, meshIndex) => {
1184
+ const byteOffset = meshIndex * GPU_MATERIAL_RECORD_BYTES;
1185
+ writeVec4(floatView, byteOffset, mesh.color);
1186
+ writeVec4(floatView, byteOffset + 16, mesh.emission);
1187
+ writeVec4(floatView, byteOffset + 32, [
1188
+ mesh.roughness,
1189
+ mesh.metallic,
1190
+ mesh.opacity,
1191
+ mesh.ior
1192
+ ]);
1193
+ writeVec4(floatView, byteOffset + 48, [
1194
+ mesh.sheenColor[0] ?? 0,
1195
+ mesh.sheenColor[1] ?? 0,
1196
+ mesh.sheenColor[2] ?? 0,
1197
+ mesh.clearcoat
1198
+ ]);
1199
+ writeVec4(floatView, byteOffset + 64, [
1200
+ mesh.clearcoatRoughness,
1201
+ mesh.specular,
1202
+ mesh.transmission,
1203
+ 0
1204
+ ]);
1205
+ writeVec4(floatView, byteOffset + 80, [
1206
+ mesh.specularColor[0] ?? 1,
1207
+ mesh.specularColor[1] ?? 1,
1208
+ mesh.specularColor[2] ?? 1,
1209
+ 1
1210
+ ]);
1211
+ writeVec4(floatView, byteOffset + 96, baseColorAtlas.resolveRect(mesh.baseColorTexture));
1212
+ writeVec4(
1213
+ floatView,
1214
+ byteOffset + 112,
1215
+ metallicRoughnessAtlas.resolveRect(mesh.metallicRoughnessTexture)
1216
+ );
1217
+ writeVec4(floatView, byteOffset + 128, normalAtlas.resolveRect(mesh.normalTexture));
1218
+ writeVec4(floatView, byteOffset + 144, occlusionAtlas.resolveRect(mesh.occlusionTexture));
1219
+ writeVec4(floatView, byteOffset + 160, emissiveAtlas.resolveRect(mesh.emissiveTexture));
1220
+ writeVec4(floatView, byteOffset + 176, [
1221
+ clampUnit(mesh.normalTexture?.scale ?? mesh.normalTexture?.strength ?? 1),
1222
+ clampUnit(mesh.occlusionTexture?.strength ?? 1),
1223
+ clampUnit(mesh.emissiveTexture?.strength ?? 1),
1224
+ 0
1225
+ ]);
1226
+ });
1227
+ return Object.freeze({
1228
+ buffer: bytes,
1229
+ count: normalized.length,
1230
+ recordBytes: GPU_MATERIAL_RECORD_BYTES,
1231
+ records: Object.freeze(normalized),
1232
+ baseColorAtlas,
1233
+ metallicRoughnessAtlas,
1234
+ normalAtlas,
1235
+ occlusionAtlas,
1236
+ emissiveAtlas
1237
+ });
1238
+ }
619
1239
  function estimateBvhLeafSortCapacity(triangleCount) {
620
1240
  return triangleCount <= 0 ? 0 : nextPowerOfTwo(triangleCount);
621
1241
  }
@@ -673,9 +1293,10 @@ function resolveAccelerationBuildMode(options = {}) {
673
1293
  }
674
1294
  return mode;
675
1295
  }
676
- function createWavefrontGpuMeshSource(meshes = []) {
1296
+ function createWavefrontGpuMeshSource(meshes = [], gpuMaterialSourceInput = null) {
677
1297
  const source = Array.isArray(meshes) ? meshes : [meshes];
678
1298
  const normalized = source.map((meshInput, meshIndex) => normalizeWavefrontMesh(meshInput, meshIndex));
1299
+ const gpuMaterialSource = gpuMaterialSourceInput ?? createWavefrontGpuMaterialSource(normalized);
679
1300
  const vertexCount = normalized.reduce((count, mesh) => count + mesh.positions.length / 3, 0);
680
1301
  const indexCount = normalized.reduce((count, mesh) => count + mesh.indices.length, 0);
681
1302
  const triangleCount = Math.floor(indexCount / 3);
@@ -727,7 +1348,7 @@ function createWavefrontGpuMeshSource(meshes = []) {
727
1348
  meshUints[meshOffset + 8] = mesh.indices.length / 3;
728
1349
  meshUints[meshOffset + 9] = meshVertexBase;
729
1350
  meshUints[meshOffset + 10] = meshVertexCount;
730
- meshUints[meshOffset + 11] = 0;
1351
+ meshUints[meshOffset + 11] = meshIndex;
731
1352
  const floatOffset = meshOffset;
732
1353
  writeVec4(meshFloats, floatOffset * 4 + 48, mesh.color);
733
1354
  writeVec4(meshFloats, floatOffset * 4 + 64, mesh.emission);
@@ -737,6 +1358,55 @@ function createWavefrontGpuMeshSource(meshes = []) {
737
1358
  mesh.opacity,
738
1359
  mesh.ior
739
1360
  ]);
1361
+ writeVec4(meshFloats, floatOffset * 4 + 96, [
1362
+ mesh.sheenColor[0] ?? 0,
1363
+ mesh.sheenColor[1] ?? 0,
1364
+ mesh.sheenColor[2] ?? 0,
1365
+ mesh.clearcoat
1366
+ ]);
1367
+ writeVec4(meshFloats, floatOffset * 4 + 112, [
1368
+ mesh.clearcoatRoughness,
1369
+ mesh.specular,
1370
+ mesh.transmission,
1371
+ 0
1372
+ ]);
1373
+ writeVec4(meshFloats, floatOffset * 4 + 128, [
1374
+ mesh.specularColor[0] ?? 1,
1375
+ mesh.specularColor[1] ?? 1,
1376
+ mesh.specularColor[2] ?? 1,
1377
+ 1
1378
+ ]);
1379
+ writeVec4(
1380
+ meshFloats,
1381
+ floatOffset * 4 + 144,
1382
+ gpuMaterialSource.baseColorAtlas.resolveRect(mesh.baseColorTexture)
1383
+ );
1384
+ writeVec4(
1385
+ meshFloats,
1386
+ floatOffset * 4 + 160,
1387
+ gpuMaterialSource.metallicRoughnessAtlas.resolveRect(mesh.metallicRoughnessTexture)
1388
+ );
1389
+ writeVec4(
1390
+ meshFloats,
1391
+ floatOffset * 4 + 176,
1392
+ gpuMaterialSource.normalAtlas.resolveRect(mesh.normalTexture)
1393
+ );
1394
+ writeVec4(
1395
+ meshFloats,
1396
+ floatOffset * 4 + 192,
1397
+ gpuMaterialSource.occlusionAtlas.resolveRect(mesh.occlusionTexture)
1398
+ );
1399
+ writeVec4(
1400
+ meshFloats,
1401
+ floatOffset * 4 + 208,
1402
+ gpuMaterialSource.emissiveAtlas.resolveRect(mesh.emissiveTexture)
1403
+ );
1404
+ writeVec4(meshFloats, floatOffset * 4 + 224, [
1405
+ clampUnit(mesh.normalTexture?.scale ?? mesh.normalTexture?.strength ?? 1),
1406
+ clampUnit(mesh.occlusionTexture?.strength ?? 1),
1407
+ clampUnit(mesh.emissiveTexture?.strength ?? 1),
1408
+ 0
1409
+ ]);
740
1410
  vertexCursor += meshVertexCount;
741
1411
  indexCursor += mesh.indices.length;
742
1412
  triangleCursor += mesh.indices.length / 3;
@@ -1039,12 +1709,14 @@ function estimateWavefrontPathTracingMemory(options = {}) {
1039
1709
  options.environmentPortalCapacity,
1040
1710
  0
1041
1711
  );
1712
+ const materialCapacity = readNonNegativeInteger("materialCapacity", options.materialCapacity, 0);
1042
1713
  const queueBytes = tilePixelCapacity * RAY_RECORD_BYTES;
1043
1714
  const hitBytes = tilePixelCapacity * HIT_RECORD_BYTES;
1044
1715
  const accumulationBytes = tilePixelCapacity * ACCUMULATION_RECORD_BYTES;
1045
1716
  const pathVertexBytes = tilePixelCapacity * (maxDepth + 1) * PATH_VERTEX_RECORD_BYTES;
1046
1717
  const sceneObjectBytes = sceneObjectCapacity * SCENE_OBJECT_RECORD_BYTES;
1047
1718
  const triangleBytes = triangleCapacity * TRIANGLE_RECORD_BYTES;
1719
+ const materialTableBytes = materialCapacity * GPU_MATERIAL_RECORD_BYTES;
1048
1720
  const bvhNodeBytes = bvhNodeCapacity * BVH_NODE_RECORD_BYTES;
1049
1721
  const bvhLeafReferenceBytes = bvhLeafSortCapacity * BVH_LEAF_REF_RECORD_BYTES;
1050
1722
  const emissiveTriangleMetadataBytes = emissiveTriangleCapacity * BVH_NODE_RECORD_BYTES;
@@ -1057,6 +1729,7 @@ function estimateWavefrontPathTracingMemory(options = {}) {
1057
1729
  pathVertexBytes,
1058
1730
  sceneObjectBytes,
1059
1731
  triangleBytes,
1732
+ materialTableBytes,
1060
1733
  bvhNodeBytes,
1061
1734
  bvhLeafReferenceBytes,
1062
1735
  emissiveTriangleMetadataBytes,
@@ -1064,7 +1737,7 @@ function estimateWavefrontPathTracingMemory(options = {}) {
1064
1737
  configBytes: CONFIG_BUFFER_BYTES,
1065
1738
  counterBytes: COUNTER_BUFFER_BYTES,
1066
1739
  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
1740
+ totalHotBufferBytes: queueBytes * 2 + hitBytes + accumulationBytes + pathVertexBytes + sceneObjectBytes + triangleBytes + materialTableBytes + bvhNodeBytes + bvhLeafReferenceBytes + emissiveTriangleMetadataBytes + environmentPortalBytes + CONFIG_BUFFER_BYTES + COUNTER_BUFFER_BYTES + INDIRECT_DISPATCH_ARGS_BYTES
1068
1741
  });
1069
1742
  }
1070
1743
  function createWavefrontPathTracingComputeConfig(options = {}) {
@@ -1078,7 +1751,7 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
1078
1751
  const samplesPerPixel = clamp(
1079
1752
  readPositiveInteger("samplesPerPixel", options.samplesPerPixel, DEFAULT_SAMPLES_PER_PIXEL),
1080
1753
  1,
1081
- 64
1754
+ MAX_SAMPLES_PER_PIXEL
1082
1755
  );
1083
1756
  const maxFramePassesPerSubmission = clamp(
1084
1757
  readPositiveInteger(
@@ -1096,7 +1769,8 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
1096
1769
  );
1097
1770
  const meshes = normalizeMeshes(options);
1098
1771
  const meshSourceShape = estimateMeshSourceShape(meshes);
1099
- const gpuMeshSource = meshes.length > 0 ? createWavefrontGpuMeshSource(meshes) : createWavefrontGpuMeshSource([]);
1772
+ const gpuMaterialSource = meshes.length > 0 ? createWavefrontGpuMaterialSource(meshes) : createWavefrontGpuMaterialSource([]);
1773
+ const gpuMeshSource = meshes.length > 0 ? createWavefrontGpuMeshSource(meshes, gpuMaterialSource) : createWavefrontGpuMeshSource([]);
1100
1774
  const meshAcceleration = accelerationBuildMode === "cpu-debug" ? createWavefrontMeshAcceleration(meshes) : Object.freeze({ nodes: Object.freeze([]), triangles: Object.freeze([]) });
1101
1775
  const emissiveTriangleIndices = createWavefrontEmissiveTriangleIndexSource(
1102
1776
  meshes,
@@ -1168,6 +1842,7 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
1168
1842
  accelerationBuildMode,
1169
1843
  gpuAccelerationBuildRequired: accelerationBuildMode === "gpu" && triangleCount > 0,
1170
1844
  gpuMeshSource,
1845
+ gpuMaterialSource,
1171
1846
  meshAcceleration,
1172
1847
  emissiveTriangleIndices,
1173
1848
  emissiveTriangleCount: emissiveTriangleIndices.count,
@@ -1198,6 +1873,7 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
1198
1873
  maxDepth,
1199
1874
  sceneObjectCapacity,
1200
1875
  triangleCapacity,
1876
+ materialCapacity: gpuMaterialSource.count,
1201
1877
  bvhNodeCapacity,
1202
1878
  bvhLeafSortCapacity,
1203
1879
  emissiveTriangleCapacity: emissiveTriangleIndices.capacity,
@@ -1274,6 +1950,24 @@ function packWavefrontSceneObjects(sceneObjects, capacity = sceneObjects.length)
1274
1950
  object.opacity,
1275
1951
  object.ior
1276
1952
  ]);
1953
+ writeVec4(floatView, byteOffset + 96, [
1954
+ object.sheenColor[0] ?? 0,
1955
+ object.sheenColor[1] ?? 0,
1956
+ object.sheenColor[2] ?? 0,
1957
+ object.clearcoat
1958
+ ]);
1959
+ writeVec4(floatView, byteOffset + 112, [
1960
+ object.clearcoatRoughness,
1961
+ object.specular,
1962
+ object.transmission,
1963
+ 0
1964
+ ]);
1965
+ writeVec4(floatView, byteOffset + 128, [
1966
+ object.specularColor[0] ?? 1,
1967
+ object.specularColor[1] ?? 1,
1968
+ object.specularColor[2] ?? 1,
1969
+ 1
1970
+ ]);
1277
1971
  });
1278
1972
  return Object.freeze({
1279
1973
  buffer: bytes,
@@ -1298,7 +1992,7 @@ function packWavefrontTriangles(triangles, capacity = triangles.length) {
1298
1992
  uintView[u32 + 3] = triangle.flags;
1299
1993
  uintView[u32 + 4] = triangle.materialRefId;
1300
1994
  uintView[u32 + 5] = triangle.mediumRefId;
1301
- uintView[u32 + 6] = 0;
1995
+ uintView[u32 + 6] = triangle.materialSlot ?? 0;
1302
1996
  uintView[u32 + 7] = 0;
1303
1997
  writeVec4(floatView, byteOffset + 32, [...triangle.v0, 0]);
1304
1998
  writeVec4(floatView, byteOffset + 48, [...triangle.v1, 0]);
@@ -1311,6 +2005,15 @@ function packWavefrontTriangles(triangles, capacity = triangles.length) {
1311
2005
  writeVec4(floatView, byteOffset + 160, triangle.color);
1312
2006
  writeVec4(floatView, byteOffset + 176, triangle.emission);
1313
2007
  writeVec4(floatView, byteOffset + 192, triangle.material);
2008
+ writeVec4(floatView, byteOffset + 208, triangle.materialResponse);
2009
+ writeVec4(floatView, byteOffset + 224, triangle.materialExtension ?? [0.08, 1, 0, 0]);
2010
+ writeVec4(floatView, byteOffset + 240, triangle.specularColor ?? [1, 1, 1, 1]);
2011
+ writeVec4(floatView, byteOffset + 256, triangle.baseColorAtlas ?? [0, 0, 1, 1]);
2012
+ writeVec4(floatView, byteOffset + 272, triangle.metallicRoughnessAtlas ?? [0, 0, 1, 1]);
2013
+ writeVec4(floatView, byteOffset + 288, triangle.normalAtlas ?? [0, 0, 1, 1]);
2014
+ writeVec4(floatView, byteOffset + 304, triangle.occlusionAtlas ?? [0, 0, 1, 1]);
2015
+ writeVec4(floatView, byteOffset + 320, triangle.emissiveAtlas ?? [0, 0, 1, 1]);
2016
+ writeVec4(floatView, byteOffset + 336, triangle.textureSettings ?? [1, 1, 1, 0]);
1314
2017
  });
1315
2018
  return Object.freeze({
1316
2019
  buffer: bytes,
@@ -1404,6 +2107,12 @@ function createConfigPayload(config, tile, frameIndex, buildRange = {}) {
1404
2107
  0,
1405
2108
  0
1406
2109
  ]);
2110
+ writeVec4(floatView, 304, [
2111
+ config.environmentMap.width ?? 1,
2112
+ config.environmentMap.height ?? 1,
2113
+ config.environmentMap.mipLevelCount ?? 1,
2114
+ config.environmentMap.hasImportanceData ? 1 : 0
2115
+ ]);
1407
2116
  return bytes;
1408
2117
  }
1409
2118
  function createTiles(width, height, tileSize) {
@@ -1577,7 +2286,8 @@ function intersectWavefrontReferenceTriangle(ray, triangle, options = {}) {
1577
2286
  position: Object.freeze(position),
1578
2287
  color: triangle.color,
1579
2288
  emission: triangle.emission,
1580
- material: triangle.material
2289
+ material: triangle.material,
2290
+ materialResponse: triangle.materialResponse
1581
2291
  });
1582
2292
  }
1583
2293
  function createWavefrontReferenceEnvironmentHit(config, ray) {
@@ -1603,7 +2313,8 @@ function createWavefrontReferenceEnvironmentHit(config, ray) {
1603
2313
  position: Object.freeze(add(ray.origin, scale(ray.direction, 1e3))),
1604
2314
  color: Object.freeze([0, 0, 0, 0]),
1605
2315
  emission: radiance,
1606
- material: Object.freeze([1, 0, 1, 1])
2316
+ material: Object.freeze([1, 0, 1, 1]),
2317
+ materialResponse: Object.freeze([0, 0, 0, 0])
1607
2318
  });
1608
2319
  }
1609
2320
  function traceWavefrontReferenceTriangles(config, ray, triangles, options = {}) {
@@ -1682,40 +2393,24 @@ function environmentMapIntegerScale(data) {
1682
2393
  }
1683
2394
  return 1;
1684
2395
  }
1685
- function readEnvironmentMapComponent(data, index, fallback, integerScale = 1) {
1686
- if (!data || index >= data.length) {
1687
- return fallback;
2396
+ function environmentMapHasSamplingData(environmentMap) {
2397
+ if (!environmentMap || !environmentMap.data) {
2398
+ return false;
1688
2399
  }
1689
- const value = Number(data[index]);
1690
- return Number.isFinite(value) ? Math.max(0, value) * integerScale : fallback;
2400
+ const width = Math.max(1, environmentMap.width ?? 1);
2401
+ const height = Math.max(1, environmentMap.height ?? 1);
2402
+ return environmentMap.data.length >= width * height * 4;
1691
2403
  }
1692
- function createEnvironmentMapUploadBytes(environmentMap, fallbackColor) {
1693
- const width = Math.max(1, environmentMap.width);
1694
- const height = Math.max(1, environmentMap.height);
1695
- const rowBytes = width * 8;
1696
- const bytesPerRow = alignTo(rowBytes, 256);
2404
+ function createRgba8TextureUpload(source) {
2405
+ const width = Math.max(1, Math.trunc(source.width));
2406
+ const height = Math.max(1, Math.trunc(source.height));
2407
+ const bytesPerRow = alignTo(width * 4, 256);
1697
2408
  const bytes = new Uint8Array(bytesPerRow * height);
1698
- const data = environmentMap.data;
1699
- 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
- );
1709
- };
2409
+ const data = source.data instanceof Uint8Array ? source.data : new Uint8Array(source.data);
1710
2410
  for (let y = 0; y < height; y += 1) {
1711
- 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);
1718
- }
2411
+ const sourceOffset = y * width * 4;
2412
+ const targetOffset = y * bytesPerRow;
2413
+ bytes.set(data.subarray(sourceOffset, sourceOffset + width * 4), targetOffset);
1719
2414
  }
1720
2415
  return Object.freeze({
1721
2416
  bytes,
@@ -1724,6 +2419,320 @@ function createEnvironmentMapUploadBytes(environmentMap, fallbackColor) {
1724
2419
  height
1725
2420
  });
1726
2421
  }
2422
+ function readEnvironmentMapComponent(data, index, fallback, integerScale = 1) {
2423
+ if (!data || index >= data.length) {
2424
+ return fallback;
2425
+ }
2426
+ const value = Number(data[index]);
2427
+ return Number.isFinite(value) ? Math.max(0, value) * integerScale : fallback;
2428
+ }
2429
+ function buildOrthonormalBasis(normal) {
2430
+ const tangentFallback = Math.abs(normal[1]) < 0.999 ? [0, 1, 0] : [1, 0, 0];
2431
+ const tangent = normalize(cross(tangentFallback, normal), [1, 0, 0]);
2432
+ const bitangent = normalize(cross(normal, tangent), [0, 0, 1]);
2433
+ return { tangent, bitangent };
2434
+ }
2435
+ function localToWorld(local, normal) {
2436
+ const basis = buildOrthonormalBasis(normal);
2437
+ return normalize(
2438
+ add(
2439
+ add(scale(basis.tangent, local[0]), scale(basis.bitangent, local[1])),
2440
+ scale(normal, local[2])
2441
+ ),
2442
+ normal
2443
+ );
2444
+ }
2445
+ function radicalInverseVdc(bits) {
2446
+ let value = bits >>> 0;
2447
+ value = (value << 16 | value >>> 16) >>> 0;
2448
+ value = ((value & 1431655765) << 1 | (value & 2863311530) >>> 1) >>> 0;
2449
+ value = ((value & 858993459) << 2 | (value & 3435973836) >>> 2) >>> 0;
2450
+ value = ((value & 252645135) << 4 | (value & 4042322160) >>> 4) >>> 0;
2451
+ value = ((value & 16711935) << 8 | (value & 4278255360) >>> 8) >>> 0;
2452
+ return value * 23283064365386963e-26;
2453
+ }
2454
+ function hammersley(index, count) {
2455
+ return [index / Math.max(count, 1), radicalInverseVdc(index)];
2456
+ }
2457
+ function importanceSampleGgx(sample, roughness, normal) {
2458
+ const alpha = Math.max(roughness * roughness, 1e-4);
2459
+ const phi = 2 * Math.PI * sample[0];
2460
+ const cosTheta = Math.sqrt((1 - sample[1]) / (1 + (alpha * alpha - 1) * sample[1]));
2461
+ const sinTheta = Math.sqrt(Math.max(0, 1 - cosTheta * cosTheta));
2462
+ const halfVector = localToWorld(
2463
+ [Math.cos(phi) * sinTheta, Math.sin(phi) * sinTheta, cosTheta],
2464
+ normal
2465
+ );
2466
+ return normalize(halfVector, normal);
2467
+ }
2468
+ function geometrySchlickGgx(nDotV, roughness) {
2469
+ const k = (roughness + 1) * (roughness + 1) / 8;
2470
+ return nDotV / Math.max(nDotV * (1 - k) + k, 1e-6);
2471
+ }
2472
+ function geometrySmith(nDotV, nDotL, roughness) {
2473
+ return geometrySchlickGgx(nDotV, roughness) * geometrySchlickGgx(nDotL, roughness);
2474
+ }
2475
+ function integrateBrdfSample(nDotV, roughness, sampleCount) {
2476
+ const viewDirection = [Math.sqrt(Math.max(0, 1 - nDotV * nDotV)), 0, nDotV];
2477
+ const normal = [0, 0, 1];
2478
+ let scaleTerm = 0;
2479
+ let biasTerm = 0;
2480
+ for (let index = 0; index < sampleCount; index += 1) {
2481
+ const xi = hammersley(index, sampleCount);
2482
+ const halfVector = importanceSampleGgx(xi, roughness, normal);
2483
+ const vDotH = Math.max(dot(viewDirection, halfVector), 0);
2484
+ const lightDirection = normalize(
2485
+ subtract(scale(halfVector, 2 * vDotH), viewDirection),
2486
+ normal
2487
+ );
2488
+ const nDotL = Math.max(lightDirection[2], 0);
2489
+ const nDotH = Math.max(halfVector[2], 0);
2490
+ if (nDotL <= 0 || nDotH <= 0 || vDotH <= 0) {
2491
+ continue;
2492
+ }
2493
+ const geometry = geometrySmith(nDotV, nDotL, roughness);
2494
+ const visibility = geometry * vDotH / Math.max(nDotH * nDotV, 1e-6);
2495
+ const fresnel = (1 - vDotH) ** 5;
2496
+ scaleTerm += (1 - fresnel) * visibility;
2497
+ biasTerm += fresnel * visibility;
2498
+ }
2499
+ return [scaleTerm / sampleCount, biasTerm / sampleCount];
2500
+ }
2501
+ function createBrdfLutUploadBytes(size = DEFAULT_BRDF_LUT_SIZE, sampleCount = 1024) {
2502
+ const cacheKey = `${Math.max(1, Math.trunc(size))}:${Math.max(1, Math.trunc(sampleCount))}`;
2503
+ const cached = BRDF_LUT_UPLOAD_CACHE.get(cacheKey);
2504
+ if (cached) {
2505
+ return cached;
2506
+ }
2507
+ const width = Math.max(1, Math.trunc(size));
2508
+ const height = Math.max(1, Math.trunc(size));
2509
+ const rowBytes = width * 8;
2510
+ const bytesPerRow = alignTo(rowBytes, 256);
2511
+ const bytes = new Uint8Array(bytesPerRow * height);
2512
+ const view = new DataView(bytes.buffer);
2513
+ for (let y = 0; y < height; y += 1) {
2514
+ const roughness = (y + 0.5) / height;
2515
+ for (let x = 0; x < width; x += 1) {
2516
+ const nDotV = Math.max((x + 0.5) / width, 1e-4);
2517
+ const [scaleTerm, biasTerm] = integrateBrdfSample(nDotV, roughness, sampleCount);
2518
+ const offset = y * bytesPerRow + x * 8;
2519
+ view.setUint16(offset, float32ToFloat16Bits(scaleTerm), true);
2520
+ view.setUint16(offset + 2, float32ToFloat16Bits(biasTerm), true);
2521
+ view.setUint16(offset + 4, float32ToFloat16Bits(0), true);
2522
+ view.setUint16(offset + 6, float32ToFloat16Bits(1), true);
2523
+ }
2524
+ }
2525
+ const upload = Object.freeze({ bytes, bytesPerRow, width, height });
2526
+ BRDF_LUT_UPLOAD_CACHE.set(cacheKey, upload);
2527
+ return upload;
2528
+ }
2529
+ function createLinearEnvironmentPixels(environmentMap, fallbackColor) {
2530
+ const width = Math.max(1, environmentMap.width);
2531
+ const height = Math.max(1, environmentMap.height);
2532
+ const pixels = new Float32Array(width * height * 4);
2533
+ const data = environmentMap.data;
2534
+ const integerScale = environmentMapIntegerScale(data);
2535
+ for (let index = 0; index < width * height; index += 1) {
2536
+ const sourceOffset = index * 4;
2537
+ const targetOffset = index * 4;
2538
+ pixels[targetOffset] = readEnvironmentMapComponent(data, sourceOffset, fallbackColor[0], integerScale);
2539
+ pixels[targetOffset + 1] = readEnvironmentMapComponent(data, sourceOffset + 1, fallbackColor[1], integerScale);
2540
+ pixels[targetOffset + 2] = readEnvironmentMapComponent(data, sourceOffset + 2, fallbackColor[2], integerScale);
2541
+ pixels[targetOffset + 3] = readEnvironmentMapComponent(data, sourceOffset + 3, fallbackColor[3] ?? 1, integerScale);
2542
+ }
2543
+ return pixels;
2544
+ }
2545
+ function environmentUvToDirection(u, v, rotationRadians = 0) {
2546
+ const angle = (u - rotationRadians / (2 * Math.PI) - 0.5) * 2 * Math.PI;
2547
+ const theta = v * Math.PI;
2548
+ const sinTheta = Math.sin(theta);
2549
+ return [
2550
+ Math.cos(angle) * sinTheta,
2551
+ Math.cos(theta),
2552
+ Math.sin(angle) * sinTheta
2553
+ ];
2554
+ }
2555
+ function sampleEnvironmentPixelsBilinear(pixels, width, height, u, v) {
2556
+ const wrappedU = (u % 1 + 1) % 1;
2557
+ const clampedV = clamp(v, 0, 1);
2558
+ const x = wrappedU * width - 0.5;
2559
+ const y = clampedV * height - 0.5;
2560
+ const x0 = (Math.floor(x) % width + width) % width;
2561
+ const y0 = clamp(Math.floor(y), 0, height - 1);
2562
+ const x1 = (x0 + 1) % width;
2563
+ const y1 = clamp(y0 + 1, 0, height - 1);
2564
+ const tx = x - Math.floor(x);
2565
+ const ty = y - Math.floor(y);
2566
+ const read = (px, py) => {
2567
+ const offset = (py * width + px) * 4;
2568
+ return [pixels[offset], pixels[offset + 1], pixels[offset + 2], pixels[offset + 3]];
2569
+ };
2570
+ const a = read(x0, y0);
2571
+ const b = read(x1, y0);
2572
+ const c = read(x0, y1);
2573
+ const d = read(x1, y1);
2574
+ const mixPair = (first, second, factor) => first * (1 - factor) + second * factor;
2575
+ return [
2576
+ mixPair(mixPair(a[0], b[0], tx), mixPair(c[0], d[0], tx), ty),
2577
+ mixPair(mixPair(a[1], b[1], tx), mixPair(c[1], d[1], tx), ty),
2578
+ mixPair(mixPair(a[2], b[2], tx), mixPair(c[2], d[2], tx), ty),
2579
+ mixPair(mixPair(a[3], b[3], tx), mixPair(c[3], d[3], tx), ty)
2580
+ ];
2581
+ }
2582
+ function directionToEnvironmentUv(direction, rotationRadians = 0) {
2583
+ const unitDirection = normalize(direction, [0, 1, 0]);
2584
+ const rotationTurns = rotationRadians / (2 * Math.PI);
2585
+ const u = ((Math.atan2(unitDirection[2], unitDirection[0]) / (2 * Math.PI) + 0.5 + rotationTurns) % 1 + 1) % 1;
2586
+ const v = Math.acos(clamp(unitDirection[1], -1, 1)) / Math.PI;
2587
+ return [u, clamp(v, 0, 1)];
2588
+ }
2589
+ function sampleEnvironmentRadiance(pixels, width, height, direction, rotationRadians = 0) {
2590
+ const [u, v] = directionToEnvironmentUv(direction, rotationRadians);
2591
+ return sampleEnvironmentPixelsBilinear(pixels, width, height, u, v);
2592
+ }
2593
+ function createFloat16RgbaUploadFromLevels(levels) {
2594
+ return levels.map((level) => {
2595
+ const rowBytes = level.width * 8;
2596
+ const bytesPerRow = alignTo(rowBytes, 256);
2597
+ const bytes = new Uint8Array(bytesPerRow * level.height);
2598
+ const view = new DataView(bytes.buffer);
2599
+ for (let y = 0; y < level.height; y += 1) {
2600
+ for (let x = 0; x < level.width; x += 1) {
2601
+ const sourceOffset = (y * level.width + x) * 4;
2602
+ const targetOffset = y * bytesPerRow + x * 8;
2603
+ view.setUint16(targetOffset, float32ToFloat16Bits(level.data[sourceOffset]), true);
2604
+ view.setUint16(targetOffset + 2, float32ToFloat16Bits(level.data[sourceOffset + 1]), true);
2605
+ view.setUint16(targetOffset + 4, float32ToFloat16Bits(level.data[sourceOffset + 2]), true);
2606
+ view.setUint16(targetOffset + 6, float32ToFloat16Bits(level.data[sourceOffset + 3]), true);
2607
+ }
2608
+ }
2609
+ return Object.freeze({ bytes, bytesPerRow, width: level.width, height: level.height });
2610
+ });
2611
+ }
2612
+ function createPrefilteredEnvironmentLevels(environmentMap, fallbackColor) {
2613
+ const sourcePixels = createLinearEnvironmentPixels(environmentMap, fallbackColor);
2614
+ const sourceWidth = Math.max(1, environmentMap.width);
2615
+ const sourceHeight = Math.max(1, environmentMap.height);
2616
+ const mipLevelCount = Math.max(1, Math.floor(Math.log2(Math.max(sourceWidth, sourceHeight))) + 1);
2617
+ const levels = [
2618
+ Object.freeze({
2619
+ width: sourceWidth,
2620
+ height: sourceHeight,
2621
+ data: sourcePixels
2622
+ })
2623
+ ];
2624
+ for (let mipLevel = 1; mipLevel < mipLevelCount; mipLevel += 1) {
2625
+ const width = Math.max(1, sourceWidth >> mipLevel);
2626
+ const height = Math.max(1, sourceHeight >> mipLevel);
2627
+ const roughness = mipLevelCount <= 1 ? 0 : mipLevel / (mipLevelCount - 1);
2628
+ const data = new Float32Array(width * height * 4);
2629
+ const sampleCount = roughness < 0.25 ? 64 : roughness < 0.6 ? 96 : 128;
2630
+ for (let y = 0; y < height; y += 1) {
2631
+ for (let x = 0; x < width; x += 1) {
2632
+ const direction = environmentUvToDirection((x + 0.5) / width, (y + 0.5) / height, environmentMap.rotationRadians);
2633
+ const normal = normalize(direction, [0, 1, 0]);
2634
+ const viewDirection = normal;
2635
+ let totalWeight = 0;
2636
+ const accum = [0, 0, 0];
2637
+ for (let sampleIndex = 0; sampleIndex < sampleCount; sampleIndex += 1) {
2638
+ const xi = hammersley(sampleIndex, sampleCount);
2639
+ const halfVector = importanceSampleGgx(xi, roughness, normal);
2640
+ const viewDotHalf = Math.max(dot(viewDirection, halfVector), 0);
2641
+ const lightDirection = normalize(
2642
+ subtract(scale(halfVector, 2 * viewDotHalf), viewDirection),
2643
+ normal
2644
+ );
2645
+ const nDotL = Math.max(dot(normal, lightDirection), 0);
2646
+ if (nDotL <= 1e-6) {
2647
+ continue;
2648
+ }
2649
+ const radiance = sampleEnvironmentRadiance(
2650
+ sourcePixels,
2651
+ sourceWidth,
2652
+ sourceHeight,
2653
+ lightDirection,
2654
+ environmentMap.rotationRadians
2655
+ );
2656
+ accum[0] += radiance[0] * nDotL;
2657
+ accum[1] += radiance[1] * nDotL;
2658
+ accum[2] += radiance[2] * nDotL;
2659
+ totalWeight += nDotL;
2660
+ }
2661
+ const offset = (y * width + x) * 4;
2662
+ data[offset] = accum[0] / Math.max(totalWeight, 1e-6);
2663
+ data[offset + 1] = accum[1] / Math.max(totalWeight, 1e-6);
2664
+ data[offset + 2] = accum[2] / Math.max(totalWeight, 1e-6);
2665
+ data[offset + 3] = 1;
2666
+ }
2667
+ }
2668
+ levels.push(Object.freeze({ width, height, data }));
2669
+ }
2670
+ return Object.freeze({
2671
+ levels,
2672
+ mipLevelCount,
2673
+ width: sourceWidth,
2674
+ height: sourceHeight
2675
+ });
2676
+ }
2677
+ function createEnvironmentSamplingTables(environmentMap, fallbackColor) {
2678
+ if (!environmentMapHasSamplingData(environmentMap)) {
2679
+ return Object.freeze({
2680
+ width: 1,
2681
+ height: 1,
2682
+ pdf: new Float32Array([1]),
2683
+ marginalCdf: new Float32Array([1]),
2684
+ conditionalCdf: new Float32Array([1]),
2685
+ hasImportanceData: false
2686
+ });
2687
+ }
2688
+ const pixels = createLinearEnvironmentPixels(environmentMap, fallbackColor);
2689
+ const width = Math.max(1, environmentMap.width);
2690
+ const height = Math.max(1, environmentMap.height);
2691
+ const pdf = new Float32Array(width * height);
2692
+ const marginalCdf = new Float32Array(height);
2693
+ const conditionalCdf = new Float32Array(width * height);
2694
+ const rowSums = new Float32Array(height);
2695
+ let totalWeight = 0;
2696
+ for (let y = 0; y < height; y += 1) {
2697
+ const theta = (y + 0.5) / height * Math.PI;
2698
+ const sinTheta = Math.max(Math.sin(theta), 1e-4);
2699
+ let rowWeight = 0;
2700
+ for (let x = 0; x < width; x += 1) {
2701
+ const offset = (y * width + x) * 4;
2702
+ const luminance = pixels[offset] * 0.2126 + pixels[offset + 1] * 0.7152 + pixels[offset + 2] * 0.0722;
2703
+ const weight = Math.max(luminance * sinTheta, 1e-6);
2704
+ pdf[y * width + x] = weight;
2705
+ rowWeight += weight;
2706
+ conditionalCdf[y * width + x] = rowWeight;
2707
+ }
2708
+ rowSums[y] = rowWeight;
2709
+ totalWeight += rowWeight;
2710
+ if (rowWeight > 0) {
2711
+ for (let x = 0; x < width; x += 1) {
2712
+ conditionalCdf[y * width + x] /= rowWeight;
2713
+ }
2714
+ } else {
2715
+ for (let x = 0; x < width; x += 1) {
2716
+ conditionalCdf[y * width + x] = (x + 1) / width;
2717
+ }
2718
+ }
2719
+ marginalCdf[y] = totalWeight;
2720
+ }
2721
+ for (let y = 0; y < height; y += 1) {
2722
+ marginalCdf[y] /= Math.max(totalWeight, 1e-6);
2723
+ }
2724
+ for (let index = 0; index < pdf.length; index += 1) {
2725
+ pdf[index] /= Math.max(totalWeight, 1e-6);
2726
+ }
2727
+ return Object.freeze({
2728
+ width,
2729
+ height,
2730
+ pdf,
2731
+ marginalCdf,
2732
+ conditionalCdf,
2733
+ hasImportanceData: true
2734
+ });
2735
+ }
1727
2736
  function createEnvironmentMapResource(device, constants, environmentMap, fallbackColor) {
1728
2737
  if (environmentMap.view) {
1729
2738
  return Object.freeze({
@@ -1733,10 +2742,14 @@ function createEnvironmentMapResource(device, constants, environmentMap, fallbac
1733
2742
  addressModeU: "repeat",
1734
2743
  addressModeV: "clamp-to-edge",
1735
2744
  magFilter: "linear",
1736
- minFilter: "linear"
2745
+ minFilter: "linear",
2746
+ mipmapFilter: "linear"
1737
2747
  }),
1738
2748
  texture: null,
1739
- ownsTexture: false
2749
+ ownsTexture: false,
2750
+ width: Math.max(1, environmentMap.width),
2751
+ height: Math.max(1, environmentMap.height),
2752
+ mipLevelCount: Math.max(1, environmentMap.mipLevelCount ?? 1)
1740
2753
  });
1741
2754
  }
1742
2755
  if (environmentMap.texture && typeof environmentMap.texture.createView === "function") {
@@ -1747,15 +2760,91 @@ function createEnvironmentMapResource(device, constants, environmentMap, fallbac
1747
2760
  addressModeU: "repeat",
1748
2761
  addressModeV: "clamp-to-edge",
1749
2762
  magFilter: "linear",
1750
- minFilter: "linear"
2763
+ minFilter: "linear",
2764
+ mipmapFilter: "linear"
1751
2765
  }),
1752
2766
  texture: environmentMap.texture,
1753
- ownsTexture: false
2767
+ ownsTexture: false,
2768
+ width: Math.max(1, environmentMap.width),
2769
+ height: Math.max(1, environmentMap.height),
2770
+ mipLevelCount: Math.max(1, environmentMap.mipLevelCount ?? 1)
1754
2771
  });
1755
2772
  }
1756
- const upload = createEnvironmentMapUploadBytes(environmentMap, fallbackColor);
2773
+ const prefiltered = createPrefilteredEnvironmentLevels(environmentMap, fallbackColor);
2774
+ const uploads = createFloat16RgbaUploadFromLevels(prefiltered.levels);
1757
2775
  const texture = device.createTexture({
1758
2776
  label: environmentMap.enabled ? "plasius.wavefront.environmentMap" : "plasius.wavefront.environmentMapFallback",
2777
+ size: { width: prefiltered.width, height: prefiltered.height },
2778
+ format: "rgba16float",
2779
+ mipLevelCount: prefiltered.mipLevelCount,
2780
+ usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
2781
+ });
2782
+ uploads.forEach((upload, mipLevel) => {
2783
+ device.queue.writeTexture(
2784
+ { texture, mipLevel },
2785
+ upload.bytes,
2786
+ { bytesPerRow: upload.bytesPerRow, rowsPerImage: upload.height },
2787
+ { width: upload.width, height: upload.height, depthOrArrayLayers: 1 }
2788
+ );
2789
+ });
2790
+ return Object.freeze({
2791
+ view: texture.createView(),
2792
+ sampler: environmentMap.sampler ?? device.createSampler({
2793
+ label: "plasius.wavefront.environmentMapSampler",
2794
+ addressModeU: "repeat",
2795
+ addressModeV: "clamp-to-edge",
2796
+ magFilter: "linear",
2797
+ minFilter: "linear",
2798
+ mipmapFilter: "linear"
2799
+ }),
2800
+ texture,
2801
+ ownsTexture: true,
2802
+ width: prefiltered.width,
2803
+ height: prefiltered.height,
2804
+ mipLevelCount: prefiltered.mipLevelCount
2805
+ });
2806
+ }
2807
+ function createEnvironmentSamplingTextureResource(device, constants, environmentMap, fallbackColor) {
2808
+ const tables = createEnvironmentSamplingTables(environmentMap, fallbackColor);
2809
+ const rowBytes = tables.width * 8;
2810
+ const bytesPerRow = alignTo(rowBytes, 256);
2811
+ const bytes = new Uint8Array(bytesPerRow * tables.height);
2812
+ const view = new DataView(bytes.buffer);
2813
+ for (let y = 0; y < tables.height; y += 1) {
2814
+ for (let x = 0; x < tables.width; x += 1) {
2815
+ const probability = tables.pdf[y * tables.width + x];
2816
+ const conditional = tables.conditionalCdf[y * tables.width + x];
2817
+ const marginal = tables.marginalCdf[y];
2818
+ const offset = y * bytesPerRow + x * 8;
2819
+ view.setUint16(offset, float32ToFloat16Bits(probability), true);
2820
+ view.setUint16(offset + 2, float32ToFloat16Bits(conditional), true);
2821
+ view.setUint16(offset + 4, float32ToFloat16Bits(marginal), true);
2822
+ view.setUint16(offset + 6, float32ToFloat16Bits(1), true);
2823
+ }
2824
+ }
2825
+ const texture = device.createTexture({
2826
+ label: "plasius.wavefront.environmentSampling",
2827
+ size: { width: tables.width, height: tables.height },
2828
+ format: "rgba16float",
2829
+ usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
2830
+ });
2831
+ device.queue.writeTexture(
2832
+ { texture },
2833
+ bytes,
2834
+ { bytesPerRow, rowsPerImage: tables.height },
2835
+ { width: tables.width, height: tables.height, depthOrArrayLayers: 1 }
2836
+ );
2837
+ return Object.freeze({
2838
+ view: texture.createView(),
2839
+ texture,
2840
+ ownsTexture: true,
2841
+ hasImportanceData: tables.hasImportanceData
2842
+ });
2843
+ }
2844
+ function createBrdfLutResource(device, constants, size = DEFAULT_BRDF_LUT_SIZE) {
2845
+ const upload = createBrdfLutUploadBytes(size);
2846
+ const texture = device.createTexture({
2847
+ label: "plasius.wavefront.brdfLut",
1759
2848
  size: { width: upload.width, height: upload.height },
1760
2849
  format: "rgba16float",
1761
2850
  usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
@@ -1768,14 +2857,36 @@ function createEnvironmentMapResource(device, constants, environmentMap, fallbac
1768
2857
  );
1769
2858
  return Object.freeze({
1770
2859
  view: texture.createView(),
1771
- sampler: environmentMap.sampler ?? device.createSampler({
1772
- label: "plasius.wavefront.environmentMapSampler",
1773
- addressModeU: "repeat",
2860
+ sampler: device.createSampler({
2861
+ label: "plasius.wavefront.brdfLutSampler",
2862
+ addressModeU: "clamp-to-edge",
1774
2863
  addressModeV: "clamp-to-edge",
1775
2864
  magFilter: "linear",
1776
2865
  minFilter: "linear"
1777
2866
  }),
1778
2867
  texture,
2868
+ ownsTexture: true,
2869
+ width: upload.width,
2870
+ height: upload.height
2871
+ });
2872
+ }
2873
+ function createAtlasTextureResource(device, constants, atlas, label) {
2874
+ const upload = createRgba8TextureUpload(atlas);
2875
+ const texture = device.createTexture({
2876
+ label,
2877
+ size: { width: upload.width, height: upload.height },
2878
+ format: "rgba8unorm",
2879
+ usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
2880
+ });
2881
+ device.queue.writeTexture(
2882
+ { texture },
2883
+ upload.bytes,
2884
+ { bytesPerRow: upload.bytesPerRow, rowsPerImage: upload.height },
2885
+ { width: upload.width, height: upload.height, depthOrArrayLayers: 1 }
2886
+ );
2887
+ return Object.freeze({
2888
+ texture,
2889
+ view: texture.createView(),
1779
2890
  ownsTexture: true
1780
2891
  });
1781
2892
  }
@@ -1821,6 +2932,24 @@ ${diagnostics}` : "";
1821
2932
  });
1822
2933
  }
1823
2934
  }
2935
+ async function assertShaderModuleCompiles(shaderModule, label) {
2936
+ if (typeof shaderModule?.compilationInfo !== "function") {
2937
+ return;
2938
+ }
2939
+ const info = await shaderModule.compilationInfo();
2940
+ const messages = Array.isArray(info?.messages) ? info.messages : [];
2941
+ const errors = messages.filter((message) => message?.type === "error");
2942
+ if (errors.length <= 0) {
2943
+ return;
2944
+ }
2945
+ const diagnostics = errors.map((message) => {
2946
+ const line = Number.isFinite(message.lineNum) ? message.lineNum : "?";
2947
+ const column = Number.isFinite(message.linePos) ? message.linePos : "?";
2948
+ return `line ${line}:${column} ${message.message}`;
2949
+ }).join("\n");
2950
+ throw new Error(`WGSL compilation preflight failed for ${label}:
2951
+ ${diagnostics}`);
2952
+ }
1824
2953
  async function createRenderPipeline(device, descriptor) {
1825
2954
  if (typeof device.createRenderPipelineAsync === "function") {
1826
2955
  return device.createRenderPipelineAsync(descriptor);
@@ -1829,6 +2958,7 @@ async function createRenderPipeline(device, descriptor) {
1829
2958
  }
1830
2959
  var WAVEFRONT_COMPUTE_WGSL = `
1831
2960
  const RAY_FLAG_GUIDED_EMISSIVE: u32 = 1u;
2961
+ const RAY_FLAG_DELTA_SAMPLE: u32 = 2u;
1832
2962
 
1833
2963
  struct RayRecord {
1834
2964
  rayId: u32,
@@ -1854,11 +2984,12 @@ struct HitRecord {
1854
2984
  primitiveId: u32,
1855
2985
  materialRefId: u32,
1856
2986
  mediumRefId: u32,
2987
+ materialSlot: u32,
1857
2988
  pad0: u32,
1858
2989
  pad1: u32,
1859
- pad2: u32,
1860
2990
  distance: f32,
1861
- pad3: vec3<f32>,
2991
+ occlusion: f32,
2992
+ pad2: vec2<f32>,
1862
2993
  position: vec4<f32>,
1863
2994
  geometricNormal: vec4<f32>,
1864
2995
  shadingNormal: vec4<f32>,
@@ -1867,6 +2998,9 @@ struct HitRecord {
1867
2998
  color: vec4<f32>,
1868
2999
  emission: vec4<f32>,
1869
3000
  material: vec4<f32>,
3001
+ materialResponse: vec4<f32>,
3002
+ materialExtension: vec4<f32>,
3003
+ specularColor: vec4<f32>,
1870
3004
  };
1871
3005
 
1872
3006
  struct SceneObject {
@@ -1879,6 +3013,9 @@ struct SceneObject {
1879
3013
  color: vec4<f32>,
1880
3014
  emission: vec4<f32>,
1881
3015
  material: vec4<f32>,
3016
+ materialResponse: vec4<f32>,
3017
+ materialExtension: vec4<f32>,
3018
+ specularColor: vec4<f32>,
1882
3019
  };
1883
3020
 
1884
3021
  struct TriangleRecord {
@@ -1888,7 +3025,7 @@ struct TriangleRecord {
1888
3025
  flags: u32,
1889
3026
  materialRefId: u32,
1890
3027
  mediumRefId: u32,
1891
- pad0: u32,
3028
+ materialSlot: u32,
1892
3029
  pad1: u32,
1893
3030
  v0: vec4<f32>,
1894
3031
  v1: vec4<f32>,
@@ -1901,6 +3038,15 @@ struct TriangleRecord {
1901
3038
  color: vec4<f32>,
1902
3039
  emission: vec4<f32>,
1903
3040
  material: vec4<f32>,
3041
+ materialResponse: vec4<f32>,
3042
+ materialExtension: vec4<f32>,
3043
+ specularColor: vec4<f32>,
3044
+ baseColorAtlas: vec4<f32>,
3045
+ metallicRoughnessAtlas: vec4<f32>,
3046
+ normalAtlas: vec4<f32>,
3047
+ occlusionAtlas: vec4<f32>,
3048
+ emissiveAtlas: vec4<f32>,
3049
+ textureSettings: vec4<f32>,
1904
3050
  };
1905
3051
 
1906
3052
  struct BvhNode {
@@ -1921,10 +3067,10 @@ struct BvhLeafRef {
1921
3067
 
1922
3068
  struct ScatterResult {
1923
3069
  direction: vec4<f32>,
3070
+ pdf: f32,
1924
3071
  flags: u32,
1925
3072
  pad0: u32,
1926
3073
  pad1: u32,
1927
- pad2: u32,
1928
3074
  };
1929
3075
 
1930
3076
  struct MeshVertex {
@@ -1945,10 +3091,19 @@ struct MeshRange {
1945
3091
  triangleCount: u32,
1946
3092
  firstVertex: u32,
1947
3093
  vertexCount: u32,
1948
- pad0: u32,
3094
+ materialSlot: u32,
1949
3095
  color: vec4<f32>,
1950
3096
  emission: vec4<f32>,
1951
3097
  material: vec4<f32>,
3098
+ materialResponse: vec4<f32>,
3099
+ materialExtension: vec4<f32>,
3100
+ specularColor: vec4<f32>,
3101
+ baseColorAtlas: vec4<f32>,
3102
+ metallicRoughnessAtlas: vec4<f32>,
3103
+ normalAtlas: vec4<f32>,
3104
+ occlusionAtlas: vec4<f32>,
3105
+ emissiveAtlas: vec4<f32>,
3106
+ textureSettings: vec4<f32>,
1952
3107
  };
1953
3108
 
1954
3109
  struct FrameConfig {
@@ -1989,6 +3144,7 @@ struct FrameConfig {
1989
3144
  _portalPad1: u32,
1990
3145
  environmentMapSettings: vec4<f32>,
1991
3146
  pathResolveSettings: vec4<f32>,
3147
+ environmentMapMeta: vec4<f32>,
1992
3148
  };
1993
3149
 
1994
3150
  struct Counters {
@@ -2051,6 +3207,15 @@ struct EnvironmentPortal {
2051
3207
  @group(0) @binding(20) var environmentMapTexture: texture_2d<f32>;
2052
3208
  @group(0) @binding(21) var environmentMapSampler: sampler;
2053
3209
  @group(0) @binding(22) var<storage, read_write> pathVertices: array<vec4<f32>>;
3210
+ @group(0) @binding(23) var baseColorAtlasTexture: texture_2d<f32>;
3211
+ @group(0) @binding(24) var metallicRoughnessAtlasTexture: texture_2d<f32>;
3212
+ @group(0) @binding(25) var normalAtlasTexture: texture_2d<f32>;
3213
+ @group(0) @binding(26) var occlusionAtlasTexture: texture_2d<f32>;
3214
+ @group(0) @binding(27) var emissiveAtlasTexture: texture_2d<f32>;
3215
+ @group(0) @binding(28) var materialAtlasSampler: sampler;
3216
+ @group(0) @binding(29) var brdfLutTexture: texture_2d<f32>;
3217
+ @group(0) @binding(30) var brdfLutSampler: sampler;
3218
+ @group(0) @binding(31) var environmentSamplingTexture: texture_2d<f32>;
2054
3219
 
2055
3220
  fn hash_u32(value: u32) -> u32 {
2056
3221
  var x = value;
@@ -2087,6 +3252,146 @@ fn safe_normalize(value: vec3<f32>, fallback: vec3<f32>) -> vec3<f32> {
2087
3252
  return value / len;
2088
3253
  }
2089
3254
 
3255
+ struct TangentBasis {
3256
+ tangent: vec3<f32>,
3257
+ bitangent: vec3<f32>,
3258
+ };
3259
+
3260
+ struct SurfaceMaterialSample {
3261
+ color: vec4<f32>,
3262
+ emission: vec4<f32>,
3263
+ material: vec4<f32>,
3264
+ materialResponse: vec4<f32>,
3265
+ materialExtension: vec4<f32>,
3266
+ specularColor: vec4<f32>,
3267
+ shadingNormal: vec3<f32>,
3268
+ occlusion: f32,
3269
+ };
3270
+
3271
+ fn srgb_to_linear_channel(value: f32) -> f32 {
3272
+ if (value <= 0.04045) {
3273
+ return value / 12.92;
3274
+ }
3275
+ return pow((value + 0.055) / 1.055, 2.4);
3276
+ }
3277
+
3278
+ fn srgb_to_linear_vec3(value: vec3<f32>) -> vec3<f32> {
3279
+ return vec3<f32>(
3280
+ srgb_to_linear_channel(value.x),
3281
+ srgb_to_linear_channel(value.y),
3282
+ srgb_to_linear_channel(value.z)
3283
+ );
3284
+ }
3285
+
3286
+ fn wrap_uv(uv: vec2<f32>) -> vec2<f32> {
3287
+ return fract(fract(uv) + vec2<f32>(1.0));
3288
+ }
3289
+
3290
+ fn atlas_sample_uv(rect: vec4<f32>, uv: vec2<f32>) -> vec2<f32> {
3291
+ let local = wrap_uv(uv);
3292
+ let clamped = clamp(local, vec2<f32>(0.001), vec2<f32>(0.999));
3293
+ return rect.xy + clamped * rect.zw;
3294
+ }
3295
+
3296
+ fn sample_atlas(textureRef: texture_2d<f32>, rect: vec4<f32>, uv: vec2<f32>) -> vec4<f32> {
3297
+ return textureSampleLevel(textureRef, materialAtlasSampler, atlas_sample_uv(rect, uv), 0.0);
3298
+ }
3299
+
3300
+ fn build_triangle_tangent_basis(
3301
+ triangle: TriangleRecord,
3302
+ fallbackNormal: vec3<f32>
3303
+ ) -> TangentBasis {
3304
+ let edge1 = triangle.v1.xyz - triangle.v0.xyz;
3305
+ let edge2 = triangle.v2.xyz - triangle.v0.xyz;
3306
+ let uv0 = triangle.uv0uv1.xy;
3307
+ let uv1 = triangle.uv0uv1.zw;
3308
+ let uv2 = triangle.uv2Pad.xy;
3309
+ let deltaUv1 = uv1 - uv0;
3310
+ let deltaUv2 = uv2 - uv0;
3311
+ let determinant = deltaUv1.x * deltaUv2.y - deltaUv1.y * deltaUv2.x;
3312
+ if (abs(determinant) <= 0.000001) {
3313
+ let tangentFallback = select(vec3<f32>(0.0, 1.0, 0.0), vec3<f32>(1.0, 0.0, 0.0), abs(fallbackNormal.y) >= 0.999);
3314
+ let tangent = safe_normalize(cross(tangentFallback, fallbackNormal), vec3<f32>(1.0, 0.0, 0.0));
3315
+ let bitangent = safe_normalize(cross(fallbackNormal, tangent), vec3<f32>(0.0, 0.0, 1.0));
3316
+ return TangentBasis(tangent, bitangent);
3317
+ }
3318
+ let inverse = 1.0 / determinant;
3319
+ let tangent = safe_normalize(
3320
+ inverse * (edge1 * deltaUv2.y - edge2 * deltaUv1.y),
3321
+ vec3<f32>(1.0, 0.0, 0.0)
3322
+ );
3323
+ let bitangent = safe_normalize(
3324
+ inverse * (-edge1 * deltaUv2.x + edge2 * deltaUv1.x),
3325
+ vec3<f32>(0.0, 0.0, 1.0)
3326
+ );
3327
+ return TangentBasis(tangent, bitangent);
3328
+ }
3329
+
3330
+ fn sample_surface_material(
3331
+ triangle: TriangleRecord,
3332
+ uv: vec2<f32>,
3333
+ geometricNormal: vec3<f32>,
3334
+ shadingNormal: vec3<f32>
3335
+ ) -> SurfaceMaterialSample {
3336
+ let baseColorTexel = sample_atlas(baseColorAtlasTexture, triangle.baseColorAtlas, uv);
3337
+ let baseColor = vec4<f32>(
3338
+ clamp(triangle.color.rgb * srgb_to_linear_vec3(baseColorTexel.rgb), vec3<f32>(0.0), vec3<f32>(1.0)),
3339
+ clamp(triangle.color.a * baseColorTexel.a, 0.0, 1.0)
3340
+ );
3341
+ let metallicRoughnessTexel = sample_atlas(
3342
+ metallicRoughnessAtlasTexture,
3343
+ triangle.metallicRoughnessAtlas,
3344
+ uv
3345
+ );
3346
+ let normalTexel = sample_atlas(normalAtlasTexture, triangle.normalAtlas, uv);
3347
+ let occlusionTexel = sample_atlas(occlusionAtlasTexture, triangle.occlusionAtlas, uv);
3348
+ let emissiveTexel = sample_atlas(emissiveAtlasTexture, triangle.emissiveAtlas, uv);
3349
+ let normalScale = clamp(triangle.textureSettings.x, 0.0, 1.0);
3350
+ let tangentBasis = build_triangle_tangent_basis(triangle, geometricNormal);
3351
+ let tangentNormal = safe_normalize(
3352
+ vec3<f32>(
3353
+ (normalTexel.x * 2.0 - 1.0) * normalScale,
3354
+ (normalTexel.y * 2.0 - 1.0) * normalScale,
3355
+ 1.0 + ((normalTexel.z * 2.0 - 1.0) - 1.0) * normalScale
3356
+ ),
3357
+ vec3<f32>(0.0, 0.0, 1.0)
3358
+ );
3359
+ let mappedNormal = safe_normalize(
3360
+ tangentBasis.tangent * tangentNormal.x +
3361
+ tangentBasis.bitangent * tangentNormal.y +
3362
+ shadingNormal * tangentNormal.z,
3363
+ shadingNormal
3364
+ );
3365
+ let emission = vec4<f32>(
3366
+ max(
3367
+ triangle.emission.rgb *
3368
+ srgb_to_linear_vec3(emissiveTexel.rgb) *
3369
+ max(triangle.textureSettings.z, 0.0),
3370
+ vec3<f32>(0.0)
3371
+ ),
3372
+ clamp(triangle.emission.a * emissiveTexel.a, 0.0, 1.0)
3373
+ );
3374
+ return SurfaceMaterialSample(
3375
+ baseColor,
3376
+ emission,
3377
+ vec4<f32>(
3378
+ clamp(triangle.material.x * metallicRoughnessTexel.y, 0.0, 1.0),
3379
+ clamp(triangle.material.y * metallicRoughnessTexel.z, 0.0, 1.0),
3380
+ clamp(triangle.material.z * baseColor.a, 0.0, 1.0),
3381
+ clamp(triangle.material.w, 1.0, 3.0)
3382
+ ),
3383
+ triangle.materialResponse,
3384
+ triangle.materialExtension,
3385
+ triangle.specularColor,
3386
+ repair_shading_normal(geometricNormal, mappedNormal),
3387
+ clamp(
3388
+ mix(1.0, occlusionTexel.x, clamp(triangle.textureSettings.y, 0.0, 1.0)),
3389
+ 0.0,
3390
+ 1.0
3391
+ )
3392
+ );
3393
+ }
3394
+
2090
3395
  fn saturate(value: f32) -> f32 {
2091
3396
  return clamp(value, 0.0, 1.0);
2092
3397
  }
@@ -2095,6 +3400,10 @@ fn max_component(value: vec3<f32>) -> f32 {
2095
3400
  return max(max(value.x, value.y), value.z);
2096
3401
  }
2097
3402
 
3403
+ fn radiance_luminance(value: vec3<f32>) -> f32 {
3404
+ return dot(value, vec3<f32>(0.2126, 0.7152, 0.0722));
3405
+ }
3406
+
2098
3407
  fn environment_map_enabled() -> bool {
2099
3408
  return config.environmentMapSettings.x > 0.5;
2100
3409
  }
@@ -2223,6 +3532,343 @@ fn environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
2223
3532
  select(vec3<f32>(1.0), portalScale, portalHit);
2224
3533
  }
2225
3534
 
3535
+ fn direct_environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
3536
+ let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
3537
+ let portalScale = environment_portal_radiance_scale(origin, rayDirection);
3538
+ let portalHit = max_component(portalScale) > 0.0001;
3539
+ if (
3540
+ config.environmentPortalCount > 0u &&
3541
+ config.environmentPortalMode == 2u &&
3542
+ !portalHit
3543
+ ) {
3544
+ return vec3<f32>(0.0);
3545
+ }
3546
+ return base_environment_radiance(rayDirection) *
3547
+ select(vec3<f32>(1.0), portalScale, portalHit);
3548
+ }
3549
+
3550
+ fn radical_inverse_vdc(bitsValue: u32) -> f32 {
3551
+ var bits = bitsValue;
3552
+ bits = (bits << 16u) | (bits >> 16u);
3553
+ bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xaaaaaaaau) >> 1u);
3554
+ bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xccccccccu) >> 2u);
3555
+ bits = ((bits & 0x0f0f0f0fu) << 4u) | ((bits & 0xf0f0f0f0u) >> 4u);
3556
+ bits = ((bits & 0x00ff00ffu) << 8u) | ((bits & 0xff00ff00u) >> 8u);
3557
+ return f32(bits) * 2.3283064365386963e-10;
3558
+ }
3559
+
3560
+ fn hammersley_2d(index: u32, count: u32) -> vec2<f32> {
3561
+ return vec2<f32>(f32(index) / max(f32(count), 1.0), radical_inverse_vdc(index));
3562
+ }
3563
+
3564
+ fn build_basis_tangent(normal: vec3<f32>) -> vec3<f32> {
3565
+ let tangentFallback = select(vec3<f32>(0.0, 1.0, 0.0), vec3<f32>(1.0, 0.0, 0.0), abs(normal.y) >= 0.999);
3566
+ return safe_normalize(cross(tangentFallback, normal), vec3<f32>(1.0, 0.0, 0.0));
3567
+ }
3568
+
3569
+ fn local_to_world(local: vec3<f32>, normal: vec3<f32>) -> vec3<f32> {
3570
+ let tangent = build_basis_tangent(normal);
3571
+ let bitangent = safe_normalize(cross(normal, tangent), vec3<f32>(0.0, 0.0, 1.0));
3572
+ return safe_normalize(tangent * local.x + bitangent * local.y + normal * local.z, normal);
3573
+ }
3574
+
3575
+ fn cosine_sample_hemisphere(sample: vec2<f32>, normal: vec3<f32>) -> vec3<f32> {
3576
+ let phi = 6.28318530718 * sample.x;
3577
+ let radius = sqrt(sample.y);
3578
+ let x = cos(phi) * radius;
3579
+ let y = sin(phi) * radius;
3580
+ let z = sqrt(max(0.0, 1.0 - sample.y));
3581
+ return local_to_world(vec3<f32>(x, y, z), normal);
3582
+ }
3583
+
3584
+ fn importance_sample_ggx(sample: vec2<f32>, roughness: f32, normal: vec3<f32>) -> vec3<f32> {
3585
+ let alpha = max(roughness * roughness, 0.0001);
3586
+ let phi = 6.28318530718 * sample.x;
3587
+ let cosTheta = sqrt((1.0 - sample.y) / max(1.0 + (alpha * alpha - 1.0) * sample.y, 0.0001));
3588
+ let sinTheta = sqrt(max(0.0, 1.0 - cosTheta * cosTheta));
3589
+ let localHalf = vec3<f32>(cos(phi) * sinTheta, sin(phi) * sinTheta, cosTheta);
3590
+ return local_to_world(localHalf, normal);
3591
+ }
3592
+
3593
+ fn distribution_ggx(normal: vec3<f32>, halfVector: vec3<f32>, roughness: f32) -> f32 {
3594
+ let alpha = max(roughness * roughness, 0.0001);
3595
+ let alpha2 = alpha * alpha;
3596
+ let nDotH = saturate(dot(normal, halfVector));
3597
+ let denominator = nDotH * nDotH * (alpha2 - 1.0) + 1.0;
3598
+ return alpha2 / max(3.14159265359 * denominator * denominator, 0.000001);
3599
+ }
3600
+
3601
+ fn geometry_schlick_ggx(nDotValue: f32, roughness: f32) -> f32 {
3602
+ let k = ((roughness + 1.0) * (roughness + 1.0)) / 8.0;
3603
+ return nDotValue / max(nDotValue * (1.0 - k) + k, 0.000001);
3604
+ }
3605
+
3606
+ fn geometry_smith(normal: vec3<f32>, viewDirection: vec3<f32>, lightDirection: vec3<f32>, roughness: f32) -> f32 {
3607
+ let nDotV = saturate(dot(normal, viewDirection));
3608
+ let nDotL = saturate(dot(normal, lightDirection));
3609
+ return geometry_schlick_ggx(nDotV, roughness) * geometry_schlick_ggx(nDotL, roughness);
3610
+ }
3611
+
3612
+ fn fresnel_schlick(cosine: f32, f0: vec3<f32>) -> vec3<f32> {
3613
+ return f0 + (vec3<f32>(1.0) - f0) * pow(1.0 - cosine, 5.0);
3614
+ }
3615
+
3616
+ fn sample_brdf_lut(nDotV: f32, roughness: f32) -> vec2<f32> {
3617
+ let uv = vec2<f32>(clamp(nDotV, 0.0, 1.0), clamp(roughness, 0.0, 1.0));
3618
+ return textureSampleLevel(brdfLutTexture, brdfLutSampler, uv, 0.0).xy;
3619
+ }
3620
+
3621
+ fn prefiltered_environment_radiance(direction: vec3<f32>, roughness: f32) -> vec3<f32> {
3622
+ let uv = environment_map_uv(direction);
3623
+ let maxLevel = max(config.environmentMapMeta.z - 1.0, 0.0);
3624
+ let lod = clamp(roughness, 0.0, 1.0) * maxLevel;
3625
+ let texel = max(textureSampleLevel(environmentMapTexture, environmentMapSampler, uv, lod).rgb, vec3<f32>(0.0));
3626
+ return texel * max(config.environmentMapSettings.y, 0.0);
3627
+ }
3628
+
3629
+ fn environment_pdf_dimensions() -> vec2<u32> {
3630
+ return vec2<u32>(
3631
+ max(u32(config.environmentMapMeta.x), 1u),
3632
+ max(u32(config.environmentMapMeta.y), 1u)
3633
+ );
3634
+ }
3635
+
3636
+ fn environment_importance_sampling_enabled() -> bool {
3637
+ return config.environmentMapMeta.w > 0.5;
3638
+ }
3639
+
3640
+ fn uniform_sphere_pdf() -> f32 {
3641
+ return 1.0 / (4.0 * 3.14159265359);
3642
+ }
3643
+
3644
+ fn sample_uniform_sphere_direction(sample: vec2<f32>) -> vec3<f32> {
3645
+ let z = 1.0 - 2.0 * sample.y;
3646
+ let radial = sqrt(max(1.0 - z * z, 0.0));
3647
+ let phi = sample.x * 6.28318530718;
3648
+ return vec3<f32>(cos(phi) * radial, z, sin(phi) * radial);
3649
+ }
3650
+
3651
+ fn environment_sampling_texel(x: u32, y: u32) -> vec4<f32> {
3652
+ return textureLoad(environmentSamplingTexture, vec2<i32>(i32(x), i32(y)), 0);
3653
+ }
3654
+
3655
+ fn environment_pdf_texel(x: u32, y: u32) -> f32 {
3656
+ return environment_sampling_texel(x, y).x;
3657
+ }
3658
+
3659
+ fn environment_row_cdf_texel(y: u32) -> f32 {
3660
+ return environment_sampling_texel(0u, y).z;
3661
+ }
3662
+
3663
+ fn environment_column_cdf_texel(x: u32, y: u32) -> f32 {
3664
+ return environment_sampling_texel(x, y).y;
3665
+ }
3666
+
3667
+ fn environment_direction_pdf(direction: vec3<f32>) -> f32 {
3668
+ if (!environment_importance_sampling_enabled()) {
3669
+ return uniform_sphere_pdf();
3670
+ }
3671
+ let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
3672
+ let uv = environment_map_uv(rayDirection);
3673
+ let dimensions = environment_pdf_dimensions();
3674
+ let width = max(f32(dimensions.x), 1.0);
3675
+ let height = max(f32(dimensions.y), 1.0);
3676
+ let x = min(u32(uv.x * width), dimensions.x - 1u);
3677
+ let y = min(u32(uv.y * height), dimensions.y - 1u);
3678
+ let discretePdf = max(environment_pdf_texel(x, y), 0.0);
3679
+ let sinTheta = sqrt(max(1.0 - rayDirection.y * rayDirection.y, 0.0));
3680
+ let solidAngle = max((2.0 * 3.14159265359 * 3.14159265359 * sinTheta) / (width * height), 0.000001);
3681
+ return discretePdf / solidAngle;
3682
+ }
3683
+
3684
+ fn sample_row_cdf(count: u32, sampleValue: f32) -> u32 {
3685
+ if (count == 0u) {
3686
+ return 0u;
3687
+ }
3688
+ var low = 0u;
3689
+ var high = count - 1u;
3690
+ loop {
3691
+ if (low >= high) {
3692
+ break;
3693
+ }
3694
+ let mid = (low + high) / 2u;
3695
+ let cdfValue = environment_row_cdf_texel(mid);
3696
+ if (sampleValue <= cdfValue) {
3697
+ high = mid;
3698
+ } else {
3699
+ low = mid + 1u;
3700
+ }
3701
+ }
3702
+ return min(low, count - 1u);
3703
+ }
3704
+
3705
+ fn sample_column_cdf(row: u32, count: u32, sampleValue: f32) -> u32 {
3706
+ if (count == 0u) {
3707
+ return 0u;
3708
+ }
3709
+ var low = 0u;
3710
+ var high = count - 1u;
3711
+ loop {
3712
+ if (low >= high) {
3713
+ break;
3714
+ }
3715
+ let mid = (low + high) / 2u;
3716
+ let cdfValue = environment_column_cdf_texel(mid, row);
3717
+ if (sampleValue <= cdfValue) {
3718
+ high = mid;
3719
+ } else {
3720
+ low = mid + 1u;
3721
+ }
3722
+ }
3723
+ return min(low, count - 1u);
3724
+ }
3725
+
3726
+ struct EnvironmentSample {
3727
+ direction: vec3<f32>,
3728
+ radiance: vec3<f32>,
3729
+ pdf: f32,
3730
+ };
3731
+
3732
+ fn sample_environment_importance(sample: vec2<f32>) -> EnvironmentSample {
3733
+ if (!environment_importance_sampling_enabled()) {
3734
+ let direction = sample_uniform_sphere_direction(sample);
3735
+ return EnvironmentSample(direction, base_environment_radiance(direction), uniform_sphere_pdf());
3736
+ }
3737
+ let dimensions = environment_pdf_dimensions();
3738
+ let row = sample_row_cdf(dimensions.y, sample.y);
3739
+ let column = sample_column_cdf(row, dimensions.x, sample.x);
3740
+ let uv = vec2<f32>(
3741
+ (f32(column) + 0.5) / max(f32(dimensions.x), 1.0),
3742
+ (f32(row) + 0.5) / max(f32(dimensions.y), 1.0)
3743
+ );
3744
+ let theta = uv.y * 3.14159265359;
3745
+ let phi = (uv.x - 0.5 - config.environmentMapSettings.z / 6.28318530718) * 6.28318530718;
3746
+ let sinTheta = sin(theta);
3747
+ let direction = vec3<f32>(cos(phi) * sinTheta, cos(theta), sin(phi) * sinTheta);
3748
+ let pdf = environment_direction_pdf(direction);
3749
+ return EnvironmentSample(direction, base_environment_radiance(direction), pdf);
3750
+ }
3751
+
3752
+ fn power_heuristic(pdfA: f32, pdfB: f32) -> f32 {
3753
+ let a2 = pdfA * pdfA;
3754
+ let b2 = pdfB * pdfB;
3755
+ return a2 / max(a2 + b2, 0.000001);
3756
+ }
3757
+
3758
+ fn visible_environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
3759
+ let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
3760
+ let visible = !scene_visibility_blocked(origin, rayDirection, 1000000.0);
3761
+ return select(vec3<f32>(0.0), direct_environment_radiance(origin, rayDirection), visible);
3762
+ }
3763
+
3764
+ fn glossy_environment_direction(
3765
+ incidentDirection: vec3<f32>,
3766
+ normal: vec3<f32>,
3767
+ roughness: f32,
3768
+ normalBlendScale: f32
3769
+ ) -> vec3<f32> {
3770
+ let reflectionDirection = reflect(incidentDirection, normal);
3771
+ let blend = clamp(roughness * roughness * normalBlendScale, 0.0, 0.92);
3772
+ return safe_normalize(mix(reflectionDirection, normal, blend), normal);
3773
+ }
3774
+
3775
+ fn surface_glossiness(hit: HitRecord) -> f32 {
3776
+ let roughness = clamp(hit.material.x, 0.0, 1.0);
3777
+ let metallic = clamp(hit.material.y, 0.0, 1.0);
3778
+ let sheen = clamp(max_component(hit.materialResponse.xyz), 0.0, 1.0);
3779
+ let clearcoat = clamp(hit.materialResponse.w, 0.0, 1.0);
3780
+ let specularWeight = clamp(hit.materialExtension.y, 0.0, 1.0);
3781
+ let transmission = clamp(hit.materialExtension.z, 0.0, 1.0);
3782
+ let baseGloss =
3783
+ max(
3784
+ clearcoat,
3785
+ max(sheen * 0.72, max(specularWeight * (0.38 + metallic * 0.62), transmission))
3786
+ );
3787
+ return clamp(baseGloss * (1.0 - roughness * 0.72) + metallic * (1.0 - roughness) * 0.35, 0.0, 1.0);
3788
+ }
3789
+
3790
+ fn surface_specular_f0(hit: HitRecord, surfaceColor: vec3<f32>) -> vec3<f32> {
3791
+ let metallic = clamp(hit.material.y, 0.0, 1.0);
3792
+ let specularWeight = clamp(hit.materialExtension.y, 0.0, 1.0);
3793
+ let specularColor = clamp(hit.specularColor.xyz, vec3<f32>(0.0), vec3<f32>(1.0));
3794
+ let dielectricF0 = vec3<f32>(0.04) * specularWeight * specularColor;
3795
+ return mix(dielectricF0, surfaceColor, metallic);
3796
+ }
3797
+
3798
+ fn surface_bsdf_sampling_weights(hit: HitRecord) -> vec3<f32> {
3799
+ let metallic = clamp(hit.material.y, 0.0, 1.0);
3800
+ let clearcoat = clamp(hit.materialResponse.w, 0.0, 1.0);
3801
+ let specularWeight = clamp(hit.materialExtension.y, 0.0, 1.0);
3802
+ let diffuseWeight = clamp(
3803
+ (1.0 - metallic) * max(1.0 - specularWeight * 0.5 - clearcoat * 0.25, 0.15),
3804
+ 0.0,
3805
+ 1.0
3806
+ );
3807
+ let specWeight = clamp(max(metallic, specularWeight * 0.75) * (1.0 - clearcoat * 0.5), 0.0, 1.0);
3808
+ let clearcoatWeight = clamp(clearcoat, 0.0, 1.0);
3809
+ let totalWeight = max(diffuseWeight + specWeight + clearcoatWeight, 0.000001);
3810
+ return vec3<f32>(
3811
+ diffuseWeight / totalWeight,
3812
+ specWeight / totalWeight,
3813
+ clearcoatWeight / totalWeight
3814
+ );
3815
+ }
3816
+
3817
+ fn evaluate_surface_bsdf(hit: HitRecord, viewDirection: vec3<f32>, lightDirection: vec3<f32>) -> vec3<f32> {
3818
+ let normal = safe_normalize(hit.shadingNormal.xyz, vec3<f32>(0.0, 1.0, 0.0));
3819
+ let surfaceColor = clamp(max(hit.color.xyz, config.ambientColor.xyz * 0.35), vec3<f32>(0.0), vec3<f32>(1.0));
3820
+ let roughness = clamp(hit.material.x, 0.0, 1.0);
3821
+ let metallic = clamp(hit.material.y, 0.0, 1.0);
3822
+ let clearcoat = clamp(hit.materialResponse.w, 0.0, 1.0);
3823
+ let clearcoatRoughness = clamp(hit.materialExtension.x, 0.0, 1.0);
3824
+ let occlusion = clamp(hit.occlusion, 0.0, 1.0);
3825
+ let nDotV = saturate(dot(normal, viewDirection));
3826
+ let nDotL = saturate(dot(normal, lightDirection));
3827
+ if (nDotV <= 0.0 || nDotL <= 0.0) {
3828
+ return vec3<f32>(0.0);
3829
+ }
3830
+ let halfVector = safe_normalize(viewDirection + lightDirection, normal);
3831
+ let vDotH = saturate(dot(viewDirection, halfVector));
3832
+ let f0 = surface_specular_f0(hit, surfaceColor);
3833
+ let fresnel = fresnel_schlick(vDotH, f0);
3834
+ let distribution = distribution_ggx(normal, halfVector, roughness);
3835
+ let geometry = geometry_smith(normal, viewDirection, lightDirection, roughness);
3836
+ let specular = (distribution * geometry * fresnel) / max(4.0 * nDotV * nDotL, 0.000001);
3837
+ let diffuseWeight = (1.0 - metallic) * (1.0 - clearcoat * 0.24) * (1.0 - clamp(max_component(fresnel), 0.0, 0.98));
3838
+ let diffuse = surfaceColor * diffuseWeight / 3.14159265359;
3839
+ let clearcoatHalf = safe_normalize(viewDirection + lightDirection, normal);
3840
+ let clearcoatDistribution = distribution_ggx(normal, clearcoatHalf, max(clearcoatRoughness, 0.02));
3841
+ let clearcoatGeometry = geometry_smith(normal, viewDirection, lightDirection, max(clearcoatRoughness, 0.02));
3842
+ let clearcoatFresnel = fresnel_schlick(saturate(dot(viewDirection, clearcoatHalf)), vec3<f32>(0.04));
3843
+ let clearcoatTerm =
3844
+ (clearcoatDistribution * clearcoatGeometry * clearcoatFresnel) /
3845
+ max(4.0 * nDotV * nDotL, 0.000001) *
3846
+ clearcoat;
3847
+ return (diffuse + specular + clearcoatTerm) * mix(0.42, 1.0, occlusion);
3848
+ }
3849
+
3850
+ fn diffuse_pdf(normal: vec3<f32>, lightDirection: vec3<f32>) -> f32 {
3851
+ return saturate(dot(normal, lightDirection)) / 3.14159265359;
3852
+ }
3853
+
3854
+ fn ggx_pdf(normal: vec3<f32>, viewDirection: vec3<f32>, lightDirection: vec3<f32>, roughness: f32) -> f32 {
3855
+ let halfVector = safe_normalize(viewDirection + lightDirection, normal);
3856
+ let nDotH = saturate(dot(normal, halfVector));
3857
+ let vDotH = saturate(dot(viewDirection, halfVector));
3858
+ let distribution = distribution_ggx(normal, halfVector, roughness);
3859
+ return (distribution * nDotH) / max(4.0 * vDotH, 0.000001);
3860
+ }
3861
+
3862
+ fn evaluate_surface_bsdf_pdf(hit: HitRecord, viewDirection: vec3<f32>, lightDirection: vec3<f32>) -> f32 {
3863
+ let normal = safe_normalize(hit.shadingNormal.xyz, vec3<f32>(0.0, 1.0, 0.0));
3864
+ let roughness = clamp(hit.material.x, 0.0, 1.0);
3865
+ let weights = surface_bsdf_sampling_weights(hit);
3866
+ let diffuseTerm = diffuse_pdf(normal, lightDirection);
3867
+ let specTerm = ggx_pdf(normal, viewDirection, lightDirection, max(roughness, 0.02));
3868
+ let clearcoatTerm = ggx_pdf(normal, viewDirection, lightDirection, max(clamp(hit.materialExtension.x, 0.0, 1.0), 0.02));
3869
+ return weights.x * diffuseTerm + weights.y * specTerm + weights.z * clearcoatTerm;
3870
+ }
3871
+
2226
3872
  fn gated_environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
2227
3873
  let portalScale = environment_portal_radiance_scale(origin, safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0)));
2228
3874
  if (
@@ -2238,9 +3884,45 @@ fn gated_environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f
2238
3884
  fn surface_path_response(hit: HitRecord) -> vec3<f32> {
2239
3885
  let color = clamp(hit.color.xyz, vec3<f32>(0.0), vec3<f32>(1.0));
2240
3886
  let opacity = clamp(hit.material.z, 0.0, 1.0);
3887
+ let occlusion = clamp(hit.occlusion, 0.0, 1.0);
2241
3888
  let materialEnergy = select(0.68, 0.92, hit.materialKind == 1u || hit.materialKind == 2u);
2242
3889
  let transparentEnergy = select(materialEnergy, 0.9, hit.hitType == 3u);
2243
- return mix(vec3<f32>(1.0), color, max(opacity, 0.18)) * transparentEnergy;
3890
+ return mix(vec3<f32>(1.0), color, max(opacity, 0.18)) * transparentEnergy * mix(0.55, 1.0, occlusion);
3891
+ }
3892
+
3893
+ fn bounded_path_response_luminance(ray: RayRecord, hit: HitRecord) -> f32 {
3894
+ let daylightFloor = max(config.pathResolveSettings.y, 0.0) * 0.08;
3895
+ let hdriFloor = max(config.environmentMapSettings.w, 0.0) * 0.02;
3896
+ let sceneFloor = max(daylightFloor, hdriFloor);
3897
+ if (sceneFloor <= 0.000001) {
3898
+ return 0.0;
3899
+ }
3900
+ let bounceRatio = select(
3901
+ 0.0,
3902
+ f32(ray.bounce) / max(f32(config.maxDepth - 1u), 1.0),
3903
+ config.maxDepth > 1u
3904
+ );
3905
+ let bounceScale = 1.0 - bounceRatio * 0.55;
3906
+ let materialScale = select(1.0, 0.34, hit.materialKind == 1u || hit.materialKind == 2u);
3907
+ let transparentScale = select(materialScale, 0.58, hit.hitType == 3u);
3908
+ let opacityScale = mix(0.55, 1.0, clamp(hit.material.z, 0.0, 1.0));
3909
+ return sceneFloor * bounceScale * transparentScale * opacityScale;
3910
+ }
3911
+
3912
+ fn stabilize_surface_path_response(ray: RayRecord, hit: HitRecord, response: vec3<f32>) -> vec3<f32> {
3913
+ let minimumLuminance = bounded_path_response_luminance(ray, hit);
3914
+ let responseLuminance = radiance_luminance(response);
3915
+ if (minimumLuminance <= 0.000001 || responseLuminance >= minimumLuminance) {
3916
+ return response;
3917
+ }
3918
+ let tintBase = max(response, max(hit.color.xyz * 0.65, config.ambientColor.xyz * 0.35));
3919
+ let tint = tintBase / max(max_component(tintBase), 0.0001);
3920
+ let lifted = select(
3921
+ tint * minimumLuminance,
3922
+ response * (minimumLuminance / max(responseLuminance, 0.0001)),
3923
+ responseLuminance > 0.0001
3924
+ );
3925
+ return clamp(lifted, vec3<f32>(0.0), vec3<f32>(0.98));
2244
3926
  }
2245
3927
 
2246
3928
  fn sunlit_baseline_radiance(normal: vec3<f32>) -> vec3<f32> {
@@ -2259,12 +3941,24 @@ fn sunlit_baseline_radiance(normal: vec3<f32>) -> vec3<f32> {
2259
3941
  return clamp_sample_radiance(sunTint * baseline * skyFacing * directionalWeight * 0.04);
2260
3942
  }
2261
3943
 
2262
- fn terminal_surface_environment_source(hit: HitRecord) -> vec3<f32> {
3944
+ fn terminal_surface_environment_source(ray: RayRecord, hit: HitRecord) -> vec3<f32> {
2263
3945
  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
3946
+ let origin = hit.position.xyz + normal * 0.003;
3947
+ let roughness = clamp(hit.material.x, 0.0, 1.0);
3948
+ let glossiness = surface_glossiness(hit);
3949
+ let normalEnvironment = gated_environment_radiance(origin, normal);
3950
+ let viewDirection = safe_normalize(-ray.direction.xyz, normal);
3951
+ let reflectionDirection = glossy_environment_direction(
3952
+ ray.direction.xyz,
3953
+ normal,
3954
+ roughness,
3955
+ mix(0.88, 0.38, glossiness)
2267
3956
  );
3957
+ let reflectionEnvironment = prefiltered_environment_radiance(reflectionDirection, roughness);
3958
+ let surfaceColor = clamp(max(hit.color.xyz, config.ambientColor.xyz * 0.35), vec3<f32>(0.0), vec3<f32>(1.0));
3959
+ let f0 = surface_specular_f0(hit, surfaceColor);
3960
+ let brdfTerm = sample_brdf_lut(saturate(dot(normal, viewDirection)), roughness);
3961
+ let specularEnvironment = reflectionEnvironment * (f0 * brdfTerm.x + vec3<f32>(brdfTerm.y));
2268
3962
  let sunlitFloor = sunlit_baseline_radiance(normal);
2269
3963
  let ambientFloor = select(
2270
3964
  max(config.ambientColor.xyz, sunlitFloor * 0.82),
@@ -2276,17 +3970,23 @@ fn terminal_surface_environment_source(hit: HitRecord) -> vec3<f32> {
2276
3970
  max(config.environmentMapSettings.w, max(0.12, config.pathResolveSettings.y * 0.42)),
2277
3971
  environment_map_enabled()
2278
3972
  );
2279
- let environmentFloor = max(ambientFloor, max(sunlitFloor, normalEnvironment * environmentInfluence));
3973
+ let glossyEnvironment = max(
3974
+ normalEnvironment,
3975
+ max(reflectionEnvironment * mix(0.24, 0.92, glossiness), specularEnvironment)
3976
+ );
3977
+ let environmentFloor = max(ambientFloor, max(sunlitFloor, glossyEnvironment * environmentInfluence));
2280
3978
  let materialFloor = select(0.7, 1.0, hit.materialKind == 0u || hit.materialKind == 3u);
2281
3979
  return clamp_sample_radiance(environmentFloor * materialFloor);
2282
3980
  }
2283
3981
 
2284
3982
  fn terminal_surface_environment_contribution(ray: RayRecord, hit: HitRecord) -> vec3<f32> {
2285
3983
  let surfaceColor = max(hit.color.xyz, config.ambientColor.xyz);
3984
+ let occlusion = mix(0.75, 1.0, clamp(hit.occlusion, 0.0, 1.0));
2286
3985
  return clamp_sample_radiance(
2287
3986
  ray.throughput.xyz *
2288
3987
  surfaceColor *
2289
- terminal_surface_environment_source(hit)
3988
+ terminal_surface_environment_source(ray, hit) *
3989
+ occlusion
2290
3990
  );
2291
3991
  }
2292
3992
 
@@ -2319,6 +4019,10 @@ fn direct_environment_portal_irradiance(origin: vec3<f32>, normal: vec3<f32>) ->
2319
4019
  );
2320
4020
  let area = max(portal.position.w, 0.0001);
2321
4021
  let distanceFalloff = clamp(area / max(distanceSquared, area * 0.25), 0.0, 2.5);
4022
+ let traceDistance = max(sqrt(distanceSquared) - 0.01, 0.01);
4023
+ if (scene_visibility_blocked(origin, direction, traceDistance)) {
4024
+ continue;
4025
+ }
2322
4026
  irradiance = irradiance +
2323
4027
  portal.color.rgb *
2324
4028
  portal.normal.w *
@@ -2330,48 +4034,79 @@ fn direct_environment_portal_irradiance(origin: vec3<f32>, normal: vec3<f32>) ->
2330
4034
  return irradiance;
2331
4035
  }
2332
4036
 
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()
4037
+ fn visibility_test_ray(origin: vec3<f32>, direction: vec3<f32>) -> RayRecord {
4038
+ let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
4039
+ return RayRecord(
4040
+ 0u,
4041
+ 0u,
4042
+ 0u,
4043
+ 0u,
4044
+ 0u,
4045
+ 0u,
4046
+ 0u,
4047
+ 0u,
4048
+ vec4<f32>(origin, 1.0),
4049
+ vec4<f32>(rayDirection, 0.0),
4050
+ vec4<f32>(1.0, 1.0, 1.0, 1.0)
2352
4051
  );
2353
- let skyIrradiance = max(ambientIrradiance, normalEnvironment * skyVisibility * environmentIrradianceScale);
4052
+ }
2354
4053
 
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);
4054
+ fn scene_visibility_blocked(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> bool {
4055
+ let testRay = visibility_test_ray(origin, direction);
4056
+ let nearest = max(maxDistance, 0.001);
2363
4057
 
2364
- let diffuseWeight = select(1.0 - metallic * 0.65, 0.22, hit.materialKind == 1u);
2365
- let diffuse = surfaceColor * (skyIrradiance + sunIrradiance + portalIrradiance) * diffuseWeight;
4058
+ for (var objectIndex = 0u; objectIndex < config.sceneObjectCount; objectIndex = objectIndex + 1u) {
4059
+ let object = sceneObjects[objectIndex];
4060
+ var current = no_candidate();
4061
+ if (object.kind == 1u) {
4062
+ current = intersect_sphere(testRay, object);
4063
+ } else if (object.kind == 2u) {
4064
+ current = intersect_box(testRay, object);
4065
+ }
4066
+ if (current.hit == 1u && current.distance < nearest) {
4067
+ return true;
4068
+ }
4069
+ }
2366
4070
 
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);
4071
+ let meshCandidate = intersect_bvh(testRay, nearest);
4072
+ return meshCandidate.hit == 1u && meshCandidate.distance < nearest;
4073
+ }
2372
4074
 
2373
- let bounceWeight = select(1.0, 0.38, ray.bounce > 0u);
2374
- return clamp_sample_radiance(ray.throughput.xyz * (diffuse + specular) * bounceWeight);
4075
+ fn surface_direct_environment_contribution(ray: RayRecord, hit: HitRecord) -> vec3<f32> {
4076
+ let normal = safe_normalize(hit.shadingNormal.xyz, vec3<f32>(0.0, 1.0, 0.0));
4077
+ let origin = hit.position.xyz + normal * 0.003;
4078
+ let viewDirection = safe_normalize(-ray.direction.xyz, normal);
4079
+ let lightSample = sample_environment_importance(vec2<f32>(
4080
+ random01(mix_seed(ray.sourcePixelId, ray.sampleId, ray.bounce, config.frameIndex, 41u)),
4081
+ random01(mix_seed(ray.sourcePixelId, ray.sampleId, ray.bounce, config.frameIndex, 43u))
4082
+ ));
4083
+ if (lightSample.pdf <= 0.000001) {
4084
+ return vec3<f32>(0.0);
4085
+ }
4086
+ let lightDirection = safe_normalize(lightSample.direction, normal);
4087
+ let nDotL = saturate(dot(normal, lightDirection));
4088
+ if (nDotL <= 0.000001) {
4089
+ return vec3<f32>(0.0);
4090
+ }
4091
+ if (scene_visibility_blocked(origin, lightDirection, 1000000.0)) {
4092
+ return vec3<f32>(0.0);
4093
+ }
4094
+ let incidentRadiance = direct_environment_radiance(origin, lightDirection);
4095
+ if (max_component(incidentRadiance) <= 0.000001) {
4096
+ return vec3<f32>(0.0);
4097
+ }
4098
+ let bsdf = evaluate_surface_bsdf(hit, viewDirection, lightDirection);
4099
+ if (max_component(bsdf) <= 0.000001) {
4100
+ return vec3<f32>(0.0);
4101
+ }
4102
+ let bsdfPdf = evaluate_surface_bsdf_pdf(hit, viewDirection, lightDirection);
4103
+ let misWeight = power_heuristic(lightSample.pdf, bsdfPdf);
4104
+ let contribution =
4105
+ ray.throughput.xyz *
4106
+ bsdf *
4107
+ incidentRadiance *
4108
+ (nDotL * misWeight / max(lightSample.pdf, 0.000001));
4109
+ return clamp_sample_radiance(contribution);
2375
4110
  }
2376
4111
 
2377
4112
  fn default_mesh_range() -> MeshRange {
@@ -2390,7 +4125,16 @@ fn default_mesh_range() -> MeshRange {
2390
4125
  0u,
2391
4126
  vec4<f32>(0.72, 0.72, 0.68, 1.0),
2392
4127
  vec4<f32>(0.0),
2393
- vec4<f32>(0.72, 0.0, 1.0, 1.45)
4128
+ vec4<f32>(0.72, 0.0, 1.0, 1.45),
4129
+ vec4<f32>(0.0, 0.0, 0.0, 0.08),
4130
+ vec4<f32>(1.0, 1.0, 1.0, 1.0),
4131
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
4132
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
4133
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
4134
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
4135
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
4136
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
4137
+ vec4<f32>(1.0, 1.0, 1.0, 0.0)
2394
4138
  );
2395
4139
  }
2396
4140
 
@@ -2486,7 +4230,7 @@ fn prepareMeshTrianglesAndLeaves(@builtin(global_invocation_id) globalId: vec3<u
2486
4230
  mesh.flags,
2487
4231
  mesh.materialRefId,
2488
4232
  mesh.mediumRefId,
2489
- 0u,
4233
+ mesh.materialSlot,
2490
4234
  0u,
2491
4235
  vec4<f32>(vertex0.position.xyz, 0.0),
2492
4236
  vec4<f32>(vertex1.position.xyz, 0.0),
@@ -2498,7 +4242,16 @@ fn prepareMeshTrianglesAndLeaves(@builtin(global_invocation_id) globalId: vec3<u
2498
4242
  vec4<f32>(uv2, 0.0, 0.0),
2499
4243
  mesh.color,
2500
4244
  mesh.emission,
2501
- mesh.material
4245
+ mesh.material,
4246
+ mesh.materialResponse,
4247
+ mesh.materialExtension,
4248
+ mesh.specularColor,
4249
+ mesh.baseColorAtlas,
4250
+ mesh.metallicRoughnessAtlas,
4251
+ mesh.normalAtlas,
4252
+ mesh.occlusionAtlas,
4253
+ mesh.emissiveAtlas,
4254
+ mesh.textureSettings
2502
4255
  );
2503
4256
 
2504
4257
  let leafBase = config.triangleCount - 1u;
@@ -2657,7 +4410,8 @@ fn make_miss(ray: RayRecord) -> HitRecord {
2657
4410
  0u,
2658
4411
  0u,
2659
4412
  -1.0,
2660
- vec3<f32>(0.0),
4413
+ 1.0,
4414
+ vec2<f32>(0.0),
2661
4415
  vec4<f32>(ray.origin.xyz + ray.direction.xyz * 1000.0, 1.0),
2662
4416
  vec4<f32>(-ray.direction.xyz, 0.0),
2663
4417
  vec4<f32>(-ray.direction.xyz, 0.0),
@@ -2665,7 +4419,10 @@ fn make_miss(ray: RayRecord) -> HitRecord {
2665
4419
  vec4<f32>(0.0),
2666
4420
  vec4<f32>(radiance, 1.0),
2667
4421
  vec4<f32>(0.0),
2668
- vec4<f32>(1.0, 0.0, 1.0, 1.0)
4422
+ vec4<f32>(1.0, 0.0, 1.0, 1.0),
4423
+ vec4<f32>(0.0, 0.0, 0.0, 0.08),
4424
+ vec4<f32>(0.08, 1.0, 0.0, 0.0),
4425
+ vec4<f32>(1.0, 1.0, 1.0, 1.0)
2669
4426
  );
2670
4427
  }
2671
4428
 
@@ -2960,6 +4717,19 @@ fn denoise_range_space(value: vec3<f32>) -> vec3<f32> {
2960
4717
  return value / (vec3<f32>(1.0) + value);
2961
4718
  }
2962
4719
 
4720
+ fn denoise_sample_count() -> f32 {
4721
+ return clamp(1.0 / max(config.projectionAndSampling.z, 0.000001), 1.0, 256.0);
4722
+ }
4723
+
4724
+ fn denoise_strength() -> f32 {
4725
+ let spp = denoise_sample_count();
4726
+ return clamp(0.44 / sqrt(spp), 0.08, 0.44);
4727
+ }
4728
+
4729
+ fn denoise_kernel_radius() -> i32 {
4730
+ return select(1i, 2i, denoise_sample_count() < 2.5);
4731
+ }
4732
+
2963
4733
  @compute @workgroup_size(64)
2964
4734
  fn generatePrimaryRays(@builtin(global_invocation_id) globalId: vec3<u32>) {
2965
4735
  let index = globalId.x;
@@ -2998,7 +4768,10 @@ fn intersectActiveQueue(@builtin(global_invocation_id) globalId: vec3<u32>) {
2998
4768
  vec4<f32>(0.0),
2999
4769
  vec4<f32>(0.0),
3000
4770
  vec4<f32>(0.0),
3001
- vec4<f32>(1.0, 0.0, 1.0, 1.0)
4771
+ vec4<f32>(1.0, 0.0, 1.0, 1.0),
4772
+ vec4<f32>(0.0, 0.0, 0.0, 0.08),
4773
+ vec4<f32>(0.08, 1.0, 0.0, 0.0),
4774
+ vec4<f32>(1.0, 1.0, 1.0, 1.0)
3002
4775
  );
3003
4776
  var candidate = no_candidate();
3004
4777
  var hitTriangle = TriangleRecord(
@@ -3020,7 +4793,16 @@ fn intersectActiveQueue(@builtin(global_invocation_id) globalId: vec3<u32>) {
3020
4793
  vec4<f32>(0.0),
3021
4794
  vec4<f32>(0.0),
3022
4795
  vec4<f32>(0.0),
3023
- vec4<f32>(1.0, 0.0, 1.0, 1.0)
4796
+ vec4<f32>(1.0, 0.0, 1.0, 1.0),
4797
+ vec4<f32>(0.0, 0.0, 0.0, 0.08),
4798
+ vec4<f32>(0.08, 1.0, 0.0, 0.0),
4799
+ vec4<f32>(1.0, 1.0, 1.0, 1.0),
4800
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
4801
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
4802
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
4803
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
4804
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
4805
+ vec4<f32>(1.0, 1.0, 1.0, 0.0)
3024
4806
  );
3025
4807
 
3026
4808
  for (var objectIndex = 0u; objectIndex < config.sceneObjectCount; objectIndex = objectIndex + 1u) {
@@ -3053,16 +4835,28 @@ fn intersectActiveQueue(@builtin(global_invocation_id) globalId: vec3<u32>) {
3053
4835
  let position = ray.origin.xyz + ray.direction.xyz * candidate.distance;
3054
4836
  let hitMaterialKind = select(hitObject.materialKind, hitTriangle.materialKind, candidate.triangleIndex != 0xffffffffu);
3055
4837
  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);
4838
+ let meshSurface = sample_surface_material(
4839
+ hitTriangle,
4840
+ candidate.uv,
4841
+ candidate.geometricNormal,
4842
+ candidate.shadingNormal
4843
+ );
4844
+ let hitColor = select(hitObject.color, meshSurface.color, candidate.triangleIndex != 0xffffffffu);
4845
+ let hitEmission = select(hitObject.emission, meshSurface.emission, candidate.triangleIndex != 0xffffffffu);
4846
+ let hitMaterial = select(hitObject.material, meshSurface.material, candidate.triangleIndex != 0xffffffffu);
4847
+ let hitMaterialResponse = select(hitObject.materialResponse, meshSurface.materialResponse, candidate.triangleIndex != 0xffffffffu);
4848
+ let hitMaterialExtension = select(hitObject.materialExtension, meshSurface.materialExtension, candidate.triangleIndex != 0xffffffffu);
4849
+ let hitSpecularColor = select(hitObject.specularColor, meshSurface.specularColor, candidate.triangleIndex != 0xffffffffu);
4850
+ let hitShadingNormal = select(candidate.shadingNormal, meshSurface.shadingNormal, candidate.triangleIndex != 0xffffffffu);
3059
4851
  let hitPrimitiveId = select(candidate.primitiveId, hitTriangle.triangleId, candidate.triangleIndex != 0xffffffffu);
3060
4852
  let hitMaterialRefId = select(candidate.materialRefId, hitTriangle.materialRefId, candidate.triangleIndex != 0xffffffffu);
3061
4853
  let hitMediumRefId = select(candidate.mediumRefId, hitTriangle.mediumRefId, candidate.triangleIndex != 0xffffffffu);
4854
+ let hitMaterialSlot = select(0u, hitTriangle.materialSlot, candidate.triangleIndex != 0xffffffffu);
4855
+ let hitOcclusion = select(1.0, meshSurface.occlusion, candidate.triangleIndex != 0xffffffffu);
3062
4856
  var hitType = 0u;
3063
4857
  if (hitMaterialKind == 4u || emission_power(hitEmission) > 0.0001) {
3064
4858
  hitType = 1u;
3065
- } else if (hitMaterialKind == 3u || hitMaterial.z < 0.999) {
4859
+ } else if (hitMaterialKind == 3u || hitMaterial.z < 0.999 || hitMaterialExtension.z > 0.001) {
3066
4860
  hitType = 3u;
3067
4861
  }
3068
4862
  atomicAdd(&counters.hitCount, 1u);
@@ -3076,19 +4870,23 @@ fn intersectActiveQueue(@builtin(global_invocation_id) globalId: vec3<u32>) {
3076
4870
  hitPrimitiveId,
3077
4871
  hitMaterialRefId,
3078
4872
  hitMediumRefId,
3079
- 0u,
4873
+ hitMaterialSlot,
3080
4874
  0u,
3081
4875
  0u,
3082
4876
  candidate.distance,
3083
- vec3<f32>(0.0),
4877
+ hitOcclusion,
4878
+ vec2<f32>(0.0),
3084
4879
  vec4<f32>(position, 1.0),
3085
4880
  vec4<f32>(candidate.geometricNormal, 0.0),
3086
- vec4<f32>(candidate.shadingNormal, 0.0),
4881
+ vec4<f32>(hitShadingNormal, 0.0),
3087
4882
  vec4<f32>(candidate.barycentric, 0.0),
3088
4883
  vec4<f32>(candidate.uv, 0.0, 0.0),
3089
4884
  hitColor,
3090
4885
  hitEmission,
3091
- hitMaterial
4886
+ hitMaterial,
4887
+ hitMaterialResponse,
4888
+ hitMaterialExtension,
4889
+ hitSpecularColor
3092
4890
  );
3093
4891
  }
3094
4892
 
@@ -3157,60 +4955,106 @@ fn sample_environment_portal_direction(hit: HitRecord, seed: u32, fallback: vec3
3157
4955
  }
3158
4956
 
3159
4957
  fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult {
4958
+ let normal = safe_normalize(hit.shadingNormal.xyz, vec3<f32>(0.0, 1.0, 0.0));
4959
+ let viewDirection = safe_normalize(-ray.direction.xyz, normal);
3160
4960
  let roughness = clamp(hit.material.x, 0.0, 1.0);
3161
- if (hit.materialKind == 1u) {
4961
+ let transmission = clamp(hit.materialExtension.z, 0.0, 1.0);
4962
+ if (hit.materialKind == 1u && roughness <= 0.02) {
3162
4963
  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,
4964
+ vec4<f32>(reflect(ray.direction.xyz, normal), 0.0),
4965
+ 1.0,
4966
+ RAY_FLAG_DELTA_SAMPLE,
3172
4967
  0u,
3173
4968
  0u
3174
4969
  );
3175
4970
  }
3176
4971
 
3177
- if (hit.materialKind == 2u || hit.materialKind == 3u) {
4972
+ if (hit.materialKind == 2u || hit.materialKind == 3u || transmission > 0.001) {
3178
4973
  let ior = max(hit.material.w, 1.01);
3179
4974
  let etaRatio = select(ior, 1.0 / ior, hit.frontFace == 1u);
3180
- let cosTheta = min(dot(-ray.direction.xyz, hit.shadingNormal.xyz), 1.0);
4975
+ let cosTheta = min(dot(-ray.direction.xyz, normal), 1.0);
3181
4976
  let sinTheta = sqrt(max(0.0, 1.0 - cosTheta * cosTheta));
3182
4977
  let cannotRefract = etaRatio * sinTheta > 1.0;
3183
4978
  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);
4979
+ let transmissionReflectChance = select(
4980
+ reflectChance,
4981
+ max(reflectChance, 1.0 - transmission),
4982
+ transmission > 0.001
4983
+ );
4984
+ if (cannotRefract || random01(seed + 23u) < transmissionReflectChance) {
4985
+ return ScatterResult(
4986
+ vec4<f32>(reflect(ray.direction.xyz, normal), 0.0),
4987
+ 1.0,
4988
+ RAY_FLAG_DELTA_SAMPLE,
4989
+ 0u,
4990
+ 0u
4991
+ );
3186
4992
  }
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
- );
4993
+ return ScatterResult(
4994
+ vec4<f32>(refract_direction(ray.direction.xyz, normal, etaRatio), 0.0),
4995
+ 1.0,
4996
+ RAY_FLAG_DELTA_SAMPLE,
4997
+ 0u,
4998
+ 0u
4999
+ );
5000
+ }
5001
+
5002
+ let guidedEmissiveAvailable = config.emissiveTriangleCount > 0u;
5003
+ let guidedPortalAvailable =
5004
+ config.environmentPortalCount > 0u && config.environmentPortalMode != 0u;
5005
+ let guidedSelector = random01(seed + 17u);
5006
+ if (guidedEmissiveAvailable && guidedSelector < 0.18) {
5007
+ let guidedDirection = sample_emissive_triangle_direction(hit, seed + 101u, normal);
5008
+ if (dot(normal, guidedDirection) > 0.000001) {
5009
+ let guidedPdf = max(evaluate_surface_bsdf_pdf(hit, viewDirection, guidedDirection), 0.000001);
5010
+ return ScatterResult(
5011
+ vec4<f32>(guidedDirection, 0.0),
5012
+ guidedPdf,
5013
+ RAY_FLAG_GUIDED_EMISSIVE,
5014
+ 0u,
5015
+ 0u
5016
+ );
5017
+ }
5018
+ }
5019
+ if (guidedPortalAvailable && guidedSelector < 0.32) {
5020
+ let guidedDirection = sample_environment_portal_direction(hit, seed + 131u, normal);
5021
+ if (dot(normal, guidedDirection) > 0.000001) {
5022
+ let guidedPdf = max(evaluate_surface_bsdf_pdf(hit, viewDirection, guidedDirection), 0.000001);
5023
+ return ScatterResult(vec4<f32>(guidedDirection, 0.0), guidedPdf, 0u, 0u, 0u);
5024
+ }
5025
+ }
5026
+
5027
+ let weights = surface_bsdf_sampling_weights(hit);
5028
+ let selector = random01(seed + 31u);
5029
+ var lightDirection = normal;
5030
+ if (selector < weights.x) {
5031
+ lightDirection = cosine_sample_hemisphere(
5032
+ vec2<f32>(random01(seed + 37u), random01(seed + 41u)),
5033
+ normal
5034
+ );
5035
+ } else if (selector < weights.x + weights.y) {
5036
+ let halfVector = importance_sample_ggx(
5037
+ vec2<f32>(random01(seed + 47u), random01(seed + 53u)),
5038
+ max(roughness, 0.02),
5039
+ normal
5040
+ );
5041
+ lightDirection = safe_normalize(reflect(-viewDirection, halfVector), normal);
5042
+ } else {
5043
+ let halfVector = importance_sample_ggx(
5044
+ vec2<f32>(random01(seed + 59u), random01(seed + 61u)),
5045
+ max(clamp(hit.materialExtension.x, 0.0, 1.0), 0.02),
5046
+ normal
5047
+ );
5048
+ lightDirection = safe_normalize(reflect(-viewDirection, halfVector), normal);
5049
+ }
5050
+ if (dot(normal, lightDirection) <= 0.000001) {
5051
+ lightDirection = cosine_sample_hemisphere(
5052
+ vec2<f32>(random01(seed + 67u), random01(seed + 71u)),
5053
+ normal
5054
+ );
5055
+ }
5056
+ let pdf = max(evaluate_surface_bsdf_pdf(hit, viewDirection, lightDirection), 0.000001);
5057
+ return ScatterResult(vec4<f32>(lightDirection, 0.0), pdf, 0u, 0u, 0u);
3214
5058
  }
3215
5059
 
3216
5060
  @compute @workgroup_size(64)
@@ -3240,10 +5084,17 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
3240
5084
  }
3241
5085
 
3242
5086
  if (hit.hitType == 2u) {
5087
+ var sourceRadiance = hit.color.xyz;
5088
+ if ((ray.flags & RAY_FLAG_DELTA_SAMPLE) == 0u) {
5089
+ let bsdfPdf = max(ray.throughput.w, 0.000001);
5090
+ let lightPdf = environment_direction_pdf(ray.direction.xyz);
5091
+ let misWeight = power_heuristic(bsdfPdf, lightPdf);
5092
+ sourceRadiance = sourceRadiance * misWeight;
5093
+ }
3243
5094
  if (deferred_path_resolve_enabled()) {
3244
- record_deferred_terminal_source(ray, hit.color.xyz);
5095
+ record_deferred_terminal_source(ray, sourceRadiance);
3245
5096
  } else {
3246
- contribution = clamp_sample_radiance(ray.throughput.xyz * max(hit.color.xyz, config.ambientColor.xyz));
5097
+ contribution = clamp_sample_radiance(ray.throughput.xyz * sourceRadiance);
3247
5098
  accumulation[ray.rayId] =
3248
5099
  accumulation[ray.rayId] + vec4<f32>(contribution * sample_weight(), 1.0);
3249
5100
  }
@@ -3251,13 +5102,13 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
3251
5102
  return;
3252
5103
  }
3253
5104
 
3254
- let response = surface_path_response(hit);
5105
+ let response = stabilize_surface_path_response(ray, hit, surface_path_response(hit));
3255
5106
  record_deferred_path_response(ray, response);
3256
5107
 
3257
5108
  let shouldEstimateDirectEnvironment =
3258
- !deferred_path_resolve_enabled() &&
3259
5109
  (hit.materialKind == 0u || hit.materialKind == 1u) &&
3260
- hit.material.z >= 0.95;
5110
+ hit.material.z >= 0.95 &&
5111
+ ray.bounce < 2u;
3261
5112
  if (shouldEstimateDirectEnvironment) {
3262
5113
  let directEnvironment = surface_direct_environment_contribution(ray, hit);
3263
5114
  accumulation[ray.rayId] =
@@ -3266,7 +5117,7 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
3266
5117
 
3267
5118
  if (ray.bounce + 1u >= config.maxDepth) {
3268
5119
  if (deferred_path_resolve_enabled()) {
3269
- record_deferred_terminal_source(ray, terminal_surface_environment_source(hit));
5120
+ record_deferred_terminal_source(ray, terminal_surface_environment_source(ray, hit));
3270
5121
  } else {
3271
5122
  let terminalEnvironment = terminal_surface_environment_contribution(ray, hit);
3272
5123
  accumulation[ray.rayId] =
@@ -3281,7 +5132,7 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
3281
5132
  let nextIndex = atomicAdd(&counters.nextCount, 1u);
3282
5133
  if (nextIndex >= config.tilePixelCount) {
3283
5134
  if (deferred_path_resolve_enabled()) {
3284
- record_deferred_terminal_source(ray, terminal_surface_environment_source(hit));
5135
+ record_deferred_terminal_source(ray, terminal_surface_environment_source(ray, hit));
3285
5136
  } else {
3286
5137
  let overflowEnvironment = terminal_surface_environment_contribution(ray, hit);
3287
5138
  accumulation[ray.rayId] =
@@ -3302,7 +5153,7 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
3302
5153
  0u,
3303
5154
  vec4<f32>(offset_origin(hit.position.xyz, hit.shadingNormal.xyz), 1.0),
3304
5155
  scatter.direction,
3305
- vec4<f32>(throughput, ray.throughput.w)
5156
+ vec4<f32>(throughput, scatter.pdf)
3306
5157
  );
3307
5158
  }
3308
5159
 
@@ -3371,8 +5222,11 @@ fn denoiseLinearRadiance(@builtin(global_invocation_id) globalId: vec3<u32>) {
3371
5222
 
3372
5223
  let pixel = vec2<i32>(i32(x), i32(y));
3373
5224
  let center = textureLoad(denoiseInputRadiance, pixel, 0).xyz;
3374
- var sum = center * 1.4;
3375
- var totalWeight = 1.4;
5225
+ let strength = denoise_strength();
5226
+ let kernelRadius = denoise_kernel_radius();
5227
+ let centerWeight = 1.7 - strength * 0.35;
5228
+ var sum = center * centerWeight;
5229
+ var totalWeight = centerWeight;
3376
5230
  let centerRange = denoise_range_space(center);
3377
5231
 
3378
5232
  for (var oy = -2i; oy <= 2i; oy = oy + 1i) {
@@ -3380,13 +5234,16 @@ fn denoiseLinearRadiance(@builtin(global_invocation_id) globalId: vec3<u32>) {
3380
5234
  if (ox == 0i && oy == 0i) {
3381
5235
  continue;
3382
5236
  }
5237
+ if (abs(ox) > kernelRadius || abs(oy) > kernelRadius) {
5238
+ continue;
5239
+ }
3383
5240
  let sx = clamp(i32(x) + ox, 0i, i32(config.canvasWidth) - 1i);
3384
5241
  let sy = clamp(i32(y) + oy, 0i, i32(config.canvasHeight) - 1i);
3385
5242
  let sampleColor = textureLoad(denoiseInputRadiance, vec2<i32>(sx, sy), 0).xyz;
3386
5243
  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);
5244
+ let rangeWeight = 1.0 / (1.0 + colorDistance * (11.0 + strength * 6.0));
5245
+ let distanceWeight = 1.0 / (1.0 + f32(ox * ox + oy * oy) * (0.62 + strength * 0.24));
5246
+ let diagonalWeight = select(1.0, 0.92, abs(ox) + abs(oy) > 1i);
3390
5247
  let weight = rangeWeight * diagonalWeight * distanceWeight;
3391
5248
  sum = sum + sampleColor * weight;
3392
5249
  totalWeight = totalWeight + weight;
@@ -3394,8 +5251,9 @@ fn denoiseLinearRadiance(@builtin(global_invocation_id) globalId: vec3<u32>) {
3394
5251
  }
3395
5252
 
3396
5253
  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));
5254
+ let outlier = saturate(length(denoise_range_space(center) - denoise_range_space(filtered)) * 2.1);
5255
+ let blend = min(0.3, strength * (0.62 + outlier * 0.12));
5256
+ let color = min(mix(center, filtered, blend), vec3<f32>(16.0));
3399
5257
  textureStore(denoisedRadianceImage, pixel, vec4<f32>(color, 1.0));
3400
5258
  }
3401
5259
 
@@ -3409,8 +5267,10 @@ fn resolveDenoisedOutputImage(@builtin(global_invocation_id) globalId: vec3<u32>
3409
5267
 
3410
5268
  let pixel = vec2<i32>(i32(x), i32(y));
3411
5269
  let center = textureLoad(finalDenoiseInputRadiance, pixel, 0).xyz;
3412
- var sum = center * 1.25;
3413
- var totalWeight = 1.25;
5270
+ let strength = denoise_strength();
5271
+ let centerWeight = 1.35 - strength * 0.25;
5272
+ var sum = center * centerWeight;
5273
+ var totalWeight = centerWeight;
3414
5274
  let centerRange = denoise_range_space(center);
3415
5275
 
3416
5276
  for (var oy = -1i; oy <= 1i; oy = oy + 1i) {
@@ -3422,8 +5282,8 @@ fn resolveDenoisedOutputImage(@builtin(global_invocation_id) globalId: vec3<u32>
3422
5282
  let sy = clamp(i32(y) + oy, 0i, i32(config.canvasHeight) - 1i);
3423
5283
  let sampleColor = textureLoad(finalDenoiseInputRadiance, vec2<i32>(sx, sy), 0).xyz;
3424
5284
  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);
5285
+ let rangeWeight = 1.0 / (1.0 + colorDistance * (12.0 + strength * 8.0));
5286
+ let distanceWeight = 1.0 / (1.0 + f32(ox * ox + oy * oy) * (0.82 + strength * 0.28));
3427
5287
  let weight = rangeWeight * distanceWeight;
3428
5288
  sum = sum + sampleColor * weight;
3429
5289
  totalWeight = totalWeight + weight;
@@ -3431,8 +5291,9 @@ fn resolveDenoisedOutputImage(@builtin(global_invocation_id) globalId: vec3<u32>
3431
5291
  }
3432
5292
 
3433
5293
  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));
5294
+ let outlier = saturate(length(denoise_range_space(center) - denoise_range_space(filtered)) * 2.2);
5295
+ let blend = min(0.18, strength * (0.42 + outlier * 0.08));
5296
+ let radiance = min(mix(center, filtered, blend), vec3<f32>(16.0));
3436
5297
  textureStore(denoisedOutputImage, pixel, vec4<f32>(tone_map_radiance(radiance), 1.0));
3437
5298
  }
3438
5299
  `;
@@ -3499,104 +5360,34 @@ function readGpuLimit(adapter, device, name) {
3499
5360
  const deviceValue = Number(device?.limits?.[name]);
3500
5361
  return Number.isFinite(deviceValue) ? deviceValue : null;
3501
5362
  }
3502
- function createAdapterInfoSnapshot(adapter) {
3503
- const info = adapter?.info;
3504
- if (!info || typeof info !== "object") {
3505
- return null;
3506
- }
3507
- return Object.freeze({
3508
- vendor: typeof info.vendor === "string" ? info.vendor : "",
3509
- architecture: typeof info.architecture === "string" ? info.architecture : "",
3510
- 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"
5363
+ function createAdapterInfoSnapshot(adapter) {
5364
+ const info = adapter?.info;
5365
+ if (!info || typeof info !== "object") {
5366
+ return null;
5367
+ }
5368
+ return Object.freeze({
5369
+ vendor: typeof info.vendor === "string" ? info.vendor : "",
5370
+ architecture: typeof info.architecture === "string" ? info.architecture : "",
5371
+ device: typeof info.device === "string" ? info.device : "",
5372
+ description: typeof info.description === "string" ? info.description : ""
5373
+ });
5374
+ }
5375
+ function createGpuAdapterParallelismDiagnostics(adapter, device) {
5376
+ return Object.freeze({
5377
+ physicalCoreCount: null,
5378
+ physicalCoreCountAvailable: false,
5379
+ physicalCoreCountUnavailableReason: "WebGPU does not expose physical GPU core counts.",
5380
+ adapterInfo: createAdapterInfoSnapshot(adapter),
5381
+ adapterLimits: Object.freeze({
5382
+ maxComputeInvocationsPerWorkgroup: readGpuLimit(adapter, device, "maxComputeInvocationsPerWorkgroup"),
5383
+ maxComputeWorkgroupSizeX: readGpuLimit(adapter, device, "maxComputeWorkgroupSizeX"),
5384
+ maxComputeWorkgroupSizeY: readGpuLimit(adapter, device, "maxComputeWorkgroupSizeY"),
5385
+ maxComputeWorkgroupSizeZ: readGpuLimit(adapter, device, "maxComputeWorkgroupSizeZ"),
5386
+ maxComputeWorkgroupsPerDimension: readGpuLimit(adapter, device, "maxComputeWorkgroupsPerDimension"),
5387
+ maxStorageBuffersPerShaderStage: readGpuLimit(adapter, device, "maxStorageBuffersPerShaderStage"),
5388
+ maxStorageBufferBindingSize: readGpuLimit(adapter, device, "maxStorageBufferBindingSize")
5389
+ }),
5390
+ configuredWorkgroupSize: WORKGROUP_SIZE
3600
5391
  });
3601
5392
  }
3602
5393
  function createEnvironmentMapSnapshot(environmentMap) {
@@ -3604,12 +5395,38 @@ function createEnvironmentMapSnapshot(environmentMap) {
3604
5395
  enabled: environmentMap.enabled,
3605
5396
  width: environmentMap.width,
3606
5397
  height: environmentMap.height,
5398
+ mipLevelCount: environmentMap.mipLevelCount ?? 1,
3607
5399
  projection: environmentMap.projection,
3608
5400
  intensity: environmentMap.intensity,
3609
5401
  rotationRadians: environmentMap.rotationRadians,
3610
- ambientStrength: environmentMap.ambientStrength
5402
+ ambientStrength: environmentMap.ambientStrength,
5403
+ hasImportanceData: environmentMap.hasImportanceData === true
3611
5404
  });
3612
5405
  }
5406
+ function nowMs() {
5407
+ if (typeof performance !== "undefined" && typeof performance.now === "function") {
5408
+ return performance.now();
5409
+ }
5410
+ return Date.now();
5411
+ }
5412
+ function estimateSubmittedGpuWorkTimeoutMs(config, tileCount, overrideTimeoutMs = null) {
5413
+ if (Number.isFinite(overrideTimeoutMs)) {
5414
+ return Math.max(1, Math.trunc(Number(overrideTimeoutMs)));
5415
+ }
5416
+ const samplesPerPixel = Math.max(
5417
+ 1,
5418
+ Number(config?.renderedSamplesPerPixel ?? config?.samplesPerPixel ?? 1)
5419
+ );
5420
+ const maxDepth = Math.max(1, Number(config?.maxDepth ?? 1));
5421
+ const deferredResolvePasses = config?.deferredPathResolve ? 1 : 0;
5422
+ const denoisePasses = config?.denoise ? samplesPerPixel < 4 ? 2 : 1 : 0;
5423
+ const tiles = Math.max(1, Number(tileCount ?? 1));
5424
+ const estimatedPasses = tiles * (samplesPerPixel * (maxDepth + 1 + deferredResolvePasses) + denoisePasses + 1);
5425
+ return Math.min(
5426
+ GPU_MAX_SUBMITTED_WORK_TIMEOUT_MS,
5427
+ GPU_SUBMITTED_WORK_TIMEOUT_MS + estimatedPasses * 5
5428
+ );
5429
+ }
3613
5430
  async function createWavefrontPathTracingComputeRenderer(options = {}) {
3614
5431
  assertAnalyticDisplayQualityPolicy(options);
3615
5432
  const constants = getGpuUsageConstants();
@@ -3814,6 +5631,60 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3814
5631
  config.environmentMap,
3815
5632
  config.environmentColor
3816
5633
  );
5634
+ const environmentSamplingResource = createEnvironmentSamplingTextureResource(
5635
+ device,
5636
+ constants,
5637
+ config.environmentMap,
5638
+ config.environmentColor
5639
+ );
5640
+ config = Object.freeze({
5641
+ ...config,
5642
+ environmentMap: Object.freeze({
5643
+ ...config.environmentMap,
5644
+ width: environmentMapResource.width,
5645
+ height: environmentMapResource.height,
5646
+ mipLevelCount: environmentMapResource.mipLevelCount,
5647
+ hasImportanceData: environmentSamplingResource.hasImportanceData
5648
+ })
5649
+ });
5650
+ const brdfLutResource = createBrdfLutResource(device, constants);
5651
+ const baseColorAtlasResource = createAtlasTextureResource(
5652
+ device,
5653
+ constants,
5654
+ config.gpuMaterialSource.baseColorAtlas,
5655
+ "plasius.wavefront.materialAtlas.baseColor"
5656
+ );
5657
+ const metallicRoughnessAtlasResource = createAtlasTextureResource(
5658
+ device,
5659
+ constants,
5660
+ config.gpuMaterialSource.metallicRoughnessAtlas,
5661
+ "plasius.wavefront.materialAtlas.metallicRoughness"
5662
+ );
5663
+ const normalAtlasResource = createAtlasTextureResource(
5664
+ device,
5665
+ constants,
5666
+ config.gpuMaterialSource.normalAtlas,
5667
+ "plasius.wavefront.materialAtlas.normal"
5668
+ );
5669
+ const occlusionAtlasResource = createAtlasTextureResource(
5670
+ device,
5671
+ constants,
5672
+ config.gpuMaterialSource.occlusionAtlas,
5673
+ "plasius.wavefront.materialAtlas.occlusion"
5674
+ );
5675
+ const emissiveAtlasResource = createAtlasTextureResource(
5676
+ device,
5677
+ constants,
5678
+ config.gpuMaterialSource.emissiveAtlas,
5679
+ "plasius.wavefront.materialAtlas.emissive"
5680
+ );
5681
+ const materialAtlasSampler = device.createSampler({
5682
+ label: "plasius.wavefront.materialAtlasSampler",
5683
+ addressModeU: "clamp-to-edge",
5684
+ addressModeV: "clamp-to-edge",
5685
+ magFilter: "linear",
5686
+ minFilter: "linear"
5687
+ });
3817
5688
  const traceBindGroupLayout = device.createBindGroupLayout({
3818
5689
  label: "plasius.wavefront.traceBindGroupLayout",
3819
5690
  entries: [
@@ -3843,7 +5714,16 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3843
5714
  { binding: 19, visibility: constants.shader.COMPUTE, buffer: { type: "read-only-storage" } },
3844
5715
  { binding: 20, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
3845
5716
  { binding: 21, visibility: constants.shader.COMPUTE, sampler: { type: "filtering" } },
3846
- { binding: 22, visibility: constants.shader.COMPUTE, buffer: { type: "storage" } }
5717
+ { binding: 22, visibility: constants.shader.COMPUTE, buffer: { type: "storage" } },
5718
+ { binding: 23, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
5719
+ { binding: 24, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
5720
+ { binding: 25, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
5721
+ { binding: 26, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
5722
+ { binding: 27, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
5723
+ { binding: 28, visibility: constants.shader.COMPUTE, sampler: { type: "filtering" } },
5724
+ { binding: 29, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
5725
+ { binding: 30, visibility: constants.shader.COMPUTE, sampler: { type: "filtering" } },
5726
+ { binding: 31, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } }
3847
5727
  ]
3848
5728
  });
3849
5729
  const accelerationBindGroupLayout = device.createBindGroupLayout({
@@ -3922,6 +5802,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3922
5802
  label: "plasius.wavefront.computeShader",
3923
5803
  code: WAVEFRONT_COMPUTE_WGSL
3924
5804
  });
5805
+ await assertShaderModuleCompiles(computeShader, "plasius.wavefront.computeShader");
3925
5806
  const pipelines = {
3926
5807
  prepareMeshTrianglesAndLeaves: await createComputePipeline(
3927
5808
  device,
@@ -4020,7 +5901,16 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4020
5901
  { binding: 19, resource: { buffer: environmentPortalBuffer } },
4021
5902
  { binding: 20, resource: environmentMapResource.view },
4022
5903
  { binding: 21, resource: environmentMapResource.sampler },
4023
- { binding: 22, resource: { buffer: pathVertexBuffer } }
5904
+ { binding: 22, resource: { buffer: pathVertexBuffer } },
5905
+ { binding: 23, resource: baseColorAtlasResource.view },
5906
+ { binding: 24, resource: metallicRoughnessAtlasResource.view },
5907
+ { binding: 25, resource: normalAtlasResource.view },
5908
+ { binding: 26, resource: occlusionAtlasResource.view },
5909
+ { binding: 27, resource: emissiveAtlasResource.view },
5910
+ { binding: 28, resource: materialAtlasSampler },
5911
+ { binding: 29, resource: brdfLutResource.view },
5912
+ { binding: 30, resource: brdfLutResource.sampler },
5913
+ { binding: 31, resource: environmentSamplingResource.view }
4024
5914
  ]
4025
5915
  });
4026
5916
  }
@@ -4073,6 +5963,11 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4073
5963
  outputView,
4074
5964
  "plasius.wavefront.bind.denoise.scratchToOutput"
4075
5965
  );
5966
+ const denoiseDirectResolveBindGroup = createDenoiseResolveBindGroup(
5967
+ radianceView,
5968
+ outputView,
5969
+ "plasius.wavefront.bind.denoise.radianceToOutput"
5970
+ );
4076
5971
  const presentBindGroupLayout = device.createBindGroupLayout({
4077
5972
  label: "plasius.wavefront.presentBindGroupLayout",
4078
5973
  entries: [
@@ -4110,23 +6005,128 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4110
6005
  let accelerationBuilt = !config.gpuAccelerationBuildRequired;
4111
6006
  let accelerationBuildCount = 0;
4112
6007
  let activeCameraOptions = options.camera ?? DEFAULT_CAMERA;
6008
+ let lastCompletedFrameTimeMs = null;
6009
+ let lastCompletedSamplesPerPixel = Math.max(1, config.samplesPerPixel);
4113
6010
  let lastGpuParallelism = createGpuParallelismDiagnostics(
4114
6011
  gpuAdapterParallelism,
4115
6012
  createGpuParallelismCounters()
4116
6013
  );
6014
+ function resolveRenderedSamplesPerPixel(renderOptions = {}, awaitGPUCompletion = true) {
6015
+ const targetSamplesPerPixel = clamp(
6016
+ readPositiveInteger(
6017
+ "samplesPerPixel",
6018
+ renderOptions.samplesPerPixel,
6019
+ config.samplesPerPixel
6020
+ ),
6021
+ 1,
6022
+ config.samplesPerPixel
6023
+ );
6024
+ const frameTimeBudgetMs = Number.isFinite(renderOptions.frameTimeBudgetMs) ? Math.max(0, Number(renderOptions.frameTimeBudgetMs)) : null;
6025
+ const minimumSamplesPerPixel = clamp(
6026
+ readPositiveInteger(
6027
+ "minimumSamplesPerPixel",
6028
+ renderOptions.minimumSamplesPerPixel,
6029
+ frameTimeBudgetMs !== null && targetSamplesPerPixel > 1 ? 1 : targetSamplesPerPixel
6030
+ ),
6031
+ 1,
6032
+ targetSamplesPerPixel
6033
+ );
6034
+ if (frameTimeBudgetMs === null || !awaitGPUCompletion || targetSamplesPerPixel <= minimumSamplesPerPixel) {
6035
+ return Object.freeze({
6036
+ renderedSamplesPerPixel: targetSamplesPerPixel,
6037
+ targetSamplesPerPixel,
6038
+ minimumSamplesPerPixel,
6039
+ frameTimeBudgetMs,
6040
+ budgetConstrained: false
6041
+ });
6042
+ }
6043
+ const estimatedSampleTimeMs = Number.isFinite(lastCompletedFrameTimeMs) && lastCompletedFrameTimeMs > 0 ? lastCompletedFrameTimeMs / Math.max(1, lastCompletedSamplesPerPixel) : null;
6044
+ if (!Number.isFinite(estimatedSampleTimeMs) || estimatedSampleTimeMs <= 0) {
6045
+ return Object.freeze({
6046
+ renderedSamplesPerPixel: minimumSamplesPerPixel,
6047
+ targetSamplesPerPixel,
6048
+ minimumSamplesPerPixel,
6049
+ frameTimeBudgetMs,
6050
+ budgetConstrained: minimumSamplesPerPixel < targetSamplesPerPixel
6051
+ });
6052
+ }
6053
+ const budgetLimitedSamples = clamp(
6054
+ Math.floor(frameTimeBudgetMs / estimatedSampleTimeMs),
6055
+ minimumSamplesPerPixel,
6056
+ targetSamplesPerPixel
6057
+ );
6058
+ return Object.freeze({
6059
+ renderedSamplesPerPixel: budgetLimitedSamples,
6060
+ targetSamplesPerPixel,
6061
+ minimumSamplesPerPixel,
6062
+ frameTimeBudgetMs,
6063
+ budgetConstrained: budgetLimitedSamples < targetSamplesPerPixel
6064
+ });
6065
+ }
6066
+ function createFrameStats({
6067
+ frameIndex,
6068
+ accelerationBuildSubmitted,
6069
+ frameSubmissionCount,
6070
+ parallelismCounters,
6071
+ renderedSamplesPerPixel,
6072
+ targetSamplesPerPixel,
6073
+ frameTimeBudgetMs,
6074
+ budgetConstrained
6075
+ }) {
6076
+ lastGpuParallelism = createGpuParallelismDiagnostics(gpuAdapterParallelism, parallelismCounters);
6077
+ const commandSubmissions = frameSubmissionCount + (accelerationBuildSubmitted ? 1 : 0);
6078
+ return Object.freeze({
6079
+ frame,
6080
+ frameIndex,
6081
+ width: config.width,
6082
+ height: config.height,
6083
+ maxDepth: config.maxDepth,
6084
+ tiles: tiles.length,
6085
+ tileSize: config.tileSize,
6086
+ samplesPerPixel: targetSamplesPerPixel,
6087
+ renderedSamplesPerPixel,
6088
+ frameTimeBudgetMs,
6089
+ budgetConstrained,
6090
+ maxFramePassesPerSubmission: config.maxFramePassesPerSubmission,
6091
+ screenRays: config.width * config.height,
6092
+ primaryRays: config.width * config.height * renderedSamplesPerPixel,
6093
+ sceneObjectCount: config.sceneObjectCount,
6094
+ triangleCount: config.triangleCount,
6095
+ emissiveTriangleCount: config.emissiveTriangleCount,
6096
+ environmentPortalCount: config.environmentPortalCount,
6097
+ environmentPortalMode: config.environmentPortalMode,
6098
+ environmentMap: createEnvironmentMapSnapshot(config.environmentMap),
6099
+ deferredPathResolve: config.deferredPathResolve,
6100
+ bvhNodeCount: config.bvhNodeCount,
6101
+ displayQuality: config.displayQuality,
6102
+ accelerationBuildMode: config.accelerationBuildMode,
6103
+ gpuAccelerationBuildRequired: config.gpuAccelerationBuildRequired,
6104
+ accelerationBuildSubmitted,
6105
+ accelerationBuilt,
6106
+ accelerationBuildCount,
6107
+ commandSubmissions,
6108
+ frameConfigSlots: frameConfigSlotCount,
6109
+ gpuParallelism: lastGpuParallelism,
6110
+ memory: config.memory
6111
+ });
6112
+ }
6113
+ function writeFrameConfigSlot(slot, tile, frameIndex, buildRange = {}) {
6114
+ if (slot >= frameConfigSlotCount) {
6115
+ throw new Error("Wavefront frame config slot capacity exceeded.");
6116
+ }
6117
+ const offset = slot * configBufferStride;
6118
+ device.queue.writeBuffer(
6119
+ configBuffer,
6120
+ offset,
6121
+ createConfigPayload(config, tile, frameIndex, buildRange)
6122
+ );
6123
+ return offset;
6124
+ }
4117
6125
  function createFrameConfigWriter(frameIndex) {
4118
6126
  let slot = 0;
4119
6127
  return (tile, buildRange = {}) => {
4120
- if (slot >= frameConfigSlotCount) {
4121
- throw new Error("Wavefront frame config slot capacity exceeded.");
4122
- }
4123
- const offset = slot * configBufferStride;
6128
+ const offset = writeFrameConfigSlot(slot, tile, frameIndex, buildRange);
4124
6129
  slot += 1;
4125
- device.queue.writeBuffer(
4126
- configBuffer,
4127
- offset,
4128
- createConfigPayload(config, tile, frameIndex, buildRange)
4129
- );
4130
6130
  return offset;
4131
6131
  };
4132
6132
  }
@@ -4171,7 +6171,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4171
6171
  passEncoder.setPipeline(pipelines.prepareMeshTrianglesAndLeaves);
4172
6172
  const prepareWorkgroups = Math.ceil(config.bvhLeafSortCapacity / WORKGROUP_SIZE);
4173
6173
  passEncoder.dispatchWorkgroups(prepareWorkgroups);
4174
- recordDirectDispatch(parallelism, [prepareWorkgroups]);
6174
+ recordDirectDispatch(parallelism, [prepareWorkgroups], WORKGROUP_SIZE);
4175
6175
  passEncoder.setPipeline(pipelines.sortBvhLeafRefs);
4176
6176
  for (let stageIndex = 0; stageIndex < config.bvhSortStages.length; stageIndex += 1) {
4177
6177
  passEncoder.setBindGroup(0, bvhBuildBindGroup, [
@@ -4179,13 +6179,13 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4179
6179
  ]);
4180
6180
  const sortWorkgroups = Math.ceil(config.bvhLeafSortCapacity / WORKGROUP_SIZE);
4181
6181
  passEncoder.dispatchWorkgroups(sortWorkgroups);
4182
- recordDirectDispatch(parallelism, [sortWorkgroups]);
6182
+ recordDirectDispatch(parallelism, [sortWorkgroups], WORKGROUP_SIZE);
4183
6183
  }
4184
6184
  passEncoder.setBindGroup(0, bvhBuildBindGroup, [0]);
4185
6185
  passEncoder.setPipeline(pipelines.writeSortedBvhLeaves);
4186
6186
  const leafWriteWorkgroups = Math.ceil(config.triangleCount / WORKGROUP_SIZE);
4187
6187
  passEncoder.dispatchWorkgroups(leafWriteWorkgroups);
4188
- recordDirectDispatch(parallelism, [leafWriteWorkgroups]);
6188
+ recordDirectDispatch(parallelism, [leafWriteWorkgroups], WORKGROUP_SIZE);
4189
6189
  passEncoder.setPipeline(pipelines.buildBvhInternalLevel);
4190
6190
  for (let levelIndex = 0; levelIndex < config.bvhBuildLevels.length; levelIndex += 1) {
4191
6191
  const buildLevel = config.bvhBuildLevels[levelIndex];
@@ -4194,7 +6194,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4194
6194
  ]);
4195
6195
  const levelWorkgroups = Math.ceil(buildLevel.count / WORKGROUP_SIZE);
4196
6196
  passEncoder.dispatchWorkgroups(levelWorkgroups);
4197
- recordDirectDispatch(parallelism, [levelWorkgroups]);
6197
+ recordDirectDispatch(parallelism, [levelWorkgroups], WORKGROUP_SIZE);
4198
6198
  }
4199
6199
  passEncoder.end();
4200
6200
  device.queue.submit([encoder.finish()]);
@@ -4210,7 +6210,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4210
6210
  generatePass.setBindGroup(0, bindGroups[0], [configOffset]);
4211
6211
  generatePass.setPipeline(pipelines.generatePrimaryRays);
4212
6212
  generatePass.dispatchWorkgroups(tileWorkgroups);
4213
- recordDirectDispatch(parallelism, [tileWorkgroups]);
6213
+ recordDirectDispatch(parallelism, [tileWorkgroups], WORKGROUP_SIZE);
4214
6214
  generatePass.end();
4215
6215
  for (let bounceIndex = 0; bounceIndex < config.maxDepth; bounceIndex += 1) {
4216
6216
  encoder.copyBufferToBuffer(
@@ -4226,10 +6226,10 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4226
6226
  passEncoder.setBindGroup(0, bindGroups[bounceIndex % 2], [configOffset]);
4227
6227
  passEncoder.setPipeline(pipelines.intersectActiveQueue);
4228
6228
  passEncoder.dispatchWorkgroupsIndirect(activeDispatchBuffer, 0);
4229
- recordIndirectDispatch(parallelism, tileWorkgroups);
6229
+ recordIndirectDispatch(parallelism, tileWorkgroups, WORKGROUP_SIZE);
4230
6230
  passEncoder.setPipeline(pipelines.resolveSurfaceRecords);
4231
6231
  passEncoder.dispatchWorkgroupsIndirect(activeDispatchBuffer, 0);
4232
- recordIndirectDispatch(parallelism, tileWorkgroups);
6232
+ recordIndirectDispatch(parallelism, tileWorkgroups, WORKGROUP_SIZE);
4233
6233
  passEncoder.setPipeline(pipelines.compactAndSwapQueues);
4234
6234
  passEncoder.dispatchWorkgroups(1);
4235
6235
  recordDirectDispatch(parallelism, [1], 1);
@@ -4244,30 +6244,45 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4244
6244
  passEncoder.setBindGroup(0, bindGroups[0], [configOffset]);
4245
6245
  passEncoder.setPipeline(pipelines.accumulateTerminalRadiance);
4246
6246
  passEncoder.dispatchWorkgroups(tileWorkgroups);
4247
- recordDirectDispatch(parallelism, [tileWorkgroups]);
6247
+ recordDirectDispatch(parallelism, [tileWorkgroups], WORKGROUP_SIZE);
4248
6248
  passEncoder.end();
4249
6249
  }
4250
- function encodeDenoise(encoder, configOffset, parallelism) {
6250
+ function encodeDenoise(encoder, configOffset, parallelism, renderedSamplesPerPixel = config.samplesPerPixel) {
4251
6251
  if (!config.denoise) {
4252
6252
  return;
4253
6253
  }
4254
6254
  const denoiseWorkgroupsX = Math.ceil(config.width / 8);
4255
6255
  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();
6256
+ const useTwoPassDenoise = renderedSamplesPerPixel < 4;
6257
+ if (useTwoPassDenoise) {
6258
+ const radiancePass = encoder.beginComputePass({
6259
+ label: "plasius.wavefront.denoiseRadiancePass"
6260
+ });
6261
+ radiancePass.setBindGroup(0, denoiseRadianceBindGroup, [configOffset]);
6262
+ radiancePass.setPipeline(pipelines.denoiseLinearRadiance);
6263
+ radiancePass.dispatchWorkgroups(denoiseWorkgroupsX, denoiseWorkgroupsY);
6264
+ recordDirectDispatch(
6265
+ parallelism,
6266
+ [denoiseWorkgroupsX, denoiseWorkgroupsY],
6267
+ WORKGROUP_SIZE
6268
+ );
6269
+ radiancePass.end();
6270
+ }
4264
6271
  const resolvePass = encoder.beginComputePass({
4265
6272
  label: "plasius.wavefront.denoiseResolvePass"
4266
6273
  });
4267
- resolvePass.setBindGroup(0, denoiseResolveBindGroup, [configOffset]);
6274
+ resolvePass.setBindGroup(
6275
+ 0,
6276
+ useTwoPassDenoise ? denoiseResolveBindGroup : denoiseDirectResolveBindGroup,
6277
+ [configOffset]
6278
+ );
4268
6279
  resolvePass.setPipeline(pipelines.resolveDenoisedOutputImage);
4269
6280
  resolvePass.dispatchWorkgroups(denoiseWorkgroupsX, denoiseWorkgroupsY);
4270
- recordDirectDispatch(parallelism, [denoiseWorkgroupsX, denoiseWorkgroupsY]);
6281
+ recordDirectDispatch(
6282
+ parallelism,
6283
+ [denoiseWorkgroupsX, denoiseWorkgroupsY],
6284
+ WORKGROUP_SIZE
6285
+ );
4271
6286
  resolvePass.end();
4272
6287
  }
4273
6288
  function encodePresent(encoder) {
@@ -4288,98 +6303,213 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4288
6303
  passEncoder.draw(3);
4289
6304
  passEncoder.end();
4290
6305
  }
4291
- function dispatchFrame(frameIndex, parallelism) {
6306
+ function dispatchFrame(frameIndex, parallelism, renderedSamplesPerPixel = config.samplesPerPixel) {
4292
6307
  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}`
6308
+ const batch = createGpuSubmissionBatcher({
6309
+ device,
6310
+ frameIndex,
6311
+ maxFramePassesPerSubmission: config.maxFramePassesPerSubmission
4297
6312
  });
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
6313
  for (const tile of tiles) {
4317
- for (let sampleIndex = 0; sampleIndex < config.samplesPerPixel; sampleIndex += 1) {
6314
+ for (let sampleIndex = 0; sampleIndex < renderedSamplesPerPixel; sampleIndex += 1) {
4318
6315
  const configOffset = writeFrameConfig(tile, {
4319
6316
  sampleIndex,
4320
- sampleWeight: 1 / config.samplesPerPixel
6317
+ sampleWeight: 1 / renderedSamplesPerPixel
4321
6318
  });
4322
- encodeTileSample(reserveEncoder(), tile, configOffset, parallelism);
6319
+ encodeTileSample(
6320
+ batch.reserve(config.maxDepth + 1),
6321
+ tile,
6322
+ configOffset,
6323
+ parallelism
6324
+ );
4323
6325
  if (config.deferredPathResolve) {
4324
- encodeTileOutput(reserveEncoder(), tile, configOffset, parallelism);
6326
+ encodeTileOutput(batch.reserve(1), tile, configOffset, parallelism);
4325
6327
  }
4326
6328
  }
4327
6329
  if (!config.deferredPathResolve) {
4328
6330
  const outputConfigOffset = writeFrameConfig(tile, {
4329
6331
  sampleIndex: 0,
4330
- sampleWeight: 1 / config.samplesPerPixel
6332
+ sampleWeight: 1 / renderedSamplesPerPixel
4331
6333
  });
4332
- encodeTileOutput(reserveEncoder(), tile, outputConfigOffset, parallelism);
6334
+ encodeTileOutput(batch.reserve(1), tile, outputConfigOffset, parallelism);
4333
6335
  }
4334
6336
  }
4335
6337
  if (config.denoise) {
4336
6338
  const denoiseConfigOffset = writeFrameConfig(
4337
6339
  { x: 0, y: 0, width: config.width, height: config.height },
4338
- { sampleIndex: 0, sampleWeight: 1 / config.samplesPerPixel }
6340
+ { sampleIndex: 0, sampleWeight: 1 / renderedSamplesPerPixel }
6341
+ );
6342
+ const denoisePassCount = renderedSamplesPerPixel < 4 ? 2 : 1;
6343
+ encodeDenoise(
6344
+ batch.reserve(denoisePassCount),
6345
+ denoiseConfigOffset,
6346
+ parallelism,
6347
+ renderedSamplesPerPixel
4339
6348
  );
4340
- encodeDenoise(reserveEncoder(), denoiseConfigOffset, parallelism);
4341
6349
  }
4342
- encodePresent(reserveEncoder());
4343
- submitCurrentEncoder();
4344
- return submissionCount;
6350
+ encodePresent(batch.reserve(1));
6351
+ return batch.flush();
4345
6352
  }
4346
- function renderOnce() {
6353
+ function renderOnce(renderOptions = {}, resolvedSamplingPlan = null) {
6354
+ const frameStartTimeMs = nowMs();
4347
6355
  frame += 1;
4348
6356
  const frameIndex = frame + config.frameIndex;
6357
+ const samplingPlan = resolvedSamplingPlan ?? resolveRenderedSamplesPerPixel(renderOptions, false);
4349
6358
  const parallelismCounters = createGpuParallelismCounters();
4350
6359
  const accelerationBuildSubmitted = dispatchGpuAccelerationBuild(frameIndex, parallelismCounters);
4351
- const frameSubmissionCount = dispatchFrame(frameIndex, parallelismCounters);
4352
- lastGpuParallelism = createGpuParallelismDiagnostics(gpuAdapterParallelism, parallelismCounters);
6360
+ const frameSubmissionCount = dispatchFrame(
6361
+ frameIndex,
6362
+ parallelismCounters,
6363
+ samplingPlan.renderedSamplesPerPixel
6364
+ );
6365
+ const frameTimeMs = Math.max(0, nowMs() - frameStartTimeMs);
4353
6366
  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,
6367
+ ...createFrameStats({
6368
+ frameIndex,
6369
+ accelerationBuildSubmitted,
6370
+ frameSubmissionCount,
6371
+ parallelismCounters,
6372
+ renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel,
6373
+ targetSamplesPerPixel: samplingPlan.targetSamplesPerPixel,
6374
+ frameTimeBudgetMs: samplingPlan.frameTimeBudgetMs,
6375
+ budgetConstrained: samplingPlan.budgetConstrained
6376
+ }),
6377
+ gpuWorkerJobs: createGpuWorkerJobDiagnostics(
6378
+ lastGpuParallelism,
6379
+ frameSubmissionCount + (accelerationBuildSubmitted ? 1 : 0),
6380
+ frameTimeMs,
6381
+ false
6382
+ )
6383
+ });
6384
+ }
6385
+ async function waitForSubmittedGpuWork(options2 = {}) {
6386
+ if (typeof device.queue.onSubmittedWorkDone !== "function") {
6387
+ return true;
6388
+ }
6389
+ const timeoutMs = Math.max(
6390
+ 1,
6391
+ Number.isFinite(options2.timeoutMs) ? Number(options2.timeoutMs) : GPU_SUBMITTED_WORK_TIMEOUT_MS
6392
+ );
6393
+ const allowTimeout = options2.allowTimeout !== false;
6394
+ const completionPromise = device.queue.onSubmittedWorkDone().then(
6395
+ () => ({ status: "done" }),
6396
+ (error) => {
6397
+ throw error;
6398
+ }
6399
+ );
6400
+ const lossPromise = typeof device.lost?.then === "function" ? device.lost.then((info) => {
6401
+ throw new Error(
6402
+ `WebGPU device lost while waiting for submitted work (${info?.reason ?? "unknown"}).`
6403
+ );
6404
+ }) : null;
6405
+ let timeoutHandle = null;
6406
+ let resolveTimeoutPromise = null;
6407
+ let timeoutSettled = false;
6408
+ const settleTimeoutPromise = (value) => {
6409
+ if (timeoutSettled) {
6410
+ return;
6411
+ }
6412
+ timeoutSettled = true;
6413
+ resolveTimeoutPromise?.(value);
6414
+ };
6415
+ const timeoutPromise = new Promise((resolve) => {
6416
+ resolveTimeoutPromise = resolve;
6417
+ timeoutHandle = setTimeout(() => settleTimeoutPromise({ status: "timeout" }), timeoutMs);
6418
+ });
6419
+ let result;
6420
+ try {
6421
+ result = await Promise.race(
6422
+ [completionPromise, timeoutPromise, lossPromise].filter(Boolean)
6423
+ );
6424
+ } finally {
6425
+ if (timeoutHandle !== null) {
6426
+ clearTimeout(timeoutHandle);
6427
+ settleTimeoutPromise({ status: "cancelled" });
6428
+ }
6429
+ }
6430
+ if (result?.status === "timeout") {
6431
+ if (!allowTimeout) {
6432
+ throw new Error(`Timed out after ${timeoutMs} ms waiting for submitted GPU work.`);
6433
+ }
6434
+ console.warn(
6435
+ `[plasius.wavefront] Submitted GPU work did not report completion within ${timeoutMs} ms; continuing.`
6436
+ );
6437
+ return false;
6438
+ }
6439
+ return true;
6440
+ }
6441
+ function dispatchFrameAwaitingGpu(frameIndex, parallelism, renderedSamplesPerPixel = config.samplesPerPixel) {
6442
+ const samplePassesPerSample = config.maxDepth + 1 + (config.deferredPathResolve ? 1 : 0);
6443
+ const denoisePassCount = config.denoise ? renderedSamplesPerPixel < 4 ? 2 : 1 : 0;
6444
+ const tailPassCount = denoisePassCount + 1;
6445
+ const sampleBatchSize = Math.max(
6446
+ 1,
6447
+ Math.floor(
6448
+ Math.max(config.maxFramePassesPerSubmission - tailPassCount, 1) / Math.max(samplePassesPerSample, 1)
6449
+ )
6450
+ );
6451
+ let submissionCount = 0;
6452
+ for (const tile of tiles) {
6453
+ for (let sampleStart = 0; sampleStart < renderedSamplesPerPixel; sampleStart += sampleBatchSize) {
6454
+ const sampleEnd = Math.min(renderedSamplesPerPixel, sampleStart + sampleBatchSize);
6455
+ const batch = createGpuSubmissionBatcher({
6456
+ device,
6457
+ frameIndex,
6458
+ maxFramePassesPerSubmission: config.maxFramePassesPerSubmission,
6459
+ startingSubmissionCount: submissionCount
6460
+ });
6461
+ let slot = 0;
6462
+ for (let sampleIndex = sampleStart; sampleIndex < sampleEnd; sampleIndex += 1) {
6463
+ const configOffset = writeFrameConfigSlot(slot, tile, frameIndex, {
6464
+ sampleIndex,
6465
+ sampleWeight: 1 / renderedSamplesPerPixel
6466
+ });
6467
+ slot += 1;
6468
+ encodeTileSample(
6469
+ batch.reserve(config.maxDepth + 1),
6470
+ tile,
6471
+ configOffset,
6472
+ parallelism
6473
+ );
6474
+ if (config.deferredPathResolve) {
6475
+ encodeTileOutput(batch.reserve(1), tile, configOffset, parallelism);
6476
+ }
6477
+ }
6478
+ if (!config.deferredPathResolve && sampleEnd >= renderedSamplesPerPixel) {
6479
+ const outputConfigOffset = writeFrameConfigSlot(slot, tile, frameIndex, {
6480
+ sampleIndex: 0,
6481
+ sampleWeight: 1 / renderedSamplesPerPixel
6482
+ });
6483
+ encodeTileOutput(batch.reserve(1), tile, outputConfigOffset, parallelism);
6484
+ }
6485
+ batch.flush();
6486
+ submissionCount += batch.getSubmissionCount();
6487
+ }
6488
+ }
6489
+ const tail = createGpuSubmissionBatcher({
6490
+ device,
6491
+ frameIndex,
4361
6492
  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
6493
+ startingSubmissionCount: submissionCount
4382
6494
  });
6495
+ if (config.denoise) {
6496
+ const denoiseConfigOffset = writeFrameConfigSlot(
6497
+ 0,
6498
+ { x: 0, y: 0, width: config.width, height: config.height },
6499
+ frameIndex,
6500
+ { sampleIndex: 0, sampleWeight: 1 / renderedSamplesPerPixel }
6501
+ );
6502
+ encodeDenoise(
6503
+ tail.reserve(denoisePassCount),
6504
+ denoiseConfigOffset,
6505
+ parallelism,
6506
+ renderedSamplesPerPixel
6507
+ );
6508
+ }
6509
+ encodePresent(tail.reserve(1));
6510
+ tail.flush();
6511
+ submissionCount += tail.getSubmissionCount();
6512
+ return submissionCount;
4383
6513
  }
4384
6514
  async function readOutputProbe(optionsForProbe = {}) {
4385
6515
  const mapMode = constants.map;
@@ -4393,6 +6523,10 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4393
6523
  size: 256,
4394
6524
  usage: constants.buffer.COPY_DST | constants.buffer.MAP_READ
4395
6525
  });
6526
+ await waitForSubmittedGpuWork({
6527
+ timeoutMs: GPU_READBACK_COMPLETION_TIMEOUT_MS,
6528
+ allowTimeout: false
6529
+ });
4396
6530
  const encoder = device.createCommandEncoder({
4397
6531
  label: "plasius.wavefront.outputProbe.copy"
4398
6532
  });
@@ -4402,6 +6536,10 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4402
6536
  { width: 1, height: 1, depthOrArrayLayers: 1 }
4403
6537
  );
4404
6538
  device.queue.submit([encoder.finish()]);
6539
+ await waitForSubmittedGpuWork({
6540
+ timeoutMs: GPU_READBACK_COMPLETION_TIMEOUT_MS,
6541
+ allowTimeout: false
6542
+ });
4405
6543
  await readback.mapAsync(mapMode.READ);
4406
6544
  const bytes = new Uint8Array(readback.getMappedRange()).slice(0, 4);
4407
6545
  readback.unmap();
@@ -4414,7 +6552,57 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4414
6552
  });
4415
6553
  }
4416
6554
  async function renderFrame(renderOptions = {}) {
4417
- const frameStats = renderOnce();
6555
+ const awaitGPUCompletion = renderOptions.awaitGPUCompletion !== false;
6556
+ const samplingPlan = resolveRenderedSamplesPerPixel(renderOptions, awaitGPUCompletion);
6557
+ const useThrottledHighSamplePath = awaitGPUCompletion && samplingPlan.renderedSamplesPerPixel >= 8;
6558
+ const submittedWorkTimeoutMs = estimateSubmittedGpuWorkTimeoutMs(
6559
+ { ...config, renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel },
6560
+ tiles.length,
6561
+ renderOptions.submittedWorkTimeoutMs
6562
+ );
6563
+ const frameStartTimeMs = nowMs();
6564
+ const submissionWaitOptions = awaitGPUCompletion ? { timeoutMs: submittedWorkTimeoutMs, allowTimeout: false } : { timeoutMs: submittedWorkTimeoutMs };
6565
+ let frameStats;
6566
+ if (useThrottledHighSamplePath) {
6567
+ frame += 1;
6568
+ const frameIndex = frame + config.frameIndex;
6569
+ const parallelismCounters = createGpuParallelismCounters();
6570
+ const accelerationBuildSubmitted = dispatchGpuAccelerationBuild(frameIndex, parallelismCounters);
6571
+ const frameSubmissionCount = dispatchFrameAwaitingGpu(
6572
+ frameIndex,
6573
+ parallelismCounters,
6574
+ samplingPlan.renderedSamplesPerPixel
6575
+ );
6576
+ frameStats = createFrameStats({
6577
+ frameIndex,
6578
+ accelerationBuildSubmitted,
6579
+ frameSubmissionCount,
6580
+ parallelismCounters,
6581
+ renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel,
6582
+ targetSamplesPerPixel: samplingPlan.targetSamplesPerPixel,
6583
+ frameTimeBudgetMs: samplingPlan.frameTimeBudgetMs,
6584
+ budgetConstrained: samplingPlan.budgetConstrained
6585
+ });
6586
+ } else {
6587
+ frameStats = renderOnce(renderOptions, samplingPlan);
6588
+ }
6589
+ if (awaitGPUCompletion) {
6590
+ await waitForSubmittedGpuWork(submissionWaitOptions);
6591
+ }
6592
+ const frameTimeMs = Math.max(0, nowMs() - frameStartTimeMs);
6593
+ if (awaitGPUCompletion) {
6594
+ lastCompletedFrameTimeMs = frameTimeMs;
6595
+ lastCompletedSamplesPerPixel = frameStats.renderedSamplesPerPixel ?? frameStats.samplesPerPixel;
6596
+ }
6597
+ frameStats = Object.freeze({
6598
+ ...frameStats,
6599
+ gpuWorkerJobs: createGpuWorkerJobDiagnostics(
6600
+ frameStats.gpuParallelism,
6601
+ frameStats.commandSubmissions,
6602
+ frameTimeMs,
6603
+ awaitGPUCompletion
6604
+ )
6605
+ });
4418
6606
  const probe = renderOptions.readOutputProbe === false ? null : await readOutputProbe(renderOptions.probe);
4419
6607
  const maxChannel = probe ? Math.max(...probe.rgba.slice(0, 3)) : 0;
4420
6608
  return Object.freeze({
@@ -4435,10 +6623,8 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4435
6623
  queueOverflow: 0
4436
6624
  });
4437
6625
  }
4438
- function updateSceneObjects(sceneObjects) {
4439
- const nextPackedScene = packWavefrontSceneObjects(sceneObjects, config.sceneObjectCapacity);
4440
- packedScene = nextPackedScene;
4441
- config = createWavefrontPathTracingComputeConfig({
6626
+ function rebuildLiveConfig(overrides = {}) {
6627
+ return createWavefrontPathTracingComputeConfig({
4442
6628
  ...options,
4443
6629
  canvas,
4444
6630
  width: config.width,
@@ -4449,26 +6635,23 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4449
6635
  sceneObjectCapacity: config.sceneObjectCapacity,
4450
6636
  sceneObjects: packedScene.objects,
4451
6637
  camera: activeCameraOptions,
4452
- frameIndex: config.frameIndex
6638
+ environmentMap: {
6639
+ ...config.environmentMap
6640
+ },
6641
+ frameIndex: config.frameIndex,
6642
+ ...overrides
4453
6643
  });
6644
+ }
6645
+ function updateSceneObjects(sceneObjects) {
6646
+ const nextPackedScene = packWavefrontSceneObjects(sceneObjects, config.sceneObjectCapacity);
6647
+ packedScene = nextPackedScene;
6648
+ config = rebuildLiveConfig();
4454
6649
  device.queue.writeBuffer(sceneObjectBuffer, 0, packedScene.buffer);
4455
6650
  return config;
4456
6651
  }
4457
6652
  function updateCamera(cameraOptions = {}) {
4458
6653
  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
- });
6654
+ config = rebuildLiveConfig();
4472
6655
  return config;
4473
6656
  }
4474
6657
  function getSnapshot() {
@@ -4523,6 +6706,25 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4523
6706
  if (environmentMapResource.ownsTexture) {
4524
6707
  environmentMapResource.texture?.destroy?.();
4525
6708
  }
6709
+ if (environmentSamplingResource.ownsTexture) {
6710
+ environmentSamplingResource.texture?.destroy?.();
6711
+ }
6712
+ brdfLutResource.texture?.destroy?.();
6713
+ if (baseColorAtlasResource.ownsTexture) {
6714
+ baseColorAtlasResource.texture?.destroy?.();
6715
+ }
6716
+ if (metallicRoughnessAtlasResource.ownsTexture) {
6717
+ metallicRoughnessAtlasResource.texture?.destroy?.();
6718
+ }
6719
+ if (normalAtlasResource.ownsTexture) {
6720
+ normalAtlasResource.texture?.destroy?.();
6721
+ }
6722
+ if (occlusionAtlasResource.ownsTexture) {
6723
+ occlusionAtlasResource.texture?.destroy?.();
6724
+ }
6725
+ if (emissiveAtlasResource.ownsTexture) {
6726
+ emissiveAtlasResource.texture?.destroy?.();
6727
+ }
4526
6728
  context.unconfigure?.();
4527
6729
  }
4528
6730
  return Object.freeze({
@@ -4675,6 +6877,48 @@ var rendererAccelerationStructurePolicies = Object.freeze(
4675
6877
  })
4676
6878
  )
4677
6879
  );
6880
+ function clampWavefrontAdaptiveSamplesPerPixel(value) {
6881
+ if (!Number.isFinite(value)) {
6882
+ return 1;
6883
+ }
6884
+ return Math.max(1, Math.min(256, Math.round(value)));
6885
+ }
6886
+ function createWavefrontAdaptiveSamplingLevels(options = {}) {
6887
+ const requestedSamplesPerPixel = clampWavefrontAdaptiveSamplesPerPixel(
6888
+ options.samplesPerPixel ?? 1
6889
+ );
6890
+ const minimumSamplesPerPixel = Math.min(
6891
+ requestedSamplesPerPixel,
6892
+ clampWavefrontAdaptiveSamplesPerPixel(options.minimumSamplesPerPixel ?? 1)
6893
+ );
6894
+ const frameTimeBudgetMs = Number.isFinite(options.frameTimeBudgetMs) ? Math.max(0, Number(options.frameTimeBudgetMs)) : 0;
6895
+ const levels = /* @__PURE__ */ new Set([minimumSamplesPerPixel, requestedSamplesPerPixel]);
6896
+ let currentSamplesPerPixel = minimumSamplesPerPixel;
6897
+ while (currentSamplesPerPixel < requestedSamplesPerPixel) {
6898
+ levels.add(currentSamplesPerPixel);
6899
+ currentSamplesPerPixel *= 2;
6900
+ }
6901
+ levels.add(Math.min(currentSamplesPerPixel, requestedSamplesPerPixel));
6902
+ return Object.freeze({
6903
+ requestedSamplesPerPixel,
6904
+ minimumSamplesPerPixel,
6905
+ frameTimeBudgetMs,
6906
+ levels: Object.freeze(
6907
+ [...levels].sort((left, right) => left - right).map(
6908
+ (samplesPerPixel) => Object.freeze({
6909
+ id: `${samplesPerPixel}spp`,
6910
+ label: `${samplesPerPixel} spp`,
6911
+ estimatedCostMs: samplesPerPixel,
6912
+ config: Object.freeze({
6913
+ samplesPerPixel,
6914
+ frameTimeBudgetMs,
6915
+ minimumSamplesPerPixel
6916
+ })
6917
+ })
6918
+ )
6919
+ )
6920
+ });
6921
+ }
4678
6922
  function createWavefrontField(name, type, description) {
4679
6923
  return Object.freeze({
4680
6924
  name,
@@ -5851,9 +8095,11 @@ export {
5851
8095
  createGpuRenderer,
5852
8096
  createRayTracingRenderPlan,
5853
8097
  createRendererDebugHooks,
8098
+ createWavefrontAdaptiveSamplingLevels,
5854
8099
  createWavefrontBvhBuildLevels,
5855
8100
  createWavefrontBvhSortStages,
5856
8101
  createWavefrontEmissiveTriangleIndexSource,
8102
+ createWavefrontGpuMaterialSource,
5857
8103
  createWavefrontGpuMeshSource,
5858
8104
  createWavefrontMeshAcceleration,
5859
8105
  createWavefrontPathTracingComputeConfig,